<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>presentKey</title>
    <link>https://presentkey.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 19 Jul 2026 00:22:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>presentKey</managingEditor>
    <item>
      <title>nvm - Node.js 버전 관리 도구</title>
      <link>https://presentkey.tistory.com/56</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;NVM(Node Version Manager)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 Node.js 버전을 설치하고, 프로젝트마다 간편하게 버전을 전환할 수 있게 해주는 도구&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;windows에서 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/coreybutler/nvm-windows/releases&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1770900711187&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Releases &amp;middot; coreybutler/nvm-windows&quot; data-og-description=&quot;A node.js version management utility for Windows. Ironically written in Go. - coreybutler/nvm-windows&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; data-og-url=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BwFcP/dJMb8Xkbtbr/7JZMNVzYerm1QMyZdjJua1/img.jpg?width=841&amp;amp;height=469&amp;amp;face=0_0_841_469,https://scrap.kakaocdn.net/dn/c5jN4Q/dJMb8SXtSm4/N74bQSkeuevggp3Kia96I1/img.jpg?width=841&amp;amp;height=469&amp;amp;face=0_0_841_469,https://scrap.kakaocdn.net/dn/iKCYW/dJMb88F0XS2/AzH0WPYjsLtuiEcD0dGkG0/img.png?width=1180&amp;amp;height=447&amp;amp;face=0_0_1180_447&quot;&gt;&lt;a href=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/coreybutler/nvm-windows/releases&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BwFcP/dJMb8Xkbtbr/7JZMNVzYerm1QMyZdjJua1/img.jpg?width=841&amp;amp;height=469&amp;amp;face=0_0_841_469,https://scrap.kakaocdn.net/dn/c5jN4Q/dJMb8SXtSm4/N74bQSkeuevggp3Kia96I1/img.jpg?width=841&amp;amp;height=469&amp;amp;face=0_0_841_469,https://scrap.kakaocdn.net/dn/iKCYW/dJMb88F0XS2/AzH0WPYjsLtuiEcD0dGkG0/img.png?width=1180&amp;amp;height=447&amp;amp;face=0_0_1180_447');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Releases &amp;middot; coreybutler/nvm-windows&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A node.js version management utility for Windows. Ironically written in Go. - coreybutler/nvm-windows&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nvm-setup.exe 파일 다운로드&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 버전 설치&lt;/p&gt;
&lt;pre id=&quot;code_1770900961885&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nvm install 19.9.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 버전 제거&lt;/p&gt;
&lt;pre id=&quot;code_1770901007206&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nvm uninstall 19.9.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치된 node 버전 목록&lt;/p&gt;
&lt;pre id=&quot;code_1770901050263&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nvm ls&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 버전 전환&lt;/p&gt;
&lt;pre id=&quot;code_1770901148756&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nvm use 22.13.1&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;.nvmrc 파일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 &lt;b&gt;.nvmrc&lt;/b&gt; 파일에 특정 버전이 기록되어 있다면, &lt;b&gt;use&lt;/b&gt; 명령어를 사용해 간단하게 버전을 전환할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;// .nvmrc 파일 내용&lt;br /&gt;19.9.0&lt;/blockquote&gt;
&lt;pre id=&quot;code_1770901406757&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nvm use&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nvm에 .nvmrc에 명시된 버전이 설치되어 있지 않은 경우, nvm install을 먼저 해야 합니다.&lt;/p&gt;</description>
      <category>Runtime &amp;amp; Environment</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/56</guid>
      <comments>https://presentkey.tistory.com/56#entry56comment</comments>
      <pubDate>Thu, 12 Feb 2026 22:07:31 +0900</pubDate>
    </item>
    <item>
      <title>code splitting, react lazy</title>
      <link>https://presentkey.tistory.com/55</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  code splitting&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react 프로젝트를 빌드하면, 모든 코드(컴포넌트, 스타일)를 하나 또는 여러 개의 파일로 묶는 번들링 과정을 거치게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 사이트에 접속하면 이 번들링된 파일을 다운받는데, 사용자에게 당장 필요하지 않은 파일들도 함께 포함되어 다운로드 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;메인 페이지('/')만 이용하는 사용자도 '/user', '/setting' 페이지 등의 코드가 포함된 파일을 다운받는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번들 사이즈가 커지면 초기 로드 시간이 오래 걸리기 때문에 서비스에 악영향을 끼칠 수 있다. 이 문제를 해결하기 위해 필요한 코드만 다운받을 수 있게하는 code splitting을 적용해 볼 수 있다. react는 dynamic import를 위한 lazy() 함수를 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  라우트별 code splitting&lt;/h2&gt;
&lt;pre id=&quot;code_1732715218696&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import HomePage from './pages/Home/HomePage';
const NotFoundPage = lazy(() =&amp;gt; import('./pages/NotFound/NotFoundPage'));
const SearchPage = lazy(() =&amp;gt; import('./pages/Search/SearchPage'));
const FeedbackPage = lazy(() =&amp;gt; import('./pages/Feedback/FeedbackPage'));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 38px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 21px; text-align: center;&quot;&gt;기존&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 21px; text-align: center;&quot;&gt;lazy 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4UACj/btsKZMWeBy1/MlsmoI7MowgphM6TQ2XV81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4UACj/btsKZMWeBy1/MlsmoI7MowgphM6TQ2XV81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4UACj/btsKZMWeBy1/MlsmoI7MowgphM6TQ2XV81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4UACj%2FbtsKZMWeBy1%2FMlsmoI7MowgphM6TQ2XV81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;341&quot; height=&quot;56&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSvZ87/btsKZDd318Y/RH3KCnJUZ49CRqyVC0oOJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSvZ87/btsKZDd318Y/RH3KCnJUZ49CRqyVC0oOJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSvZ87/btsKZDd318Y/RH3KCnJUZ49CRqyVC0oOJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSvZ87%2FbtsKZDd318Y%2FRH3KCnJUZ49CRqyVC0oOJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;204&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 페이지를 제외한 나머지 페이지에 lazy 적용 시, js 파일이 여러 파일로 분할된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 사용자가 '/search' 진입 시, 검색 페이지 컴포넌트를 네트워크 요청을 통해 동적으로 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Icon code splitting&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날씨 아이콘을 모두 import하여 DayIcons, NightIcons 객체로 매핑하고 있다. 이로 인해 화면에 표시되지 않는 아이콘(비/눈)들까지 모두 로드되고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gw9gX/btsKYyR7FEH/SnyNc644uBjXRbZTIZL0Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gw9gX/btsKYyR7FEH/SnyNc644uBjXRbZTIZL0Ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gw9gX/btsKYyR7FEH/SnyNc644uBjXRbZTIZL0Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGw9gX%2FbtsKYyR7FEH%2FSnyNc644uBjXRbZTIZL0Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;583&quot; height=&quot;351&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1732718631355&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import DayClearIcon from './DayClearIcon';
import DayCloudyIcon from './DayCloudyIcon';
import DaySnowIcon from './DaySnowIcon';
import NightClearIcon from './NightClearIcon';
import NightCloudyIcon from './NightCloudyIcon';
import NightSnowFlurriesIcon from './NightSnowFlurriesIcon';
import NightSnowIcon from './NightSnowIcon';
(...import 낮/밤Icon from '';)

/**
 * 시간대별 날씨 아이콘 표시
 */
const WeatherIcon = ({ fcstTime, skyStatus, rainType }: WeatherIconProps) =&amp;gt; {
  const hour = Number(fcstTime.slice(0, 2));
  const icons = hour &amp;gt;= 6 &amp;amp;&amp;amp; hour &amp;lt;= 18 ? DayIcons : NightIcons;
  let Icon;

  if (rainType === 'NONE') Icon = icons[skyStatus];
  else Icon = icons[rainType];

  return Icon ? &amp;lt;Icon /&amp;gt; : &amp;lt;img src='' alt='' /&amp;gt;;
};

export default WeatherIcon;

const DayIcons = {
  CLEAR: DayClearIcon,
  PARTLY_CLOUDY: DayPartlyCloudyIcon,
  CLOUDY: DayCloudyIcon,
  RAIN: DayRainIcon,
  RAIN_DROP: DayRainDropIcon,
  RAIN_AND_SNOW: DayRainAndSnowIcon,
  RAIN_AND_SNOW_FLURRIES: DayRainAndSnowFlurriesIcon,
  SHOWER: DayShowerIcon,
  SNOW: DaySnowIcon,
  SNOW_FLURRIES: DaySnowFlurriesIcon,
};

const NightIcons = {
  CLEAR: NightClearIcon,
  PARTLY_CLOUDY: NightPartlyCloudyIcon,
  CLOUDY: NightCloudyIcon,
  RAIN: NightRainIcon,
  RAIN_DROP: NightRainDropIcon,
  RAIN_AND_SNOW: NightRainAndSnowIcon,
  RAIN_AND_SNOW_FLURRIES: NightRainAndSnowFlurriesIcon,
  SHOWER: NightShowerIcon,
  SNOW: NightSnowIcon,
  SNOW_FLURRIES: NightSnowFlurriesIcon,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 lazy를 적용하면, 화면에 보여지는 아이콘들만 로드할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1732720404962&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const WeatherIcon = ({ fcstTime, skyStatus, rainType }: WeatherIconProps) =&amp;gt; {
  const hour = Number(fcstTime.slice(0, 2));
  const icons = hour &amp;gt;= 6 &amp;amp;&amp;amp; hour &amp;lt;= 18 ? DayIcons : NightIcons;
  let Icon;

  if (rainType === 'NONE') Icon = icons[skyStatus];
  else Icon = icons[rainType];

  return Icon ? (
    &amp;lt;Suspense fallback={&amp;lt;IconLoading $width={24} $height={24} /&amp;gt;}&amp;gt;
      &amp;lt;Icon /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  ) : (
    &amp;lt;img src='' alt='' /&amp;gt;
  );
};

export default WeatherIcon;

const DayIcons = {
  CLEAR: lazy(() =&amp;gt; import('./DayClearIcon')),
  PARTLY_CLOUDY: lazy(() =&amp;gt; import('./DayPartlyCloudyIcon')),
  CLOUDY: lazy(() =&amp;gt; import('./DayCloudyIcon')),
  RAIN: lazy(() =&amp;gt; import('./DayRainIcon')),
  RAIN_DROP: lazy(() =&amp;gt; import('./DayRainDropIcon')),
  RAIN_AND_SNOW: lazy(() =&amp;gt; import('./DayRainAndSnowIcon')),
  RAIN_AND_SNOW_FLURRIES: lazy(() =&amp;gt; import('./DayRainAndSnowFlurriesIcon')),
  SHOWER: lazy(() =&amp;gt; import('./DayShowerIcon')),
  SNOW: lazy(() =&amp;gt; import('./DaySnowIcon')),
  SNOW_FLURRIES: lazy(() =&amp;gt; import('./DaySnowFlurriesIcon')),
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg6yll/btsKZMaOH93/r7BzKEAyOfWBGd9lkNpTpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg6yll/btsKZMaOH93/r7BzKEAyOfWBGd9lkNpTpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg6yll/btsKZMaOH93/r7BzKEAyOfWBGd9lkNpTpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg6yll%2FbtsKZMaOH93%2Fr7BzKEAyOfWBGd9lkNpTpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;95&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날씨 아이콘과 동일하게 동작하는 다른 아이콘들에게도 lazy를 적용한 결과, index 번들 파일의 크기(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;597kb &amp;rarr; 499.36kb&lt;/span&gt;)가 줄어들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;30&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJCJHn/btsKZsDSyBv/vtXZR5KSsrnxBZjAp5TMdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJCJHn/btsKZsDSyBv/vtXZR5KSsrnxBZjAp5TMdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJCJHn/btsKZsDSyBv/vtXZR5KSsrnxBZjAp5TMdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJCJHn%2FbtsKZsDSyBv%2FvtXZR5KSsrnxBZjAp5TMdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;24&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;30&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  개선 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 네트워크 탭에서 확인한 결과&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 51px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;개선 전&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;개선 후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;transferred&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;629 kB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;558 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;resources&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;1.9 MB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;1.7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework &amp;amp; Library/React</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/55</guid>
      <comments>https://presentkey.tistory.com/55#entry55comment</comments>
      <pubDate>Wed, 27 Nov 2024 23:05:12 +0900</pubDate>
    </item>
    <item>
      <title>개발 및 배포 환경(클라이언트) -&amp;gt; 서버 쿠키 전송 문제</title>
      <link>https://presentkey.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://presentkey.tistory.com/53&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 서버 &amp;rarr; 클라이언트(localhost)로 쿠키 전송 문제를 해결했었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 클라이언트 &amp;rarr; 서버로 POST 요청을 보냈는데 쿠키가 전송되지 않고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 서버(https://custom-domain.com)에서 생성한 쿠키의 도메인은 [ custom-domain.com ] 이고, 클라이언트 도메인은 [ localhost ] 로 같은 도메인이 아니기 때문에 전송되지 않았던 것이다. (+ SameSite: Lax)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 인증을 구현하기 위해선 도메인을 일치시킬 필요가 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ (개발 환경) 백엔드 서버를 로컬에서 띄우자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경에서 위 문제를 해결하기 위해 백엔드 서버를 로컬에서 띄우기로 했다. 그러면 클라이언트는 http://localhost:5173 이고, 서버는 http://localhost:8080이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버(http://localhost:8080)로 api 요청을 보내 쿠키가 생성되면 클라이언트(http://localhost:5173)로 전달된 쿠키의 도메인은 [ localhost ] 가 된다. 이제 도메인이 일치하기 때문에 GET, POST 등 모든 요청에 대해 클라이언트 &amp;rarr; 서버로 쿠키가 전송된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ (배포 환경) 서브 도메인 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프론트 도메인이 [ front-service.com ]이고, 백엔드 서버 도메인이 [ custom-domain.com ] 이면 같은 도메인이 아니기 때문에 쿠키 전송이 안 될 것이다. 그래서 프론트와 백의 도메인을 일치시켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 서버 도메인 [ custom-domain.com ] 을 사용자들이 서비스에 접속할 수 있는 주소로 변경하고, 서버는 서브 도메인을 적용해 [ api.custom-domain.com ] 으로 변경했다. 추가로 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;api.custom-domain.com 서버에서 생성한 쿠키는 api.custom-domain.com 에서만 유효하기 때문에 쿠키의 domain 속성 domain=custom-domain 을 설정해줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;하지만 최신 브라우저로 확인해보면 별도의 domain 속성을 지정하지 않아도 도메인과 서브도메인간 쿠키 전송이 잘 되고있다.&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;(파이어폭스는 저장된 쿠키가 보이지는 않지만 api 요청 시, 쿠키가 잘 전송되고 있다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;서브 도메인&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;서브 도메인은 도메인 이름 앞에 붙는 추가적인 식별자이다.&amp;nbsp;&lt;br /&gt;api.example.com, m.example.com, admin.example.com 등 모두 example.com 도메인의 서브 도메인이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;i&gt;쿠키 domain 속성&lt;br /&gt;&lt;br /&gt;&lt;/i&gt;&lt;/b&gt;- 도메인 속성 지정하지 않았을 때&lt;br /&gt;기본적으로 쿠키가 설정된 서버의 도메인에만 적용됩니다. ex) api.example.com에서 생성된 쿠키는 api.example.com에서만 유효하고, example.com 및 다른 서브 도메인에서는 전송되지 않습니다.&lt;br /&gt;(최신 브라우저에서는 지정하지 않아도 쿠키 전송이 되는거 같습니다.)&lt;br /&gt;&lt;br /&gt;- 도메인 속성 지정&lt;br /&gt;domain=example.com 으로 설정하면 example.com 및 다른 서브 도메인에서도 쿠키가 전송됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web Fundamentals</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/54</guid>
      <comments>https://presentkey.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 26 Sep 2024 03:31:50 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 &amp;rarr; 프론트엔드(localhost) 쿠키 전송 문제</title>
      <link>https://presentkey.tistory.com/53</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  출처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 출처: &lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;&lt;a href=&quot;https://www.devyummi.com/page?id=66934f448a005e7d8f32213a&quot;&gt;https://www.devyummi.com/page?id=66934f448a005e7d8f32213a&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0rLdD/btsJDbLdSf9/GpxGQQaUPudZO2EOON2uD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0rLdD/btsJDbLdSf9/GpxGQQaUPudZO2EOON2uD0/img.png&quot; data-alt=&quot;이미지 출처: https://www.devyummi.com/page?id=66934f448a005e7d8f32213a&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0rLdD/btsJDbLdSf9/GpxGQQaUPudZO2EOON2uD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0rLdD%2FbtsJDbLdSf9%2FGpxGQQaUPudZO2EOON2uD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;572&quot; height=&quot;322&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: https://www.devyummi.com/page?id=66934f448a005e7d8f32213a&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 책임을 백엔드가 맡고 있는 카카오, 구글 소셜 로그인을 구현중이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인 성공 시, 백엔드에서 생성한 jwt 토큰 쿠키가 브라우저(localhost)에 저장되어야 하는데 저장이 되고 있지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;♀️ 첫 번째 시도&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 환경
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;프론트엔드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://localhost:5173&lt;/li&gt;
&lt;li&gt;window.location.href = 'http://1.11.111.11:8080/oauth2/authorization/kakao';
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;소셜 로그인 버튼 클릭 시, 페이지 이동&lt;/li&gt;
&lt;li&gt;http프로토콜&lt;/li&gt;
&lt;li&gt;서버 주소(아이피): 1.11.111.11:8080 (해당 주소는 실제 서버 아이피가 아님)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;백엔드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키 설정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SameSite = lax&lt;/li&gt;
&lt;li&gt;secure = false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 환경에서 테스트 시, 클라이언트 localhost에 jwt 토큰 쿠키가 저장되지 않고 http://1.11.111.11:8080 서버쪽에 쿠키가 저장되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qnQmF/btsJDGw152j/1u318hKNW85dcUA4GBteY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qnQmF/btsJDGw152j/1u318hKNW85dcUA4GBteY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qnQmF/btsJDGw152j/1u318hKNW85dcUA4GBteY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqnQmF%2FbtsJDGw152j%2F1u318hKNW85dcUA4GBteY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;867&quot; height=&quot;183&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;♀️ 두 번째 시도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 localhost 도메인이 달라 쿠키가 저장되지 않는다고 생각했다. 백엔드의 쿠키 설정을 수정해보았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SameSite = none&lt;/li&gt;
&lt;li&gt;secure = true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 시도와 마찬가지로 서버쪽에 쿠키가 저장되었다. 다음 설정들도 클라이언트쪽으로 쿠키가 전송되지 않았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트 localhost https 적용&lt;/li&gt;
&lt;li&gt;백엔드 쿠키 설정 domain:localhost 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  간단히 쿠키 속성에 대해 알아보자&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;domain
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿠키가 전송될 수 있는 도메인을 지정하는 속성&lt;/li&gt;
&lt;li&gt;domain=test.com으로 설정하면, test.com과 sub.test.com에서도 쿠키가 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;secure
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿠키가 https 연결에서만 전송되도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HttpOnly
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자바스크립트로 쿠키를 읽거나 수정할 수 없도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SameSite
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Strict: 오직 같은 사이트에서의 요청에만 전송된다.&lt;/li&gt;
&lt;li&gt;lax: 같은 사이트에서의 요청 및 Get 요청, 링크 이동 등에는 쿠키가 전송된다. 단, POST, PUT, DELETE 요청은 쿠키가 전송되지 않는다.&lt;/li&gt;
&lt;li&gt;none: 모든 사이트 간 요청에서 전송된다. 단 secure=true 속성이 설정되야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;♀️ 세 번째 시도(성공)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 주소를 아이피가 아닌 도메인으로 설정했다. 그 결과 브라우저 localhost에 jwt 토큰 쿠키가 정상적으로 저장되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 환경
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;프론트엔드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://localhost:5173&lt;/li&gt;
&lt;li&gt;window.location.href = 'https://custom-domain.com/oauth2/authorization/kakao';
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;아이피 주소가 아닌 도메인으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;백엔드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿠키 설정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SameSite = lax&lt;/li&gt;
&lt;li&gt;secure = true&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  첫 번째 시도에서는 왜 쿠키를 전달받지 못했을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 localhost는 특수한 도메인으로 간주된다. SameSite가 lax 또는 strict로 설정된 경우, 일부 브라우저 localhost는 서버에서 보내준 쿠키를 저장할 수 있다. 또한, secure=true로 설정되었어도 https가 아닌 http 환경에서 secure 쿠키가 동작할 수 있다. (firefox의 경우 strict로 설정된 경우 쿠키가 저장되지 않았다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localhost가 보안 정책에서 조금 느슨하다면, 첫 번째 시도에서 쿠키는 localhost에 저장되어야 한다. 하지만 저장되지 않았는데 이 이유를 gpt는 다음과 같이 답했다.(100% 신뢰하지 마세요!)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저는 IP 주소와 도메인 이름을 다르게 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;IP 주소로 접속했을 때와 정상적인 도메인 이름으로 접속했을 때 브라우저는 쿠키의 SameSite, Secure 속성을 다르게 적용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;특히, 도메인 이름이 아닌 IP 주소로 접근하면 쿠키가 SameSite=Strict나 SameSite=Lax 설정에서 쿠키 전송이 차단될 가능성이 더 높습니다.&lt;/li&gt;
&lt;li&gt;IP 주소는 브라우저의 보안 모델에서 도메인처럼 신뢰되지 않을 수 있으며, 특히 보안 쿠키 정책이 더 엄격하게 적용될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 정책 상 IP 주소(http://1.11.111.11:8080/oauth2/authorization/kakao)로 접근하여 쿠키 전송이 차단될 걸로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  두 번째 시도에서는 왜 쿠키를 전달받지 못했을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SameSite = None으로 설정하여 모든 사이트간 쿠키가 전송되지만, http 프로토콜을 사용하는 백엔드(http://1.11.111.11:8080)가 secure = true 쿠키 설정이 되어 있어 전송되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  케이스 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;window.location.href = http://1.11.111.11:8080~~~
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SameSite = lax, secure = false: 1.11.111.11 서버에 jwt 토큰 쿠키 저장 (localhost로 전송 X)&lt;/li&gt;
&lt;li&gt;SameSite = none, secure = true: 1.11.111.11 서버에 jwt 토큰 쿠키 저장 (localhost로 전송 X)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;window.location.href = 'https://custom-domain~~~
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SameSite = lax, secure = false: localhost에 jwt 토큰 쿠키 저장&lt;/li&gt;
&lt;li&gt;SameSite = lax, secure = true: localhost에 jwt 토큰 쿠키 저장&lt;/li&gt;
&lt;li&gt;SameSite = none, secure = true: localhost에 jwt 토큰 쿠키 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web Fundamentals</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/53</guid>
      <comments>https://presentkey.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 15 Sep 2024 23:14:41 +0900</pubDate>
    </item>
    <item>
      <title>touch-action css 속성</title>
      <link>https://presentkey.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스와이프 동작을 통해 snackbar를 없애는 Pointer 이벤트 로직을 구현했다. 데스크탑에서는 정상적으로 스와이프가 되었지만, 모바일에서는 snackbar가 화면 밖으로 이동하지 못하는 현상이 있었다.&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 문제 해결&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모바일 브라우저는 터치 이벤트를 감지하면 스크롤 동작, 줌 등 기본 동작을 우선 처리하려고 해서 직접 구현한 스와이프 터치 반응이 제한될 수 있다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;touch-action: none 속성을 추가하여 브라우저의 기본 터치 동작을 차단하고 직접 구현한 터치 동작을 우선적으로 처리하도록 변경했다.&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;touch-action: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auto: 브라우저가 터치 동작을 자동으로 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;none: 모든 터치 동작을 비활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pan-x: 수평 방향의 스크롤만 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pan-y: 수직 방향의 스크롤만 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pinch-zoom: 확대/축소 제스처를 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;manipulation: 스크롤과 확대/축소만 허용하고 다른 제스처(더블 탭) 등은 차단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web Fundamentals/HTML &amp;amp; CSS</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/52</guid>
      <comments>https://presentkey.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 5 Sep 2024 20:10:40 +0900</pubDate>
    </item>
    <item>
      <title>Exclude와 Omit</title>
      <link>https://presentkey.tistory.com/51</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Exclude&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니언 타입에서 특정 타입을 제거할 때 사용하는 유틸리티 타입.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;유니언 타입은 여러 타입 중 하나가 될 수 있는 값을 의미한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1725465527580&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Fruit = 'apple' | 'banana' | 'orange'

type MyFruit = Exclude&amp;lt;Fruit, 'orange'&amp;gt; // 'apple' | 'banana'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyFruit는 orange를 제외한 apple과 banana를 타입으로 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Omit&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 타입에서 특정 속성을 제거한 새로운 타입을 생성하는 유틸리티 타입.&lt;/p&gt;
&lt;pre id=&quot;code_1725465891613&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Person = {
  name: string;
  age: number;
  email: string;
};

type PersonName = Omit&amp;lt;Person, 'age' | 'email'&amp;gt;; // { name: string }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PersonName은 age, email 속성이 제거된 { name: string } 타입을 가집니다.&lt;/p&gt;</description>
      <category>Language/TypeScript</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/51</guid>
      <comments>https://presentkey.tistory.com/51#entry51comment</comments>
      <pubDate>Thu, 5 Sep 2024 01:07:12 +0900</pubDate>
    </item>
    <item>
      <title>emotion의 theme 타입 정의하기 (typeof 연산자)</title>
      <link>https://presentkey.tistory.com/50</link>
      <description>&lt;pre id=&quot;code_1724158842403&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// theme.ts
export const theme = {
  colors: {
    blueGrey: colors.blueGrey,
    red: colors.red,
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724158873856&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// colors.ts
export const colors = {
  blueGrey: {
    50: '#F9FAFB',
    100: '#EBEEF2',
    200: '#CED5DF',
    300: '#B2BECC',
    400: '#95A7BA',
    500: '#788DA3',
    600: '#627384',
    700: '#53616F',
    800: '#434D58',
    900: '#323941',
  },
  red: {
    300: '#FFA59B',
    600: '#D62D36',
    700: '#B32A2E',
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 내에 사용될 색상을 지정한 뒤 사용해보면 다음과 같은 type 오류가 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@emotion/react에 선언된 Theme 타입을 보면 빈 객체로 선언되있기 때문에 colors 속성 존재를 모르기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4QHjd/btsI80QN2GR/h74qRrjTyHdfsnwkrKdpg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4QHjd/btsI80QN2GR/h74qRrjTyHdfsnwkrKdpg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4QHjd/btsI80QN2GR/h74qRrjTyHdfsnwkrKdpg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4QHjd%2FbtsI80QN2GR%2Fh74qRrjTyHdfsnwkrKdpg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;111&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C5afx/btsI9GKUJ07/sitYgwsqsbYMZAhM92T3MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C5afx/btsI9GKUJ07/sitYgwsqsbYMZAhM92T3MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C5afx/btsI9GKUJ07/sitYgwsqsbYMZAhM92T3MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC5afx%2FbtsI9GKUJ07%2FsitYgwsqsbYMZAhM92T3MK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;109&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에 맞게 Theme 인터페이스를 확장할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  typeof: &lt;a href=&quot;https://www.typescriptlang.org/ko/docs/handbook/2/typeof-types.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.typescriptlang.org/ko/docs/handbook/2/typeof-types.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typeof 연산자를 통해 theme 객체의 타입(구조)를 그대로 가져와 ThemeType을 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 다음&lt;/span&gt; emotion.d.ts 파일을 생성하여 Theme을 ThemeType으로 확장시키면 type오류가 사라진다.&lt;/p&gt;
&lt;pre id=&quot;code_1724161936887&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// theme.ts
export const theme :Theme = {
  colors: {
    blueGrey: colors.blueGrey,
    red: colors.red,
  },
};

export type ThemeType = typeof theme;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724160057413&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// emotion.d.ts
import { ThemeType } from '@/styles/theme';
import '@emotion/react';

declare module '@emotion/react' {
  export interface Theme extends ThemeType {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IZn8B/btsJagx7rTF/p9jr45kEwq58hK6bEeKG31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IZn8B/btsJagx7rTF/p9jr45kEwq58hK6bEeKG31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IZn8B/btsJagx7rTF/p9jr45kEwq58hK6bEeKG31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIZn8B%2FbtsJagx7rTF%2Fp9jr45kEwq58hK6bEeKG31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;597&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 blueGrey 900이 string으로만 표시되기 때문에 정확히 어떤 값을 갖는지 알 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;colors 객체에 as const 키워드를 붙여&amp;nbsp; 객체의 각 요소를 리터럴 타입으로 유지할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리터럴 타입: 문자열이나 숫자에 정확한 값을 지정하는 것&lt;/blockquote&gt;
&lt;pre id=&quot;code_1724162619680&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// colors.ts
export const colors = {
  blueGrey: {
    50: '#F9FAFB',
    100: '#EBEEF2',
    200: '#CED5DF',
    300: '#B2BECC',
    400: '#95A7BA',
    500: '#788DA3',
    600: '#627384',
    700: '#53616F',
    800: '#434D58',
    900: '#323941',
  },
  red: {
    300: '#FFA59B',
    600: '#D62D36',
    700: '#B32A2E',
  },
} as const; // as const 키워드!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnOpLh/btsI9YR6exh/n5MudubGCFr0zPiX65JmM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnOpLh/btsI9YR6exh/n5MudubGCFr0zPiX65JmM1/img.png&quot; data-alt=&quot;as const 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnOpLh/btsI9YR6exh/n5MudubGCFr0zPiX65JmM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnOpLh%2FbtsI9YR6exh%2Fn5MudubGCFr0zPiX65JmM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;759&quot; height=&quot;135&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;as const 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qHBps/btsI8JPlpue/5WpAKxDkHcuJE3oobsb2j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qHBps/btsI8JPlpue/5WpAKxDkHcuJE3oobsb2j1/img.png&quot; data-alt=&quot;as const 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qHBps/btsI8JPlpue/5WpAKxDkHcuJE3oobsb2j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqHBps%2FbtsI8JPlpue%2F5WpAKxDkHcuJE3oobsb2j1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;91&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;91&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;as const 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Language/TypeScript</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/50</guid>
      <comments>https://presentkey.tistory.com/50#entry50comment</comments>
      <pubDate>Tue, 20 Aug 2024 23:04:18 +0900</pubDate>
    </item>
    <item>
      <title>Context API 리렌더링 되는 경우 알아보기</title>
      <link>https://presentkey.tistory.com/49</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;a href=&quot;https://solo5star.dev/posts/42/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://solo5star.dev/posts/42/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;a href=&quot;https://velog.io/@velopert/react-context-tutorial#%EA%B0%92%EA%B3%BC-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%95%A8%EC%88%98%EB%A5%BC-%EB%91%90%EA%B0%9C%EC%9D%98-context%EB%A1%9C-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@velopert/react-context-tutorial#%EA%B0%92%EA%B3%BC-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%ED%95%A8%EC%88%98%EB%A5%BC-%EB%91%90%EA%B0%9C%EC%9D%98-context%EB%A1%9C-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;a href=&quot;https://velog.io/@dahyeon405/Context-API%EC%9D%98-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%8C%80%ED%95%9C-%EC%98%A4%ED%95%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@dahyeon405/Context-API%EC%9D%98-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EB%8C%80%ED%95%9C-%EC%98%A4%ED%95%B4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Provider 내부에 컴포넌트가 있는 경우 (&amp;lt;Text /&amp;gt;)&lt;/h2&gt;
&lt;pre id=&quot;code_1723472833366&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const CounterValueContext = createContext(0);
const CounterActionContext = createContext({
  increase: () =&amp;gt; {},
  decrease: () =&amp;gt; {},
});

export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [counter, setCounter] = useState(0);
  const increase = () =&amp;gt; setCounter((prev) =&amp;gt; prev + 1);
  const decrease = () =&amp;gt; setCounter((prev) =&amp;gt; prev - 1);
  console.log('Counter Provider');

  return (
    &amp;lt;CounterValueContext.Provider value={counter}&amp;gt;
      &amp;lt;CounterActionContext.Provider value={{ increase, decrease }}&amp;gt;
        &amp;lt;Text /&amp;gt;
        {children}
      &amp;lt;/CounterActionContext.Provider&amp;gt;
    &amp;lt;/CounterValueContext.Provider&amp;gt;
  );
}

export function useCounter() {
  const value = useContext(CounterValueContext);
  return value;
}

export function useSetCounter() {
  const value = useContext(CounterActionContext);
  return value;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723472891174&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function ContextPage() {
  console.log('Context Page');
  return (
    &amp;lt;CounterProvider&amp;gt;
      &amp;lt;Plus /&amp;gt;
      &amp;lt;Number /&amp;gt;
      &amp;lt;Minus /&amp;gt;
      &amp;lt;Hello /&amp;gt;
    &amp;lt;/CounterProvider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723472944470&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Text.tsx
export default function Text() {
  console.log('Text');
  return &amp;lt;h3&amp;gt;Counter Text&amp;lt;/h3&amp;gt;;
}

// Plus.tsx
export default function Plus() {
  const { increase } = useSetCounter();
  console.log('Plus');

  return &amp;lt;button onClick={increase}&amp;gt;+&amp;lt;/button&amp;gt;;
}

// Minus.tsx
export default function Minus() {
  const { decrease } = useSetCounter();
  console.log('Minus');

  return &amp;lt;button onClick={decrease}&amp;gt;-&amp;lt;/button&amp;gt;;
}

// Number.tsx
export default function Number() {
  const count = useCounter();
  console.log('Number');

  return &amp;lt;div&amp;gt;{count}&amp;lt;/div&amp;gt;;
}

// Hello.tsx
export default function Hello() {
  console.log('Hello');
  return &amp;lt;div&amp;gt;Hello!&amp;lt;/div&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;184&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGHcX2/btsI2jhSHEg/Gb1toiKQUeGLQlkYnYxOJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGHcX2/btsI2jhSHEg/Gb1toiKQUeGLQlkYnYxOJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGHcX2/btsI2jhSHEg/Gb1toiKQUeGLQlkYnYxOJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGHcX2%2FbtsI2jhSHEg%2FGb1toiKQUeGLQlkYnYxOJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;184&quot; height=&quot;176&quot; data-origin-width=&quot;184&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Hello /&amp;gt; 컴포넌트를 제외한 &amp;lt;Text /&amp;gt;, &amp;lt;Plus /&amp;gt;, &amp;lt;Minus /&amp;gt;, &amp;lt;Number /&amp;gt; 컴포넌트 모두 counter 상태가 변경될때마다 리렌더링이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Text /&amp;gt;는 CounterProvider의 내부 상태가 변경되었기 때문에 리렌더링되었고, 리렌더링 되면서 increase, decrease 함수도 새로 만들어지기 때문에 context의 값을 바라보고 있는(useCounter, useSetCounter) 모든 자식 컴포넌트들이 리렌더링 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리렌더링을 막는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context의 값을 사용하고 있지 않은 &amp;lt;Text /&amp;gt;의 리렌더링을 막는 방법은 &amp;lt;Text /&amp;gt; 컴포넌트에 React memo 를 적용하거나 &amp;lt;Hello /&amp;gt; 컴포넌트처럼 children으로 렌더링 하는 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;increase, decrease 함수를 사용하고 있는 컴포넌트의 리렌더링을 막는 방법은 useMemo를 적용한 actions 객체를 만들어서 전달하는 것이다. 이러면 &amp;lt;CounterProvider /&amp;gt;, &amp;lt;Number /&amp;gt; 컴포넌트만 리렌더링 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1723475519077&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [counter, setCounter] = useState(0);
  const actions = useMemo(
    () =&amp;gt; ({
      increase() {
        setCounter((prev) =&amp;gt; prev + 1);
      },
      decrease() {
        setCounter((prev) =&amp;gt; prev - 1);
      },
    }),
    [],
  );
  console.log('Counter Provider');

  return (
    &amp;lt;CounterValueContext.Provider value={counter}&amp;gt;
      &amp;lt;CounterActionContext.Provider value={actions}&amp;gt;
        {children}
      &amp;lt;/CounterActionContext.Provider&amp;gt;
    &amp;lt;/CounterValueContext.Provider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;increase, decrease 각 함수에 useCallback을 적용해 value = {{ increase, decrease }} 로 전달한 경우, 렌더링 최적화가 적용되지 않는다. 왜냐하면 &amp;lt;CounterProvider /&amp;gt;가 리렌더링 되면서 increase, decrease 함수는 메모이제이션 됐지만, value로 전달하는 객체는 새로 만들어지기때문에 최적화가 되지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 increase 함수 하나만 전달한다면 useCallback만으로 최적화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  children으로 컴포넌트를 렌더링 한 경우&lt;/h2&gt;
&lt;pre id=&quot;code_1723478011483&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [counter, setCounter] = useState(0);
  const actions = useMemo(
    () =&amp;gt; ({
      increase() {
        setCounter((prev) =&amp;gt; prev + 1);
      },
      decrease() {
        setCounter((prev) =&amp;gt; prev - 1);
      },
    }),
    [],
  );
  console.log('Counter Provider');

  return (
    &amp;lt;CounterValueContext.Provider value={counter}&amp;gt;
      &amp;lt;CounterActionContext.Provider value={actions}&amp;gt;
        {children}
      &amp;lt;/CounterActionContext.Provider&amp;gt;
    &amp;lt;/CounterValueContext.Provider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723477847802&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function ContextPage() {
  console.log('Context Page');
  return (
    &amp;lt;CounterProvider&amp;gt;
      &amp;lt;Text&amp;gt;
        &amp;lt;HeadingNumber /&amp;gt;
      &amp;lt;/Text&amp;gt;
      &amp;lt;Plus /&amp;gt;
      &amp;lt;Number /&amp;gt;
      &amp;lt;Minus /&amp;gt;
      &amp;lt;Hello /&amp;gt;
    &amp;lt;/CounterProvider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723477905331&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Text.tsx
export default function Text({ children }: { children: React.ReactNode }) {
  console.log('Text');

  return (
    &amp;lt;h3&amp;gt;
      Counter Text
      {children}
    &amp;lt;/h3&amp;gt;
  );
}

// HeadingNumber.tsx
export default function HeadingNumber() {
  const count = useCounter();
  console.log('Heading Number');

  return &amp;lt;span&amp;gt; ★ {count} ★&amp;lt;/span&amp;gt;;
}

// Hello.tsx
export default function Hello() {
  console.log('Hello');
  const count = useCounter();

  return &amp;lt;div&amp;gt;Hello!&amp;lt;/div&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &amp;lt;Text /&amp;gt;는 children으로 &amp;lt;HeadingNumber /&amp;gt; 컴포넌트를 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &amp;lt;Hello /&amp;gt;는 count 상태를 가지지만 사용하지 않는다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 17px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/traqi/btsI2H3B8NH/o3E7R9giFofsTb3m3vkysk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/traqi/btsI2H3B8NH/o3E7R9giFofsTb3m3vkysk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/traqi/btsI2H3B8NH/o3E7R9giFofsTb3m3vkysk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftraqi%2FbtsI2H3B8NH%2Fo3E7R9giFofsTb3m3vkysk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;247&quot; height=&quot;169&quot; data-origin-width=&quot;247&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;167&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUAZzd/btsI3flfdk9/SrcetLPHyQfLd84xxUtjQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUAZzd/btsI3flfdk9/SrcetLPHyQfLd84xxUtjQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUAZzd/btsI3flfdk9/SrcetLPHyQfLd84xxUtjQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUAZzd%2FbtsI3flfdk9%2FSrcetLPHyQfLd84xxUtjQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;167&quot; height=&quot;206&quot; data-origin-width=&quot;167&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count 상태가 변경되면 &amp;lt;Counter Provider /&amp;gt;와 useCounter()를 사용한 &amp;lt;Heading Number /&amp;gt;, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;lt;Number /&amp;gt;&lt;/span&gt;, &amp;lt;Hello /&amp;gt; 컴포넌트만 리렌더링 된다. (actions는 useMemo를 적용했기 때문에, &amp;lt;Plus /&amp;gt; &amp;lt;Minus /&amp;gt; 컴포넌트는 리렌더링 발생하지 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Text /&amp;gt;는 &amp;lt;HeadingNumber /&amp;gt;가 리렌더링 되었어도 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;context 값을 사용하지 않기 때문에&lt;span&gt; &lt;/span&gt;&lt;/span&gt;리렌더링 되지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 경우들을 통해 Context API의 모든 자식 컴포넌트들이 리렌더링 되는 것은 아니라는 사실이다!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  하나의 context provider로 값과 함수를 사용한다면?&lt;/h2&gt;
&lt;pre id=&quot;code_1723480520904&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const CounterContext = createContext({
  counter: 0,
  actions: { increase: () =&amp;gt; {}, decrease: () =&amp;gt; {} },
});

export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [counter, setCounter] = useState(0);
  const actions = useMemo(
    () =&amp;gt; ({
      increase() {
        setCounter((prev) =&amp;gt; prev + 1);
      },
      decrease() {
        setCounter((prev) =&amp;gt; prev - 1);
      },
    }),
    [],
  );
  const value = useMemo(() =&amp;gt; ({ counter, actions }), [counter, actions]);
  console.log('Counter Provider');

  return (
    &amp;lt;CounterContext.Provider value={value}&amp;gt;{children}&amp;lt;/CounterContext.Provider&amp;gt;
  );
}

export function useCounter() {
  const value = useContext(CounterContext);
  return value;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723480581305&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Plus.tsx
export default function Plus() {
  const {
    actions: { increase },
  } = useCounter();
  console.log('Plus');

  return &amp;lt;button onClick={increase}&amp;gt;+&amp;lt;/button&amp;gt;;
}

// Minus.tsx
export default function Minus() {
  const {
    actions: { decrease },
  } = useCounter();
  console.log('Minus');

  return &amp;lt;button onClick={decrease}&amp;gt;-&amp;lt;/button&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1PLtd/btsI3f6CmRL/G2L9tmv31DbR59ieOj61k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1PLtd/btsI3f6CmRL/G2L9tmv31DbR59ieOj61k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1PLtd/btsI3f6CmRL/G2L9tmv31DbR59ieOj61k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1PLtd%2FbtsI3f6CmRL%2FG2L9tmv31DbR59ieOj61k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;146&quot; height=&quot;257&quot; data-origin-width=&quot;146&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 provider를 통해 값을 전달하고 있는 경우 context의 값을 사용하고 있는 모든 컴포넌트에서 리렌더링이 발생한다. actions는 useMemo를 적용했어도, counter가 변경될 때마다 value의 useMemo dependencies가 바뀌기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 상태가 자주 변경되는 경우에는 값과 상태 변경 함수 provider를 분리하여 관리하는게 좋다.&lt;/p&gt;</description>
      <category>Framework &amp;amp; Library/React</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/49</guid>
      <comments>https://presentkey.tistory.com/49#entry49comment</comments>
      <pubDate>Tue, 13 Aug 2024 01:57:52 +0900</pubDate>
    </item>
    <item>
      <title>react 프로젝트 세팅(prettier, stylelint, .vscode, path alias)</title>
      <link>https://presentkey.tistory.com/48</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  설치 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm + vite + react + typescript + vscode&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  prettier&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prettier는 나 또는 팀원들이 작성한 코드를 일관된 스타일로 작성하도록 도와주는 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode setting에서 개인 설정을 적용할 수 있고, 팀원들의 코드 스타일을 맞추려면 프로젝트 루트 폴더에&amp;nbsp; &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.prettierrc.json&lt;/span&gt; 파일을 생성하고 원하는 설정을 넣어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(프로젝트 prettierrc.json이 vscode 세팅보다 우선순위가 높다.)&lt;/p&gt;
&lt;pre id=&quot;code_1722935158078&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;singleQuote&quot;: true,
  &quot;jsxSingleQuote&quot;: true,
  &quot;semi&quot;: true,
  &quot;printWidth&quot;: 80
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;prettier 옵션:&lt;/b&gt; &lt;a href=&quot;https://prettier.io/docs/en/options&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://prettier.io/docs/en/options&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  stylelint + scss + .vscode(extensions.json, settings.json)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 설치 및 세팅&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stylelint는 css 작성 규칙을 정해 오류를 방지하고 일관된 스타일로 작성하도록 도와주는 도구이다.&lt;/p&gt;
&lt;pre id=&quot;code_1722935724891&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm add -D stylelint stylelint-config-standard-scss
pnpm add -D stylelint-config-recess-order&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- stylelint-config-standard-scss: scss를 위한 stylelint 표준 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- stylelint-config-recess-order: css 속성을 정렬하는 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 폴더에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.stylelintrc.json&lt;/span&gt; 파일을 생성하고 규칙 등등을 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1722936191274&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;extends&quot;: [
    &quot;stylelint-config-standard-scss&quot;,
    &quot;stylelint-config-recess-order&quot;
  ],
  &quot;ignoreFiles&quot;: [&quot;src/index.scss&quot;],
  &quot;rules&quot;: {
    &quot;at-rule-no-unknown&quot;: null,
    &quot;scss/at-rule-no-unknown&quot;: true,
    &quot;selector-class-pattern&quot;: &quot;^([a-z][a-z0-9]*)(-[a-z0-9]+)*$&quot;,
    &quot;selector-pseudo-class-no-unknown&quot;: [
      true,
      {
        &quot;ignorePseudoClasses&quot;: [&quot;global&quot;]
      }
    ],
    &quot;keyframes-name-pattern&quot;: &quot;^([a-z][a-z0-9]*)(-[a-z0-9]+)*$&quot;,
    &quot;max-nesting-depth&quot;: 3,
    &quot;no-descending-specificity&quot;: null
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;stylelint rule:&lt;/b&gt; &lt;a href=&quot;https://stylelint.io/user-guide/rules&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stylelint.io/user-guide/rules&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;package.json&lt;/span&gt; 파일에 stylelint script를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;pnpm stylelint&quot; 명령어를 입력하여 규칙에 어긋나는 css 코드들을 확인할 수 있다. 하지만 매번 명령어를 입력해야 하는 번거로움이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1722937147757&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;scripts&quot;: {
    ...,
    &quot;stylelint&quot;: &quot;stylelint '**/*.{css,scss}' --fix&quot;
  },&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. extenstion&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어 필요 없이 코드 작성 중에 바로 확인할 수 있도록 도와주는 &quot;stylelint&quot; extenstion이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(scripts - stylelint는 제거해도 괜찮습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nOtMw/btsIUD2YsOk/Je1Qvq6135OMWeLhjFwARK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nOtMw/btsIUD2YsOk/Je1Qvq6135OMWeLhjFwARK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nOtMw/btsIUD2YsOk/Je1Qvq6135OMWeLhjFwARK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnOtMw%2FbtsIUD2YsOk%2FJe1Qvq6135OMWeLhjFwARK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;164&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;extenstion 설치 후, vscode 설정(settings.json)에 다음 옵션을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722938172114&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;css.validate&quot;: false,
  &quot;less.validate&quot;: false,
  &quot;scss.validate&quot;: false,
  &quot;stylelint.validate&quot;: [&quot;css&quot;, &quot;scss&quot;],
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.fixAll.stylelint&quot;: &quot;explicit&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 바로바로 vscode에서 문제를 확인할 수 있다. 그리고 stylelint 규칙 중 autofixed 표시가 있는 규칙은 저장 시, 자동으로 수정된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cobSUc/btsIVbZeMiI/oRAKuG4EuruYv6XwfVR8N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cobSUc/btsIVbZeMiI/oRAKuG4EuruYv6XwfVR8N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cobSUc/btsIVbZeMiI/oRAKuG4EuruYv6XwfVR8N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcobSUc%2FbtsIVbZeMiI%2FoRAKuG4EuruYv6XwfVR8N1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;329&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- 프로젝트 설정 공유&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위와 같은 개발 환경 세팅을 팀원들과 공유하려면 어떻게 해야할까?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 폴더에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;.vscode&lt;/span&gt; 폴더를 통해 관리하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;243&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TRBEY/btsIUxBCUWQ/vY90GmTP6KJ6krMjXZVNC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TRBEY/btsIUxBCUWQ/vY90GmTP6KJ6krMjXZVNC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TRBEY/btsIUxBCUWQ/vY90GmTP6KJ6krMjXZVNC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTRBEY%2FbtsIUxBCUWQ%2FvY90GmTP6KJ6krMjXZVNC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;243&quot; height=&quot;88&quot; data-origin-width=&quot;243&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;함께 사용해야 할 extension이 있는 경우 &quot;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;작업 영역에 추가 권장 사항&lt;/span&gt;&quot;을 클릭하면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;extensions.json&lt;/span&gt; 파일에 추가된다. 이러면 해당 extension이 없는 팀원은 vscode 알림으로 설치 권고를 받게된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCCQLP/btsIWGYbB6V/6Us6TvAGl5TcZ16giaJapk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCCQLP/btsIWGYbB6V/6Us6TvAGl5TcZ16giaJapk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCCQLP/btsIWGYbB6V/6Us6TvAGl5TcZ16giaJapk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCCQLP%2FbtsIWGYbB6V%2F6Us6TvAGl5TcZ16giaJapk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;403&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode setting은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;settings.json&lt;/span&gt; 파일을 생성하여 필요한 옵션들을 넣어주면 프로젝트 팀원 간 설정이 공유된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  path alias 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path alias는 경로에 별칭을 붙이는 것으로, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&quot;../../../compoents/Box&quot;&lt;/span&gt; 와 같은 경로를 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&quot;@components/Box&quot;&lt;/span&gt; 표현으로 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite.config.ts 파일을 열어 다음 속성을 설정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722939692578&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

// https://vitejs.dev/config/
export default defineConfig({
  ...,
  resolve: {
    alias: [{ find: '@', replacement: resolve(__dirname, 'src') }],
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;path&quot;를 사용하기 위해서 @types/node가 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1722939884689&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pnpm add -D @types/node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 tsconfig.json도 설정해줘야 하는데, 현재 vite으로 react 설치 시 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;tsconfig.app.json&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;tsconfig.json&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;tsconfig.node.json&lt;/span&gt; 세 개의 파일이 존재한다. 그 중 tsconfig.app.json에 설정해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1722940139274&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    ...,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;]
    }
  },
  ...,
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Framework &amp;amp; Library/React</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/48</guid>
      <comments>https://presentkey.tistory.com/48#entry48comment</comments>
      <pubDate>Tue, 6 Aug 2024 19:31:11 +0900</pubDate>
    </item>
    <item>
      <title>animation, transition event</title>
      <link>https://presentkey.tistory.com/47</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Animation 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 애니메이션이 시작, 종료, 반복될 때 발생하는 이벤트이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;animationstart&lt;/li&gt;
&lt;li&gt;animationend&lt;/li&gt;
&lt;li&gt;animationiteration&lt;/li&gt;
&lt;/ul&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;pre id=&quot;code_1718792987557&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// react
export default function EventPage() {
  const animationStart = () =&amp;gt; console.log('animation start');
  const animationEnd = () =&amp;gt; console.log('animation end');
  const animationIteration = () =&amp;gt; console.log('animation iteration');

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h3&amp;gt;animation event&amp;lt;/h3&amp;gt;
      &amp;lt;div
        className={styles.box}
        onAnimationStart={animationStart}
        onAnimationEnd={animationEnd}
        onAnimationIteration={animationIteration}
      &amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

// css
.box {
  width: 100px;
  height: 100px;
  background-color: #5f9ea0;
  animation: spin 2s;
  animation-iteration-count: 3;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Transtion 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/transitionend_event&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/Element/transitionend_event&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CSS 트랜지션이 시작, 종료, 취소될 때 발생하는 이벤트이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;transtionstart&lt;/li&gt;
&lt;li&gt;transitionend&lt;/li&gt;
&lt;li&gt;transitioncancel&lt;/li&gt;
&lt;/ul&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;pre id=&quot;code_1718794999521&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// react
// transitionEnd 이벤트 외 지원하지 않는 이벤트는 직접 등록해야 한다.
export default function EventPage() {
  const divRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const transitionStart = () =&amp;gt; console.log('transition start');
  const transitionEnd = () =&amp;gt; console.log('transition end');
  const transitionCancel = () =&amp;gt; console.log('transition cancel');

  useEffect(() =&amp;gt; {
    const divElement = divRef.current;
    if (!divElement) return;
    divElement.addEventListener('transitionstart', transitionStart);
    divElement.addEventListener('transitioncancel', transitionCancel);

    return () =&amp;gt; {
      divElement.removeEventListener('transitionstart', transitionStart);
      divElement.removeEventListener('transitioncancel', transitionCancel);
    };
  }, []);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h3&amp;gt;animation event&amp;lt;/h3&amp;gt;
      &amp;lt;div
        className={styles.box2}
        ref={divRef}
        onTransitionEnd={transitionEnd}
      &amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

// CSS
.box2 {
  width: 100px;
  height: 100px;
  background-color: #5f9ea0;
  transition: transform 2s ease;
}

.box2:hover {
  transform: scaleX(2);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Language/JavaScript</category>
      <author>presentKey</author>
      <guid isPermaLink="true">https://presentkey.tistory.com/47</guid>
      <comments>https://presentkey.tistory.com/47#entry47comment</comments>
      <pubDate>Wed, 19 Jun 2024 20:04:37 +0900</pubDate>
    </item>
  </channel>
</rss>