<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>fromfavor</title>
    <link>https://fromfavor.tistory.com/</link>
    <description>개발을 하러 가야 한다 </description>
    <language>ko</language>
    <pubDate>Sat, 8 Aug 2026 03:32:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>favor</managingEditor>
    <image>
      <title>fromfavor</title>
      <url>https://tistory1.daumcdn.net/tistory/8598472/attach/c98d876fcbe940db8ecb87e864634143</url>
      <link>https://fromfavor.tistory.com</link>
    </image>
    <item>
      <title>섹션 18. React Router v7</title>
      <link>https://fromfavor.tistory.com/32</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;React Router v7 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 애플리케이션은 싱글 페이지 애플리케이션 기반 --&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;URL 경로에 따라 동적인 애플리케이션 화면을 보여주기 위해서는 React Router 사용&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;&lt;a href=&quot;https://reactrouter.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://reactrouter.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1783490593922&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;React Router Official Documentation&quot; data-og-description=&quot;4,048,871,903 Downloads on npm&quot; data-og-host=&quot;reactrouter.com&quot; data-og-source-url=&quot;https://reactrouter.com/&quot; data-og-url=&quot;https://reactrouter.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://reactrouter.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://reactrouter.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;React Router Official Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;4,048,871,903 Downloads on npm&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;reactrouter.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;React Router 모드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Declarative
&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;배우기 쉽고 적용하기 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Data
&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;페이지 전환 시 필요한 데이터를 자동으로 로드하거나 에러 로딩 상태를 처리해주는 기능 탑재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Framework
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 모드에 폼 처리 기능, 기본 UI가 추가된 형태&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React Router v7 설치&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;&lt;b&gt;npm i react-router&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어를 사용하여 다운로드&amp;nbsp;&lt;/span&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;src/routes/index.tsx&lt;/li&gt;
&lt;li&gt;src/routes/pages/About.tsx&lt;/li&gt;
&lt;li&gt;src/routes/pages/Home.tsx&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783491727906&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Main.tsx

import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import Router from &quot;./routes&quot;;

createRoot(document.getElementById(&quot;root&quot;)!).render(
  &amp;lt;StrictMode&amp;gt;
    &amp;lt;Router /&amp;gt;
  &amp;lt;/StrictMode&amp;gt;,
);&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_1783491771308&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

import { createBrowserRouter, RouterProvider } from &quot;react-router&quot;;
import Home from &quot;./pages/Home&quot;;
import About from &quot;./pages/About&quot;;

const router = createBrowserRouter([
  {
    path: &quot;/&quot;,
    element: &amp;lt;Home /&amp;gt;,
  },
  {
    path: &quot;/about&quot;,
    element: &amp;lt;About /&amp;gt;,
  },
]);

// Router로 내부 함수 이름 변경
export default function Router() { 
  return (
    &amp;lt;&amp;gt;
      &amp;lt;RouterProvider router={router} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createBrowserRouter([...])
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;어떤 주소에 대해 어떤 컴포넌트를 보여줄지 객체 형태로 매핑하여 &lt;span style=&quot;color: #ee2323;&quot;&gt;라우터 인스턴스를 생성하는 함수&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;path: &quot;/&quot;
&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;element: &amp;lt;Home /&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;lt;Home /&amp;gt; 컴포넌트 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;RouterProvider router={router} /&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위에서 정의한 router 설계도를 리액트 앱 전체에 주입&lt;/li&gt;
&lt;li&gt;실제로 라우팅 시스템이 작동하도록 provider 역할을 하는 컴포넌트&amp;nbsp;&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;
&lt;pre id=&quot;code_1783491822986&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/Home.tsx

export default function Home() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Home Component&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783491841319&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/About.tsx

export default function About() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;About Component&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React Router v7 기본&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;route 객체 : createBrowserRouter() 내부 배열 각각의 객체&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;router instance 객체 : route 객체가 묶여 하나의 네비게이션 동작을 수행하게 하는 전체적인 객체&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;element 속성에 JSX 문법을 직접적으로 작성하는 것도 가능&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783577664424&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    path: &quot;/post&quot;,
    element: &amp;lt;h1&amp;gt;Post Page&amp;lt;/h1&amp;gt;,
}&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;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 전달할 때에는 element가 아닌 &lt;span style=&quot;color: #ee2323;&quot;&gt;Component 속성을 사용&lt;/span&gt;하는 방법도 존재&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783577703118&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    path: &quot;/&quot;,
    Component: Home,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 중첩 라우트와 인덱스 라우트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;route를 설정할 때 children 속성을 사용하면 route를 중첩시켜서 부모-자식 관계를 정의 가능&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;color: #ee2323;&quot;&gt;children 속성을 사용한 부모 컴포넌트에 &amp;lt;Outlet /&amp;gt; 컴포넌트를 작성해주어야 함&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Outlet /&amp;gt; 위치에 자식 컴포넌트가 렌더링됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783578570464&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

import { createBrowserRouter, RouterProvider } from &quot;react-router&quot;;
import DashboardHome from &quot;./pages/dashboard/DashboardHome&quot;;
import Dashboard from &quot;./pages/dashboard/Dashboard&quot;;

const router = createBrowserRouter([
  {
    path: &quot;/dashboard&quot;,
    Component: Dashboard,
    children: [
      {
        path: &quot;&quot;, // /dashboard 주소 --&amp;gt; Dashboard 컴포넌트와 DashboardHome 컴포넌트를 모두 보여줌
        Component: DashboardHome,
      },
    ],
  },
]);

export default function Router() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;RouterProvider router={router} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ &amp;lt;Outlet /&amp;gt; 컴포넌트 추가&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783578618442&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/dashboard/Dashboard.tsx

import { Outlet } from &quot;react-router&quot;;

export default function Dashboard() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Dashboard Component&amp;lt;/h1&amp;gt;
      &amp;lt;Outlet /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783578657616&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/dashboard/DashboardHome.tsx

export default function DashboardHome() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;DashboardHome Component&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;p data-ke-size=&quot;size16&quot;&gt;1. path 속성 사용&lt;/p&gt;
&lt;pre id=&quot;code_1783578777169&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  path: &quot;/dashboard&quot;,
  Component: Dashboard,
  children: [
    {
      path: &quot;&quot;, // /dashboard 주소 --&amp;gt; Dashboard 컴포넌트와 DashboardHome 컴포넌트를 모두 보여줌
      Component: DashboardHome,
    },
  ],
},&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;2. index 속성 사용&lt;/p&gt;
&lt;pre id=&quot;code_1783578813716&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  path: &quot;/dashboard&quot;,
  Component: Dashboard,
  children: [
    {
      index: true, // /dashboard 주소 --&amp;gt; Dashboard 컴포넌트와 DashboardHome 컴포넌트를 모두 보여줌
      Component: DashboardHome,
    },
  ],
},&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;&lt;span style=&quot;color: #ee2323;&quot;&gt;* 중첩된 라우트의 path에는 /를 붙이지 X&lt;/span&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;React Router v7 레이아웃 라우트&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃 라우트 : 중첩 라우트 기법과 동일하지만, 부모 라우트에 path 속성을 지정하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 주소창의 URL 경로를 변경하지 않고, &lt;span style=&quot;color: #ee2323;&quot;&gt;여러 독립된 페이지에서 공통 UI를 감싸주는 껍데기 역할을 하기 때문&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;레이아웃으로 사용할 공통 뼈대 컴포넌트는&lt;b&gt; src/pages/layouts/&lt;/b&gt; 하위에 생성&amp;nbsp;&lt;/p&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;820&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDdg53/dJMcahE2irQ/PGKfyfRk8NJV7ql4MsMBPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDdg53/dJMcahE2irQ/PGKfyfRk8NJV7ql4MsMBPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDdg53/dJMcahE2irQ/PGKfyfRk8NJV7ql4MsMBPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDdg53%2FdJMcahE2irQ%2FPGKfyfRk8NJV7ql4MsMBPK%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;400&quot; height=&quot;197&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1783579824810&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/layouts/Default.tsx

import { Outlet } from &quot;react-router&quot;;

export default function Default() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;header&amp;gt;
        &amp;lt;h1&amp;gt;Header&amp;lt;/h1&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;Outlet /&amp;gt;
      &amp;lt;footer&amp;gt;
        &amp;lt;h1&amp;gt;Footer&amp;lt;/h1&amp;gt;
      &amp;lt;/footer&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783579942072&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

import { createBrowserRouter, RouterProvider } from &quot;react-router&quot;;
import Home from &quot;./pages/Home&quot;;
import About from &quot;./pages/About&quot;;
import Dashboard from &quot;./pages/dashboard/Dashboard&quot;;
import DashboardHome from &quot;./pages/dashboard/DashboardHome&quot;;
import DashboardSetting from &quot;./pages/dashboard/DashboardSetting&quot;;
import Default from &quot;./pages/layouts/Default&quot;;

const router = createBrowserRouter([
  {
    // default 설정
    Component: Default, 
    children: [
      {
        path: &quot;/&quot;,
        Component: Home,
      },
      {
        path: &quot;/about&quot;,
        Component: About,
      },
    ],
  },
]);

export default function Router() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;RouterProvider router={router} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 라우트 프리픽스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우트 프리픽스 : element, Component 속성 없이 &lt;span style=&quot;color: #ee2323;&quot;&gt;오직 path 속성만 가진 부모 라우트 아래에 children을 중첩&lt;/span&gt;시키는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ㄴ 화면을 띄워주는 공통 껍데기(레이아웃)는 없지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;하위 자식 라우트들의 URL 시작 주소를 한 곳에서 묶어 관리&lt;/span&gt;하고 싶을&amp;nbsp; 사용&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783581922074&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  path: &quot;/group&quot;,
  // Component: DashboardLayout,
  children: [
    {
      path: &quot;dashboard&quot;,
      Component: Dashboard,
      children: [
        {
          index: true,
          Component: DashboardHome,
        }, {
          path: &quot;setting&quot;,
          Component: DashboardSetting,
          children: [
            {
              path: &quot;custom&quot;,
              element: &amp;lt;h1&amp;gt;DashboardSetting&amp;lt;/h1&amp;gt;
            },
          ],
        },
      ],
    },
  ],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 동적 세그먼트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 세그먼트 : URL 경로 중 고정되지 않고 사용자의 데이터(id 등)에 따라 &lt;span style=&quot;color: #ee2323;&quot;&gt;동적으로 변하는 주소 영역&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;useParams 훅 : 동적 세그먼트로 지정된 URL 값을 컴포넌트 내부에서 읽어올 때 사용하는 리액트 라우터 전용 훅&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783582450064&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

{
  path: &quot;/post/:id&quot;,
  Component: Post,
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783582474485&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/Post.tsx

import { useParams } from &quot;react-router&quot;;

export default function Post() {
  const { id } = useParams();
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Post: {id}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;pre id=&quot;code_1783582869686&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

{
  path: &quot;/post/:id/detail/:detail&quot;,
  Component: PostDetail,
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783582902567&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/PostDetail.tsx

import { useParams } from &quot;react-router&quot;;

export default function PostDetail() {
  const { id, detail } = useParams();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;
        PostDetail: {id} / {detail}
      &amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;* 동적 세그먼트는 각기 다른 값이어야 함&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ex) /post/:id/detail/:id (X)&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;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;React Router v7 옵셔널 세그먼트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵셔널 세그먼트 : 라우트 설정 시 URL 경로의 &lt;span style=&quot;color: #ee2323;&quot;&gt;특정 구역(세그먼트)을 선택 사항으로 지정하여 생략할 수 있도록&lt;/span&gt; 만드는 기법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) path: &quot;/post/:id?&quot; --&amp;gt; id를 생략해도 페이지 접속 가능&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;React Router v7 스플랫&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스플랫 라우트 : 라우터에 명시적으로 정의되지 않은 모든 예외 경로를 잡아내기 위해 사용하는 라우트 지정 기법&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;== Catch-all 라우트 또는 Wildcard 라우트&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ path 속성 값의 맨 끝에 * 기호를 붙여 선언&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) path: &quot;*&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783583924951&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

{
  path: &quot;/post/*&quot;,
  Component: Post,
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783583946919&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/Post.tsx

import { useParams } from &quot;react-router&quot;;

export default function Post() {
  const { &quot;*&quot;: splat } = useParams();
  console.log(splat);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Post&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;* 라우터의 맨 밑바닥에 배치하여 404 페이지 커스텀도 가능&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 NavLink, Link&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Link 컴포넌트 : &amp;lt;a&amp;gt; 태그 역할을 대체하여 페이지 간 이동을 구현하는 컴포넌트&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783584409705&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Link to=&quot;/&quot;&amp;gt;Home&amp;lt;/Link&amp;gt;&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;NavLink 컴포넌트 : Link의 기능을 확장하여, 현재 URL 경로의 일치 여부에 따라 다양한 스타일 상태를 동적으로 제공하는 특수 컴포넌트&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 현재 주소창의 URL과 to 속성에 지정한 주소가 일치할 경우, 해당 태그에 active 클래스를 자동으로 부여&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;속성이나 children에 콜백 함수 형식을 사용하여 isActive, isPending, isTransitioning 속성 값을 받아 컴포넌트 스타일 커스텀 가능&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783584418024&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;NavLink to=&quot;/&quot;&amp;gt;Home&amp;lt;/NavLink&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783584583420&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;NavLink
  to=&quot;/about&quot;
  className={({ isActive, isPending, isTransitioning }) =&amp;gt;
    [
      isActive ? &quot;isActive&quot; : &quot;&quot;,
      isPending ? &quot;isPending&quot; : &quot;&quot;,
      isTransitioning ? &quot;isTransitioning&quot; : &quot;&quot;
    ].join(&quot; &quot;)
  }
&amp;gt;
  About
&amp;lt;/NavLink&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 &amp;lt;Form&amp;gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Form 컴포넌트 : &amp;lt;form&amp;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;p data-ke-size=&quot;size16&quot;&gt;action 속성을 활용하여 폼이 제출되었을 때 데이터를 전송할 대상 URL 경로를 지정&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783845061361&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/auth/Login.tsx

import { Form, useActionData } from &quot;react-router&quot;;

export default function Login() {
  const data = useActionData();
  return (
    &amp;lt;&amp;gt;
      {/* action에 지정된 주소로 페이지 전환 */}
      &amp;lt;Form action=&quot;/&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; autoComplete=&quot;off&quot; /&amp;gt;
        &amp;lt;input type=&quot;password&quot; name=&quot;pw&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
      &amp;lt;/Form&amp;gt;
      &amp;lt;p&amp;gt;{data &amp;amp;&amp;amp; data.message}&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 navigate&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useNavigate 훅 : 컴포넌트 내부의 자바스크립트 코드 영역에서 프로그래밍 방식으로 주소를 강제 이동시킬 때 사용하는 훅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783845663069&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;navigate(&quot;/&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;ex) navigate(-1) --&amp;gt; 이전 페이지로 뒤로 가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783845549416&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/auth/Login.tsx

import { Form, useActionData, useNavigate } from &quot;react-router&quot;;

export default function Login() {
  const data = useActionData();
  const navigate = useNavigate();
  return (
    &amp;lt;&amp;gt;
      {/* &amp;lt;form&amp;gt;&amp;lt;/form&amp;gt; */}
      &amp;lt;Form method=&quot;post&quot;&amp;gt;
        &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; autoComplete=&quot;off&quot; /&amp;gt;
        &amp;lt;input type=&quot;password&quot; name=&quot;pw&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
      &amp;lt;/Form&amp;gt;
      &amp;lt;p&amp;gt;{data &amp;amp;&amp;amp; data.message}&amp;lt;/p&amp;gt;

      &amp;lt;button onClick={() =&amp;gt; navigate(&quot;/&quot;)}&amp;gt;메인으로 이동&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 loader 속성으로 데이터 패칭&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect 기반 데이터 패칭 (전통적인 방식)&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;컴포넌트 마운트 --&amp;gt; useEffect 실행 --&amp;gt; 비동기 데이터 패칭 --&amp;gt; 상태 업데이트 --&amp;gt; 리렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UX
&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;서버에서 데이터를 받기 전까진 빈 배열이나 스피너가 먼저 노출되는 현상 발생&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;loader 기반 데이터 패칭 (최신 React Router v7 방식)&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;주소 클릭 --&amp;gt; 라우터가 백엔드 데이터 패칭 (loader) 완료할 때까지 대기 --&amp;gt; 데이터 로드 완료 --&amp;gt; 화면 전환 및 컴포넌트 렌더링&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UX
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;주소를 클릭해도 서버에서 데이터를 모두 가져오기 전까지는 화면 전환 X&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;
&lt;pre id=&quot;code_1783846901118&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

import { createBrowserRouter, RouterProvider } from &quot;react-router&quot;;
import Home from &quot;./pages/Home&quot;;
import Post from &quot;./pages/post/Post&quot;;
import Default from &quot;./layouts/Default&quot;;
import PostLoader from &quot;./pages/post/PostLoader&quot;;
import { axiosInstance } from &quot;../api/axios&quot;;

const router = createBrowserRouter([
  {
    Component: Default,
    children: [
      {
        path: &quot;/&quot;,
        Component: Home,
      },
      {
        path: &quot;/post&quot;,
        Component: Post,
      },
      {
        path: &quot;/post-loader&quot;,
        Component: PostLoader,
        loader: async () =&amp;gt; {
          const { data } = await axiosInstance.get(&quot;/posts&quot;);
          return data;
        },
      },
    ],
  },
]);

export default function Router() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;RouterProvider router={router} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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_1783846930447&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/Post.tsx

import { useEffect, useState } from &quot;react&quot;;
import { axiosInstance } from &quot;../../../api/axios&quot;;

export default function Post() {
  const [posts, setPosts] = useState([]);
  useEffect(() =&amp;gt; {
    const fetchPosts = async () =&amp;gt; {
      const { data } = await axiosInstance.get(&quot;/posts&quot;);
      setPosts(data);
    };
    fetchPosts();
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Post Component&amp;lt;/h1&amp;gt;
      &amp;lt;pre&amp;gt;{JSON.stringify(posts, null, 2)}&amp;lt;/pre&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783846951270&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/PostLoader.tsx

import { useLoaderData } from &quot;react-router&quot;;

export default function PostLoader() {
  const posts = useLoaderData();
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;PostLoader Component&amp;lt;/h1&amp;gt;
      &amp;lt;pre&amp;gt;{JSON.stringify(posts, null, 2)}&amp;lt;/pre&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 loader + loading&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loader와 로딩 처리 기법 : lodaer 함수가 서버에서 데이터를 완전히 패칭할 때까지 홤녀이 멈추는 현상을 방지하기 위해 로딩 컴포넌트를 지정하는 방식&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;b&gt;hydrateFallback 속성&lt;/b&gt;에 지정된 컴포넌트 내용이 화면에 먼저 노출됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783848113177&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

{
   path: &quot;/post-loader&quot;,
   Component: PostLoader,
   loader: async () =&amp;gt; {
     const { data } = await axiosInstance.get(&quot;/posts&quot;);
     return data;
   },
   HydrateFallback: PostLoading,
},&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783848150990&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/PostLoading.tsx

export default function PostLoading() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Router v7 loading + error&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;errorElement 속성 : 데이터 패칭이나 컴포넌트 렌더링 도중 에러가 발생했을 때, 화면 전체가 멈추는 것을 방지하기 위해 예외 처리용 커스텀 에러 화면을 지정하는 속성&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;useRouteError 훅 : erroElement로 지정된 컴포넌트 내부에서, 실제로 발생한 구체적인 에러 객체(에러 메세지, 상태 코드 등)를 읽어올 때 사용하는 훅&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783848219279&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/index.tsx

{
  path: &quot;/post-loader&quot;,
  Component: PostLoader,
  loader: async () =&amp;gt; {
    const { data } = await axiosInstance.get(&quot;/posts&quot;);
    return data;
    },
    HydrateFallback: PostLoading,
    errorElement: &amp;lt;PostError /&amp;gt;,
},&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1783848245949&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/routes/pages/post/PostError.tsx

import { isRouteErrorResponse, useRouteError } from &quot;react-router&quot;;

export default function PostError() {
  const error = useRouteError();

  // isRouteErrorResponse : 라우트에서 response 관련된 에러인지 아닌지 판단하는 내장 함수
  if (isRouteErrorResponse(error)) {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;{error.status}&amp;lt;/h1&amp;gt;
        &amp;lt;h1&amp;gt;{error.data.message}&amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  } else if (error instanceof Error) {
    return (
      &amp;lt;div&amp;gt;
        &amp;lt;h1&amp;gt;알 수 없는 에러&amp;lt;/h1&amp;gt;
        &amp;lt;h1&amp;gt;{error.message}&amp;lt;/h1&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  } else {
    return (
      &amp;lt;&amp;gt;
        &amp;lt;h1&amp;gt;에러 발생&amp;lt;/h1&amp;gt;
      &amp;lt;/&amp;gt;
    );
  }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/32</guid>
      <comments>https://fromfavor.tistory.com/32#entry32comment</comments>
      <pubDate>Sun, 12 Jul 2026 18:28:34 +0900</pubDate>
    </item>
    <item>
      <title>리액트 - 모달 구현</title>
      <link>https://fromfavor.tistory.com/31</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;모달(Modal)&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;모달이란?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;모달 : 기존 화면 위에 겹쳐서 나타나는 대화 상자나 팝업 창을 의미 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 모달 vs 팝업&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모달&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모달 창을 닫거나, 내부에서 확인 또는 취소 액션을 하기 전까지는 원래 보던 화면(부모)을 건드릴 수 X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&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;/li&gt;
&lt;li&gt;팝업 창이 떠있어도 사용자는 뒤에 있는 화면을 클릭하거나 스크롤할 수 O&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;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 모달 구현하기&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kD2ZJ/dJMcadie0Tf/keFXfJKKFfZT1Jn862VZ6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kD2ZJ/dJMcadie0Tf/keFXfJKKFfZT1Jn862VZ6K/img.png&quot; data-origin-width=&quot;1555&quot; data-origin-height=&quot;846&quot; data-is-animation=&quot;false&quot; width=&quot;500&quot; height=&quot;272&quot; style=&quot;width: 51.3832%; margin-right: 10px;&quot; data-widthpercent=&quot;51.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kD2ZJ/dJMcadie0Tf/keFXfJKKFfZT1Jn862VZ6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkD2ZJ%2FdJMcadie0Tf%2FkeFXfJKKFfZT1Jn862VZ6K%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;1555&quot; height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3EENe/dJMcaccwucc/i6LNKn6uOWnYDhhgNnGOK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3EENe/dJMcaccwucc/i6LNKn6uOWnYDhhgNnGOK0/img.png&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;843&quot; data-is-animation=&quot;false&quot; width=&quot;500&quot; height=&quot;295&quot; style=&quot;width: 47.454%;&quot; data-widthpercent=&quot;48.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3EENe/dJMcaccwucc/i6LNKn6uOWnYDhhgNnGOK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3EENe%2FdJMcaccwucc%2Fi6LNKn6uOWnYDhhgNnGOK0%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;1431&quot; height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1782369840831&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/App.tsx

import { useState } from &quot;react&quot;;
import Modal from &quot;./components/Modal&quot;;
import ModalContent from &quot;./components/ModalContent&quot;;
import ModalContent2 from &quot;./components/ModalContent2&quot;;

export default function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const toggleModal = () =&amp;gt; setIsModalOpen((prev) =&amp;gt; !prev);

  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen  bg-yellow-200&quot;&amp;gt;
      &amp;lt;button
        className=&quot;border-3 border-blue-500 bg-white px-2 py-1 rounded-lg cursor-pointer&quot;
        onClick={toggleModal}
      &amp;gt;
        모달 열기
      &amp;lt;/button&amp;gt;
      {isModalOpen &amp;amp;&amp;amp; (
        &amp;lt;Modal onClose={toggleModal}&amp;gt;
          &amp;lt;ModalContent /&amp;gt;
          &amp;lt;ModalContent2 /&amp;gt;
        &amp;lt;/Modal&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;const [isModalOpen, setIsModalOpen] = useState(false)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;isModalOpen : 현재 모달의 상태를 담는 변수&amp;nbsp;&lt;/li&gt;
&lt;li&gt;setIsModalOpen : 모달을 켰다 껐다할 수 있는 변경 함수&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const toggleModal = () =&amp;gt; setIsModalOpen((prev) =&amp;gt; !prev)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이전 상태가 false --&amp;gt; !false --&amp;gt; true&lt;/li&gt;
&lt;li&gt;이전 상태가 true --&amp;gt; !true --&amp;gt; false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1782369856325&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Modal.tsx

import type { ComponentPropsWithoutRef } from &quot;react&quot;;
import cancelIcon from &quot;../assets/closeIcon.png&quot;;

interface ModalProps extends ComponentPropsWithoutRef&amp;lt;&quot;div&quot;&amp;gt; {
  onClose: () =&amp;gt; void;
}

export default function Modal({ children, onClose, ...props }: ModalProps) {
  return (
    // 모달 창 바깥 영역
    &amp;lt;div
      role=&quot;presentation&quot;
      className=&quot;fixed inset-0 z-99 flex w-full h-full items-center justify-center bg-gray-900/50 backdrop-blur-sm&quot;
      onClick={onClose}
      {...props}
    &amp;gt;
      {/* 모달 영역 */}
      &amp;lt;section
        role=&quot;dialog&quot;
        className=&quot;relative rounded-xl bg-[#1b1b22] px-5 py-10&quot;
        onClick={(e) =&amp;gt; e.stopPropagation()}
      &amp;gt;
        {/* close 버튼 */}
        &amp;lt;button
          className=&quot;absolute right-4 top-4 w-3 h-3 cursor-pointer&quot;
          onClick={onClose}
        &amp;gt;
          &amp;lt;img
            src={cancleIcon}
            className=&quot;w-full h-full object-contain invert brightness-200&quot;
          /&amp;gt;
        &amp;lt;/button&amp;gt;
        {children}
      &amp;lt;/section&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;extends ComponentPropsWithoutRef&amp;lt;&quot;div&quot;&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ModalProps에는 onClose, children만 존재 --&amp;gt; 모달 컴포넌트 자체에 id, className 등의 속성 사용 불가능&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;lt;div&amp;gt; 태그가 가질 수 있는 모든 속성들을 가져와서 사용하겠다는 의미&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;ㄴ ref(리액트에서 DOM을 직접 가리키는 역할)만 빼고&lt;/li&gt;
&lt;li&gt;if 모달이 아니라 커스텀 버튼을 만드는 중이었다면?) &amp;lt;&quot;button&quot;&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;({ children, onClos, ...props }: ModalProps)
&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;children, onClose만 골라 쓰고 나머지는 props로 받겠다는 의미&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;div role=&quot;presentation&quot; className=&quot;fixed inset-0 z-99&quot; onClick={onClose}&amp;gt;{...props}&amp;lt;/div&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;role=&quot;presentation&quot; : 웹 접근성 표준 속성 --&amp;gt; 시각용 레이어임을 선언&amp;nbsp;&lt;/li&gt;
&lt;li&gt;fixed inset-0 z-99 : 뒤의 일반 본문 글자나 이미지들이 모달 배경 위로 오지 못하도록 설정&amp;nbsp;&lt;/li&gt;
&lt;li&gt;onClick={onClose} : 모달 바깥을 클릭해도 모달 창이 닫힐 수 있도록 설정&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;{...props} : 부모 컴포넌트가 모달을 부를 때 넘겨준 속성 중에서, children과 onClose를 제외한 나머지 속성(id, 추가 className 등)을 spread하여 주입하겠다는 의미&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;section role=&quot;dialog&quot; onClick={(e) =&amp;gt; e.stopPropagation()}&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;role=&quot;dialog&quot; : 웹 접근성 표준 속성 --&amp;gt; 여기서부터는 사용자가 상호작용해야 하는 영역임을 선언&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; onClick={(e) =&amp;gt; e.stopPropagation() : 버블링 방지&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;img className=&quot;object-contain invert brightness-200&quot; alt=&quot;close&quot; /&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;object-contain : 이미지의 가로세로 원본 비율 유지&amp;nbsp;&lt;/li&gt;
&lt;li&gt;invert : 일반 텍스트가 아닌 PNG 이미지 파일이기 때문에 색상을 정반대로 반전&amp;nbsp;&lt;/li&gt;
&lt;li&gt;brightness-200 : 밝기를 200% 증가&amp;nbsp;&lt;/li&gt;
&lt;li&gt;alt=&quot;close&quot; : 웹 접근성 표준 속성&amp;nbsp; --&amp;gt; 이미지의 정체를 알려주는 대체 텍스트&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;{children}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;App 컴포넌트에서 &amp;lt;Modal&amp;gt;&amp;lt;/Modal&amp;gt; 태그 사이에 다른 컴포넌트를 집어넣음&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ㄴ 해당 컴포넌트들을 children으로 묶어 Modal 컴포넌트에게 전달&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>React</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/31</guid>
      <comments>https://fromfavor.tistory.com/31#entry31comment</comments>
      <pubDate>Thu, 25 Jun 2026 16:17:43 +0900</pubDate>
    </item>
    <item>
      <title>섹션 17. 데이터 통신 심화</title>
      <link>https://fromfavor.tistory.com/30</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;useTransition(fetch) 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useTransition : 상태 업데이트를 낮은 우선 순위의 트랜지션으로 처리, 비동기 함수도 처리할 수 있도록 기능이 확장&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;p data-ke-size=&quot;size16&quot;&gt;useTransition 훅의 기본적인 문법 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;const [isPending, startTransition] = useTransition()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isPending
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;실행 중인 트랜지션이 있으면 true, 없으면 false&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;startTransition
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;코드를 트랜지션으로 실행할 수 있게 해주는 함수&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;== 해당 함수로 작성한 코드는 낮은 우선순위로 처리 (여유가 있을 때 처리)&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useTransition + 데이터 패칭 실전&lt;/h3&gt;
&lt;pre id=&quot;code_1781776237754&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import axios from &quot;axios&quot;;
import { useEffect, useState, useTransition } from &quot;react&quot;;

interface Post {
  id: number;
  title: string;
  views: number;
}

export default function App() {
  const [posts, setPosts] = useState&amp;lt;Post[]&amp;gt;([]);
  const [isPending, startTransition] = useTransition();

  useEffect(() =&amp;gt; {
    startTransition(async () =&amp;gt; {
      const { data } = await axios.get(&quot;http://localhost:3000/posts&quot;);
      setPosts(data);
    });
  }, []);

  if (isPending) return &amp;lt;h3&amp;gt;loading...&amp;lt;/h3&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;useTransition&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {posts.map((post) =&amp;gt; (
          &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ isPending : true == 데이터 패칭 중&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;useActionState 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 React와 같은 클라이언트 기반 라이브러리에서는 form 태그의 action 속성을 직접적으로 사용하는 경우가 거의 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;∵ action은 서버측에서 처리되는 기능이기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ action 속성을 활용하는 새로운 방식 등장&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;useActionState : form의 액션 결과를 기반으로 상태를 업데이트 할 수 있도록 도와줌&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;useActionState 훅의 기본적인 문법 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;const [state, formAction, isPending] = useActionState(fn, initialState)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state
&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;formAction
&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;action 속성에 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;isPending
&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;fn
&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;비동기 함수도 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;initialState
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useActionState 실습 - 1&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 form 태그는 제출 버튼을 누르면, 브라우저가 페이지를 새로고침하면서 데이터를 서버로 전송&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;useActionState를 사용하여 &amp;lt;form action={formAction}&amp;gt;처럼 action 속성에 자바스크립트 함수를 바로 대입 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; 제출 버튼을 누르면, &lt;span style=&quot;color: #ee2323;&quot;&gt;브라우저가 새로고침을 일으키지 못하도록 막은 후 주입한 formAction 함수를 비동기적으로 실행해 서버와 데이터를 주고받음&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781776989319&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { useActionState } from &quot;react&quot;;

export default function App() {
  const [count, formAction, isPending] = useActionState(async (count) =&amp;gt; {
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, 2000));
    return count + 1;
  }, 0);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;form action={formAction}&amp;gt;
        &amp;lt;h1&amp;gt;Count: {count}&amp;lt;/h1&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;증가&amp;lt;/button&amp;gt;
        {isPending &amp;amp;&amp;amp; &amp;lt;p&amp;gt;제출 중...&amp;lt;/p&amp;gt;}
      &amp;lt;/form&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useActionState 실습 - 2&lt;/h3&gt;
&lt;pre id=&quot;code_1781779220910&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { useActionState } from &quot;react&quot;;

export default function App() {
  const [count, formAction, isPending] = useActionState(
    async (count: number, formData: FormData) =&amp;gt; {
      await new Promise((resolve) =&amp;gt; setTimeout(resolve, 2000));
      const amount = Number(formData.get(&quot;amount&quot;));

      return count + amount;
    },
    0,
  );

  return (
    &amp;lt;&amp;gt;
      &amp;lt;form action={formAction}&amp;gt;
        &amp;lt;h1&amp;gt;Count: {count}&amp;lt;/h1&amp;gt;
        &amp;lt;input type=&quot;number&quot; name=&quot;amount&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot; disabled={isPending}&amp;gt;
          증가
        &amp;lt;/button&amp;gt;
        {isPending &amp;amp;&amp;amp; &amp;lt;p&amp;gt;제출 중...&amp;lt;/p&amp;gt;}
      &amp;lt;/form&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ disabled={isPending} 트랜지션을 처리 중일 때는 해당 버튼을 비활성화&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;form action&lt;/h3&gt;
&lt;pre id=&quot;code_1781779422192&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form action={async() =&amp;gt; {...}}&amp;gt;
  ...
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 액션 속성에 함수만 직접 할당해서 사용하는 방법도 존재&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ useActionState과 달리 &lt;span style=&quot;color: #ee2323;&quot;&gt;자동으로 상태값이나 isPending과 같은 로딩 상태가 제공되지 않음&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;React 18에서의 form 컨트롤 방법&lt;/p&gt;
&lt;pre id=&quot;code_1781779788981&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { useState } from &quot;react&quot;;

export default function App() {
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const email = formData.get(&quot;email&quot;);
    const pw = formData.get(&quot;pw&quot;);

    // api
    setIsLoading(true);
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, 2000));
    setIsLoading(false);
    console.log(`Login Success ${email}/${pw}`);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;form onSubmit={handleSubmit}&amp;gt;
        &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; autoComplete=&quot;off&quot; /&amp;gt;
        &amp;lt;input type=&quot;password&quot; name=&quot;pw&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot; disabled={isLoading}&amp;gt;
          로그인
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;React 19 이상에서의 form 컨트롤 방법&lt;/p&gt;
&lt;pre id=&quot;code_1781780066432&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { useState } from &quot;react&quot;;

export default function App() {
  const [isLoading, setIsLoading] = useState(false);

  const handleSubmit = async (formData: FormData) =&amp;gt; {
    const email = formData.get(&quot;email&quot;);
    const pw = formData.get(&quot;pw&quot;);

    // api
    setIsLoading(true);
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, 2000));
    setIsLoading(false);
    console.log(`Login Success ${email}/${pw}`);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;form action={handleSubmit}&amp;gt;
        &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; autoComplete=&quot;off&quot; /&amp;gt;
        &amp;lt;input type=&quot;password&quot; name=&quot;pw&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot; disabled={isLoading}&amp;gt;
          로그인
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ action 속성에 함수 직접 할당 가능&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;color: #ee2323;&quot;&gt;* useActionState 훅 방식 : 상태 및 진행 상태까지 관리하고 싶을 때 사용,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;action 속성에 함수 할당 방식 : 단순한 작업을 빠르게 처리하고 싶을 때 사용&lt;/span&gt;&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;useFormStatus&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useFormStatus : 컴포넌트가 분리되어 있는 상황에서도 &lt;span style=&quot;color: #ee2323;&quot;&gt;상위 form에 대한 상태 정보를 가져올 수 있는 리액트 훅&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;useFormStatus 훅의 기본적인 문법 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;const { pending, data, method, action } = useFormStatus()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pending
&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;data
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;formData 객체 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;method
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GET, POST&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;action
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cT48EO/dJMcad3rkMm/YszYL5Sc1ztw4mjhvFDtTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cT48EO/dJMcad3rkMm/YszYL5Sc1ztw4mjhvFDtTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT48EO/dJMcad3rkMm/YszYL5Sc1ztw4mjhvFDtTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcT48EO%2FdJMcad3rkMm%2FYszYL5Sc1ztw4mjhvFDtTK%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;1312&quot; height=&quot;234&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byY64W/dJMcabYXkzs/kK3Qvwyp8PTlvEISlww7y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byY64W/dJMcabYXkzs/kK3Qvwyp8PTlvEISlww7y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byY64W/dJMcabYXkzs/kK3Qvwyp8PTlvEISlww7y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyY64W%2FdJMcabYXkzs%2FkK3Qvwyp8PTlvEISlww7y0%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;697&quot; height=&quot;97&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781782423315&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Button from &quot;./components/html/Button&quot;;
import Input from &quot;./components/html/Input&quot;;

export default function App() {
  const formAction = async (formData: FormData) =&amp;gt; {
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, 2000));
    console.log(formData.get(&quot;email&quot;));
    console.log(formData.get(&quot;pw&quot;));
  };
  return (
    &amp;lt;&amp;gt;
      &amp;lt;form action={formAction}&amp;gt;
        &amp;lt;Input type=&quot;text&quot; name=&quot;email&quot; autoComplete=&quot;off&quot; /&amp;gt;
        &amp;lt;Input type=&quot;password&quot; name=&quot;pw&quot; /&amp;gt;
        &amp;lt;Button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/Button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781782443130&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/html/Input.tsx

type InputProps = React.ComponentPropsWithoutRef&amp;lt;&quot;input&quot;&amp;gt;;
export default function Input(props: InputProps) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;input {...props} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781782468780&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/html/Input.tsx

import { useFormStatus } from &quot;react-dom&quot;;

type ButtonProps = React.ComponentPropsWithoutRef&amp;lt;&quot;button&quot;&amp;gt;;

export default function Button({ children, ...props }: ButtonProps) {
  const { pending, data, method, action } = useFormStatus();
  console.log(pending);
  console.log(data);
  console.log(method);
  console.log(action);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button {...props} disabled={pending}&amp;gt;
        {children}
      &amp;lt;/button&amp;gt;
      {data &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Logging in with ID: {String(data.get(&quot;email&quot;))}&amp;lt;/p&amp;gt;}
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useOptimistic&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 업데이트 : 서버의 응답을 기다리지 않고 먼저 사용자 인터페이스(UI)를 업데이트하는 기법&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 서버 요청이 성공할 것이라고 가정하고 UI를 먼저 변경 --&amp;gt; 서버에서 요청이 실패되면 UI를 다시 원래 상태로 되돌리는 Rollback 로직이 필요&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;useOptimistic : 낙관적 업데이트를 쉽게 구현할 수 있도록 설계된 훅&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;useOptimistic 훅의 기본적인 문법 형태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;const [optimisticState, addOptimistic] = useOptimistic(state, (currentState, optimistic) =&amp;gt; { ... })&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;optimisticState&lt;br /&gt;
&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;addOptimistic
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;낙관적 업데이트를 실행할 때 호출하는 함수&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;state
&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;(currentState, optimistic) =&amp;gt; { ... }
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;낙관적 업데이트 시 상태를 어떻게 바꿀지 정의하는 함수&amp;nbsp;&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useOptimistic 실전 - 1&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useOptimistic 미사용 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781887227079&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import axios from &quot;axios&quot;;
import { Heart } from &quot;lucide-react&quot;;
import { useEffect, useState } from &quot;react&quot;;

interface Posts {
  id: number;
  isLike: boolean;
}

export default function App() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);

  const updateLike = async (id: number, isLike: boolean) =&amp;gt; {
    const { data } = await axios.patch(`http://localhost:3000/posts/${id}`, {
      isLike: !isLike,
    });

    setPosts((posts) =&amp;gt;
      posts.map((post) =&amp;gt; (post.id === data.id ? data : post)),
    );
    console.log(data);
  };

  useEffect(() =&amp;gt; {
    const fetchPosts = async () =&amp;gt; {
      try {
        setIsLoading(true);
        const { data } = await axios.get(&quot;http://localhost:3000/posts&quot;);
        setPosts(data);
      } catch (e) {
        console.error(&quot;에러 발생&quot; + e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchPosts();
  }, []);

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      {/* fill: 'none', stroke: 'currentColor' */}
      {/* fill: 'rgb(255,0,0)', stroke: 'rgb(255,0,0)' */}
      {posts.map((post) =&amp;gt; (
        &amp;lt;Heart
          key={post.id}
          fill={post.isLike ? &quot;rgb(255, 0, 0)&quot; : &quot;none&quot;}
          stroke={post.isLike ? &quot;rgb(255, 0, 0)&quot; : &quot;currentColor&quot;}
          onClick={() =&amp;gt; updateLike(post.id, post.isLike)}
        /&amp;gt;
      ))}
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useOptimistic 실전 - 2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useOptimistic 사용 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781887959807&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import axios from &quot;axios&quot;;
import { Heart } from &quot;lucide-react&quot;;
import {
  startTransition,
  useEffect,
  useOptimistic,
  useRef,
  useState,
} from &quot;react&quot;;

interface Posts {
  id: number;
  isLike: boolean;
}

export default function App() {
  const controller = useRef&amp;lt;AbortController | null&amp;gt;(null);
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);

  const [optimisticPosts, addOptimisticPosts] = useOptimistic(
    posts,
    (statePosts, id) =&amp;gt; {
      return statePosts.map((statePost) =&amp;gt;
        statePost.id === id
          ? { ...statePost, isLike: !statePost.isLike }
          : statePost,
      );
    },
  );

  const updateLike = (id: number, isLike: boolean) =&amp;gt; {
    controller.current?.abort();
    controller.current = new AbortController();
    startTransition(async () =&amp;gt; {
      addOptimisticPosts(id);

      try {
        const { data } = await axios.patch(
          `http://localhost:3000/posts/${id}`,
          {
            isLike: !isLike,
          },
          {
            signal: controller.current?.signal,
          },
        );
        setPosts((posts) =&amp;gt;
          posts.map((post) =&amp;gt; (post.id === data.id ? data : post)),
        );
      } catch (e) {
        console.error(&quot;에러:&quot;, e);
      }
    });
  };

  useEffect(() =&amp;gt; {
    const fetchPosts = async () =&amp;gt; {
      try {
        setIsLoading(true);
        const { data } = await axios.get(&quot;http://localhost:3000/posts&quot;);
        setPosts(data);
      } catch (e) {
        console.error(&quot;에러 발생&quot; + e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchPosts();
  }, []);

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      {/* fill: 'none', stroke: 'currentColor' */}
      {/* fill: 'rgb(255,0,0)', stroke: 'rgb(255,0,0)' */}
      {optimisticPosts.map((post) =&amp;gt; (
        &amp;lt;Heart
          key={post.id}
          fill={post.isLike ? &quot;rgb(255, 0, 0)&quot; : &quot;none&quot;}
          stroke={post.isLike ? &quot;rgb(255, 0, 0)&quot; : &quot;currentColor&quot;}
          onClick={() =&amp;gt; updateLike(post.id, post.isLike)}
        /&amp;gt;
      ))}
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;use + suspense&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;use 훅 : Promise 객체를 동기 함수처럼 사용할 수 있게 하여 데이터 통신을 보다 간결하게 처리할 수 있도록 하는 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ Suspense 컴포넌트와 함께 사용&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;Suspense 컴포넌트 : 컴포넌트에서 비동기 처리가 완료될 때까지 대기하게 하는 동안, fallback 속성에 지정된 UI를 표시하는 역할&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;pre id=&quot;code_1782099043731&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Suspense } from &quot;react&quot;;
import ChildComponent from &quot;./components/ChildComponent&quot;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Suspense fallback={...}&amp;gt;
        &amp;lt;ChildComponent promise={...} /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ fallback 속성을 사용해 로딩 UI를 지정 / promise 객체를 하위 컴포넌트에 전달&lt;/p&gt;
&lt;pre id=&quot;code_1782099213960&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function ChildComponent({
  promise,
}: {
  promise: Promise&amp;lt;unknown&amp;gt;;
}) {
  const data = use(promise);
  return (...);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ promise 객체를 활용하여 use 훅으로 비동기가 처리될 때까지 대기&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;use + suspense 실전&lt;/h3&gt;
&lt;pre id=&quot;code_1782099911618&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { Suspense } from &quot;react&quot;;
import { axiosInstance } from &quot;./api/axios&quot;;
import Posts from &quot;./components/Posts&quot;;

// Promise 객체는 상태 업데이트를 직접적으로 활용하지 않기 때문에 컴포넌트 내부에 정의할 필요 X
async function fetchPosts() {
  const { data } = await axiosInstance.get(&quot;/posts&quot;);

  return data; // Promise 객체
}

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;gt;
        &amp;lt;Posts promise={fetchPosts()} /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782099930661&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Posts.tsx

import { use } from &quot;react&quot;;

export default function Posts({
  promise,
}: {
  promise: Promise&amp;lt;{ id: number; title: string; views: number }[]&amp;gt;;
}) {
  const posts = use(promise);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;pre&amp;gt;{JSON.stringify(posts, null, 2)}&amp;lt;/pre&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;error boundary&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;use 훅과 Suspense를 조합해서 사용할 때, 데이터 요청에 실패하는 경우 앱 전체가 종료되는 crash 현상이 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; 에러 핸들링을 위해 error boundary 사용&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;error boundary : 자바스크립트 오류를 처리하기 위한 컴포넌트&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;&lt;b&gt;npm install react-error-boundary&lt;/b&gt; 명령어를 사용하여 다운로드&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782100745194&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { Suspense } from &quot;react&quot;;
import { axiosInstance } from &quot;./api/axios&quot;;
import Posts from &quot;./components/Posts&quot;;
import { ErrorBoundary } from &quot;react-error-boundary&quot;;

async function fetchPosts() {
  const { data } = await axiosInstance.get(&quot;/posts222&quot;);
  return data; // Promise 객체
}
export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;ErrorBoundary fallback={&amp;lt;p&amp;gt;Error!&amp;lt;/p&amp;gt;}&amp;gt;
        &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;Loading..&amp;lt;/p&amp;gt;}&amp;gt;
          &amp;lt;Posts promise={fetchPosts()} /&amp;gt;
        &amp;lt;/Suspense&amp;gt;
      &amp;lt;/ErrorBoundary&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;^ 에러가 생기면 crash 현상이 발생하는 게 아니라 fallback 속성의 JSX 요소를 통해 UI 렌더링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSX 요소가 아닌 컴포넌트 자체를 렌더링하고 싶을 때에는 FallbackComponent 속성을 사용&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;tip&lt;/h3&gt;
&lt;pre id=&quot;code_1782101318642&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { Suspense } from &quot;react&quot;;
import { axiosInstance } from &quot;./api/axios&quot;;
import Posts from &quot;./components/Posts&quot;;
import { ErrorBoundary } from &quot;react-error-boundary&quot;;
import Loading from &quot;./components/Loading&quot;;

async function fetchPosts() {
  const { data } = await axiosInstance.get(&quot;/posts&quot;);
  return data; // Promise 객체
}
export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;ErrorBoundary fallback={&amp;lt;p&amp;gt;Error!&amp;lt;/p&amp;gt;}&amp;gt;
        &amp;lt;Suspense fallback={&amp;lt;Loading /&amp;gt;}&amp;gt;
          &amp;lt;Posts promise={fetchPosts()} /&amp;gt;
        &amp;lt;/Suspense&amp;gt;
      &amp;lt;/ErrorBoundary&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782101342866&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Loading.tsx

import { useEffect, useState } from &quot;react&quot;;

export default function Loading() {
  const [isShow, setIsShow] = useState(false);

  useEffect(() =&amp;gt; {
    const timer = setTimeout(() =&amp;gt; {
      setIsShow(true);
    }, 500); // 0.5초 뒤에 setIsShow를 true로 변경
    return () =&amp;gt; clearTimeout(timer);
  }, []);

  return &amp;lt;&amp;gt;{isShow &amp;amp;&amp;amp; &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;}&amp;lt;/&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^&amp;nbsp;npm run server로 서버 구동 시 (0.5초 미만) 로딩 화면 렌더링 X / npm run delay로 서버 구동 시 (0.5초 이상) 로딩 화면 렌더링 O&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;tmdb 회원가입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TMDB : 무료로 사용할 수 있는 영화 API&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;영화 목록 렌더링 - 1&lt;/h3&gt;
&lt;pre id=&quot;code_1782103701130&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/types/movie.d.ts

type MovieType = {
  adult: boolean;
  backdrop_path: string;
  genre_ids: number[];
  id: number;
  original_language: string;
  original_title: string;
  overview: string;
  popularity: number;
  poster_path: string;
  release_date: string;
  title: string;
  video: boolean;
  vote_average: number;
  vote_count: number;
};&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_1782103669150&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Movie from &quot;./components/movies/Movie&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Movie /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782103726639&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/Movie.tsx

import { useEffect, useState } from &quot;react&quot;;
import MovieHeader from &quot;./MovieHeader&quot;;
import MovieList from &quot;./MovieList&quot;;
import MovieMain from &quot;./MovieMain&quot;;
import axios from &quot;axios&quot;;
import { axiosInstance } from &quot;../../api/axios&quot;;

export default function Movie() {
  const [nowData, setNowData] = useState&amp;lt;MovieType[]&amp;gt;([]); // Now Playing API를 호출한 결과(results) 배열을 할당
  const [nowLoading, setNowLoading] = useState(false);
  const [nowError, setNowError] = useState&amp;lt;Error | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();
    const { signal } = controller;
    const fetchCategory = async (
      endPoint: string,
      setData: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;MovieType[]&amp;gt;&amp;gt;,
      setLoading: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;boolean&amp;gt;&amp;gt;,
      setError: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;Error | null&amp;gt;&amp;gt;,
    ) =&amp;gt; {
      setLoading(true);
      setError(null);

      try {
        const {
          data: { results },
        } = await axiosInstance.get(`/${endPoint}`, {
          signal,
        });
        setData(results);
        console.log(results);
      } catch (e) {
        console.log(e);
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;CanceledError&quot;) setError(e);
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    };
    fetchCategory(&quot;now_playing&quot;, setNowData, setNowLoading, setNowError);

    return () =&amp;gt; controller.abort();
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MovieHeader /&amp;gt;
      &amp;lt;MovieMain /&amp;gt;
      &amp;lt;MovieList
        title=&quot;Now Playing&quot;
        movies={nowData}
        loading={nowLoading}
        error={nowError}
      /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782103754519&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/MovieList.tsx

import MovieListItem from &quot;./MovieListItem&quot;;

export default function MovieList({
  title,
  movies,
  loading,
  error,
}: {
  title: string;
  movies: MovieType[];
  loading: boolean;
  error: Error | null;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;article className=&quot;bg-black px-4 pt-4 xs:px-0&quot;&amp;gt;
        &amp;lt;section className=&quot;container mx-auto py-8 text-white&quot;&amp;gt;
          &amp;lt;span className=&quot;text-yellow-600&quot;&amp;gt;ONLINE STREAMING&amp;lt;/span&amp;gt;
          &amp;lt;h2 className=&quot;text-[36px] font-bold mb-8&quot;&amp;gt;{title}&amp;lt;/h2&amp;gt;
          &amp;lt;div className=&quot;grid grid-cols-2 md:grid-cols-4 gap-6 sm:px-0&quot;&amp;gt;
            {/* 아이템 1개 */}
            {movies &amp;amp;&amp;amp;
              movies.map((movie) =&amp;gt; (
                &amp;lt;MovieListItem key={movie.id} {...movie} /&amp;gt;
              ))}
          &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
      &amp;lt;/article&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782103797964&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/MovieListItem.tsx

import { star } from &quot;../../assets/movies/assets&quot;;

export default function MovieListItem({
  title,
  vote_average,
  release_date,
  poster_path,
}: MovieType) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;img
          src={`https://image.tmdb.org/t/p/w500${poster_path}`}
          alt=&quot;&quot;
          className=&quot;rounded-md w-full&quot;
        /&amp;gt;
        &amp;lt;div className=&quot;flex justify-between items-center font-bold mt-4 mb-2 text-lg&quot;&amp;gt;
          &amp;lt;h4 className=&quot;line-clamp-1&quot;&amp;gt;{title}&amp;lt;/h4&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;flex justify-between items-center text-sm text-gray-200&quot;&amp;gt;
          &amp;lt;div className=&quot;flex items-center gap-2 font-bold&quot;&amp;gt;
            &amp;lt;img
              src={star}
              alt=&quot;star&quot;
              width={18}
              height={18}
              className=&quot;object-contain&quot;
            /&amp;gt;
            &amp;lt;span className=&quot;text-yellow-500&quot;&amp;gt;{vote_average.toFixed(1)}&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;span className=&quot;text-yellow-500 font-bold&quot;&amp;gt;{release_date}&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영화 목록 렌더링 - 2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로딩, 에러 UI 처리&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782104393131&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/Movie.tsx

import { useEffect, useState } from &quot;react&quot;;
import MovieHeader from &quot;./MovieHeader&quot;;
import MovieList from &quot;./MovieList&quot;;
import MovieMain from &quot;./MovieMain&quot;;
import { axiosInstance } from &quot;../../api/axios&quot;;

export default function Movie() {
  const [nowData, setNowData] = useState&amp;lt;MovieType[]&amp;gt;([]); // Now Playing API를 호출한 결과(results) 배열을 할당
  const [nowLoading, setNowLoading] = useState(false);
  const [nowError, setNowError] = useState&amp;lt;Error | null&amp;gt;(null);

  const [popData, setPopData] = useState&amp;lt;MovieType[]&amp;gt;([]); // Popular API를 호출한 결과(results) 배열을 할당
  const [popLoading, setPopLoading] = useState(false);
  const [popError, setPopError] = useState&amp;lt;Error | null&amp;gt;(null);

  const [topData, setTopData] = useState&amp;lt;MovieType[]&amp;gt;([]); // Top Rated API를 호출한 결과(results) 배열을 할당
  const [topLoading, setTopLoading] = useState(false);
  const [topError, setTopError] = useState&amp;lt;Error | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();
    const { signal } = controller;
    const fetchCategory = async (
      endPoint: string,
      setData: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;MovieType[]&amp;gt;&amp;gt;,
      setLoading: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;boolean&amp;gt;&amp;gt;,
      setError: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;Error | null&amp;gt;&amp;gt;,
    ) =&amp;gt; {
      setLoading(true);
      setError(null);

      await new Promise((resolve) =&amp;gt;
        setTimeout(
          resolve,
          [3000, 4000, 5000, 6000, 7000][Math.floor(Math.random() * 5)],
        ),
      );
      try {
        const {
          data: { results },
        } = await axiosInstance.get(`/${endPoint}`, {
          signal,
        });
        setData(results);
        console.log(results);
      } catch (e) {
        console.log(e);
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;CanceledError&quot;) setError(e);
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    };
    fetchCategory(&quot;now_playing&quot;, setNowData, setNowLoading, setNowError);
    fetchCategory(&quot;popular&quot;, setPopData, setPopLoading, setPopError);
    fetchCategory(&quot;top_rated&quot;, setTopData, setTopLoading, setTopError);

    return () =&amp;gt; controller.abort();
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;MovieHeader /&amp;gt;
      &amp;lt;MovieMain /&amp;gt;
      &amp;lt;MovieList
        title=&quot;Now Playing&quot;
        movies={nowData}
        loading={nowLoading}
        error={nowError}
      /&amp;gt;
      &amp;lt;MovieList
        title=&quot;Popular&quot;
        movies={popData}
        loading={popLoading}
        error={popError}
      /&amp;gt;
      &amp;lt;MovieList
        title=&quot;Top Rated&quot;
        movies={topData}
        loading={topLoading}
        error={topError}
      /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782104412016&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/MovieList.tsx

import MovieError from &quot;./MovieError&quot;;
import MovieListItem from &quot;./MovieListItem&quot;;
import MovieLoader from &quot;./MovieLoader&quot;;

export default function MovieList({
  title,
  movies,
  loading,
  error,
}: {
  title: string;
  movies: MovieType[];
  loading: boolean;
  error: Error | null;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;article className=&quot;bg-black px-4 pt-4 xs:px-0&quot;&amp;gt;
        &amp;lt;section className=&quot;container mx-auto py-8 text-white&quot;&amp;gt;
          &amp;lt;span className=&quot;text-yellow-600&quot;&amp;gt;ONLINE STREAMING&amp;lt;/span&amp;gt;
          &amp;lt;h2 className=&quot;text-[36px] font-bold mb-8&quot;&amp;gt;{title}&amp;lt;/h2&amp;gt;
          &amp;lt;div className=&quot;grid grid-cols-2 md:grid-cols-4 gap-6 sm:px-0&quot;&amp;gt;
            {/* 아이템 1개 */}
            {movies &amp;amp;&amp;amp;
              movies.map((movie) =&amp;gt; (
                &amp;lt;MovieListItem key={movie.id} {...movie} /&amp;gt;
              ))}

            {/* loading... */}
            {loading &amp;amp;&amp;amp; &amp;lt;MovieLoader /&amp;gt;}

            {/* error... */}
            {error &amp;amp;&amp;amp; &amp;lt;MovieError error={error} /&amp;gt;}
          &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
      &amp;lt;/article&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무한 스크롤링&lt;/h3&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;gt; 스크롤이 바닥에 닿았는지 확인하는 방법이 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ react-intersection-observer 라이브러리 활용&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;b&gt;npm i react-intersection-observer&lt;/b&gt; 명령어를 사용하여 다운로드&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782105138660&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/Movie.tsx

import { useEffect, useState } from &quot;react&quot;;
import MovieHeader from &quot;./MovieHeader&quot;;
import MovieList from &quot;./MovieList&quot;;
import MovieMain from &quot;./MovieMain&quot;;
import { axiosInstance } from &quot;../../api/axios&quot;;
import { useInView } from &quot;react-intersection-observer&quot;;

export default function Movie() {
  const [nowData, setNowData] = useState&amp;lt;MovieType[]&amp;gt;([]);
  const [nowLoading, setNowLoading] = useState(false);
  const [nowError, setNowError] = useState&amp;lt;Error | null&amp;gt;(null);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const { ref } = useInView({
    threshold: 0.5, // 지정한 div 태그가 화면에 50% 이상 드러나면 바닥에 도달했다고 판단
    rootMargin: &quot;200px&quot;, // 스크롤이 맨 밑바닥에 닿기 200px 직전에 미리 감지
    onChange: (inView: boolean) =&amp;gt; { 
      if (inView &amp;amp;&amp;amp; !nowLoading &amp;amp;&amp;amp; hasMore) {
        setPage((page) =&amp;gt; page + 1); // 다음 페이지 
      }
    },
  });

  useEffect(() =&amp;gt; {
    const controller = new AbortController();
    const { signal } = controller;
    const fetchCategory = async (
      endpoint: string,
      setData: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;MovieType[]&amp;gt;&amp;gt;,
      setLoading: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;boolean&amp;gt;&amp;gt;,
      setError: React.Dispatch&amp;lt;React.SetStateAction&amp;lt;Error | null&amp;gt;&amp;gt;,
    ) =&amp;gt; {
      setLoading(true);
      setError(null);

      await new Promise((resolve) =&amp;gt;
        setTimeout(
          resolve,
          [3000, 4000, 5000, 6000, 7000][Math.floor(Math.random() * 5)],
        ),
      );
      try {
        const {
          data: { results, total_pages },
        } = await axiosInstance.get(`/${endpoint}?page=${page}`, {
          signal,
        });
        setHasMore(page &amp;lt; total_pages);
        if (page === 1) setData(results);
        else setData((data) =&amp;gt; [...data, ...results]);
      } catch (e) {
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;CanceledError&quot;) setError(e);
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    };
    fetchCategory(&quot;now_playing&quot;, setNowData, setNowLoading, setNowError);

    return () =&amp;gt; controller.abort();
  }, [page]);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;MovieHeader /&amp;gt;
      &amp;lt;MovieMain /&amp;gt;
      &amp;lt;MovieList
        title=&quot;Now Playing&quot;
        movies={nowData}
        loading={nowLoading}
        error={nowError}
      /&amp;gt;
      &amp;lt;div ref={ref}&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;Suspense + Error Boundary + use&lt;/h3&gt;
&lt;pre id=&quot;code_1782107495421&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/movieStore.ts

import { create } from &quot;zustand&quot;;

type MovieStore = {
  page: number;
  setPage: (page: number) =&amp;gt; void;
};

export const useMovieStore = create&amp;lt;MovieStore&amp;gt;()((set) =&amp;gt; ({
  page: 1,
  setPage: (page: number) =&amp;gt; set({ page }),
}));&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_1782107424171&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Movie from &quot;./components/movies/Movie&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Movie /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782107442974&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/Movie.tsx

import { Suspense, useMemo } from &quot;react&quot;;
import { axiosInstance } from &quot;../../api/axios&quot;;
import MovieHeader from &quot;./MovieHeader&quot;;
import MovieList from &quot;./MovieList&quot;;
import MovieMain from &quot;./MovieMain&quot;;
import MovieLoader from &quot;./MovieLoader&quot;;
import { ErrorBoundary } from &quot;react-error-boundary&quot;;
import MovieError from &quot;./MovieError&quot;;
import { useMovieStore } from &quot;../../store/movieStore&quot;;

async function fetchCategory(endPoint: string, page: number) {
  await new Promise((resolve) =&amp;gt;
    setTimeout(
      resolve,
      [3000, 4000, 5000, 6000, 7000][Math.floor(Math.random() * 5)],
    ),
  );
  const { data } = await axiosInstance.get(`${endPoint}?page=${page}`);

  return data;
}

export default function Movie() {
  const page = useMovieStore((state) =&amp;gt; state.page);
  const fetchCategoryMemo = useMemo(
    () =&amp;gt; fetchCategory(&quot;now_playing&quot;, page),
    [page],
  );
  return (
    &amp;lt;&amp;gt;
      &amp;lt;MovieHeader /&amp;gt;
      &amp;lt;MovieMain /&amp;gt;
      &amp;lt;ErrorBoundary fallback={&amp;lt;MovieError title=&quot;Now Playing&quot; /&amp;gt;}&amp;gt;
        &amp;lt;Suspense fallback={&amp;lt;MovieLoader title=&quot;Now Playing&quot; /&amp;gt;}&amp;gt;
          &amp;lt;MovieList title=&quot;Now Playing&quot; promise={fetchCategoryMemo} /&amp;gt;
        &amp;lt;/Suspense&amp;gt;
      &amp;lt;/ErrorBoundary&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782107544045&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/MovieList.tsx

import { use } from &quot;react&quot;;
import MovieListItem from &quot;./MovieListItem&quot;;
import { useMovieStore } from &quot;../../store/movieStore&quot;;

export default function MovieList({
  title,
  promise,
}: {
  title: string;
  promise: Promise&amp;lt;{ results: MovieType[]; total_pages: number }&amp;gt;;
}) {
  const { results: movies, total_pages } = use(promise);
  const page = useMovieStore((state) =&amp;gt; state.page);
  const setPage = useMovieStore((state) =&amp;gt; state.setPage);
  const pageUp = () =&amp;gt; {
    const currentPage = Math.min(page + 1, total_pages);
    setPage(currentPage);
  };
  const pageDonw = () =&amp;gt; {
    const currentPage = Math.max(1, page - 1);
    setPage(currentPage);
  };
  return (
    &amp;lt;&amp;gt;
      &amp;lt;article className=&quot;bg-black px-4 pt-4 xs:px-0&quot;&amp;gt;
        &amp;lt;section className=&quot;container mx-auto py-8 text-white&quot;&amp;gt;
          &amp;lt;span className=&quot;text-yellow-600&quot;&amp;gt;ONLINE STREAMING&amp;lt;/span&amp;gt;
          &amp;lt;h2 className=&quot;text-[36px] font-bold mb-8&quot;&amp;gt;{title}&amp;lt;/h2&amp;gt;
          &amp;lt;div className=&quot;grid grid-cols-2 md:grid-cols-4 gap-6 sm:px-0&quot;&amp;gt;
            {/* 아이템 1개 */}
            {movies &amp;amp;&amp;amp;
              movies.map((movie) =&amp;gt; (
                &amp;lt;MovieListItem key={movie.id} {...movie} /&amp;gt;
              ))}
          &amp;lt;/div&amp;gt;
          {/* 이전, 다음 버튼 */}
          &amp;lt;div className=&quot;grid grid-cols-2 gap-4 my-8&quot;&amp;gt;
            &amp;lt;button
              className=&quot;flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors&quot;
              onClick={pageDonw}
            &amp;gt;
              Prev
            &amp;lt;/button&amp;gt;
            &amp;lt;button
              className=&quot;flex items-center justify-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors&quot;
              onClick={pageUp}
            &amp;gt;
              Next
            &amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
      &amp;lt;/article&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1782107573049&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/movies/MovieListItem.tsx

import { star } from &quot;../../assets/movies/assets&quot;;

export default function MovieListItem({
  poster_path,
  release_date,
  title,
  vote_average,
}: MovieType) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;img
          src={`https://image.tmdb.org/t/p/w500${poster_path}`}
          alt=&quot;&quot;
          className=&quot;rounded-md w-full&quot;
        /&amp;gt;
        &amp;lt;div className=&quot;flex justify-between items-center font-bold mt-4 mb-2 text-lg&quot;&amp;gt;
          &amp;lt;h4 className=&quot;line-clamp-1&quot;&amp;gt;{title}&amp;lt;/h4&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;flex justify-between items-center text-sm text-gray-200&quot;&amp;gt;
          &amp;lt;div className=&quot;flex items-center gap-2 font-bold&quot;&amp;gt;
            &amp;lt;img
              src={star}
              alt=&quot;star&quot;
              width={18}
              height={18}
              className=&quot;object-contain&quot;
            /&amp;gt;
            &amp;lt;span className=&quot;text-yellow-500&quot;&amp;gt;{vote_average.toFixed(1)}&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;span className=&quot;text-yellow-500 font-bold&quot;&amp;gt;{release_date}&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/30</guid>
      <comments>https://fromfavor.tistory.com/30#entry30comment</comments>
      <pubDate>Mon, 22 Jun 2026 14:53:26 +0900</pubDate>
    </item>
    <item>
      <title>리액트 애니메이션 - Framer Motion</title>
      <link>https://fromfavor.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://motion.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://motion.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1781251130927&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Motion: JavaScript &amp;amp; React animation library&quot; data-og-description=&quot;Motion (prev Framer Motion) is a fast, production-grade animation library for React, JavaScript and Vue. Build smooth UI animations at a tiny footprint.&quot; data-og-host=&quot;motion.dev&quot; data-og-source-url=&quot;https://motion.dev/&quot; data-og-url=&quot;https://motion.dev/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boSXkg/dJMb88GdrI0/CpEjoNikKeqU6FSFy8OeKk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/PsXqw/dJMb8PGEC17/O3Fm73gnxzVcgnE91bIEVk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bvdnRW/dJMb8PGEC16/5wykA3zVtjYDJ2rK23N4w0/img.png?width=640&amp;amp;height=480&amp;amp;face=255_167_393_305&quot;&gt;&lt;a href=&quot;https://motion.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://motion.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boSXkg/dJMb88GdrI0/CpEjoNikKeqU6FSFy8OeKk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/PsXqw/dJMb8PGEC17/O3Fm73gnxzVcgnE91bIEVk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bvdnRW/dJMb8PGEC16/5wykA3zVtjYDJ2rK23N4w0/img.png?width=640&amp;amp;height=480&amp;amp;face=255_167_393_305');&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;Motion: JavaScript &amp;amp; React animation library&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Motion (prev Framer Motion) is a fast, production-grade animation library for React, JavaScript and Vue. Build smooth UI animations at a tiny footprint.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;motion.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ Framer Motion의 핵심 속성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;initial
&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;애니메이션이 처음 등장할 때의 초기 스타일&amp;nbsp;&lt;/li&gt;
&lt;li&gt;x축 위치, 투명도 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;animate
&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;컴포넌트가 최종적으로 도달해야 하는 스타일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;transition
&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;애니메이션의 지속 시간(duration), 효과(spring, ease), 지연 시간(delay) 등&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Get started&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Install&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;npm install motion&lt;/b&gt; 명령어를 사용하여 다운로드&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Create your first animation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9Q558/dJMcaayTvSK/klfgBj99A9bzlNQhp9tT70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9Q558/dJMcaayTvSK/klfgBj99A9bzlNQhp9tT70/img.png&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;840&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;227&quot; data-widthpercent=&quot;49.97&quot; style=&quot;width: 49.3852%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9Q558/dJMcaayTvSK/klfgBj99A9bzlNQhp9tT70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9Q558%2FdJMcaayTvSK%2FklfgBj99A9bzlNQhp9tT70%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;1477&quot; height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCGO3i/dJMcagFTVry/RueHJBnvqNJQ0KmY2Qk1Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCGO3i/dJMcagFTVry/RueHJBnvqNJQ0KmY2Qk1Uk/img.png&quot; data-origin-width=&quot;1479&quot; data-origin-height=&quot;840&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;227&quot; data-widthpercent=&quot;50.03&quot; style=&quot;width: 49.452%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCGO3i/dJMcagFTVry/RueHJBnvqNJQ0KmY2Qk1Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCGO3i%2FdJMcagFTVry%2FRueHJBnvqNJQ0KmY2Qk1Uk%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;1479&quot; height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;360도 회전하는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781253818828&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/App.tsx

import Rotate from &quot;./components/Rotate&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Rotate /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781253836803&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Rotate.tsx

import * as motion from &quot;motion/react-client&quot;;

export default function Rotate() {
  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;motion.div
        className=&quot;w-24 h-24 bg-[#ff0055] rounded-lg&quot;
        animate={{ rotate: 360 }}
        transition={{ duration: 2 }}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;motion.div
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일반 HTML div 태그에 애니메이션 능력을 부여한 Framer Motion 전용 컴포넌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;animate={{ rotate: 360 }}
&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;360도 회전&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;transition={{ duration: 1 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;목표 상태까지 도달하는 데 1초 동안 움직이도록 속도 조절&amp;nbsp;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;* 해당 코드에 initial 속성 X&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;--&amp;gt; 컴포넌트가 Mount 되는 순간(새로고침)에 자동 실행&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFfNts/dJMcagsjeqb/4hhNgLhKOjAf1oyCL24Hs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFfNts/dJMcagsjeqb/4hhNgLhKOjAf1oyCL24Hs1/img.png&quot; data-origin-width=&quot;1537&quot; data-origin-height=&quot;844&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;220&quot; data-widthpercent=&quot;49.76&quot; style=&quot;width: 49.1776%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFfNts/dJMcagsjeqb/4hhNgLhKOjAf1oyCL24Hs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFfNts%2FdJMcagsjeqb%2F4hhNgLhKOjAf1oyCL24Hs1%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;1537&quot; height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R9ZD6/dJMcaf7XSgS/yk0uckYFZdwYi0H71r0i8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R9ZD6/dJMcaf7XSgS/yk0uckYFZdwYi0H71r0i8k/img.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;832&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;218&quot; data-widthpercent=&quot;50.24&quot; style=&quot;width: 49.6596%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R9ZD6/dJMcaf7XSgS/yk0uckYFZdwYi0H71r0i8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR9ZD6%2FdJMcaf7XSgS%2Fyk0uckYFZdwYi0H71r0i8k%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;1530&quot; height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;크기가 2배 커지는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781254758478&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Rotate.tsx

import * as motion from &quot;motion/react-client&quot;;

export default function Rotate() {
  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;motion.div
        className=&quot;w-24 h-24 bg-[#ff0055] rounded-lg&quot;
        animate={{ scale: 2 }}
        transition={{ duration: 2 }}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;animate={{ scale: 2 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;크기 2배 증가&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;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Enter animation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RZ4iA/dJMcafGY8u6/QFbHhR1gv4UUn0AH2NxA41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RZ4iA/dJMcafGY8u6/QFbHhR1gv4UUn0AH2NxA41/img.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;850&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;221&quot; style=&quot;width: 49.6605%; margin-right: 10px;&quot; data-widthpercent=&quot;50.24&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RZ4iA/dJMcafGY8u6/QFbHhR1gv4UUn0AH2NxA41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRZ4iA%2FdJMcafGY8u6%2FQFbHhR1gv4UUn0AH2NxA41%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;1540&quot; height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wZdeU/dJMcabxKFnN/fvxvGB3SWTFDNMUC0uhRH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wZdeU/dJMcabxKFnN/fvxvGB3SWTFDNMUC0uhRH1/img.png&quot; data-origin-width=&quot;1525&quot; data-origin-height=&quot;850&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;223&quot; style=&quot;width: 49.1768%;&quot; data-widthpercent=&quot;49.76&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wZdeU/dJMcabxKFnN/fvxvGB3SWTFDNMUC0uhRH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwZdeU%2FdJMcabxKFnN%2FfvxvGB3SWTFDNMUC0uhRH1%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;1525&quot; height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;탄성을 가지고 등장하는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781256135348&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/EnterAnimation.tsx

import * as motion from &quot;motion/react-client&quot;;

export default function EnterAnimation() {
  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;motion.div
        className=&quot;w-24 h-24 bg-[#ff0055] rounded-full&quot;
        initial={{ opacity: 0, scale: 0 }}
        animate={{ opacity: 1, scale: 1 }}
        transition={{
          duration: 0.4,
          scale: { type: &quot;spring&quot;, visualDuration: 0.4, bounce: 0.5 },
        }}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;initial={{ opacity: 0, scale: 0 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;투명도 0, 크기 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;animate={{ opacity: 1, scale: 1}}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;투명도 1, 크기 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;transition={{ duration: 0.4, scale: { type: &quot;spring&quot;, visualDuration: 0.4, bounce: 0.5}, }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;type: &quot;spring&quot; : 고무공처럼 탄성을 가지고 움직이도록 설정&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;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Hover &amp;amp; tap animation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJltq/dJMcafGYGr8/Fit9IvFcifi2FuLHrZUaZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJltq/dJMcafGYGr8/Fit9IvFcifi2FuLHrZUaZK/img.png&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;826&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;224&quot; style=&quot;width: 49.8344%; margin-right: 10px;&quot; data-widthpercent=&quot;50.42&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJltq/dJMcafGYGr8/Fit9IvFcifi2FuLHrZUaZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJltq%2FdJMcafGYGr8%2FFit9IvFcifi2FuLHrZUaZK%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;1476&quot; height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVHBuu/dJMcaf7XTpg/tRKAKTPGh0UZnW7KN488Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVHBuu/dJMcaf7XTpg/tRKAKTPGh0UZnW7KN488Hk/img.png&quot; data-origin-width=&quot;1483&quot; data-origin-height=&quot;844&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;228&quot; style=&quot;width: 49.0028%;&quot; data-widthpercent=&quot;49.58&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVHBuu/dJMcaf7XTpg/tRKAKTPGh0UZnW7KN488Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVHBuu%2FdJMcaf7XTpg%2FtRKAKTPGh0UZnW7KN488Hk%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;1483&quot; height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;hover, tap 시 크기가 바뀌는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781256833093&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Gestures.tsx

import * as motion from &quot;motion/react-client&quot;;

export default function Gestures() {
  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;motion.div
        className=&quot;w-24 h-24 bg-[#ff0055] rounded-lg&quot;
        whileHover={{ scale: 1.2 }}
        whileTap={{ scale: 0.8 }}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;whileHover={{ scale: 1.2 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;박스 위에 마우스 커서를 올렸을 때 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;기존 크기보다 1.2배 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;whileTap={{ scale: 0.8 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;박스를 클릭하고 있는 순간 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;기존 크기보다 0.8배로 감소&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;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ Layout animation&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b54yJX/dJMcadbc1V0/wpd22xOYVQgDnluYYkmWo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b54yJX/dJMcadbc1V0/wpd22xOYVQgDnluYYkmWo1/img.png&quot; data-origin-width=&quot;1537&quot; data-origin-height=&quot;829&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;216&quot; style=&quot;width: 49.7096%; margin-right: 10px;&quot; data-widthpercent=&quot;50.29&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b54yJX/dJMcadbc1V0/wpd22xOYVQgDnluYYkmWo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb54yJX%2FdJMcadbc1V0%2Fwpd22xOYVQgDnluYYkmWo1%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;1537&quot; height=&quot;829&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAUjzn/dJMcaaTaX8J/9OENKkG81UrqCWLgIQ2FYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAUjzn/dJMcaaTaX8J/9OENKkG81UrqCWLgIQ2FYK/img.png&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;835&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;218&quot; style=&quot;width: 49.1276%;&quot; data-widthpercent=&quot;49.71&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAUjzn/dJMcaaTaX8J/9OENKkG81UrqCWLgIQ2FYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAUjzn%2FdJMcaaTaX8J%2F9OENKkG81UrqCWLgIQ2FYK%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;1530&quot; height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;토글 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781258120072&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/LayoutAnimation.tsx

import * as motion from &quot;motion/react-client&quot;;
import { useState } from &quot;react&quot;;

export default function LayoutAnimation() {
  const [isOn, setIsOn] = useState(false);

  const toggleSwitch = () =&amp;gt; {
    setIsOn(!isOn);
  };

  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;button
        className={`w-24 h-12 bg-[#ff0055]/30 rounded-full cursor-pointer flex p-1 items-center ${isOn ? &quot;justify-start&quot; : &quot;justify-end&quot;}`}
        onClick={toggleSwitch}
      &amp;gt;
        &amp;lt;motion.div
          className=&quot;w-8 h-8 bg-[#ff0055] rounded-full&quot;
          layout
          transition={{ type: &quot;spring&quot;, visualDuration: 0.2, bounce: 0.2 }}
        /&amp;gt;
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bg-[#ff0055]/30&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;약 30% 투명도&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;layout
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;justify-start, justify-end 등 바뀐 정렬 방식을 알아채고 자동으로 이동&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;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Sudv6/dJMcadWEhPc/sL4M8qCf2JkBvrhL32HGF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sudv6/dJMcadWEhPc/sL4M8qCf2JkBvrhL32HGF1/img.png&quot; data-origin-width=&quot;1531&quot; data-origin-height=&quot;844&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;221&quot; style=&quot;width: 49.0975%; margin-right: 10px;&quot; data-widthpercent=&quot;49.68&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sudv6/dJMcadWEhPc/sL4M8qCf2JkBvrhL32HGF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSudv6%2FdJMcadWEhPc%2FsL4M8qCf2JkBvrhL32HGF1%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;1531&quot; height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/okSjX/dJMcad3nwGF/BcYivcmK1ifyLVR0SsRTQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/okSjX/dJMcad3nwGF/BcYivcmK1ifyLVR0SsRTQ1/img.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;838&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;218&quot; style=&quot;width: 49.7397%;&quot; data-widthpercent=&quot;50.32&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/okSjX/dJMcad3nwGF/BcYivcmK1ifyLVR0SsRTQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FokSjX%2FdJMcad3nwGF%2FBcYivcmK1ifyLVR0SsRTQ1%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;1540&quot; height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;nav 선택값에 따라 데이터를 출력하는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781339180959&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/SharedLayoutAnimation.tsx

import { AnimatePresence } from &quot;motion/react&quot;;
import * as motion from &quot;motion/react-client&quot;;
import { useState } from &quot;react&quot;;

export default function SharedLayoutAnimation() {
  const [selectedTab, setSelectedTab] = useState(tabs[0]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;flex items-center justify-center min-h-screen&quot;&amp;gt;
        &amp;lt;div className=&quot;flex flex-col w-120 h-90 max-w-[calc(100% - 40px)] max-h-[calc(100% - 40px)] rounded-lg bg-white overflow-hidden shadow-2xl&quot;&amp;gt;
          &amp;lt;nav className=&quot;bg-[#fdfdfd] pt-1 px-1 border-b border-[#eeeeee]&quot;&amp;gt;
            &amp;lt;ul className=&quot;flex w-full p-0 m-0 list-none font-medium text-sm&quot;&amp;gt;
              {tabs.map((item) =&amp;gt; (
                &amp;lt;motion.li
                  className=&quot;rounded-md w-full px-3.5 py-2.5 relative bg-white cursor-pointer h-6 flex justify-between items-center flex-1 min-w-0 select-none&quot;
                  key={item.label}
                  initial={false}
                  animate={{
                    backgroundColor: item === selectedTab ? &quot;#eee&quot; : &quot;#eee0&quot;,
                  }}
                  onClick={() =&amp;gt; setSelectedTab(item)}
                &amp;gt;
                  {`${item.icon} ${item.label}`}
                  {item === selectedTab ? (
                    &amp;lt;motion.div
                      className=&quot;absolute -bottom-0.5 left-0 right-0 h-0.5 bg-[#ff0055]&quot;
                      layoutId=&quot;underline&quot;
                      id=&quot;underline&quot;
                    /&amp;gt;
                  ) : null}
                &amp;lt;/motion.li&amp;gt;
              ))}
            &amp;lt;/ul&amp;gt;
          &amp;lt;/nav&amp;gt;

          &amp;lt;main className=&quot;flex justify-center items-center flex-1&quot;&amp;gt;
            &amp;lt;AnimatePresence mode=&quot;wait&quot;&amp;gt;
              &amp;lt;motion.div
                className=&quot;flex items-center justify-center text-[128px] leading-none select-none&quot;
                key={selectedTab ? selectedTab.label : &quot;empty&quot;}
                initial={{ y: 10, opacity: 0 }}
                animate={{ y: 0, opacity: 1 }}
                exit={{ y: -10, opacity: 0 }}
                transition={{ duration: 0.2 }}
              &amp;gt;
                {selectedTab ? selectedTab.icon : &quot; &quot;}
              &amp;lt;/motion.div&amp;gt;
            &amp;lt;/AnimatePresence&amp;gt;
          &amp;lt;/main&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

/**
 * ==============   Data   ================
 */
const allIngredients = [
  { icon: &quot; &quot;, label: &quot;Tomato&quot; },
  { icon: &quot; &quot;, label: &quot;Lettuce&quot; },
  { icon: &quot; &quot;, label: &quot;Cheese&quot; },
  { icon: &quot; &quot;, label: &quot;Carrot&quot; },
  { icon: &quot; &quot;, label: &quot;Banana&quot; },
  { icon: &quot; &quot;, label: &quot;Blueberries&quot; },
  { icon: &quot; &quot;, label: &quot;Champers?&quot; },
];

const [tomato, lettuce, cheese] = allIngredients;
const tabs = [tomato, lettuce, cheese];&lt;/code&gt;&lt;/pre&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;최상위 부모 &amp;lt;div&amp;gt; : 화면 전체를 사용, 박스를 화면 가로/세로 정중앙에 배치하기 위함&lt;/li&gt;
&lt;li&gt;중간 카드 &amp;lt;div&amp;gt; : 탭과 이모지가 담길 컨테이너&lt;/li&gt;
&lt;li&gt;&amp;lt;nav&amp;gt; : 상단 메뉴
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;lt;motion.li&amp;gt; : 각각의 탭 버튼 자체를 담당하는 컴포넌트&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&amp;lt;motion.div&amp;gt; : 탭 메뉴 밑의 핑크색 밑줄&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;main&amp;gt; : 콘텐츠&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max-w-[calc(100% - 40px)]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;최대 너비를 브라우저 전체 가로폭(100%)에서 40px을 뺀 크기까지만 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;list-none
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;lt;ul&amp;gt;, &amp;lt;li&amp;gt; 태그 사용 시 나타나는 왼쪽 점을 지워주는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;select-none&lt;br /&gt;
&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;&lt;span style=&quot;color: #ee2323;&quot;&gt;initial={false}&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;처음 켤 때 빌드업 애니메이션을 생략&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ㄴ 첫 로딩 시 불필요한 색상 빌드업을 차단&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;mode=&quot;wait&quot;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;AnimatePresence 내부에서 교체가 일어날 때 기존 컴포넌트가 완전히 사라질 때까지 새 컴포넌트를 대기시킴&lt;/li&gt;
&lt;li&gt;화면 전환이 깔끔해짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;leading-none
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;폰트 줄간격을 1로 만들어 위아래 여백을 삭제&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;allIngredients&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;확장성을 위한 전체 데이터베이스 역할&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ Exit animations&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dISeho/dJMcaaMoNSC/CykSB99kurUKihFH9n1a41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dISeho/dJMcaaMoNSC/CykSB99kurUKihFH9n1a41/img.png&quot; data-origin-width=&quot;1525&quot; data-origin-height=&quot;840&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;220&quot; style=&quot;width: 49.6334%; margin-right: 10px;&quot; data-widthpercent=&quot;50.22&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dISeho/dJMcaaMoNSC/CykSB99kurUKihFH9n1a41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdISeho%2FdJMcaaMoNSC%2FCykSB99kurUKihFH9n1a41%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;1525&quot; height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBzpS/dJMcadCfNS4/HnAmfRfxh8zWlfb0nI5eW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBzpS/dJMcadCfNS4/HnAmfRfxh8zWlfb0nI5eW0/img.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;849&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;222&quot; style=&quot;width: 49.2038%;&quot; data-widthpercent=&quot;49.78&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBzpS/dJMcadCfNS4/HnAmfRfxh8zWlfb0nI5eW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBzpS%2FdJMcadCfNS4%2FHnAmfRfxh8zWlfb0nI5eW0%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;1528&quot; height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;버튼으로 등장, 퇴장하는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781335126921&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/ExitAnimation.tsx

import { AnimatePresence, motion } from &quot;motion/react&quot;;
import { useState } from &quot;react&quot;;

export default function ExitAnimation() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;div className=&quot;flex flex-col relative w-24 h-44&quot;&amp;gt;
        &amp;lt;AnimatePresence initial={false}&amp;gt;
          {isVisible ? (
            &amp;lt;motion.div
              className=&quot;w-24 h-24 bg-[#ff0055] rounded-lg&quot;
              initial={{ opacity: 0, scale: 0 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0 }}
              key=&quot;box&quot;
            /&amp;gt;
          ) : null}
        &amp;lt;/AnimatePresence&amp;gt;
        &amp;lt;motion.button
          className=&quot;bg-[#ff0055] rounded-lg py-2.5 absolute bottom-0 left-0 right-0 cursor-pointer text-white&quot;
          onClick={() =&amp;gt; setIsVisible(!isVisible)}
          whileTap={{ y: 1 }}
        &amp;gt;
          {isVisible ? &quot;Hide&quot; : &quot;Show&quot;}
        &amp;lt;/motion.button&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt;AnimatePresence&amp;gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;리액트) 조건부 렌더링 사용 --&amp;gt; 조건이 false가 되면 컴포넌트를 화면에서 즉시 삭제&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;AnimatePresence가 사라져야 할 컴포넌트를 화면에 붙잡아둠&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ㄴ exit 애니메이션을 보여줘야 하기 때문에&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;initial={false}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;initial : 컴포넌트가 화면에 처음 등장할 때의 애니메이션을 제어하는 속성&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;exit={{ opacity: 0, scale: 0 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;exit : 컴포넌트가 화면에서 사라질 때의 애니메이션을 제어하는 속성&amp;nbsp;&lt;/li&gt;
&lt;li&gt;투명도 0, 크기 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;key=&quot;box&quot;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Framer Motion은 어떤 컴포넌트가 사라졌고 생겼는지를 key 값을 기준으로 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;whileTap={{ y: 1 }}
&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;y축으로 1픽셀만큼 내려감&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ SVG aniamtions&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVy1gJ/dJMcadPPSss/28jGD0ieIxsV7wIR5tQR10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVy1gJ/dJMcadPPSss/28jGD0ieIxsV7wIR5tQR10/img.png&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;837&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;218&quot; style=&quot;width: 49.6709%; margin-right: 10px;&quot; data-widthpercent=&quot;50.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVy1gJ/dJMcadPPSss/28jGD0ieIxsV7wIR5tQR10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVy1gJ%2FdJMcadPPSss%2F28jGD0ieIxsV7wIR5tQR10%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;1534&quot; height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JVvn5/dJMcaf7YkHt/oqMXQ5AGumNGX82UnuhpC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JVvn5/dJMcaf7YkHt/oqMXQ5AGumNGX82UnuhpC0/img.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;850&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;220&quot; style=&quot;width: 49.1663%;&quot; data-widthpercent=&quot;49.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JVvn5/dJMcaf7YkHt/oqMXQ5AGumNGX82UnuhpC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJVvn5%2FdJMcaf7YkHt%2FoqMXQ5AGumNGX82UnuhpC0%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;1542&quot; height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;인터랙티브 드래그에 따라 실시간으로 디자인이 변경되는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781341611186&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/UseTransfomr.tsx

import { motion, useMotionValue, useTransform } from &quot;motion/react&quot;;

export default function UseTransform() {
  const x = useMotionValue(0);
  const xInput = [-100, 0, 100];
  const background = useTransform(x, xInput, [
    &quot;linear-gradient(180deg, #ff008c 0%, rgb(211, 9, 225) 100%)&quot;,
    &quot;linear-gradient(180deg, #7700ff 0%, rgb(68, 0, 255) 100%)&quot;,
    &quot;linear-gradient(180deg, rgb(230, 255, 0) 0%, rgb(3, 209, 0) 100%)&quot;,
  ]);
  const color = useTransform(x, xInput, [
    &quot;rgb(211, 9, 225)&quot;,
    &quot;rgb(68, 0, 255)&quot;,
    &quot;rgb(3, 209, 0)&quot;,
  ]);
  const tickPath = useTransform(x, [10, 100], [0, 1]);
  const crossPathA = useTransform(x, [-10, -55], [0, 1]);
  const crossPathB = useTransform(x, [-50, -100], [0, 1]);

  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen&quot;&amp;gt;
      &amp;lt;motion.div
        className=&quot;flex justify-center items-center flex-1 w-125 h-75 max-w-full rounded-3xl&quot;
        style={{ background }}
      &amp;gt;
        &amp;lt;motion.div
          className=&quot;w-35 h-35 bg-white rounded-3xl p-5 cursor-grab&quot;
          style={{ x }}
          drag=&quot;x&quot;
          dragConstraints={{ left: 0, right: 0 }}
          dragElastic={0.5}
        &amp;gt;
          &amp;lt;svg viewBox=&quot;0 0 50 50&quot;&amp;gt;
            {/* 배경 원형 패스 */}
            &amp;lt;motion.path
              fill=&quot;none&quot;
              strokeWidth=&quot;2&quot;
              stroke={color}
              d=&quot;M 0, 20 a 20, 20 0 1,0 40,0 a 20, 20 0 1,0 -40,0&quot;
              style={{ x: 5, y: 5 }}
            /&amp;gt;
            {/* 틱(V) 아이콘 패스 */}
            &amp;lt;motion.path
              id=&quot;tick&quot;
              fill=&quot;none&quot;
              strokeWidth=&quot;2&quot;
              stroke={color}
              d=&quot;M14,26 L 22,33 L 35,16&quot;
              strokeDasharray=&quot;0 1&quot;
              style={{ pathLength: tickPath }}
            /&amp;gt;
            {/* 엑스(X) 첫 번째 선 패스 */}
            &amp;lt;motion.path
              fill=&quot;none&quot;
              strokeWidth=&quot;2&quot;
              stroke={color}
              d=&quot;M17,17 L33,33&quot;
              strokeDasharray=&quot;0 1&quot;
              style={{ pathLength: crossPathA }}
            /&amp;gt;
            {/* 엑스(X) 두 번째 선 패스 */}
            &amp;lt;motion.path
              id=&quot;cross&quot;
              fill=&quot;none&quot;
              strokeWidth=&quot;2&quot;
              stroke={color}
              d=&quot;M33,17 L17,33&quot;
              strokeDasharray=&quot;0 1&quot;
              style={{ pathLength: crossPathB }}
            /&amp;gt;
          &amp;lt;/svg&amp;gt;
        &amp;lt;/motion.div&amp;gt;
      &amp;lt;/motion.div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;const x = useMotionValue(0)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;useMotionValue : 컴포넌트를 리렌더링하지 않고도 박스의 현재 좌표 값을 부드럽게 업데이트하며 저장&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;0 : 맨처음 시작하는 정중앙 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const xInput = [-100, 0, 100]
&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;-100 : 왼쪽으로 100px&lt;/li&gt;
&lt;li&gt;100 : 오른쪽으로 100px&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const background
&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;const color
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SVG(동그라미, 체크, 엑스 패스) 색상 값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const tickPath = useTransform(x, [10, 100], [0, 1])
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;useTransform : 값 매핑 훅&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;useTransform(감시할값, [입력기준점들], [결과물들])&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;배열 안의 요소들을 매핑&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;체크 패스를 그리는 네비게이터&amp;nbsp;&lt;/li&gt;
&lt;li&gt;x 좌표가 10px에서 100px로 이동하는 동안&lt;/li&gt;
&lt;li&gt;패스의 선 길이 비율을 0(0%)에서 1(100%)로 매핑&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const corssPath
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;엑스 패스를 2단계로 나눠 그리는 네비게이터&lt;/li&gt;
&lt;li&gt;const crossPathA = useTransform(x, [-10, -55], [0, 1])
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;x 좌표가 -10px에서 -55px로 이동하는 동안&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const crossPathB = useTransform(x, [-50, -100], [0, 1])
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;x 좌표가 -50px에서 -100px로 이동하는 동안&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;drag=&quot;x&quot;
&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;x로 지정했기 때문에 양옆으로만 이동 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;dragConstraints={{ left: 0, right: 0 }}
&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;마우스 버튼에서 손을 떼는 순간 정중앙으로 되돌아오도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;dragElastic={0.5}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;드래그할 때 묵직함을 주는 고무줄 탄성도&amp;nbsp;&lt;/li&gt;
&lt;li&gt;0 : 탄성 X --&amp;gt; 좌우로 움직이지 X&lt;/li&gt;
&lt;li&gt;1 : 저항력 X --&amp;gt; 마우스 커서가 가볍게 따라옴&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;strokeDasharray=&quot;0 1&quot;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;선그리기 애니메이션을 위한 SVG 세팅의 밑작업&lt;/li&gt;
&lt;li&gt;선을 0만큼 그리고, 공백을 1만큼 채우라는 뜻&lt;/li&gt;
&lt;li&gt;== 처음에는 선을 완전히 숨겨 안 보이도록 만듦&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;d=&quot;M 0,20 a 20,20 0 1,0 40,0 a 20,20 0 1,0 -40,0&quot;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;M(Move to) : 시작점으로 이동 / a(Arc) : 부드러운 곡선&amp;nbsp;&lt;/li&gt;
&lt;li&gt;M 0,20 : (0, 20) 좌표 지점에서 시작&lt;/li&gt;
&lt;li&gt;a 20,20 0 1,0 40,0
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;20,20 : 곡선의 가로 반지름 20px, 세로 반지름 20px&lt;/li&gt;
&lt;li&gt;0 : 회전 각도 0도&lt;/li&gt;
&lt;li&gt;1,0 : 위쪽으로 볼록한 반원을 그리며 시계방향으로 돌기&lt;/li&gt;
&lt;li&gt;40,0 : 최종 목적지 좌표 (40, 0)&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OverView&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt; Keyframes&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8NOFW/dJMcacpXj0W/YXokVZjHJkmi7fNqTnxoN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8NOFW/dJMcacpXj0W/YXokVZjHJkmi7fNqTnxoN0/img.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;843&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;220&quot; style=&quot;width: 32.6613%; margin-right: 10px;&quot; data-widthpercent=&quot;33.44&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8NOFW/dJMcacpXj0W/YXokVZjHJkmi7fNqTnxoN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8NOFW%2FdJMcacpXj0W%2FYXokVZjHJkmi7fNqTnxoN0%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;1536&quot; height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvQk6t/dJMcahSfPTP/bzKWPWGaxgipYVHkCJlTN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvQk6t/dJMcahSfPTP/bzKWPWGaxgipYVHkCJlTN1/img.png&quot; data-origin-width=&quot;1527&quot; data-origin-height=&quot;843&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;221&quot; style=&quot;width: 32.4699%; margin-right: 10px;&quot; data-widthpercent=&quot;33.24&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvQk6t/dJMcahSfPTP/bzKWPWGaxgipYVHkCJlTN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvQk6t%2FdJMcahSfPTP%2FbzKWPWGaxgipYVHkCJlTN1%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;1527&quot; height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8qyGv/dJMcahkwlfJ/3LCxOCPzBkD4dx2Seb8k1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8qyGv/dJMcahkwlfJ/3LCxOCPzBkD4dx2Seb8k1k/img.png&quot; data-origin-width=&quot;1525&quot; data-origin-height=&quot;840&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;220&quot; style=&quot;width: 32.5432%;&quot; data-widthpercent=&quot;33.32&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8qyGv/dJMcahkwlfJ/3LCxOCPzBkD4dx2Seb8k1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8qyGv%2FdJMcahkwlfJ%2F3LCxOCPzBkD4dx2Seb8k1k%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;1525&quot; height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;시간 흐름에 따라 모양이 달라지는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781346984166&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Keyframes.tsx

import * as motion from &quot;motion/react-client&quot;;

export default function Keyframes() {
  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;motion.div
        className=&quot;w-24 h-24 bg-white rounded-md&quot;
        animate={{
          scale: [1, 2, 2, 1, 1],
          rotate: [0, 0, 180, 180, 0],
          borderRadius: [&quot;0%&quot;, &quot;0%&quot;, &quot;50%&quot;, &quot;50%&quot;, &quot;0%&quot;],
        }}
        transition={{
          duration: 2,
          ease: &quot;easeInOut&quot;,
          times: [0, 0.2, 0.5, 0.8, 1],
          repeat: Infinity,
          repeatDelay: 1,
        }}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;animate={{ scale: [1, 2, 2, 1, 1], rotate: [0, 0, 180, 180, 0], borderRadius: [&quot;0%&quot;, &quot;0%&quot;, &quot;50%&quot;, &quot;50%&quot;, &quot;0%&quot;] }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;scale: [1, 2, 2, 1, 1]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;크기를 1배 --&amp;gt; 2배 --&amp;gt; 2배 유지 --&amp;gt; 1배 --&amp;gt; 1배 유지 순으로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rotate: [0, 0, 180, 180, 0]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;회전각을 0도 --&amp;gt; 0도 유지 --&amp;gt; 180도 회전 --&amp;gt; 180도 유지 --&amp;gt; 0도 순으로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;borderRadius: [&quot;0%&quot;, &quot;0%&quot;, &quot;50%&quot;, &quot;50%&quot;, &quot;0%&quot;]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모양을 네모 --&amp;gt; 네모 유지 --&amp;gt; 동그라미 --&amp;gt; 동그라미 유지 --&amp;gt; 네모 순으로 변환&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;transition={{ duration: 2, ease: &quot;easeInOut&quot;, times: [0, 0.2, 0.5, 0.8, 1], repeat: Infinity, repeatDelay: 1 }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;duration: 2
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;전체 한 바퀴 도는 애니메이션의 총 재생 시간이 2초&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ease: &quot;easeInOut&quot;
&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;times: [0, 0.2, 0.5, 0.8, 1]
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;배열 안의 배열들이 전체 2초 중 어느 타이밍에 일어날지 정하는 타임라인 인터벌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;repeat: Infinity
&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;repeatDelay: 1
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다음 애니메이션 시작까지 1초 대기&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt; Motion along a path&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brvAbQ/dJMcacpXqpy/pOsq2yfRVeSHe2e4ECM8ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brvAbQ/dJMcacpXqpy/pOsq2yfRVeSHe2e4ECM8ik/img.png&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;840&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;201&quot; style=&quot;width: 32.4474%; margin-right: 10px;&quot; data-widthpercent=&quot;33.22&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brvAbQ/dJMcacpXqpy/pOsq2yfRVeSHe2e4ECM8ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrvAbQ%2FdJMcacpXqpy%2FpOsq2yfRVeSHe2e4ECM8ik%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;1674&quot; height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwG0RW/dJMcabxKODj/WkdkOxMA5KFfKgqkAbBpq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwG0RW/dJMcabxKODj/WkdkOxMA5KFfKgqkAbBpq1/img.png&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;841&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;201&quot; style=&quot;width: 32.4089%; margin-right: 10px;&quot; data-widthpercent=&quot;33.18&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwG0RW/dJMcabxKODj/WkdkOxMA5KFfKgqkAbBpq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwG0RW%2FdJMcabxKODj%2FWkdkOxMA5KFfKgqkAbBpq1%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;1674&quot; height=&quot;841&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wsFBf/dJMcaar8x6R/x8rJ1kfBOphekrueVkZUYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wsFBf/dJMcaar8x6R/x8rJ1kfBOphekrueVkZUYK/img.png&quot; data-origin-width=&quot;1677&quot; data-origin-height=&quot;832&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;198&quot; style=&quot;width: 32.8181%;&quot; data-widthpercent=&quot;33.6&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wsFBf/dJMcaar8x6R/x8rJ1kfBOphekrueVkZUYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwsFBf%2FdJMcaar8x6R%2Fx8rJ1kfBOphekrueVkZUYK%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;1677&quot; height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;포물선으로 날아가는 div 박스와 파동으로 흔들리는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781368238698&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/AddToBasket.tsx

import { motion, arc, useAnimate, useMotionValue } from &quot;motion/react&quot;;
import { useRef, useState } from &quot;react&quot;;

const PRODUCT_SIZE = 160;
const BASKET_BOX = 56;
const FLY_SCALE = BASKET_BOX / PRODUCT_SIZE;

type Direction = &quot;auto&quot; | &quot;cw&quot; | &quot;ccw&quot;;

interface AddToBasketProps {
  strength?: number;
  peak?: number;
  rotate?: number;
  duration?: number;
  basketVelocityFactor?: number;
  direction?: Direction;
}

export default function AddToBasket({
  strength = 0.5,
  peak = 0.15,
  rotate = 0.9,
  duration = 0.45,
  basketVelocityFactor = 0.05,
  direction = &quot;cw&quot;,
}: AddToBasketProps = {}) {
  const [scope, animate] = useAnimate();
  const productRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const basketRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const ringRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const [isFlying, setIsFlying] = useState(false);
  const productX = useMotionValue(0);
  const productY = useMotionValue(0);

  const addToBasket = async () =&amp;gt; {
    const product = productRef.current;
    const basket = basketRef.current;
    const ring = ringRef.current;
    if (!product || !basket || !ring || isFlying) return;
    setIsFlying(true);

    const from = product.getBoundingClientRect();
    const to = basket.getBoundingClientRect();
    const dx = to.left + to.width / 2 - (from.left + from.width / 2);
    const dy = to.top + to.height / 2 - (from.top + from.height / 2);

    // product를 조종하는 animate
    await animate(
      product,
      {
        x: dx,
        y: dy,
        scale: FLY_SCALE,
        opacity: [1, 1, 0],
      },
      {
        duration,
        path: arc({
          strength,
          peak,
          rotate,
          direction: direction === &quot;auto&quot; ? undefined : direction,
        }),
        ease: [0.74, 0.18, 0.93, 0.69],
        opacity: { inherit: true, times: [0, 0.95, 1] },
      },
    );

    // basket을 조종하는 animate
    animate(
      basket,
      { x: 0, y: 0 },
      {
        type: &quot;spring&quot;,
        stiffness: 500,
        damping: 12,
        x: {
          inherit: true,
          velocity: productX.getVelocity() * basketVelocityFactor,
        },
        y: {
          inherit: true,
          velocity: productY.getVelocity() * basketVelocityFactor,
        },
      },
    );

    // 파동을 조종하는 animate
    animate(
      ring,
      { scale: [1, 2.2], opacity: [0.8, 0] },
      { duration: 0.5, ease: &quot;easeOut&quot; },
    );

    // 충돌 이후 product를 조종하는 animate
    animate(
      product,
      {
        x: 0,
        y: 0,
        scale: 0.9,
        rotate: 0,
        opacity: 0,
        clipPath: &quot;inset(0%)&quot;,
      },
      { duration: 0 },
    );

    // 재등장하는 product를 조종하는 animate
    await animate(
      product,
      { opacity: 1, scale: 1 },
      {
        scale: { type: &quot;spring&quot;, visualDuration: 0.4, bounce: 0.35 },
        opacity: { duration: 0.25, ease: &quot;easeOut&quot; },
      },
    );

    setIsFlying(false);
  };

  return (
    &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
      &amp;lt;div
        className=&quot;absolute inset-0 flex items-center justify-center overflow-hidden font-mono text-white&quot;
        ref={scope}
      &amp;gt;
        &amp;lt;style&amp;gt;{`#sandbox { position: relative }`}&amp;lt;/style&amp;gt;
        &amp;lt;div
          className=&quot;absolute top-20 right-20 flex items-center justify-center bg-[--layer] border border-[--border] text-[#ff0055] will-change-transform&quot;
          style={{ width: BASKET_BOX, height: BASKET_BOX }}
          ref={basketRef}
        &amp;gt;
          &amp;lt;motion.div
            className=&quot;absolute -inset-px border border-[--accent] opacity-0 pointer-events-none will-change-transform&quot;
            ref={ringRef}
          /&amp;gt;
          &amp;lt;BasketIcon /&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div className=&quot;flex flex-col items-center gap-4.5&quot;&amp;gt;
          &amp;lt;motion.div
            className=&quot;flex items-center justify-center bg-[--layer] border border-[--border] will-change-transform&quot;
            style={{
              x: productX,
              y: productY,
              width: PRODUCT_SIZE,
              height: PRODUCT_SIZE,
            }}
            ref={productRef}
          &amp;gt;
            &amp;lt;span className=&quot;text-7xl leading-none select-none&quot;&amp;gt; &amp;lt;/span&amp;gt;
          &amp;lt;/motion.div&amp;gt;

          &amp;lt;div className=&quot;flex items-baseline gap-3 font-mono text-xs tracking-wide&quot;&amp;gt;
            &amp;lt;span className=&quot;text-white&quot;&amp;gt;Campus 00s&amp;lt;/span&amp;gt;
            &amp;lt;span className=&quot;text-[#ff0055]&quot;&amp;gt;&amp;pound;128&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;

          &amp;lt;motion.button
            className=&quot;mt-1 px-6.5 py-3 border-none bg-[#ff0055] text-[--background] font-mono text-xs tracking-[0.12em] uppercase cursor-pointer&quot;
            type=&quot;button&quot;
            onClick={addToBasket}
            disabled={isFlying}
            whileHover={{ scale: 1.03 }}
            whileTap={{ scale: 0.97 }}
            style={{
              opacity: isFlying ? 0.55 : 1,
              pointerEvents: isFlying ? &quot;none&quot; : &quot;auto&quot;,
            }}
          &amp;gt;
            Add to Basket
          &amp;lt;/motion.button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
function BasketIcon() {
  return (
    &amp;lt;svg
      xmlns=&quot;http://www.w3.org/2000/svg&quot;
      width=&quot;26&quot;
      height=&quot;26&quot;
      viewBox=&quot;0 0 24 24&quot;
      fill=&quot;none&quot;
      stroke=&quot;currentColor&quot;
      strokeWidth=&quot;2&quot;
      strokeLinecap=&quot;round&quot;
      strokeLinejoin=&quot;round&quot;
    &amp;gt;
      &amp;lt;path d=&quot;m15 11-1 9&quot; /&amp;gt;
      &amp;lt;path d=&quot;m19 11-4-7&quot; /&amp;gt;
      &amp;lt;path d=&quot;M2 11h20&quot; /&amp;gt;
      &amp;lt;path d=&quot;m3.5 11 1.6 7.4a2 2 0 0 0 2 1.6h9.8a2 2 0 0 0 2-1.6l1.7-7.4&quot; /&amp;gt;
      &amp;lt;path d=&quot;M4.5 15.5h15&quot; /&amp;gt;
      &amp;lt;path d=&quot;m5 11 4-7&quot; /&amp;gt;
      &amp;lt;path d=&quot;m9 11 1 9&quot; /&amp;gt;
    &amp;lt;/svg&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;const productRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;useRef 훅 : 실제 HTML 태그에 DOM 접근을 하거나, 화면 리렌더링 없이 값을 저장할 수 있게 해주는 역할&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;실제 신발 상자(product) &amp;lt;div&amp;gt; 태그의 위치 좌표와 알맹이 속성에 접근하기 위한 화살표 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const productX = useMotionValue(0)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;useMotionValue() : 리액트 화면을 리렌더링하지 않고도, 움직이는 요소의 수치를 부드럽게 저장, 추적할 수 있게 해주는 역할&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;움직이는 product의 실시간 x좌표값과 물리 속도를 측정하기 위한 엔진 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const addToBasket = async () =&amp;gt; {}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;async(Asynchronous) : 함수 내부에서 비동기 제어를 할 것임을 선언&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;await : 해당 작업이 완벽하게 끝날 때까지 아랫줄의 코드들이 실행되지 않도록 정지&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const product = productRef.current
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;.current : useRef 안에 들어있는 실제 HTML 태그를 꺼내오도록 하는 속성&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const from = product.getBoundingClientRect()
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;getBoundingClientRect() : 브라우저 화면 내에서 현재 태그의 실제 x, y 좌표와 크기 정보를 실시간으로 따오는 함수&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const dx = to.left + to.width / 2 - (from.left + from.width / 2)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;product의 정중앙 지점부터 basket의 정중앙 지점까지 가로로 몇 픽셀, 세로로 몇 픽셀 이동해야 하는지 계산해 낸 최종 거리값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;stiffness : 500
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;spring이 얼마나 팽팽한지 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;dampting : 12&amp;nbsp;
&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;小 : 공기 저항 &amp;darr; / 大 : 공기 저항&amp;nbsp;&amp;uarr;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;x: { inherit: true, velocity: productX.getVelocity() * basketVelocityFactor }
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;inherit: true
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;product가 날아오던 힘과 방향을 그대로 상속받아 basket에게 충격 전달&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;style={{ width: BASKET_BOX, heitght: BASKET_BOX }}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;tailwind css에서는 상수값만큼 값을 줄 수가 X&lt;/li&gt;
&lt;li&gt;style로 따로 빼서 값 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ref={basketRef}
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;미리 선언해둔 화살표 변수(basketRef)를 실제 HTML 태그에 수동으로 바인딩&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;앞으로 basketRef.current를 호출하면, 방금 그린 장바구니 &amp;lt;div&amp;gt; 태그 알맹이를 통째로 꺼내 전달하게 됨&lt;/span&gt;&amp;nbsp;&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;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt; Animate Content&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CvhzK/dJMcaffVmNm/XaeQSLlM8EWklPGxXuKJ01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CvhzK/dJMcaffVmNm/XaeQSLlM8EWklPGxXuKJ01/img.png&quot; data-origin-width=&quot;1537&quot; data-origin-height=&quot;840&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;219&quot; style=&quot;width: 49.8142%; margin-right: 10px;&quot; data-widthpercent=&quot;50.4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CvhzK/dJMcaffVmNm/XaeQSLlM8EWklPGxXuKJ01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCvhzK%2FdJMcaffVmNm%2FXaeQSLlM8EWklPGxXuKJ01%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;1537&quot; height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDQvYj/dJMcabYTdGq/XSQN1OxqabfQHZtykskWQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDQvYj/dJMcabYTdGq/XSQN1OxqabfQHZtykskWQk/img.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;853&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;222&quot; style=&quot;width: 49.0231%;&quot; data-widthpercent=&quot;49.6&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDQvYj/dJMcabYTdGq/XSQN1OxqabfQHZtykskWQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDQvYj%2FdJMcabYTdGq%2FXSQN1OxqabfQHZtykskWQk%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;1536&quot; height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;0부터 100까지 숫자가 증가하는 div 박스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781348725184&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/HTMLContent.tsx

import { animate } from &quot;motion&quot;;
import { motion, useMotionValue, useTransform } from &quot;motion/react&quot;;
import { useEffect } from &quot;react&quot;;

export default function HTMLContent() {
  const count = useMotionValue(0);
  const rounded = useTransform(() =&amp;gt; Math.round(count.get()));

  useEffect(() =&amp;gt; {
    const controls = animate(count, 100, { duration: 5 });
    return () =&amp;gt; controls.stop();
  }, [count]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;flex items-center justify-center min-h-screen bg-[#0f1115]&quot;&amp;gt;
        &amp;lt;motion.div className=&quot;font-mono text-6xl leading-none text-[#ff0055]&quot;&amp;gt;
          {rounded}
        &amp;lt;/motion.div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;const count = useMotionValue(0)
&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;const rounded = useTransform(() =&amp;gt; Math.round(count.get()))
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;count가 변할 때마다 가져와서 반올림한 정수만 rounded에 저장&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;useEffect(() =&amp;gt; {}, [count])
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컴포넌트가 마운트 된 순간 + count를 감지하여 값이 변할 때마다 함수 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const controls = animate(count, 100, { duration: 5 })
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;count 안의 숫자를 5초 동안, 100까지 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;return () =&amp;gt; controls.stop()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트가 화면에서 사라질 때는 애니메이션 즉시 중단&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>React</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/29</guid>
      <comments>https://fromfavor.tistory.com/29#entry29comment</comments>
      <pubDate>Sat, 13 Jun 2026 19:04:43 +0900</pubDate>
    </item>
    <item>
      <title>섹션 15. 전역 상태 관리 - Zustand</title>
      <link>https://fromfavor.tistory.com/28</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand : 전역 상태 관리 라이브러리&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ Redux Toolkit, Context API보다 적은 코드와 직관적인 방식으로 상태 관리 가능&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zustand-demo.pmnd.rs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://zustand-demo.pmnd.rs/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1781171912462&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Zustand&quot; data-og-description=&quot;&quot; data-og-host=&quot;zustand-demo.pmnd.rs&quot; data-og-source-url=&quot;https://zustand-demo.pmnd.rs/&quot; data-og-url=&quot;https://zustand-demo.pmnd.rs/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://zustand-demo.pmnd.rs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://zustand-demo.pmnd.rs/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Zustand&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;zustand-demo.pmnd.rs&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand 사용하기&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;&lt;b&gt;npm install zustand&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 명령어를 사용하여 다운로드&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;폴더 구조 설계&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/store/ 하위에 ts 파일로 생성&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;store 파일 생성 및 기본 뼈대 정의&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store : 애플리케이션의 전역 상태(State)와 이를 변경하는 비즈니스 로직(Action)을 한데 모아 관리하는 보관소&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;store에서 사용할 데이터와 함수의 타입 설계도를 interface로 정의&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Zustand의 &lt;span style=&quot;color: #ee2323;&quot;&gt;create 훅을 사용하여 컴포넌트들이 구독할 전역 store 생성&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;export const use***Store = create((set, get) =&amp;gt; ({}));&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;set : 스토어 내부의 데이터를 안전하게 변경하기 위해 사용하는 상태 변경 콜백 함수&lt;/li&gt;
&lt;li&gt;get : 특정 액션(함수) 내부에서 현재 스토어에 담긴 최신 상태값들을 동기적으로 참조하기 위해 사용하는 현재 상태 조회 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;442&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qhzPE/dJMcahdGUfq/CeFJGnLTKzzRpyATW6FgAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qhzPE/dJMcahdGUfq/CeFJGnLTKzzRpyATW6FgAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qhzPE/dJMcahdGUfq/CeFJGnLTKzzRpyATW6FgAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqhzPE%2FdJMcahdGUfq%2FCeFJGnLTKzzRpyATW6FgAk%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;400&quot; height=&quot;399&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1781172680412&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/countStore.ts

import { create } from &quot;zustand&quot;;

type CountStore = {
  count: number;
  increment: () =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
}

// use***Store
export const useCountStore = create&amp;lt;CountStore&amp;gt;((set) =&amp;gt; ({
  count: 0,
  decrement: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count - 1 })),
  increment: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count + 1 })),
  reset: () =&amp;gt; set({ count: 0 })
}));&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_1781172754147&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/CountOutside.tsx

import { useCountStore } from &quot;../store/countStore&quot;;

export default function CountOutside() {
  const count = useCountStore((state) =&amp;gt; state.count);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;CountOutside: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781172804884&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/CountDisplay.tsx

import { useCountStore } from &quot;../store/countStore&quot;;

export default function CountDisplay() {
  const count = useCountStore((state) =&amp;gt; state.count);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Count: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781172975408&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/CountButton.tsx

import { useCountStore } from &quot;../store/countStore&quot;;

export default function CountButton() {
  const increment = useCountStore((state) =&amp;gt; state.increment);
  const decrement = useCountStore((state) =&amp;gt; state.decrement);
  const reset = useCountStore((state) =&amp;gt; state.reset);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={decrement}&amp;gt;감소&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={reset}&amp;gt;리셋&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={increment}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand 매개변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 외부에서 매개변수를 전달받거나 비동기 함수를 사용할 때에도 매우 쉽게 처리 가능&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;pre id=&quot;code_1781198383709&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/CountButton.tsx

import { useCountStore } from &quot;../store/countStore&quot;;

export default function CountButton() {
  const increment = useCountStore((state) =&amp;gt; state.increment);
  const decrement = useCountStore((state) =&amp;gt; state.decrement);
  const reset = useCountStore((state) =&amp;gt; state.reset);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={decrement}&amp;gt;감소&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={reset}&amp;gt;리셋&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; increment(5)}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ increment(5) --&amp;gt; 5라는 매개변수 전달&lt;/p&gt;
&lt;pre id=&quot;code_1781198427203&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/countStore.ts

import { create } from &quot;zustand&quot;;

type CountStore = {
  count: number;
  increment: (amount: number) =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
}

export const useCountStore = create&amp;lt;CountStore&amp;gt;((set) =&amp;gt; ({
  count: 0,
  increment: (amount: number) =&amp;gt; set((state) =&amp;gt; ({ count: state.count + amount })), // 전달받은 값만큼 증가 
  decrement: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count - 1 })),
  reset: () =&amp;gt; set({ count: 0 })
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ amount: number&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;pre id=&quot;code_1781198573791&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/countStore.ts

import { create } from &quot;zustand&quot;;

type CountStore = {
  count: number;
  increment: (amount: number) =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};

export const useCountStore = create&amp;lt;CountStore&amp;gt;((set) =&amp;gt; ({
  count: 0,
  increment: async (amount: number) =&amp;gt; { // async 
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, 1000)); // await new Promise
    set((state) =&amp;gt; ({ count: state.count + amount }));
  },

  decrement: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count - 1 })),
  reset: () =&amp;gt; set({ count: 0 }),
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ async/await를 활용해 1초의 지연 후 상태를 갱신하는 비동기 함수 구현&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;Zustand persist&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand의 상태 데이터는 브라우저를 새로고침하면 모두 초기화됨&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;상태를 새로고침해도 유지되게 하기 위해서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;local storage, session storage&lt;/span&gt;에 상태를 동기화시켜야 함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 다른 전역 상태 관리 라이브러리는 그러한 스토리지 작업을 직접 해야 해야 했지만, &lt;b&gt;Zustand는 작업을 대신 처리해주는 persist 미들웨어 기능&lt;/b&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;color: #ee2323;&quot;&gt;미들웨어(middleware) : 특정 라이브러리의 핵심 동작을 확장하거나 가로채서 추가적인 기능을 수행할 수 있도록 지원하는 소프트웨어 구조&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 어떤 하나의 기능을 지칭하는 것은 X&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;Persist 미들웨어 구현 문법 및 사용법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;create 함수의 콜백 함수를 &lt;b&gt;persist() 미들웨어 함수&lt;/b&gt;로 감싸줌&lt;/li&gt;
&lt;li&gt;persist()의 &lt;b&gt;두 번째 매개변수로 객체 지정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;storage: createJSONStorage(() =&amp;gt; sessionStorage) : 생략 시 기본값은 localStorage&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;해당 객체에 name 속성을 정하여 스토리지에 저장될 고유 Key를 선언&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781199323810&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/countStore.ts

import { create } from &quot;zustand&quot;;
import { createJSONStorage, persist } from &quot;zustand/middleware&quot;;

type CountStore = {
  count: number;
  increment: (amount: number) =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};

export const useCountStore = create&amp;lt;CountStore&amp;gt;()(
  persist(
    (set) =&amp;gt; ({
      count: 0,
      increment: (amount: number) =&amp;gt; {
        set((state) =&amp;gt; ({ count: state.count + amount }));
      },
      decrement: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count - 1 })),
      reset: () =&amp;gt; set({ count: 0 }),
    }),
    {
      name: &quot;count-storage&quot;,
      storage: createJSONStorage(() =&amp;gt; sessionStorage) // 세션 스토리지에 저장되도록 설정 
    },
  ),
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand subscribeWithSelector&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;subscribeWithSelector 미들웨어 : &lt;span style=&quot;color: #ee2323;&quot;&gt;특정 상태가 변경될 때를 감지해서 특정 로직을 수행할 수 있는 구독 기능&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;color: #333333; text-align: start;&quot;&gt;subscribeWithSelector&lt;/span&gt; 미들웨어 구현 문법 및 사용법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스토어를 만들 때 &lt;b&gt;subscribeWithSelector 미들웨어 함수&lt;/b&gt;로 감싸줌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구독할 변수, 변수값에 변화가 생기면 실행할 함수 설정&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781200317725&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/countStore.ts

import { create } from &quot;zustand&quot;;
import {
  createJSONStorage,
  persist,
  subscribeWithSelector,
} from &quot;zustand/middleware&quot;;

type CountStore = {
  count: number;
  increment: (amount: number) =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};

export const useCountStore = create&amp;lt;CountStore&amp;gt;()(
  subscribeWithSelector(
    // subscribeWithSelector 미들웨어 적용
    persist(
      (set) =&amp;gt; ({
        count: 0,
        increment: (amount: number) =&amp;gt; {
          set((state) =&amp;gt; ({ count: state.count + amount }));
        },
        decrement: () =&amp;gt; set((state) =&amp;gt; ({ count: state.count - 1 })),
        reset: () =&amp;gt; set({ count: 0 }),
      }),
      {
        name: &quot;count-storage&quot;,
        storage: createJSONStorage(() =&amp;gt; sessionStorage), 
      },
    ),
  ),
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781200337059&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Count.tsx

import { useEffect } from &quot;react&quot;;
import CountGroup from &quot;./CountGroup&quot;;
import { useCountStore } from &quot;../store/countStore&quot;;

export default function Count() {
  useEffect(() =&amp;gt; {
    const unsubscibe = useCountStore.subscribe(
      (state) =&amp;gt; state.count, // count 상태를 구독하고 있다가, 
      (newCount) =&amp;gt; { // 상태에 변화가 생기면 해당 콜백 함수를 실행 
        console.log(&quot;newCount&quot; + newCount);
      },
    );
    return () =&amp;gt; {
      unsubscibe();
    };
  }, []);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;CountGroup /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand immer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;immer 미들웨어 : 사용자가 불변성을 고려하지 않아도 자동으로 불변성을 처리해주는 미들웨어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;&lt;b&gt;npm install immer&lt;/b&gt; 명령어를 사용하여 다운로드&amp;nbsp;&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;subscribeWithSelector 미들웨어 전체를 감싸는 것 X&lt;/li&gt;
&lt;li&gt;set 함수가 콜백 함수로 전달되는 함수의 영역을 감싸는 것 O&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781201042512&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/count/countStore.ts

import { create } from &quot;zustand&quot;;
import {
  createJSONStorage,
  persist,
  subscribeWithSelector,
} from &quot;zustand/middleware&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

type CountStore = {
  count: number;
  increment: (amount: number) =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};

export const useCountStore = create&amp;lt;CountStore&amp;gt;()(
  subscribeWithSelector(
    persist(
      immer((set) =&amp;gt; ({ // immer 미들웨어 적용
        count: 0,
        increment: (amount: number) =&amp;gt; {
          set((state) =&amp;gt; {
            state.count += amount;
          });
        },
        decrement: () =&amp;gt;
          set((state) =&amp;gt; {
            state.count -= 1;
          }),
        reset: () =&amp;gt;
          set((state) =&amp;gt; {
            state.count = 0;
          }),
      })),
      {
        name: &quot;count-storage&quot;,
        storage: createJSONStorage(() =&amp;gt; sessionStorage),
      },
    ),
  ),
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zustand devtools&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 가벼운 상태 관리 라이브러리 --&amp;gt; 자체 GUI 디버깅 도구를 내장하고 있지 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 대신 브라우저 확장 프로그램인 Redux Devtools와 완벽하게 연동되는 미들웨어를 제공&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;서비스 배포(Production) 환경에서 상태 변화 흐름이 무방비하게 노출되면 보안 취약점이 될 수 있고, 성능 저하를 유발하기도 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;there4; 개발(Development) 환경에서만 DevTools가 켜지도록 조건부 제어하는 것이 필수적&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;DevTools 자동화 설정 문법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;devtools 미들웨어의 두 번째 매개변수 객체에 설정 주입&lt;/li&gt;
&lt;li&gt;name : 디버거 창 내에서 각 스토어를 식별하기 위한 고유 이름표 지정&lt;/li&gt;
&lt;li&gt;enabled : 환경 변수를 활용하여 현재 빌드 모드가 development일 때만 동적으로 활성화 처리&amp;nbsp;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;{ enabled: false } --&amp;gt; 연동되지 않도록 설정&lt;/li&gt;
&lt;li&gt;ㄴ 개발모드에선 true, 그렇지 않을 때에는 false라고 지정하면 됨&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;{ enabled: import.meta.env.MODE === &quot;development&quot; }&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ㄴ 자동화&lt;/span&gt;&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;
&lt;pre id=&quot;code_1781201580052&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/countStore.ts

import { create } from &quot;zustand&quot;;
import {
  createJSONStorage,
  devtools,
  persist,
  subscribeWithSelector,
} from &quot;zustand/middleware&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

type CountStore = {
  count: number;
  increment: (amount: number) =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};

export const useCountStore = create&amp;lt;CountStore&amp;gt;()(
  devtools( // devtools 미들웨어 적용
    subscribeWithSelector(
      persist(
        immer((set) =&amp;gt; ({
          count: 0,
          increment: (amount: number) =&amp;gt; {
            set((state) =&amp;gt; {
              state.count += amount;
            });
          },
          decrement: () =&amp;gt;
            set((state) =&amp;gt; {
              state.count -= 1;
            }),
          reset: () =&amp;gt;
            set((state) =&amp;gt; {
              state.count = 0;
            }),
        })),
        {
          name: &quot;count-storage&quot;,
          storage: createJSONStorage(() =&amp;gt; sessionStorage),
        },
      ),
    ),
  ),
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/28</guid>
      <comments>https://fromfavor.tistory.com/28#entry28comment</comments>
      <pubDate>Fri, 12 Jun 2026 16:48:33 +0900</pubDate>
    </item>
    <item>
      <title>섹션 16. 데이터 통신</title>
      <link>https://fromfavor.tistory.com/27</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 패칭 기초 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 통신 : 컴포넌트가 서버에 외부 데이터를 요청하고, 그 응답을 받아 상태로 반영하여 UI를 업데이트하는 과정&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;API(Application Programming Interface) : 클라이언트가 서버와 통신하기 위한 통신 규칙&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REST API&lt;/li&gt;
&lt;li&gt;GraphQL&lt;/li&gt;
&lt;/ul&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;color: #ee2323;&quot;&gt;REST API : 주소로서 데이터를 구분하는 API 설계 방식&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) https://api.example.com/user/1?lang=ko&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 전체적인 주소 : URL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- https:// : 프로토콜&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- api.example.com : 도메인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- user, 1 : path, 세그먼트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- lang=ko : Query String&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(S) 통신 : 내부적으로 메소드를 활용하여 데이터 통신의 역할을 구분&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;GET
&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;POST
&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;PUT/PATCH
&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;DELETE
&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;json-server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json-server : JSON 파일만 있으면 바로 실행해볼 수 있는 가짜 REST API 서버 도구&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;b&gt;npm i json-server@0.17.3&lt;/b&gt; 명령어를 사용하여 다운로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;npm run server&lt;/b&gt; 명령어를 사용하여 서버 구동&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;thunder client&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;thunder client Extension : Visual Studio Code에서 API를 호출하고 그 결과를 눈으로 볼 수 있게 해주는 도구&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;PUT, PATCH의 차이점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PUT
&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;PATCH
&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;json-server 추가 기능&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;페이징(Pagination) 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;_page&lt;/b&gt;와 &lt;b&gt;_limit&lt;/b&gt; 쿼리 스트링을 사용하여 대량의 데이터를 쪼개서 요청 가능&lt;/li&gt;
&lt;li&gt;기본 페이징 (1페이지 당 기본 10개 출력)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GET) http://localhost:3000/posts?_page=1&lt;/li&gt;
&lt;li&gt;--&amp;gt; 1~10번까지의 데이터 10개 출력&amp;nbsp;&lt;/li&gt;
&lt;li&gt;GET) http://localhost:3000/posts?_page=2&lt;/li&gt;
&lt;li&gt;--&amp;gt; 11~20번까지의 데이터 10개 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;페이지 당 개수 제한(limit) 설정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GET) http://localhost:3000/posts?_page=1&amp;amp;_limit=5&lt;/li&gt;
&lt;li&gt;--&amp;gt; 1페이지에 해당하는 데이터 5개 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 텍스트 검색(Full-text Search) 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;q 파라미터&lt;/b&gt;를 사용하면 데이터 객체 내의 모든 필드(id, title, content 등)를 대상으로 키워드 포함 여부를 검사&lt;/li&gt;
&lt;li&gt;기본 검색
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GET) http://localhost:3000/posts?q=Post #22&lt;/li&gt;
&lt;li&gt;--&amp;gt; 데이터 중 어느 필드에든 &quot;Post #22&quot;라는 문자열이 포함된 데이터를 모두 찾아 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;응답 지연(Delay) 시뮬레이션 기능&lt;br /&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;li&gt;package.json 스크립트 등록&amp;nbsp;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&quot;scripts&quot; : { &quot;delay&quot; : &quot;npx json-server server/db.json --delay 2000&quot; }&lt;/li&gt;
&lt;li&gt;--&amp;gt; 터미널에 &lt;b&gt;npm run delay&lt;/b&gt; 명령어를 입력했을 때, 모든 API 요청이 내가 지정한 시간(2초)만큼 지연된 후에 응답함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fetch - 1 (basic)&lt;/h3&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;gt; 사이드 이펙트를 처리할 때 사용하는 useEffect 훅 이용&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780992570727&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Fetch from &quot;./components/Fetch&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Fetch /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780992590534&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Fetch.tsx

import { useEffect, useState } from &quot;react&quot;;

interface Posts { // 서버에서 받아올 데이터의 타입 정의
  id: number;
  title: string;
  views: number;
}

export default function Fetch() {

  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]); // 서버에서 가져온 데이터들을 저장할 변수 선언 

  // useEffect [] 빈 배열 --&amp;gt; 컴포넌트가 처음 켜질 때 딱 한 번만 실행 
  useEffect(() =&amp;gt; {
    fetch(&quot;http://localhost:3000/posts&quot;).then((response) =&amp;gt; {
      console.log(response);
      return response.json();
    }).then((data) =&amp;gt; setPosts(data));
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Fetch&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {posts.map((post) =&amp;gt; &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;)}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;useEffect에서 의존성 배열의 3가지 상태와 타이밍&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;b&gt;useEffect(() =&amp;gt; {}, []);&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;컴포넌트가 브라우저 화면에 최초로 딱 한 번 등장했을 때만 본문 함수를 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;다른 상태가 바뀌어서 컴포넌트가 리렌더링되어도 재실행 X&lt;/li&gt;
&lt;li&gt;초기 화면을 구성하기 위한 서버 데이터 최초 요청을 위해 쓰임&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&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;b&gt;useEffect(() =&amp;gt; {});&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;컴포넌트가 처음 켜질 때를 비롯하여 컴포넌트 내부의 상태가 하나라도 바뀌어서 리렌더링 될 때마다 본문 함수 재실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배열 안에 특정 상태값을 넣었을 때&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;useEffect(() =&amp;gt; {}, [keyword]);&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;컴포넌트가 처음 켜질 때 실행되고, 이후에 keyword라는 변수의 값이 바뀔 때마다 본문 함수를 실행&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;사용자가 검색창에 타이핑을 할 때마다 실시간으로 서버에서 다른 데이터를 검색해서 가져와야 할 때 쓰임&amp;nbsp;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;.then() : 기다렸다가 서버 응답을 받은 후에 순서대로 코드를 처리할 수 있도록 하는 비동기 안전장치&amp;nbsp;&lt;/span&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;--&amp;gt; 만약 .then()이 없다면 데이터를 다 받지도 않았는데 화면을 그리려 해서 undefined 에러가 발생&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;fetch - 2 (loading)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침을 하면 렌더링된 데이터가 깜빡거림&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;∵ 컴포넌트가 화면에 그려진 이후에 useEffect가 실행돼서 fetch API조차 비동기로 동작하기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;isLoading 상태를 정의하여 관리&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780994439447&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Fetch.tsx

import { useEffect, useState } from &quot;react&quot;;

interface Posts { 
  id: number;
  title: string;
  views: number;
}

export default function Fetch() {

  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]); 
  const [isLoading, setIsLoading] = useState(false); // 로딩 중일 때를 관리하기 위한 상태 정의

  useEffect(() =&amp;gt; {
    setIsLoading(true); // 바꿔주기
    fetch(&quot;http://localhost:3000/posts&quot;).then((response) =&amp;gt; {
      console.log(response);
      return response.json();
    }).then((data) =&amp;gt; setPosts(data))
    .finally(() =&amp;gt; { // 바꿔주기
      setIsLoading(false);
    });
  }, []);

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Fetch&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {posts.map((post) =&amp;gt; &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;)}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fetch - 3 (error)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 통신 에러&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;404 Error
&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;브라우저가 보낸 요청이 물리적으로 서버 컴퓨터에 완벽하게 도착했고, 서버가 Response를 돌려준 상황&lt;/li&gt;
&lt;li&gt;ㄴ 서버의 응답 : URL에 해당하는 데이터나 페이지가 내 컴퓨터에 존재하지 않음&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ex) API 주소에 오타가 났을 때, 서버 데이터베이스에서 삭제된 데이터를 조회하려고 할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(failed) network 에러
&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 통신 단계까지 도달하지 못함&amp;nbsp;&lt;/li&gt;
&lt;li&gt;브라우저가 서버 IP 주소를 못 찾거나, 인터넷 선이 끊겨있어 아예 연결을 맺지 못함&lt;/li&gt;
&lt;li&gt;ex) 로컬에서 server를 안 켜놓고 프론트엔드에서 fetch를 날렸을 때, CORS 에러가 발생해 브라우저가 강제로 요청을 차단했을 때&amp;nbsp;&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;
&lt;pre id=&quot;code_1780995372777&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Fetch.tsx

import { useEffect, useState } from &quot;react&quot;;

interface Posts {
  id: number;
  title: string;
  views: number;
}

export default function Fetch() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() =&amp;gt; {
    setIsLoading(true);
    fetch(&quot;http://localhost:3000/posts&quot;)
      .then((response) =&amp;gt; {
        if (!response.ok) throw new Error(&quot;네트워크 통신 오류&quot;);
        console.log(response);
        return response.json();
      })
      .then((data) =&amp;gt; setPosts(data))
      .catch((e) =&amp;gt; {
        console.log(e);
      })
      .finally(() =&amp;gt; {
        setIsLoading(false);
      });
  }, []);

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Fetch&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {posts.map((post) =&amp;gt; (
          &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;상황 A: 정상 작동&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;setIsLoading(true) --&amp;gt; 화면에 Loading... 출력&lt;/li&gt;
&lt;li&gt;fetch 성공 --&amp;gt; response.ok가 true이므로 if문 통과&lt;/li&gt;
&lt;li&gt;.json() 포장 뜯기 성공 --&amp;gt; setPosts(data)로 데이터 저장&amp;nbsp;&lt;/li&gt;
&lt;li&gt;.finally() 발동 --&amp;gt; setIsLoading(false)&lt;/li&gt;
&lt;li&gt;화면에 정상적인 포스트 리스트 출력&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 B: API 주소가 오타났을 때 (404 에러)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;setIsLoading(true) --&amp;gt; 화면에 Loading... 출력&lt;/li&gt;
&lt;li&gt;서버 컴퓨터엔 갔는데 주소가 존재 X --&amp;gt; response.ok가 false가 됨&amp;nbsp;&lt;/li&gt;
&lt;li&gt;throw new Error(...) 실행 --&amp;gt; 강제 에러&amp;nbsp;&lt;/li&gt;
&lt;li&gt;.then()은 패스, .catch()로 이동&lt;/li&gt;
&lt;li&gt;.catch((e) =&amp;gt; { ... }) 내부가 실행되면서 콘솔창에 에러 기록&lt;/li&gt;
&lt;li&gt;.finally()는 무조건 실행되므로 setIsLoading(false)&lt;/li&gt;
&lt;li&gt;화면 로딩은 멈추고 빈 리스트 출력&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 C: json-server를 아예 안 켰을 때 (failed Network 에러)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;setIsLoading(true) --&amp;gt; 화면에 Loading... 출력&lt;/li&gt;
&lt;li&gt;서버 컴퓨터를 못 찾아서 fetch가 시작하자마자 에러&amp;nbsp;&lt;/li&gt;
&lt;li&gt;.then()은 패스, .catch()로 이동&lt;/li&gt;
&lt;li&gt;.catch((e) =&amp;gt; { ... }) 내부가 실행되면서 콘솔창에 에러 기록&lt;/li&gt;
&lt;li&gt;.finally()는 무조건 실행되므로 setIsLoading(false)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;--&amp;gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; error&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;nbsp;상태를 정의하여 관리&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780996214041&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Fetch.tsx

import { useEffect, useState } from &quot;react&quot;;

interface Posts {
  id: number;
  title: string;
  views: number;
}

export default function Fetch() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {
    setIsLoading(true);
    setError(&quot;&quot;); // 빈 문자열
    fetch(&quot;http://localhost:3000/posts&quot;)
      .then((response) =&amp;gt; {
        if (!response.ok) throw new Error(&quot;네트워크 통신 오류&quot;);
        console.log(response);
        return response.json();
      })
      .then((data) =&amp;gt; setPosts(data))
      .catch((e) =&amp;gt; { // 에러가 발생했을 때 
        console.log(e);
        setError(e instanceof Error ? e.message : &quot;unknown error&quot;); 
      })
      .finally(() =&amp;gt; {
        setIsLoading(false);
      });
  }, []);

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  if (error) return &amp;lt;p&amp;gt;Error: {error}&amp;lt;/p&amp;gt; // 에러 출력

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Fetch&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {posts.map((post) =&amp;gt; (
          &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;&lt;span style=&quot;color: #ee2323;&quot;&gt;* 리액트에서 데이터 통신을 할 때는 최소 3개의 상태가 필요&lt;/span&gt;&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;li&gt;로딩을 제어할 상태&lt;/li&gt;
&lt;li&gt;에러를 제어할 상태&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fetch - 4 (signal)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbortController : 웹 브라우저가 제공하는 내장 API로, 하나 이상의 웹 요청을 원하는 시점에 강제로 중단(Abort)할 수 있는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Signal : controller와 fetch 요청을 연결해줌&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;new AbortController()&lt;/b&gt;를 통해 인스턴스 객체 생성 (signal 세트)&lt;/li&gt;
&lt;li&gt;fetch() &lt;b&gt;매개변수에 signal을 넘겨주어&lt;/b&gt;, 해당 요청이 controller의 명령을 받도록 연결&lt;/li&gt;
&lt;li&gt;원하는 시점에 &lt;b&gt;controller.abort() 메소드&lt;/b&gt;를 호출하면, 연결되어 있던 fetch 요청이 즉시 취소됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780996685936&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Fetch.tsx

import { useEffect, useState } from &quot;react&quot;;

interface Posts {
  id: number;
  title: string;
  views: number;
}

export default function Fetch() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {

    const controller = new AbortController(); // Abort 객체 

    setIsLoading(true);
    setError(&quot;&quot;); 
    fetch(&quot;http://localhost:3000/posts&quot;, {signal: controller.signal}) // signal 넘겨주기
      .then((response) =&amp;gt; {
        if (!response.ok) throw new Error(&quot;네트워크 통신 오류&quot;);
        console.log(response);
        return response.json();
      })
      .then((data) =&amp;gt; setPosts(data))
      .catch((e) =&amp;gt; { 
        console.log(e);
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;AbortError&quot;) setError(e.message);
      })
      .finally(() =&amp;gt; {
        if (!controller.signal.aborted) setIsLoading(false);
      });

      return () =&amp;gt; controller.abort(); // 취소

  }, []);

  if (isLoading) return &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;;

  if (error) return &amp;lt;p&amp;gt;Error: {error}&amp;lt;/p&amp;gt; 

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Fetch&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {posts.map((post) =&amp;gt; (
          &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fetch - 5 (async)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect 훅 자체를 async로 지정 불가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; use Effect 내부에서 async 함수를 새롭게 정의한 뒤 바로 실행시키기 가능&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;async/await 방식&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;li&gt;try-catch 문 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781000123816&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Fetch.tsx

import { useEffect, useState } from &quot;react&quot;;
interface Posts {
  id: number;
  title: string;
  views: number;
}
export default function Fetch() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();

    const fetchPosts = async () =&amp;gt; {
      // 데이터 통신이 일어나기 전에 실행돼야 함 
      setIsLoading(true);
      setError(&quot;&quot;);

      // try-catch 문
      try {
        const response = await fetch(&quot;http://localhost:3000/posts&quot;, {
          signal: controller.signal,
        });
        if (!response.ok) throw new Error(&quot;네트워크 통신 오류&quot;);
        const data = await response.json();
        setPosts(data);
      } catch (e) {
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;AbortError&quot;) setError(e.message);
      } finally {
        if (!controller.signal.aborted) setIsLoading(false);
      }
    };

    fetchPosts();

    return () =&amp;gt; controller.abort();
  }, []);

  if (error) return &amp;lt;p&amp;gt;Error: {error}&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Fetch&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {isLoading ? (
          &amp;lt;p&amp;gt;Loading... &amp;lt;/p&amp;gt;
        ) : (
          posts.map((post) =&amp;gt; &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;)
        )}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fetch - crud&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST/PUT/PATCH처럼 가공할 데이터가 있을 때에는 headers, body 필요&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;headers
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;body 안에 보낼 데이터의 형식을 알림&lt;/li&gt;
&lt;li&gt;&quot;Content-Type&quot;: &quot;application/json&quot; : JSON 텍스트 양식임을 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;body
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자바스크립트 객체를 서버가 읽을 수 있는 문자열, JSON으로 변환해서 주입&amp;nbsp;&lt;/li&gt;
&lt;li&gt;ㄴ JSON.stringify({ ... })&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1781002636099&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/FetchCrud.tsx

export default function FetchCrud () {

  const fetchGet = async() =&amp;gt; {
    const response = await fetch(&quot;http://localhost:3000/posts&quot;);
    const data = await response.json();
    console.log(data);
  };

  const fetchPost = async() =&amp;gt; {
    const response = await fetch(&quot;http://localhost:3000/posts&quot;, {
      method: &quot;POST&quot;,
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;
      },
      body: JSON.stringify({
        title: &quot;a new title&quot;,
        views: 155
      }),
    });

    const data = await response.json();

    console.log(data);
  };

  const fetchPut = async() =&amp;gt; {
    const response = await fetch(&quot;http://localhost:3000/posts/1&quot;, {
      method: &quot;PUT&quot;,
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;
      },
      body: JSON.stringify({
        title: &quot;a modify title&quot;,
        views: 155
      }),
    });

    const data = await response.json();

    console.log(data);
  };

  const fetchPatch = async() =&amp;gt; {
    const response = await fetch(&quot;http://localhost:3000/posts/1&quot;, {
      method: &quot;PATCH&quot;,
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;
      },
      body: JSON.stringify({
        title: &quot;a patch title&quot;,
        views: 155
      }),
    });

    const data = await response.json();

    console.log(data);
  };

  const fetchDelete = async() =&amp;gt; {
    // 돌려받을 내용물이 없기 때문에 const data = await response.json() 생략
   
    await fetch(&quot;http://localhost:3000/posts/101&quot;, {
      method: &quot;DELETE&quot;
    });
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={fetchGet}&amp;gt;GET&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchPost}&amp;gt;POST&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchPut}&amp;gt;PUT&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchPatch}&amp;gt;PATCH&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchDelete}&amp;gt;DELETE&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fetch - axios&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;npm install axios&lt;/b&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;Fetch 대비 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 변환&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;응답 스트림을 객체로 변환하는 .json() 과정이 생략&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;서버가 돌려준 데이터는 항상 response.data 안에 객체 형태로 자동 바인딩&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동 직렬화
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;POST/PUT 요청 시 &lt;span style=&quot;color: #ee2323;&quot;&gt;JSON.stringify() 가공이나 헤더(Content-Type) 지정을 생략&lt;/span&gt;하고, 순수 자바스크립트 객체를 바디에 곧바로 담아 전송 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;직관적인 에러 핸들링
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;fetch와 달리 404, 500 발생 시 별도의 if (!response.ok) 검증 없이도 자동으로 catch 구역으로 예외를 던져줌&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;
&lt;pre id=&quot;code_1781162901283&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/AxiosCrud.tsx

import axios from &quot;axios&quot;;

export default function AxiosCrud() {
  const fetchGet = async () =&amp;gt; {
    const { data } = await axios.get(&quot;http://localhost:3000/posts&quot;);
    console.log(data);
  };
  
  const fetchPost = async () =&amp;gt; {
    const { data } = await axios.post(&quot;http://localhost:3000/posts&quot;, {
      title: &quot;a axios data&quot;,
      views: 50,
    });
    console.log(data);
  };
  
  const fetchPut = async () =&amp;gt; {
    const { data } = await axios.put(&quot;http://localhost:3000/posts/101&quot;, {
      title: &quot;a axios modify data&quot;,
      view: 500,
    });
    console.log(data);
  };
  
  const fetchPatch = async () =&amp;gt; {
    const { data } = await axios.patch(&quot;http://localhost:3000/posts/101&quot;, {
      title: &quot;a axios modify data&quot;,
      view: 500,
    });
    console.log(data);
  };
  
  const fetchDelete = async () =&amp;gt; {
    const { data, status } = await axios.delete(
      &quot;http://localhost:3000/posts/101&quot;,
    );
    console.log(data, status);
  };
  
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={fetchGet}&amp;gt;GET&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchPost}&amp;gt;POST&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchPut}&amp;gt;PUT&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchPatch}&amp;gt;PATCH&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={fetchDelete}&amp;gt;DELETE&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;axios - total&lt;/h3&gt;
&lt;pre id=&quot;code_1781163996087&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Axios.tsx

import { useEffect, useState } from &quot;react&quot;;
import axios from &quot;axios&quot;;

interface Posts {
  id: number;
  title: string;
  views: number;
}
export default function Axios() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();
    const axiosPosts = async () =&amp;gt; {
      setIsLoading(true);
      setError(&quot;&quot;);
      try {
        const { data } = await axios.get(&quot;http://localhost:3000/posts&quot;, {
          signal: controller.signal,
        });
        setPosts(data);

      } catch (e) {
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;CanceledError&quot;) setError(e.message);
      } finally {
        if (!controller.signal.aborted) setIsLoading(false);
      }
    };

    axiosPosts();

    return () =&amp;gt; controller.abort();
  }, []);

  if (error) return &amp;lt;p&amp;gt;Error: {error}&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Axios&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {isLoading ? (
          &amp;lt;p&amp;gt;Loading... &amp;lt;/p&amp;gt;
        ) : (
          posts.map((post) =&amp;gt; &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;)
        )}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;axios - instance&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;axios.create() 메소드를 활용하여 인스턴스 객체를 간편하게 생성 가능&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 공통으로 사용할 baseURL, timeout, headers 등의 설정을 미리 주입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781164310088&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/api/axios.ts

import axios from &quot;axios&quot;;

// axios 인스턴스 객체 생성
export const axiosInstance = axios.create({
  baseURL: &quot;http://localhost:3000&quot;,
  timeout: 5000, // 5s
  headers: { // 생략 가능
    &quot;Content-Type&quot;: &quot;application/json&quot;,
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1781164350882&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Axios.tsx

import { useEffect, useState } from &quot;react&quot;;
import { axiosInstance } from &quot;../api/axios&quot;;

interface Posts {
  id: number;
  title: string;
  views: number;
}
export default function Axios() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();
    const axiosPosts = async () =&amp;gt; {
      setIsLoading(true);
      setError(&quot;&quot;);
      try {
        // axios --&amp;gt; axiosInstance
        const { data } = await axiosInstance.get(&quot;/posts&quot;, {
          signal: controller.signal,
        });
        setPosts(data);

      } catch (e) {
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;CanceledError&quot;) setError(e.message);
      } finally {
        if (!controller.signal.aborted) setIsLoading(false);
      }
    };

    axiosPosts();

    return () =&amp;gt; controller.abort();
  }, []);

  if (error) return &amp;lt;p&amp;gt;Error: {error}&amp;lt;/p&amp;gt;;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h3&amp;gt;Axios&amp;lt;/h3&amp;gt;
      &amp;lt;ul&amp;gt;
        {isLoading ? (
          &amp;lt;p&amp;gt;Loading... &amp;lt;/p&amp;gt;
        ) : (
          posts.map((post) =&amp;gt; &amp;lt;li key={post.id}&amp;gt;{post.title}&amp;lt;/li&amp;gt;)
        )}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Post Browser 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스켈레톤 UI : 데이터가 로딩 중일 때, 멈춰있는 듯한 회전 스피너 대신 실제 콘텐츠가 그려질 레이아웃의 뼈대를 회색 음영이나 애니메이션으로 먼저 보여주는 패턴&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;Post Browser 데이터 렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;axios 인스턴스 객체 생성&lt;/p&gt;
&lt;pre id=&quot;code_1781166320425&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/api/axios.ts

import axios from &quot;axios&quot;;

export const axiosInstance = axios.create({
  baseURL: &quot;http://localhost:3000&quot;,
  timeout: 5000,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&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;Posts 타입 정의&lt;/p&gt;
&lt;pre id=&quot;code_1781166383215&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/types/post.d.ts

interface Posts {
  id: number;
  title: string;
  views: number;
}&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;PostList.tsx 수정 - 데이터가 있을 때&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781166418099&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/PostList.tsx

import { useEffect, useState } from &quot;react&quot;;
import PostCard from &quot;./PostCard&quot;;
import { axiosInstance } from &quot;../api/axios&quot;;
import LoadingState from &quot;./LoadingState&quot;;
import ErrorState from &quot;./ErrorState&quot;;

export default function PostList() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  useEffect(() =&amp;gt; {
    const fetchPosts = async () =&amp;gt; {
      try {
        setIsLoading(true);
        const { data } = await axiosInstance.get(&quot;/posts&quot;);
        setPosts(data);
      } catch (e) {
        setError(e instanceof Error ? e.message : &quot;unknown error&quot;);
      } finally {
        setIsLoading(false);
      }
    };
    fetchPosts();
  }, []);

  return (
    &amp;lt;div className=&quot;mb-8&quot;&amp;gt;
      {/* 데이터가 없을 때 */}
      {/* &amp;lt;NoData /&amp;gt; */}
      {/* 로딩 중일 때 */}
      {/* &amp;lt;LoadingState /&amp;gt; */}
      {/* 에러가 발생했을 때 */}
      {/* &amp;lt;ErrorState /&amp;gt; */}

      {/* 데이터가 있을 때 */}
      {isLoading ? (
        &amp;lt;LoadingState /&amp;gt;
      ) : error ? (
        &amp;lt;ErrorState /&amp;gt;
      ) : (
        &amp;lt;div className=&quot;grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6&quot;&amp;gt;
          {posts.map((post) =&amp;gt; &amp;lt;PostCard key={post.id} {...post} /&amp;gt;)}
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}&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;PostCard.tsx 수정&lt;/p&gt;
&lt;pre id=&quot;code_1781166448586&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/PostCard.tsx

import { Eye } from &quot;lucide-react&quot;;

export default function PostCard({id, title, views}: Posts) {
  return (
    &amp;lt;div className=&quot;bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden border border-gray-100&quot;&amp;gt;
      &amp;lt;div className=&quot;h-2 bg-gradient-to-r from-indigo-500 to-teal-500&quot;&amp;gt;&amp;lt;/div&amp;gt;
      &amp;lt;div className=&quot;p-6&quot;&amp;gt;
        &amp;lt;div className=&quot;flex justify-between items-start mb-4&quot;&amp;gt;
          &amp;lt;h2 className=&quot;text-lg font-semibold text-gray-900 truncate&quot;&amp;gt;
            {title}
          &amp;lt;/h2&amp;gt;
          &amp;lt;div className=&quot;flex items-center text-gray-500 font-medium text-sm&quot;&amp;gt;
            &amp;lt;Eye className=&quot;h-4 w-4 mr-1&quot; /&amp;gt;
            &amp;lt;span&amp;gt;{views}&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div className=&quot;mt-4 flex justify-between items-center&quot;&amp;gt;
          &amp;lt;span className=&quot;text-xs font-medium px-2.5 py-0.5 rounded-full bg-indigo-50 text-indigo-700&quot;&amp;gt;
            ID: {id}
          &amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Post Browser 페이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zunstand + Immer 페이징 스토어&lt;/p&gt;
&lt;pre id=&quot;code_1781169088853&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/postStore.ts

import { create } from &quot;zustand&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

interface PostStore {
  currentPage: number;
  limit: number;
  getTotalPages: () =&amp;gt; number;
  setCurrentPages: (page: number) =&amp;gt; void;
  setLimit: (amount: number) =&amp;gt; void;
}

export const usePostStore = create&amp;lt;PostStore&amp;gt;()(
  immer((set, get) =&amp;gt; ({
    currentPage: 1,
    limit: 10,
    getTotalPages: () =&amp;gt; Math.ceil(100 / get().limit),
    setCurrentPages: (page: number) =&amp;gt; set((state) =&amp;gt; {
      state.currentPage = page;
    }),
    setLimit: (amount: number) =&amp;gt; set((state) =&amp;gt; {
      state.limit = amount;
    })
  })),
);&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;Pagination.tsx 수정&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781169052615&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/Pagination.tsx

import {
  ChevronLeft,
  ChevronRight,
  ChevronsLeft,
  ChevronsRight,
} from &quot;lucide-react&quot;;
import { usePostStore } from &quot;../store/postStore&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;

export default function Pagination() {
  const currentPage = usePostStore((state) =&amp;gt; state.currentPage);
  const totalPages = usePostStore((state) =&amp;gt; state.getTotalPages());
  const setCurrentPages = usePostStore((state) =&amp;gt; state.setCurrentPages);

  const getPageNumbers = () =&amp;gt; {
    const pageNumbers = [];
    const maxPagesToShow = 5;

    if (totalPages &amp;lt; maxPagesToShow) {
      for (let i = 1; i &amp;lt;= totalPages; i++) {
        pageNumbers.push(i);
      }
    } else {
      let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
      let endPage = startPage + maxPagesToShow - 1;

      if (endPage &amp;gt; totalPages) {
        endPage = totalPages;
        startPage = Math.max(1, endPage - maxPagesToShow + 1);
      }

      for (let i = startPage; i &amp;lt;= endPage; i++) {
        pageNumbers.push(i);
      }
    }
    return pageNumbers;
  };

  const pageNumbers = getPageNumbers();
  console.log(currentPage, totalPages, pageNumbers);

  return (
    &amp;lt;div className=&quot;flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg shadow-sm&quot;&amp;gt;
      &amp;lt;div className=&quot;flex flex-1 justify-between sm:hidden&quot;&amp;gt;
        {/* 현재 페이지가 1페이지면 : text-gray-300 cursor-not-allowed  */}
        {/* 그게 아니라면(기본값): text-gray-700 hover:bg-gray-50 */}
        &amp;lt;button
          className={twMerge(
            `relative inline-flex items-center rounded-md px-4 py-2 text-sm font-medium`,
            currentPage === 1
              ? &quot;text-gray-300 cursor-not-allowed&quot;
              : &quot;text-gray-700 hover:bg-gray-50&quot;,
          )}
          onClick={() =&amp;gt; setCurrentPages(Math.max(1, currentPage - 1))}
        &amp;gt;
          Previous
        &amp;lt;/button&amp;gt;
        {/* page === totalPage -&amp;gt; text-gray-300 cursor-not-allowed */}
        {/* text-gray-700 hover:bg-gray-50 */}
        &amp;lt;button
          className={twMerge(
            `relative ml-3 inline-flex items-center rounded-md px-4 py-2 text-sm font-medium`,
            currentPage === totalPages
              ? &quot;text-gray-300 cursor-not-allowed&quot;
              : &quot;text-gray-700 hover:bg-gray-50&quot;,
          )}
          onClick={() =&amp;gt; setCurrentPages(Math.min(currentPage + 1, totalPages))}
        &amp;gt;
          Next
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;hidden sm:flex sm:flex-1 sm:items-center sm:justify-between&quot;&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;p className=&quot;text-sm text-gray-700&quot;&amp;gt;
            Showing page &amp;lt;span className=&quot;font-medium&quot;&amp;gt;1&amp;lt;/span&amp;gt; of{&quot; &quot;}
            &amp;lt;span className=&quot;font-medium&quot;&amp;gt;1&amp;lt;/span&amp;gt; pages
          &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div&amp;gt;
          &amp;lt;nav
            className=&quot;isolate inline-flex -space-x-px rounded-md shadow-sm&quot;
            aria-label=&quot;Pagination&quot;
          &amp;gt;
            {/* 클릭하면 1 페이지로 */}
            {/* page === 1 -&amp;gt; cursor-not-allowed */}
            {/* hover:bg-gray-50 */}
            &amp;lt;button
              className={twMerge(
                `relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0`,
                currentPage === 1 ? &quot;cursor-not-allowed&quot; : &quot;hover:bg-gray-50&quot;,
              )}
              onClick={() =&amp;gt; setCurrentPages(1)}
            &amp;gt;
              &amp;lt;span className=&quot;sr-only&quot;&amp;gt;First page&amp;lt;/span&amp;gt;
              &amp;lt;ChevronsLeft className=&quot;h-5 w-5&quot; aria-hidden=&quot;true&quot; /&amp;gt;
            &amp;lt;/button&amp;gt;

            {/* 클릭하면 1페이지 감소 */}
            {/* page === 1 -&amp;gt; cursor-not-allowed */}
            {/* hover:bg-gray-50 */}
            &amp;lt;button
              className={twMerge(
                `relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0`,
                currentPage === 1 ? &quot;cursor-not-allowed&quot; : &quot;hover:bg-gray-50&quot;,
              )}
              onClick={() =&amp;gt; setCurrentPages(Math.max(1, currentPage - 1))}
            &amp;gt;
              &amp;lt;span className=&quot;sr-only&quot;&amp;gt;Previous&amp;lt;/span&amp;gt;
              &amp;lt;ChevronLeft className=&quot;h-5 w-5&quot; aria-hidden=&quot;true&quot; /&amp;gt;
            &amp;lt;/button&amp;gt;

            {/* 현재 페이지는 z-10 bg-indigo-600 text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 */}
            {/* 그게아니라면(기본값) text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 */}
            {pageNumbers.map((page) =&amp;gt; (
              &amp;lt;button
                key={page}
                className={twMerge(
                  `relative inline-flex items-center px-4 py-2 text-sm font-semibold`,
                  currentPage === page
                    ? &quot;z-10 bg-indigo-600 text-white focus:z-20 focus-visible:outline-offset-2 focus-visible:outline-indigo-600&quot;
                    : &quot;text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0&quot;,
                )}
                onClick={() =&amp;gt; setCurrentPages(page)}
              &amp;gt;
                {page}
              &amp;lt;/button&amp;gt;
            ))}

            {/* 클릭하면 맨 1페이지 증가 */}
            {/* 현재 페이지가 총 페이지랑 같으면 : cursor-not-allowed */}
            {/* 그게 아니라면(기본값): hover:bg-gray-50 */}
            &amp;lt;button
              className={twMerge(
                `relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0 hover:bg-gray-50`,

                currentPage === totalPages
                  ? &quot;cursor-not-allowed&quot;
                  : &quot;hover:bg-gray-50&quot;,
              )}
              onClick={() =&amp;gt; setCurrentPages(Math.min(currentPage + 1, totalPages))}
            &amp;gt;
              &amp;lt;span className=&quot;sr-only&quot;&amp;gt;Next&amp;lt;/span&amp;gt;
              &amp;lt;ChevronRight className=&quot;h-5 w-5&quot; aria-hidden=&quot;true&quot; /&amp;gt;
            &amp;lt;/button&amp;gt;

            {/* 클릭하면 맨 마지막 페이지로 */}
            {/* 현재 페이지가 총 페이지랑 같으면 : cursor-not-allowed */}
            {/* 그게 아니라면(기본값): hover:bg-gray-50 */}
            &amp;lt;button
              className={twMerge(
                `relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 focus:z-20 focus:outline-offset-0 hover:bg-gray-50`,
                currentPage === totalPages
                  ? &quot;cursor-not-allowed&quot;
                  : &quot;hover:bg-gray-50&quot;,
              )}
              onClick={() =&amp;gt; setCurrentPages(totalPages)}
            &amp;gt;
              &amp;lt;span className=&quot;sr-only&quot;&amp;gt;Last page&amp;lt;/span&amp;gt;
              &amp;lt;ChevronsRight className=&quot;h-5 w-5&quot; aria-hidden=&quot;true&quot; /&amp;gt;
            &amp;lt;/button&amp;gt;
          &amp;lt;/nav&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&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;PostHeader.tsx 수정&lt;/p&gt;
&lt;pre id=&quot;code_1781169136794&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/PostHeader.tsx

import { LayoutGrid, Search } from &quot;lucide-react&quot;;
import { usePostStore } from &quot;../store/postStore&quot;;

export default function PostHeader() {
  const setLimit = usePostStore((state) =&amp;gt; state.setLimit);
  const setCurrentPages = usePostStore((state) =&amp;gt; state.setCurrentPages);

  return (
    &amp;lt;div className=&quot;mb-8 space-y-4&quot;&amp;gt;
      &amp;lt;div className=&quot;flex items-center justify-between&quot;&amp;gt;
        &amp;lt;h1 className=&quot;text-2xl sm:text-3xl font-bold text-gray-900&quot;&amp;gt;
          &amp;lt;span className=&quot;inline-flex items-center gap-2&quot;&amp;gt;
            &amp;lt;LayoutGrid className=&quot;h-6 w-6 text-indigo-600&quot; /&amp;gt;
            &amp;lt;span&amp;gt;Posts Browser&amp;lt;/span&amp;gt;
          &amp;lt;/span&amp;gt;
        &amp;lt;/h1&amp;gt;

        &amp;lt;div className=&quot;flex items-center gap-2&quot;&amp;gt;
          &amp;lt;label
            htmlFor=&quot;pageSize&quot;
            className=&quot;text-sm font-medium text-gray-700&quot;
          &amp;gt;
            Show:
          &amp;lt;/label&amp;gt;
          &amp;lt;select
            id=&quot;pageSize&quot;
            className=&quot;block w-20 rounded-md border-gray-300 py-1.5 px-3 bg-white text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm&quot;
            onChange={(e) =&amp;gt; {
              setCurrentPages(1);
              setLimit(Number(e.target.value));
            }}
            defaultValue={10}
          &amp;gt;
            &amp;lt;option value={5}&amp;gt;5&amp;lt;/option&amp;gt;
            &amp;lt;option value={10}&amp;gt;10&amp;lt;/option&amp;gt;
            &amp;lt;option value={20}&amp;gt;20&amp;lt;/option&amp;gt;
            &amp;lt;option value={50}&amp;gt;50&amp;lt;/option&amp;gt;
          &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;relative rounded-md shadow-sm  w-full&quot;&amp;gt;
        &amp;lt;div className=&quot;pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 w-full&quot;&amp;gt;
          &amp;lt;Search className=&quot;h-5 w-5 text-gray-400&quot; aria-hidden=&quot;true&quot; /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          placeholder=&quot;Search posts by title...&quot;
          className=&quot;block w-full rounded-md border-0 py-3 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Post Browser 검색&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 위한 변수 추가 선언&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781170358553&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/store/postStore.ts

import { create } from &quot;zustand&quot;;
import { immer } from &quot;zustand/middleware/immer&quot;;

interface PostStore {
  currentPage: number;
  limit: number;
  term: string;
  getTotalPages: () =&amp;gt; number;
  setCurrentPages: (page: number) =&amp;gt; void;
  setLimit: (amount: number) =&amp;gt; void;
  setTerm: (text: string) =&amp;gt; void;
}

export const usePostStore = create&amp;lt;PostStore&amp;gt;()(
  immer((set, get) =&amp;gt; ({
    currentPage: 1,
    limit: 10,
    term: &quot;&quot;, // 검색을 위한 변수 선언 
    getTotalPages: () =&amp;gt; Math.ceil(100 / get().limit),
    setCurrentPages: (page: number) =&amp;gt; set((state) =&amp;gt; {
      state.currentPage = page;
    }),
    setLimit: (amount: number) =&amp;gt; set((state) =&amp;gt; {
      state.limit = amount;
    }),
    setTerm: (text: string) =&amp;gt; set((state) =&amp;gt; {
      state.term = text;
    })
  })),
);&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;PostHeader.tsx 수정 - 검색 기능&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1781170437445&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/PostHeader.tsx

import { LayoutGrid, Search } from &quot;lucide-react&quot;;
import { usePostStore } from &quot;../store/postStore&quot;;
import { useEffect, useState } from &quot;react&quot;;

export default function PostHeader() {
  const setLimit = usePostStore((state) =&amp;gt; state.setLimit);
  const setCurrentPages = usePostStore((state) =&amp;gt; state.setCurrentPages);
  const setTerm = usePostStore((state) =&amp;gt; state.setTerm);

  // 디바운스 기법 활용 
  const [input, setInput] = useState(&quot;&quot;);
  useEffect(() =&amp;gt; {
    const timer = setTimeout(() =&amp;gt; {
      setCurrentPages(1);
      setTerm(input);
    }, 1000);

    return () =&amp;gt; clearTimeout(timer);
  }, [input, setTerm]);

  return (
    &amp;lt;div className=&quot;mb-8 space-y-4&quot;&amp;gt;
      &amp;lt;div className=&quot;flex items-center justify-between&quot;&amp;gt;
        &amp;lt;h1 className=&quot;text-2xl sm:text-3xl font-bold text-gray-900&quot;&amp;gt;
          &amp;lt;span className=&quot;inline-flex items-center gap-2&quot;&amp;gt;
            &amp;lt;LayoutGrid className=&quot;h-6 w-6 text-indigo-600&quot; /&amp;gt;
            &amp;lt;span&amp;gt;Posts Browser&amp;lt;/span&amp;gt;
          &amp;lt;/span&amp;gt;
        &amp;lt;/h1&amp;gt;

        &amp;lt;div className=&quot;flex items-center gap-2&quot;&amp;gt;
          &amp;lt;label
            htmlFor=&quot;pageSize&quot;
            className=&quot;text-sm font-medium text-gray-700&quot;
          &amp;gt;
            Show:
          &amp;lt;/label&amp;gt;
          &amp;lt;select
            id=&quot;pageSize&quot;
            className=&quot;block w-20 rounded-md border-gray-300 py-1.5 px-3 bg-white text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm&quot;
            onChange={(e) =&amp;gt; {
              setCurrentPages(1);
              setLimit(Number(e.target.value));
            }}
            defaultValue={10}
          &amp;gt;
            &amp;lt;option value={5}&amp;gt;5&amp;lt;/option&amp;gt;
            &amp;lt;option value={10}&amp;gt;10&amp;lt;/option&amp;gt;
            &amp;lt;option value={20}&amp;gt;20&amp;lt;/option&amp;gt;
            &amp;lt;option value={50}&amp;gt;50&amp;lt;/option&amp;gt;
          &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;relative rounded-md shadow-sm  w-full&quot;&amp;gt;
        &amp;lt;div className=&quot;pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 w-full&quot;&amp;gt;
          &amp;lt;Search className=&quot;h-5 w-5 text-gray-400&quot; aria-hidden=&quot;true&quot; /&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          placeholder=&quot;Search posts by title...&quot;
          className=&quot;block w-full rounded-md border-0 py-3 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm&quot;
          onChange={(e) =&amp;gt; setInput(e.target.value)}
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&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;PostList.tsx 수정 - 데이터가 없을 때&lt;/p&gt;
&lt;pre id=&quot;code_1781170479055&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/PostList.tsx

import { useEffect, useState } from &quot;react&quot;;
import PostCard from &quot;./PostCard&quot;;
import { axiosInstance } from &quot;../api/axios&quot;;
import LoadingState from &quot;./LoadingState&quot;;
import ErrorState from &quot;./ErrorState&quot;;
import { usePostStore } from &quot;../store/postStore&quot;;
import NoData from &quot;./NoData&quot;;

export default function PostList() {
  const [posts, setPosts] = useState&amp;lt;Posts[]&amp;gt;([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(&quot;&quot;);

  // 사용자가 입력한 검색어 
  const term = usePostStore((state) =&amp;gt; state.term); 

  const currentPage = usePostStore((state) =&amp;gt; state.currentPage);
  const limit = usePostStore((state) =&amp;gt; state.limit);

  useEffect(() =&amp;gt; {
    const controller = new AbortController();

    const fetchPosts = async () =&amp;gt; {
      try {
        setIsLoading(true);
        const { data } = await axiosInstance.get(
          `/posts?_page=${currentPage}&amp;amp;_limit=${limit}&amp;amp;q=${encodeURIComponent(term)}`, { signal: controller.signal }
        );
        setPosts(data);
      } catch (e) {
        if (e instanceof Error &amp;amp;&amp;amp; e.name !== &quot;CanceledError&quot;) setError(e.message);
      } finally {
        if (!controller.signal.aborted) setIsLoading(false);
      }
    };
    fetchPosts();

    return () =&amp;gt; controller.abort();
  }, [currentPage, limit, term]); // 의존성 배열에 term 추가 

  return (
    &amp;lt;div className=&quot;mb-8&quot;&amp;gt;
      {/* 데이터가 없을 때 */}
      {/* &amp;lt;NoData /&amp;gt; */}
      {/* 로딩 중일 때 */}
      {/* &amp;lt;LoadingState /&amp;gt; */}
      {/* 에러가 발생했을 때 */}
      {/* &amp;lt;ErrorState /&amp;gt; */}

      {/* 데이터가 있을 때 */}
      {isLoading ? (
        &amp;lt;LoadingState /&amp;gt;
      ) : error ? (
        &amp;lt;ErrorState /&amp;gt;
      ) : (
        posts.length &amp;gt; 0 ? &amp;lt;div className=&quot;grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6&quot;&amp;gt;
          {posts.map((post) =&amp;gt; (
            &amp;lt;PostCard key={post.id} {...post} /&amp;gt;
          ))}
        &amp;lt;/div&amp;gt; : &amp;lt;NoData /&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}&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;디바운스(Debounce) : 웹 개발에서 자주 발생하는 이벤트(키보드 타이핑, 스크롤, 마우스 움직임 등)가 &lt;span style=&quot;color: #ee2323;&quot;&gt;연속해서 게릴라성으로 터질 때, 이를 매번 처리하지 않고 가장 마지막에 터진 이벤트만 딱 한 번 실행&lt;/span&gt;하도록 통제하는 기법&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 새로운 이벤트가 유입될 때마다 기존 setTimeout 타이머를 초기화(&lt;b&gt;clearTimeout&lt;/b&gt;)하고 새 타이머를 설정하는 매커니즘&amp;nbsp;&lt;/p&gt;</description>
      <category>React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/27</guid>
      <comments>https://fromfavor.tistory.com/27#entry27comment</comments>
      <pubDate>Thu, 11 Jun 2026 18:38:19 +0900</pubDate>
    </item>
    <item>
      <title>섹션 13. 전역 상태 관리 - Context API</title>
      <link>https://fromfavor.tistory.com/26</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;전역 상태 관리가 필요한 이유&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb4GGO/dJMcageHovK/hKXntWt9eSbjTa4m0ExeXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb4GGO/dJMcageHovK/hKXntWt9eSbjTa4m0ExeXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb4GGO/dJMcageHovK/hKXntWt9eSbjTa4m0ExeXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb4GGO%2FdJMcageHovK%2FhKXntWt9eSbjTa4m0ExeXk%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;400&quot; height=&quot;387&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CounOutside, CountDisplay, CountButton에서 사용할 상태를 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 공통 부모 컴포넌트인 App 컴포넌트 내부에 정의&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;Count, CountGroup 컴포넌트는 상태값을 사용하지 않지만 props를 받아서 하위 컴포넌트로 전달해야 함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ &lt;span style=&quot;color: #ee2323;&quot;&gt;props drilling&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;props drilling 해결 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Context API&lt;/li&gt;
&lt;li&gt;Redux ToolKit&lt;/li&gt;
&lt;li&gt;Zustand&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Context 객체를 생성한 후에 해당 객체를 통해 데이터의 공유 범위와 공유할 데이터를 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; Context 객체의 공유 범위에 포함된 다른 컴포넌트들은 공유하는 데이터를 가져와서 사용할 수 있음&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;Context 객체를 받을 변수는 대문자로 시작&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;기존 App.tsx 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780655244286&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { createContext, useState } from &quot;react&quot;;
import Count from &quot;./components/Count&quot;;
import CountOutside from &quot;./components/CountOutside&quot;;

export default function App() {
  const [count, setCount] = useState(0);

  const increment = () =&amp;gt; {
    setCount((prevCount) =&amp;gt; prevCount + 1);
    console.log(count);
  };

  const decrement = () =&amp;gt; {
    setCount((prevCount) =&amp;gt; prevCount - 1);
  };
  const reset = () =&amp;gt; {
    setCount(0);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Count
        count={count}
        increment={increment}
        decrement={decrement}
        reset={reset}
      /&amp;gt;
      &amp;lt;CountOutside count={count} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;변경 App.tsx 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780655650782&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { createContext, useState } from &quot;react&quot;;
import Count from &quot;./components/Count&quot;;
import CountOutside from &quot;./components/CountOutside&quot;;

type CounterContextType = {
  count: number;
  increment: () =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
}
export const CounterContext = createContext&amp;lt;CounterContextType | null&amp;gt;(null);

export default function App() {
  const [count, setCount] = useState(0);

  const increment = () =&amp;gt; {
    setCount((prevCount) =&amp;gt; prevCount + 1);
    console.log(count);
  };

  const decrement = () =&amp;gt; {
    setCount((prevCount) =&amp;gt; prevCount - 1);
  };
  const reset = () =&amp;gt; {
    setCount(0);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;CounterContext value={{ count, increment, decrement, reset }}&amp;gt;
        &amp;lt;Count
          count={count}
          increment={increment}
          decrement={decrement}
          reset={reset}
        /&amp;gt;
        &amp;lt;CountOutside count={count} /&amp;gt;
      &amp;lt;/CounterContext&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ CounterContext 내부의 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;기존 CountOutside.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1780655887531&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountOutside.tsx

export default function CountOutside({ count }: { count: number }) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;CountOutside: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;변경 CountOutside.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1780656003888&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountOutside.tsx

import { useContext } from &quot;react&quot;;
import { CounterContext } from &quot;../App&quot;;

export default function CountOutside() {

  const { count } = useContext(CounterContext)!;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;CountOutside: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ CounterContext는 CounterContextType or null 타입 --&amp;gt; null이 아님을 명시해주기 위해 ! 붙이기&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;기존 CountDisplay.tsx 코드&lt;/p&gt;
&lt;pre id=&quot;code_1780656086262&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountDisplay.tsx

export default function CountDisplay({
  count,
}: {
  count: number;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Count: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;변경 CountDisplay.tsx 코드&lt;/p&gt;
&lt;pre id=&quot;code_1780656156666&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountDisplay.tsx

import { useContext } from &quot;react&quot;;
import { CounterContext } from &quot;../App&quot;;

export default function CountDisplay() {

  const { count } = useContext(CounterContext)!;
  
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Count: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;기존 CountButton.tsx 코드&lt;/p&gt;
&lt;pre id=&quot;code_1780656204217&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountButton.tsx

export default function CountButton({
  increment,
  decrement,
  reset,
}: {
  increment: () =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
}) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={decrement}&amp;gt;감소&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={reset}&amp;gt;리셋&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={increment}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;변경 CountButton.tsx&lt;/p&gt;
&lt;pre id=&quot;code_1780656264962&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountButton.tsx

import { useContext } from &quot;react&quot;;
import { CounterContext } from &quot;../App&quot;;

export default function CountButton() {

  const { increment, decrement, reset } = useContext(CounterContext)!;

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={decrement}&amp;gt;감소&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={reset}&amp;gt;리셋&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={increment}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context API 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context는 별도의 파일로 분리해서 작성하는 게 코드의 유지보수 및 가독성 측면에서 좋음&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;src/context/counter/&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CounterContext.ts
&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;어떤 데이터와 어떤 함수들이 전역으로 공유될지 규격 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CounterProvider.tsx
&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;실제로 리액트의 useState 메모리를 할당하고, 비즈니스 로직(데이터를 변경하는 함수들)을 모아둔 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;useCounter.ts
&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;하위 컴포넌트가 Context 내부 데이터에 접근할 때 쓰는 전용 인터페이스(커스텀 훅)이자 예외 처리기&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;
&lt;pre id=&quot;code_1780673161795&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/CounterContext.ts

import { createContext } from &quot;react&quot;;

type CounterContextType = {
  count: number;
  increment: () =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};
export const CounterContext = createContext&amp;lt;CounterContextType | null&amp;gt;(null);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780673193804&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/CounterProvider.tsx

import { useState } from &quot;react&quot;;
import { CounterContext } from &quot;./CounterContext&quot;;

export default function CounterProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  const increment = () =&amp;gt; {
    setCount((prevCount) =&amp;gt; prevCount + 1);
    console.log(count);
  };

  const decrement = () =&amp;gt; {
    setCount((prevCount) =&amp;gt; prevCount - 1);
  };
  const reset = () =&amp;gt; {
    setCount(0);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;CounterContext value={{ count, increment, decrement, reset }}&amp;gt;{children}&amp;lt;/CounterContext&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780673221451&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/useCounter.ts

import { useContext } from &quot;react&quot;;
import { CounterContext } from &quot;./CounterContext&quot;;

export default function useCounter() {
  const context = useContext(CounterContext);

  if (!context) {
    throw new Error(&quot;useCounter는 CounterProvider 안에서만 사용 가능합니다&quot;); // null 방지
  } 

  return context;
}&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;변경 App.tsx 코드&lt;/p&gt;
&lt;pre id=&quot;code_1780673259864&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Count from &quot;./components/Count&quot;;
import CountOutside from &quot;./components/CountOutside&quot;;
import CounterProvider from &quot;./context/counter/CounterProvider&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;CounterProvider&amp;gt;
        &amp;lt;Count /&amp;gt;
        &amp;lt;CountOutside /&amp;gt;
      &amp;lt;/CounterProvider&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ CounterContext를 호출하는 것이 아닌 CounterProvider를 호출&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;변경 CountOutside.tsx 코드&lt;/p&gt;
&lt;pre id=&quot;code_1780673340192&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountOutside.tsx

import useCounter from &quot;../context/counter/useCounter&quot;;

export default function CountOutside() {

  const { count } = useCounter();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;CountOutside: {count}&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context API 리렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 리렌더링 : 상태 변화와 직접적인 관련이 없거나 화면 인터페이스가 변하지 않는데도 부모 컴포넌트의 영향으로 발생하는 리렌더링&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ React.memo 함수 또는 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;CounterProvider 내부의 상태값(count)이 변경됨 --&amp;gt; CounterProvider 컴포넌트 자체가 리렌더링 --&amp;gt; value={{ count, increment, ... }} 안의 객체가 매번 새로운 메모리 주소로 다시 생성됨 --&amp;gt; CounterContext를 사용하는 모든 하위 컴포넌트는 새로운 value를 받았다고 판단 --&amp;gt; 전부 강제 리렌더링&amp;nbsp; --&amp;gt; 숫자(count)를 사용하지 않는 CountButton 컴포넌트까지 렌더링&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780674450455&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/CounterContext.ts

import { createContext } from &quot;react&quot;;

type CounterContextType = {
  count: number;
};
type CounterContextActionType = {
  increment: () =&amp;gt; void;
  decrement: () =&amp;gt; void;
  reset: () =&amp;gt; void;
};
export const CounterContext = createContext&amp;lt;CounterContextType | null&amp;gt;(null);
export const CounterContextAction = createContext&amp;lt;CounterContextActionType | null&amp;gt;(null);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780674474079&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/CounterProvider.tsx

import { useMemo, useState } from &quot;react&quot;;
import { CounterContext, CounterContextAction } from &quot;./CounterContext&quot;;

export default function CounterProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);
  const increment = () =&amp;gt; {
    setCount((count) =&amp;gt; count + 1);
  };
  const decrement = () =&amp;gt; {
    setCount((count) =&amp;gt; count - 1);
  };
  const reset = () =&amp;gt; {
    setCount(0);
  };

  const memoization = useMemo(
    () =&amp;gt; ({ count, increment, decrement, reset }),
    [],
  );

  return (
    &amp;lt;&amp;gt;
      &amp;lt;CounterContextAction value={memoization}&amp;gt;
        &amp;lt;CounterContext value={{ count }}&amp;gt;{children}&amp;lt;/CounterContext&amp;gt;
      &amp;lt;/CounterContextAction&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ useMemo 훅을 사용하여 메모이제이션&lt;/p&gt;
&lt;pre id=&quot;code_1780674528276&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/useCounter.ts

import { useContext } from &quot;react&quot;;
import { CounterContext, CounterContextAction } from &quot;./CounterContext&quot;;

export function useCounter() {
  const context = useContext(CounterContext);
  if (!context) {
    throw new Error(&quot;useCounter는 CounterProvider 안에서만 사용가능합니다.&quot;);
  }
  return context;
}

export function useCounterAction() {
  const context = useContext(CounterContextAction);
  if (!context) {
    throw new Error(&quot;useCounter는 CounterProvider 안에서만 사용가능합니다.&quot;);
  }
  return context;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context API reducer&lt;/h3&gt;
&lt;pre id=&quot;code_1780675548853&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/reducer/counterReducer.ts

export default function counterReducer(
  count: number,
  action: { type: string },
) {
  switch (action.type) {
    case &quot;INCREMENT&quot;:
      return count + 1;
    case &quot;DECREMENT&quot;:
      return count - 1;
    case &quot;RESET&quot;:
      return 0;
    default:
      return count;
  }
}&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_1780675576365&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/CounterContext.ts

import { createContext } from &quot;react&quot;;

type CounterContextType = {
  count: number;
};
type CounterContextActionType = React.ActionDispatch&amp;lt;[action: { type: string }]&amp;gt;;

export const CounterContext = createContext&amp;lt;CounterContextType | null&amp;gt;(null);
export const CounterContextAction =
  createContext&amp;lt;CounterContextActionType | null&amp;gt;(null);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780675603522&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/CounterProvider.tsx

import { useReducer } from &quot;react&quot;;
import { CounterContext, CounterContextAction } from &quot;./CounterContext&quot;;
import counterReducer from &quot;../../reducer/counterReducer&quot;;

export default function CounterProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, countDispatch] = useReducer(counterReducer, 0);
  
  return (
    &amp;lt;&amp;gt;
      &amp;lt;CounterContextAction value={ countDispatch }&amp;gt;
        &amp;lt;CounterContext value={{ count }}&amp;gt;{ children }&amp;lt;/CounterContext&amp;gt;
      &amp;lt;/CounterContextAction&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780675627259&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/counter/useCounter.ts

import { useContext } from &quot;react&quot;;
import { CounterContext, CounterContextAction } from &quot;./CounterContext&quot;;

export function useCounter() {
  const context = useContext(CounterContext);
  if (!context) {
    throw new Error(&quot;useCounter는 CounterProvider 안에서만 사용가능합니다.&quot;);
  }
  return context;
}

export function useCounterAction() {
  const context = useContext(CounterContextAction);
  if (!context) {
    throw new Error(&quot;useCounter는 CounterProvider 안에서만 사용가능합니다.&quot;);
  }
  return context;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780675653150&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CountButtont.sx

import { useCounterAction } from &quot;../context/counter/useCounter&quot;;

export default function CountButton() {
  console.log(&quot;CountButton&quot;);
  const countDispatch = useCounterAction();
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; countDispatch({ type: &quot;DECREMENT&quot; })}&amp;gt;감소&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; countDispatch({ type: &quot;RESET&quot; })}&amp;gt;리셋&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; countDispatch({ type: &quot;INCREMENT&quot; })}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context API 2개 적용해보기&lt;/h3&gt;
&lt;pre id=&quot;code_1780676599715&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/theme/ThemeContext.ts

import { createContext } from &quot;react&quot;;

type ThemeContextType = {
  theme: string;
};

type ThemeContextActionType = {
  changeTheme: () =&amp;gt; void;
};

export const ThemeContext = createContext&amp;lt;ThemeContextType | null&amp;gt;(null);
export const ThemeContextAction = createContext&amp;lt;ThemeContextActionType | null&amp;gt;(null);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780676622404&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/theme/ThemeProvider.tsx

import React, { useMemo, useState } from &quot;react&quot;;
import { ThemeContext, ThemeContextAction } from &quot;./ThemeContext&quot;;

export default function ThemeProvider({
  children,
}: {
  children: React.ReactNode;
}) {

  const [theme, setTheme] = useState(&quot;light&quot;);
  const changeTheme = () =&amp;gt; {
    setTheme((prevTheme) =&amp;gt; prevTheme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;);
  }
  const memoization = useMemo(() =&amp;gt; ({ changeTheme }), []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;ThemeContextAction value={ memoization }&amp;gt;
        &amp;lt;ThemeContext value={{ theme }} &amp;gt;{children}&amp;lt;/ThemeContext&amp;gt;
      &amp;lt;/ThemeContextAction&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780676642068&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/theme/useTheme.ts

import { useContext } from &quot;react&quot;;
import { ThemeContext, ThemeContextAction } from &quot;./ThemeContext&quot;;

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error(&quot;useTheme는 ThemeProvider 안에서만 사용가능합니다.&quot;);
  }
  return context;
}

export function useThemeAction() {
  const context = useContext(ThemeContextAction);
  if (!context) {
    throw new Error(&quot;useTheme는 ThemeProvider 안에서만 사용가능합니다.&quot;);
  }
  return context;
}&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_1780676663593&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Theme.tsx

import { useTheme } from &quot;../context/theme/useTheme&quot;;

export default function Theme () {

  const { theme } = useTheme();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Theme: { theme }&amp;lt;/h1&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780676679748&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ThemeButton.tsx

import { useThemeAction } from &quot;../context/theme/useTheme&quot;;

export default function ThemeButton() {
  const { changeTheme } = useThemeAction();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={changeTheme}&amp;gt;테마 변경&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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_1780676693137&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Count from &quot;./components/Count&quot;;
import CountOutside from &quot;./components/CountOutside&quot;;
import Theme from &quot;./components/Theme&quot;;
import ThemeButton from &quot;./components/ThemeButton&quot;;
import CounterProvider from &quot;./context/counter/CounterProvider&quot;;
import ThemeProvider from &quot;./context/theme/ThemeProvider&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;ThemeProvider&amp;gt;
        &amp;lt;CounterProvider&amp;gt;
          &amp;lt;Count /&amp;gt;
          &amp;lt;CountOutside /&amp;gt;
          &amp;lt;Theme /&amp;gt;
          &amp;lt;ThemeButton /&amp;gt;
        &amp;lt;/CounterProvider&amp;gt;
      &amp;lt;/ThemeProvider&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile 컨텍스트 객체 만들기&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 애플리케이션에서 사용할 타입 정의&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ src/types/&lt;/p&gt;
&lt;pre id=&quot;code_1780893116839&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/types/settings.d.ts

interface UserPreferences { // 서비스 안에서 다루는 데이터의 규격만 정의 == 데이터 모델 
  language: &quot;ko&quot; | &quot;en&quot; | &quot;ja&quot;;
  fontSize: &quot;small&quot; | &quot;medium&quot; | &quot;large&quot;;
  notifications: {
    email: boolean;
    push: boolean;
    desktop: boolean;
  };
  colorScheme: &quot;system&quot; | &quot;light&quot; | &quot;dark&quot;;
}

// 설정을 전역에 공유하기 위함 
interface PreferencesContextType { // 상태(State) 공유용 컨텍스트 타입
  preferences: UserPreferences;
}

interface PreferencesContextActionType { // 기능(Action) 공유용 컨텍스트 타입 
  updateLanguage: (language: UserPreferences[&quot;language&quot;]) =&amp;gt; void;
  updateFontSize: (size: UserPreferences[&quot;fontSize&quot;]) =&amp;gt; void;
  updateNotifications: (
    key: keyof UserPreferences[&quot;notifications&quot;],
    value: boolean,
  ) =&amp;gt; void;
  updateColorScheme: (scheme: UserPreferences[&quot;colorScheme&quot;]) =&amp;gt; void;
}&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;2. 컨텍스트 객체, 프로바이더 컴포넌트, 커스텀 훅 생성&lt;/p&gt;
&lt;pre id=&quot;code_1780894963541&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/setting/SettingContext.ts

import { createContext } from &quot;react&quot;;

export const SettingContext = createContext&amp;lt;PreferencesContextType | null&amp;gt;(
  null,
);

export const SettingContextAction =
  createContext&amp;lt;PreferencesContextActionType | null&amp;gt;(null);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780894989904&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/setting/SettingProvider.tsx

import React, { useMemo, useState } from &quot;react&quot;;
import { SettingContext, SettingContextAction } from &quot;./SettingContext&quot;;

const defaultValue: UserPreferences = {
  language: &quot;ko&quot;,
  fontSize: &quot;medium&quot;,
  notifications: {
    email: false,
    push: false,
    desktop: false,
  },
  colorScheme: &quot;system&quot;,
};

export default function SettingProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [preferences, setPreferences] = useState&amp;lt;UserPreferences&amp;gt;(defaultValue);

  const updateLanguage = (language: UserPreferences[&quot;language&quot;]) =&amp;gt; {
    setPreferences((prevPreferences) =&amp;gt; ({ ...prevPreferences, language }));
  };

  const updateFontSize = (fontSize: UserPreferences[&quot;fontSize&quot;]) =&amp;gt; {
    setPreferences((prevPreferences) =&amp;gt; ({ ...prevPreferences, fontSize }));
  };

  const updateNotifications = (
    key: keyof UserPreferences[&quot;notifications&quot;],
    value: boolean,
  ) =&amp;gt; {
    setPreferences((prevPreferences) =&amp;gt; ({
      ...prevPreferences,
      notifications: { ...prevPreferences.notifications, [key]: value },
    }));
  };

  const updateColorScheme = (colorScheme: UserPreferences[&quot;colorScheme&quot;]) =&amp;gt; {
    setPreferences((prevPreferences) =&amp;gt; ({ ...prevPreferences, colorScheme }));
  };

  const memoization = useMemo(
    () =&amp;gt; ({
      updateLanguage,
      updateFontSize,
      updateNotifications,
      updateColorScheme,
    }),
    [],
  );

  return (
    &amp;lt;&amp;gt;
      &amp;lt;SettingContextAction value={memoization}&amp;gt;
        &amp;lt;SettingContext value={{ preferences }}&amp;gt;{children}&amp;lt;/SettingContext&amp;gt;
      &amp;lt;/SettingContextAction&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780895016027&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/setting/useSetting.ts

import { useContext } from &quot;react&quot;;
import { SettingContext, SettingContextAction } from &quot;./SettingContext&quot;;

export function useSetting() {
  const context = useContext(SettingContext);
  if (!context) {
    throw new Error(
      &quot;useSetting은 SettingProvider 내부에서만 사용할 수 있습니다.&quot;,
    );
  }

  return context;
}

export function useSettingAction() {
  const context = useContext(SettingContextAction);
  if (!context) {
    throw new Error(
      &quot;useSettingAction은 SettingProvider 내부에서만 사용할 수 있습니다.&quot;,
    );
  }

  return context;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile 글자 크기&lt;/h3&gt;
&lt;pre id=&quot;code_1780895926747&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/FontSizeSetting.tsx

import { Type } from &quot;lucide-react&quot;;
import { useSetting, useSettingAction } from &quot;../context/setting/useSetting&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;

export default function FontSizeSetting() {
  const { preferences } = useSetting();
  const { updateFontSize } = useSettingAction();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm&quot;&amp;gt;
        &amp;lt;div className=&quot;flex items-center gap-3 mb-4&quot;&amp;gt;
          &amp;lt;Type className=&quot;text-blue-500&quot; size={24} /&amp;gt;
          &amp;lt;h2 className=&quot;text-lg font-semibold text-gray-900 dark:text-white&quot;&amp;gt;
            글자 크기
          &amp;lt;/h2&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;grid grid-cols-3 gap-3&quot;&amp;gt;
          {([&quot;small&quot;, &quot;medium&quot;, &quot;large&quot;] as const).map((size) =&amp;gt; (
            &amp;lt;button
              key={size}
              className={twMerge(
                &quot;px-4 py-2 rounded-lg text-sm font-medium transition-colors&quot;,
                preferences.fontSize === size
                  ? &quot;bg-blue-500 text-white&quot;
                  : &quot;bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600&quot;,
              )}
              onClick={() =&amp;gt; updateFontSize(size)}
            &amp;gt;
              {size === &quot;small&quot; ? &quot;작게&quot; : size === &quot;medium&quot; ? &quot;보통&quot; : &quot;크게&quot;}
            &amp;lt;/button&amp;gt;
          ))}

          
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780895993101&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/setting/SettingProvider.tsx

.
.
.

useEffect(() =&amp;gt; {
    document.documentElement.style.fontSize = {
      small: &quot;14px&quot;,
      medium: &quot;16px&quot;,
      large: &quot;18px&quot;
    }[preferences.fontSize];
  }, [preferences]);
  
  .
  .
  .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile 알림 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1780934901106&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/AlarmSetting.tsx

import { Bell } from &quot;lucide-react&quot;;
import { useSetting, useSettingAction } from &quot;../context/setting/useSetting&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;

export default function AlarmSetting() {
  const { preferences } = useSetting();
  const { updateNotifications } = useSettingAction();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm&quot;&amp;gt;
        &amp;lt;div className=&quot;flex items-center gap-3 mb-4&quot;&amp;gt;
          &amp;lt;Bell className=&quot;text-blue-500&quot; size={24} /&amp;gt;
          &amp;lt;h2 className=&quot;text-lg font-semibold text-gray-900 dark:text-white&quot;&amp;gt;
            알림 설정
          &amp;lt;/h2&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;space-y-4&quot;&amp;gt;
          {(
            Object.keys(
              preferences.notifications,
            ) as (keyof UserPreferences[&quot;notifications&quot;])[]
          ).map((key) =&amp;gt; (
            &amp;lt;label key={key} className=&quot;flex items-center justify-between&quot;&amp;gt;
              &amp;lt;span className=&quot;text-gray-700 dark:text-gray-300 capitalize&quot;&amp;gt;
                {key === &quot;email&quot;
                  ? &quot;이메일 알림&quot;
                  : key === &quot;push&quot;
                    ? &quot;푸시 알림&quot;
                    : &quot;데스크톱 알림&quot;}
              &amp;lt;/span&amp;gt;
              
              &amp;lt;button
                className={twMerge(
                  &quot;relative inline-flex h-6 w-11 items-center rounded-full transition-colors&quot;,
                  preferences.notifications[key]
                    ? &quot;bg-blue-500&quot;
                    : &quot;bg-gray-300 dark:bg-gray-600&quot;,
                )}
                onClick={() =&amp;gt;
                  updateNotifications(key, !preferences.notifications[key])
                }
              &amp;gt;
                
                &amp;lt;span
                  className={twMerge(
                    &quot;inline-block h-4 w-4 transform rounded-full bg-white transition-transform translate-x-1&quot;,
                    preferences.notifications[key]
                      ? &quot;translate-x-6&quot;
                      : &quot;translate-x-1&quot;,
                  )}
                /&amp;gt;
              &amp;lt;/button&amp;gt;
            &amp;lt;/label&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile 테마 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1780935856795&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/ThemeSetting.tsx

import { Monitor, Moon, Sun } from &quot;lucide-react&quot;;
import { useSetting, useSettingAction } from &quot;../context/setting/useSetting&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;

export default function ThemeSetting() {
  const { preferences } = useSetting();
  const { updateColorScheme } = useSettingAction();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm&quot;&amp;gt;
        &amp;lt;div className=&quot;flex items-center gap-3 mb-4&quot;&amp;gt;
          &amp;lt;Sun className=&quot;text-blue-500&quot; size={24} /&amp;gt;
          &amp;lt;h2 className=&quot;text-lg font-semibold text-gray-900 dark:text-white&quot;&amp;gt;
            테마 설정
          &amp;lt;/h2&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;grid grid-cols-3 gap-3&quot;&amp;gt;
          {([&quot;system&quot;, &quot;light&quot;, &quot;dark&quot;] as const).map((scheme) =&amp;gt; (
            &amp;lt;button
              key={scheme}
              className={twMerge(
                &quot;flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors&quot;,
                preferences.colorScheme === scheme
                  ? &quot;bg-blue-500 text-white&quot;
                  : &quot;bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600&quot;,
              )}
              onClick={() =&amp;gt; updateColorScheme(scheme)}
            &amp;gt;
              {scheme === &quot;system&quot; ? (
                &amp;lt;&amp;gt;
                  &amp;lt;Monitor size={16} /&amp;gt;
                  &amp;lt;span&amp;gt;시스템&amp;lt;/span&amp;gt;
                &amp;lt;/&amp;gt;
              ) : scheme === &quot;light&quot; ? (
                &amp;lt;&amp;gt;
                  &amp;lt;Sun size={16} /&amp;gt;
                  &amp;lt;span&amp;gt;라이트&amp;lt;/span&amp;gt;
                &amp;lt;/&amp;gt;
              ) : (
                &amp;lt;&amp;gt;
                  &amp;lt;Moon size={16} /&amp;gt;
                  &amp;lt;span&amp;gt;다크&amp;lt;/span&amp;gt;
                &amp;lt;/&amp;gt;
              )}
            &amp;lt;/button&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780935889278&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/setting/SettingProvider.tsx

.
.
.

useEffect(() =&amp;gt; {
    document.documentElement.style.fontSize = {
      small: &quot;14px&quot;,
      medium: &quot;16px&quot;,
      large: &quot;18px&quot;
    }[preferences.fontSize];

    if (preferences.colorScheme === &quot;system&quot;) {
      document.documentElement.classList.remove(&quot;light&quot;, &quot;dark&quot;);
      if (window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches) {
        document.documentElement.classList.add(&quot;dark&quot;);
      } else {
        document.documentElement.classList.add(&quot;light&quot;);
      }
    } else {
      document.documentElement.classList.remove(&quot;light&quot;, &quot;dark&quot;);
      document.documentElement.classList.add(preferences.colorScheme);
    }

  }, [preferences]);
  
  .
  .
  .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile 언어 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1780938154167&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/libs/i18n.ts

export const translations = {
  ko: {
    profileTitle: &quot;사용자 설정&quot;,
    languageSetting: &quot;언어 설정&quot;,
    fontSize: {
      label: &quot;글자 크기&quot;,
      small: &quot;작게&quot;,
      medium: &quot;보통&quot;,
      large: &quot;크게&quot;,
    },
    notifications: {
      label: &quot;알림 설정&quot;,
      email: &quot;이메일 알림&quot;,
      push: &quot;푸시 알림&quot;,
      desktop: &quot;데스크톱 알림&quot;,
    },
    theme: {
      label: &quot;테마 설정&quot;,
      system: &quot;시스템&quot;,
      light: &quot;라이트&quot;,
      dark: &quot;다크&quot;,
    },
  },
  en: {
    profileTitle: &quot;User Settings&quot;,
    languageSetting: &quot;Language Setting&quot;,
    fontSize: {
      label: &quot;Font Size&quot;,
      small: &quot;Small&quot;,
      medium: &quot;Medium&quot;,
      large: &quot;Large&quot;,
    },
    notifications: {
      label: &quot;Notification Settings&quot;,
      email: &quot;Email Notifications&quot;,
      push: &quot;Push Notifications&quot;,
      desktop: &quot;Desktop Notifications&quot;,
    },
    theme: {
      label: &quot;Theme Setting&quot;,
      system: &quot;System&quot;,
      light: &quot;Light&quot;,
      dark: &quot;Dark&quot;,
    },
  },
  ja: {
    profileTitle: &quot;ユーザー設定&quot;,
    languageSetting: &quot;言語設定&quot;,
    fontSize: {
      label: &quot;文字サイズ&quot;,
      small: &quot;小&quot;,
      medium: &quot;中&quot;,
      large: &quot;大&quot;,
    },
    notifications: {
      label: &quot;通知設定&quot;,
      email: &quot;メール通知&quot;,
      push: &quot;プッシュ通知&quot;,
      desktop: &quot;デスクトップ通知&quot;,
    },
    theme: {
      label: &quot;テーマ設定&quot;,
      system: &quot;システム&quot;,
      light: &quot;ライト&quot;,
      dark: &quot;ダーク&quot;,
    },
  },
} as const;

export type SupportedLanguage = keyof typeof translations;
export type LocaleKey = keyof (typeof translations)[&quot;ko&quot;];&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780938177432&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/libs/useTranslation.ts

import { useSetting } from &quot;../context/setting/useSetting&quot;;
import { SupportedLanguage, translations } from &quot;./i18n&quot;;

export default function useTranslation() {
  const { preferences } = useSetting();
  const lang = preferences.language as SupportedLanguage;
  const t = translations[lang]; // 객체 변수
  return { t, lang };
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1780938109453&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/LanguageSetting.tsx

import { Languages } from &quot;lucide-react&quot;;
import { useSetting, useSettingAction } from &quot;../context/setting/useSetting&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;
import useTranslation from &quot;../libs/useTranslation&quot;;

export default function LanguageSetting() {
  const { preferences } = useSetting();
  const { updateLanguage } = useSettingAction();
  const { t } = useTranslation();

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm&quot;&amp;gt;
        &amp;lt;div className=&quot;flex items-center gap-3 mb-4&quot;&amp;gt;
          &amp;lt;Languages className=&quot;text-blue-500&quot; size={24} /&amp;gt;
          &amp;lt;h2 className=&quot;text-lg font-semibold text-gray-900 dark:text-white&quot;&amp;gt;
            {t.languageSetting}
          &amp;lt;/h2&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className=&quot;grid grid-cols-3 gap-3&quot;&amp;gt;
          {([&quot;ko&quot;, &quot;en&quot;, &quot;ja&quot;] as const).map((lang) =&amp;gt; (
            &amp;lt;button
              key={lang}
              className={twMerge(
                &quot;px-4 py-2 rounded-lg text-sm font-medium transition-colors&quot;,
                preferences.language === lang
                  ? &quot;bg-blue-500 text-white&quot;
                  : &quot;bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600&quot;,
              )}
              onClick={() =&amp;gt; updateLanguage(lang)}
            &amp;gt;
              {lang === &quot;ko&quot;
                ? &quot;한국어&quot;
                : lang === &quot;en&quot;
                  ? &quot;English&quot;
                  : &quot;日本語&quot;}
            &amp;lt;/button&amp;gt;
          ))}

          
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile 값 저장하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침 해도 이전 값이 유지되도록 설정&lt;/p&gt;
&lt;pre id=&quot;code_1780938523562&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/context/setting/SettingProvider.tsx

import React, { useEffect, useMemo, useState } from &quot;react&quot;;
import { SettingContext, SettingContextAction } from &quot;./SettingContext&quot;;

const defaultValue: UserPreferences = {
  language: &quot;ko&quot;,
  fontSize: &quot;medium&quot;,
  notifications: {
    email: false,
    push: false,
    desktop: false,
  },
  colorScheme: &quot;system&quot;,
};

export default function SettingProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [preferences, setPreferences] = useState&amp;lt;UserPreferences&amp;gt;(() =&amp;gt; {
    const save = localStorage.getItem(&quot;preferences&quot;);
    return save ? JSON.parse(save) : defaultValue;
  });

  .
  .
  .

  useEffect(() =&amp;gt; {

    localStorage.setItem(&quot;preferences&quot;, JSON.stringify(preferences));

    document.documentElement.style.fontSize = {
      small: &quot;14px&quot;,
      medium: &quot;16px&quot;,
      large: &quot;18px&quot;
    }[preferences.fontSize];

    if (preferences.colorScheme === &quot;system&quot;) {
      document.documentElement.classList.remove(&quot;light&quot;, &quot;dark&quot;);
      if (window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches) {
        document.documentElement.classList.add(&quot;dark&quot;);
      } else {
        document.documentElement.classList.add(&quot;light&quot;);
      }
    } else {
      document.documentElement.classList.remove(&quot;light&quot;, &quot;dark&quot;);
      document.documentElement.classList.add(preferences.colorScheme);
    }

  }, [preferences]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;SettingContextAction value={memoization}&amp;gt;
        &amp;lt;SettingContext value={{ preferences }}&amp;gt;{children}&amp;lt;/SettingContext&amp;gt;
      &amp;lt;/SettingContextAction&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserProfile useLayoutEffect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다크 모드를 설정한 상태에서 새로고침을 하면, 아주 잠깐 라이트 모드 디자인이 보여졌다가 다크 모드로 전환되면서 화면이 번쩍거리는 버그 발생&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;∵ 다크 모드를 판단하고 DOM을 조작하는 로직이 useEffect 훅에서 처리되기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ useEffect 훅은 컴포넌트가 화면에 렌더링 되고 난 이후에 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;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;useLayoutEffect 훅 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ useLayoutEffect 훅은 useEffect 훅과 완전히 똑같지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;컴포넌트가 그려지기 전에 동기적으로 함수 로직이 실행된다는 점에서 차이점 O&lt;/span&gt;&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;UserProfile useLayoutEffect - 2&lt;/h3&gt;
&lt;pre id=&quot;code_1780939277316&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ScrollComparison.tsx

import { useEffect, useLayoutEffect, useRef } from &quot;react&quot;;

const items = Array.from({ length: 402 }, (_, i) =&amp;gt; `Item ${i + 1}`);

export default function ScrollComparison() {
  const refEffect = useRef&amp;lt;HTMLDivElement&amp;gt;(null);
  const refLayout = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  useEffect(() =&amp;gt; {
    refEffect.current?.scrollTo({top: refEffect.current.scrollHeight});
  }, []);

  useLayoutEffect(() =&amp;gt; {
    refLayout.current?.scrollTo({top: refLayout.current.scrollHeight});
  }, []);

  const boxClass =
    &quot;border rounded-lg h-48 w-full overflow-auto bg-gray-100 p-2&quot;;

  return (
    &amp;lt;div className=&quot;space-y-8 p-8 max-w-2xl mx-auto&quot;&amp;gt;
      {/* useEffect 스크롤 박스 */}
      &amp;lt;div&amp;gt;
        &amp;lt;h2 className=&quot;text-lg font-semibold mb-2&quot;&amp;gt;useEffect 스크롤&amp;lt;/h2&amp;gt;
        &amp;lt;div ref={refEffect} className={boxClass}&amp;gt;
          {items.map((text) =&amp;gt; (
            &amp;lt;div key={text} className=&quot;py-1&quot;&amp;gt;
              {text}
            &amp;lt;/div&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
        &amp;lt;p className=&quot;mt-2 text-sm text-gray-600&quot;&amp;gt;
          렌더 후 스크롤 &amp;rarr; 처음엔 위에서 시작하다가 아래로 내려가는 걸 볼 수
          있습니다.
        &amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;

      {/* useLayoutEffect 스크롤 박스 */}
      &amp;lt;div&amp;gt;
        &amp;lt;h2 className=&quot;text-lg font-semibold mb-2&quot;&amp;gt;useLayoutEffect 스크롤&amp;lt;/h2&amp;gt;
        &amp;lt;div ref={refLayout} className={boxClass}&amp;gt;
          {items.map((text) =&amp;gt; (
            &amp;lt;div key={text} className=&quot;py-1&quot;&amp;gt;
              {text}
            &amp;lt;/div&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
        &amp;lt;p className=&quot;mt-2 text-sm text-gray-600&quot;&amp;gt;
          렌더 전 스크롤 &amp;rarr; 처음부터 맨 아래에 렌더되어 깜빡임이 없습니다.
        &amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&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;^ useEffect 훅을 사용한 코드는 시각적 깜빡임이 발생하는 반면, useLayoutEffect 훅을 사용한 코드는 깜빡임 없이 자연스럽게 동작&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;* DOM 조작이 필요한 상황에서는 레이아웃 시동 직후, 브라우저가 화면을 실제로 그리기 전에 동기적으로 작업을 처리하는 것이 유리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 발생하는 UI의 시각적 깜빡임을 방지하고 싶을 때 useLayoutEffect 훅을 사용&lt;/p&gt;</description>
      <category>React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/26</guid>
      <comments>https://fromfavor.tistory.com/26#entry26comment</comments>
      <pubDate>Tue, 9 Jun 2026 02:27:05 +0900</pubDate>
    </item>
    <item>
      <title>리액트 - Toggle Button 구현</title>
      <link>https://fromfavor.tistory.com/25</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Toggle Switch&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzrLXv/dJMcajoLPtj/wjIp4nidQ24I9Odg0WoAh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzrLXv/dJMcajoLPtj/wjIp4nidQ24I9Odg0WoAh0/img.png&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;93&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.0744%; margin-right: 10px;&quot; data-widthpercent=&quot;50.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzrLXv/dJMcajoLPtj/wjIp4nidQ24I9Odg0WoAh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzrLXv%2FdJMcajoLPtj%2FwjIp4nidQ24I9Odg0WoAh0%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;318&quot; height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ0W1E/dJMcag6Jsiv/Qh0nfKriLhPtv3WQ6U1OZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ0W1E/dJMcag6Jsiv/Qh0nfKriLhPtv3WQ6U1OZ1/img.png&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;94&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;49.34&quot; style=&quot;width: 48.7628%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ0W1E/dJMcag6Jsiv/Qh0nfKriLhPtv3WQ6U1OZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ0W1E%2FdJMcag6Jsiv%2FQh0nfKriLhPtv3WQ6U1OZ1%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;313&quot; height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779953174991&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { useState } from &quot;react&quot;;

function App() {

  const [hide, setHide] = useState(false);
  
  const handleHideMode = (hide: boolean) =&amp;gt; {
    setHide(() =&amp;gt; !hide);
  };
  
  return (
    &amp;lt;&amp;gt;
      ...
      &amp;lt;HideCompletedToggleButton hide={hide} handleHideMode={handleHideMode} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779953302267&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/HideCompletedToggleButton.tsx

export default function HideCompletedToggleButton({
  hide,
  handleHideMode,
}: {
  hide: boolean;
  handleHideMode: (hide: boolean) =&amp;gt; void;
}) {

  return (
    &amp;lt;div className=&quot;flex items-center gap-2&quot;&amp;gt;
      &amp;lt;p className=&quot;text-sm&quot;&amp;gt;완료 숨기기&amp;lt;/p&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; handleHideMode(hide)}
        className={`relative w-12 h-6 rounded-full transition-colors duration-300 ${hide ? &quot;bg-blue-500&quot; : &quot;bg-gray-300&quot;}`}
      &amp;gt;
        {/* 토글 버튼 안에서 움직이는 동그라미 */}
        &amp;lt;div
          className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform duration-300
          ${hide ? &quot;translate-x-7&quot; : &quot;translate-x-1&quot;}`}
        /&amp;gt;
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&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;duration-300
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;transition-duration: 300ms;&lt;/li&gt;
&lt;li&gt;애니메이션 시간을 0.3초로 설정&lt;/li&gt;
&lt;li&gt;스위치 내부의 동그라미가 0.3초 동안 부드럽게 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;${hide ? &quot;translate-x-7&quot; : &quot;translate-x-1&quot;}
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;translate-x-7 === transform: translateX(1.75rem);&amp;nbsp;&lt;/li&gt;
&lt;li&gt;--&amp;gt; x축으로 오른쪽으로 이동&lt;/li&gt;
&lt;li&gt;동그라미의 위치를 ON, OFF에 따라 지정&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;
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/25</guid>
      <comments>https://fromfavor.tistory.com/25#entry25comment</comments>
      <pubDate>Thu, 28 May 2026 16:32:55 +0900</pubDate>
    </item>
    <item>
      <title>Context API</title>
      <link>https://fromfavor.tistory.com/24</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context를 사용해 데이터를 깊게 전달하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;보통의 경우, 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달&lt;br /&gt;ㄴ 그러나 중간에 많은 컴포넌트를 거쳐야 하거나, 애플리케이션의 많은 컴포넌트에서 동일한 정보가 필요한 경우에는 props를 전달하는 것이 번거롭고 불편할 수 O&lt;br /&gt;Context는 부모 컴포넌트가 깊이에 상관 없이, 트리 아래에 있는 모든 컴포넌트에 정보를 명시적으로 사용할 수 있게 해줌&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ Props 전달하기의 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Props 전달하기 : UI 트리를 통해 해당 데이터를 사용하는 컴포넌트에 명시적으로 데이터를 전달하는 방법&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;Props drilling : 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상이 트리 상 높이 위치하여 높게까지 state를 끌어올리는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ Context: Props 전달하기의 대안&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context는 부모 컴포넌트가 그 아래의 트리 전체에 데이터를 전달할 수 있도록 해줌&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;크기 조정을 위해 level prop을 전달하는 코드&lt;/p&gt;
&lt;pre id=&quot;code_1779781433193&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.js

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    &amp;lt;Section&amp;gt;
      &amp;lt;Heading level={1}&amp;gt;Title&amp;lt;/Heading&amp;gt;
      &amp;lt;Section&amp;gt;
        &amp;lt;Heading level={2}&amp;gt;Heading&amp;lt;/Heading&amp;gt;
        &amp;lt;Heading level={2}&amp;gt;Heading&amp;lt;/Heading&amp;gt;
        &amp;lt;Heading level={2}&amp;gt;Heading&amp;lt;/Heading&amp;gt;
        &amp;lt;Section&amp;gt;
          &amp;lt;Heading level={3}&amp;gt;Sub-heading&amp;lt;/Heading&amp;gt;
          &amp;lt;Heading level={3}&amp;gt;Sub-heading&amp;lt;/Heading&amp;gt;
          &amp;lt;Heading level={3}&amp;gt;Sub-heading&amp;lt;/Heading&amp;gt;
          &amp;lt;Section&amp;gt;
            &amp;lt;Heading level={4}&amp;gt;Sub-sub-heading&amp;lt;/Heading&amp;gt;
            &amp;lt;Heading level={4}&amp;gt;Sub-sub-heading&amp;lt;/Heading&amp;gt;
            &amp;lt;Heading level={4}&amp;gt;Sub-sub-heading&amp;lt;/Heading&amp;gt;
          &amp;lt;/Section&amp;gt;
        &amp;lt;/Section&amp;gt;
      &amp;lt;/Section&amp;gt;
    &amp;lt;/Section&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779781445388&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Section.js

export default function Section({ children }) {
  return (
    &amp;lt;section className=&quot;section&quot;&amp;gt;
      {children}
    &amp;lt;/section&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779781457476&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Heading.js

export default function Heading({ level, children }) {
  switch (level) {
    case 1:
      return &amp;lt;h1&amp;gt;{children}&amp;lt;/h1&amp;gt;;
    case 2:
      return &amp;lt;h2&amp;gt;{children}&amp;lt;/h2&amp;gt;;
    case 3:
      return &amp;lt;h3&amp;gt;{children}&amp;lt;/h3&amp;gt;;
    case 4:
      return &amp;lt;h4&amp;gt;{children}&amp;lt;/h4&amp;gt;;
    case 5:
      return &amp;lt;h5&amp;gt;{children}&amp;lt;/h5&amp;gt;;
    case 6:
      return &amp;lt;h6&amp;gt;{children}&amp;lt;/h6&amp;gt;;
    default:
      throw Error('Unknown level: ' + level);
  }
}&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_1779781566771&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Section&amp;gt;
  &amp;lt;Heading level={3}&amp;gt;About&amp;lt;/Heading&amp;gt;
  &amp;lt;Heading level={3}&amp;gt;Photos&amp;lt;/Heading&amp;gt;
  &amp;lt;Heading level={3}&amp;gt;Videos&amp;lt;/Heading&amp;gt;
&amp;lt;/Section&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ 각각의 &amp;lt;Heading&amp;gt;에 level prop을 전달&lt;/p&gt;
&lt;pre id=&quot;code_1779781574918&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Section level={3}&amp;gt;
  &amp;lt;Heading&amp;gt;About&amp;lt;/Heading&amp;gt;
  &amp;lt;Heading&amp;gt;Photos&amp;lt;/Heading&amp;gt;
  &amp;lt;Heading&amp;gt;Videos&amp;lt;/Heading&amp;gt;
&amp;lt;/Section&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ &amp;lt;Section&amp;gt; 컴포넌트에 level prop을 전달&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;Heading&amp;gt; 컴포넌트가 가장 가까운 &amp;lt;Section&amp;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Props만으로는 불가능 --&amp;gt; Context 활용&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Context 생성&lt;/li&gt;
&lt;li&gt;데이터가 필요한 컴포넌트에서 context를 사용&lt;/li&gt;
&lt;li&gt;데이터를 지정하는 컴포넌트에서 context를 제공&amp;nbsp;&lt;/li&gt;
&lt;/ol&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;color: #ee2323;&quot;&gt;* Context는 부모가 트리 내부 전체에, 심지어 멀리 떨어진 컴포넌트에조차 데이터를 제공할 수 있도록 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ 1단계: Context 생성하기&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;먼저 context를 생성, 이후 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 함&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779781832081&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(1);&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;createContext의 유일한 인자는 기본값&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ 2단계: Context 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 useContext 훅과 생성한 Context를 가져옴&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779868147961&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';&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;지금은 Heading 컴포넌트가 props에서 level을 읽어오지만, level prop을 제거하고 위에서 가져온 context인 LevelContext에서 값을 읽도록 바꿈&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779868194253&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}&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;Heading이 아닌 Section이 대신 level을 받도록 JSX 업데이트&lt;/p&gt;
&lt;pre id=&quot;code_1779868250315&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Section level={4}&amp;gt;
  &amp;lt;Heading&amp;gt;Sub-sub-heading&amp;lt;/Heading&amp;gt;
  &amp;lt;Heading&amp;gt;Sub-sub-heading&amp;lt;/Heading&amp;gt;
  &amp;lt;Heading&amp;gt;Sub-sub-heading&amp;lt;/Heading&amp;gt;
&amp;lt;/Section&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt; 3단계: Context 제공하기&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1779868348588&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Section.js

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    &amp;lt;section className=&quot;section&quot;&amp;gt;
      &amp;lt;LevelContext value={level}&amp;gt;
        {children}
      &amp;lt;/LevelContext&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}&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;이제 level prop을 각 Heading 컴포넌트에 전달할 필요 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 위의 가장 가까운 Section에게 제목 레벨을 확인함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;level prop을 &amp;lt;Section&amp;gt;에 전달&lt;/li&gt;
&lt;li&gt;Section은 자식을 &amp;lt;LevelContext value={level}&amp;gt;로 감싸줌&lt;/li&gt;
&lt;li&gt;Heading은 useContext(LevelContext)를 사용해 가장 근처의 LevelContext의 값을 요청&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ 같은 컴포넌트에서 context를 사용하며 제공하기&lt;/h4&gt;
&lt;pre id=&quot;code_1779868511298&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Page() {
  return (
    &amp;lt;Section level={1}&amp;gt;
      ...
      &amp;lt;Section level={2}&amp;gt;
        ...
        &amp;lt;Section level={3}&amp;gt;
          ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ 지금은 각각의 섹션에 level을 수동으로 지정해야 함&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;Context를 통해 위의 컴포넌트에서 정보를 읽을 수 있으므로, 각 Section은 위의 Section에서 level을 읽고 자동으로 level+1을 아래로 전달할 수 있음&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779868832493&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    &amp;lt;section className=&quot;section&quot;&amp;gt;
      &amp;lt;LevelContext value={level + 1}&amp;gt;
        {children}
      &amp;lt;/LevelContext&amp;gt;
    &amp;lt;/section&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️ Context로 중간 컴포넌트 지나치기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context를 사용하면 주변에 적응하고 렌더링되는 위치에 따라 자신을 다르게 표시하는 컴포넌트를 작성 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt; Context를 사용하기 전에 고려할 것&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;어떤 props를 여러 레벨 깊이로 전달해야 한다고 해서 해당 정보를 context에 넣어야 하는 것은 아님&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&gt;context를 사용하기 전 고려해볼 몇 가지 대안&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Props 전달하기로 시작하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 X&lt;/li&gt;
&lt;li&gt;데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컴포넌트를 추출하고 JSX를 children으로 전달하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트를 추출하는 것을 잊은 겨웅 多&lt;/li&gt;
&lt;li&gt;ex) posts처럼 직접 사용하지 않는 props를 &amp;lt;Layout posts={posts} /&amp;gt;와 같이 전달 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt; Context 사용 예시&amp;nbsp;&lt;/span&gt;&lt;/h4&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;사용자가 모양을 변경할 수 있는 애플리케이션의 경우에 context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&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;/li&gt;
&lt;li&gt;Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;라우팅
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context를 사용&lt;/li&gt;
&lt;li&gt;이것이 모든 링크의 활성화 여부를 알 수 있는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상태 관리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 됨&lt;/li&gt;
&lt;li&gt;reducer를 context와 함께 사용하는 것은 복잡한 state를 관리하고 번거로운 작업 없이 멀리 있는 컴포넌트까지 값을 전달하는 방법&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;
&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;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;Reducer와 Context로 앱 확장하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reducer를 사용하면 컴포넌트의 state 업데이트 로직 통합 가능&lt;br /&gt;Context를 사용하면 다른 컴포넌트들에 정보 전달 가능&lt;br /&gt;Reducer와 context를 함께 사용하면 복잡한 화면의 state를 관리 가능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt; Reducer와 context를 결합하기&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;Reducer는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 됨&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779871452633&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.js

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Day off in Kyoto&amp;lt;/h1&amp;gt;
      &amp;lt;AddTask
        onAddTask={handleAddTask}
      /&amp;gt;
      &amp;lt;TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t =&amp;gt; {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t =&amp;gt; t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Philosopher&amp;rsquo;s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779871469102&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AppTask.js

import { useState } from 'react';

export default function AddTask({ onAddTask }) {
  const [text, setText] = useState('');
  return (
    &amp;lt;&amp;gt;
      &amp;lt;input
        placeholder=&quot;Add task&quot;
        value={text}
        onChange={e =&amp;gt; setText(e.target.value)}
      /&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; {
        setText('');
        onAddTask(text);
      }}&amp;gt;Add&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779871482475&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TaskList.js

import { useState } from 'react';

export default function TaskList({
  tasks,
  onChangeTask,
  onDeleteTask
}) {
  return (
    &amp;lt;ul&amp;gt;
      {tasks.map(task =&amp;gt; (
        &amp;lt;li key={task.id}&amp;gt;
          &amp;lt;Task
            task={task}
            onChange={onChangeTask}
            onDelete={onDeleteTask}
          /&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}

function Task({ task, onChange, onDelete }) {
  const [isEditing, setIsEditing] = useState(false);
  let taskContent;
  if (isEditing) {
    taskContent = (
      &amp;lt;&amp;gt;
        &amp;lt;input
          value={task.text}
          onChange={e =&amp;gt; {
            onChange({
              ...task,
              text: e.target.value
            });
          }} /&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; setIsEditing(false)}&amp;gt;
          Save
        &amp;lt;/button&amp;gt;
      &amp;lt;/&amp;gt;
    );
  } else {
    taskContent = (
      &amp;lt;&amp;gt;
        {task.text}
        &amp;lt;button onClick={() =&amp;gt; setIsEditing(true)}&amp;gt;
          Edit
        &amp;lt;/button&amp;gt;
      &amp;lt;/&amp;gt;
    );
  }
  return (
    &amp;lt;label&amp;gt;
      &amp;lt;input
        type=&quot;checkbox&quot;
        checked={task.done}
        onChange={e =&amp;gt; {
          onChange({
            ...task,
            done: e.target.checked
          });
        }}
      /&amp;gt;
      {taskContent}
      &amp;lt;button onClick={() =&amp;gt; onDelete(task.id)}&amp;gt;
        Delete
      &amp;lt;/button&amp;gt;
    &amp;lt;/label&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ 현재 task state 및 dispatch 함수는 최상위 TaskApp 컴포넌트에서만 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 다른 컴포넌트가 작업 목록을 읽거나 변경하려면 현재 state와 해당 state를 변경하는 이벤트 핸들러를 명시적으로 props로 전달해야 함&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;Reducer와 context를 결합하는 방법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Context를 생성&lt;/li&gt;
&lt;li&gt;State와 dispatch 함수를 context에 넣음&lt;/li&gt;
&lt;li&gt;트리 안에서 context를 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt; 1단계: Context 생성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useReducer 훅은 현재 tasks와 업데이트할 수 있는 dispatch 함수를 반환&lt;/p&gt;
&lt;pre id=&quot;code_1779872080305&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);&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;트리를 통해 전달하려면, 두 개의 별개의 context를 생성해야 함&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TaskContext는 현재 tasks 리스트를 제공&lt;/li&gt;
&lt;li&gt;TaskDispatchContext는 컴포넌트에서 action을 dispatch하는 함수를 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779872326334&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TasksContext.js

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;^ 두 context에 모두 기본값으로 null을 전달, 실제 값은 TaskApp 컴포넌트에서 제공될 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 2단계: State와 dispatch 함수를 context에 넣기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;이제 TaskApp 컴포넌트에서 두 context를 모두 가져올 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;useReducer()에서 반환된 tasks 및 dispatch를 가져와 아래 트리 전체에 제공&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779872539175&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    &amp;lt;TasksContext value={tasks}&amp;gt;
      &amp;lt;TasksDispatchContext value={dispatch}&amp;gt;
        ...
      &amp;lt;/TasksDispatchContext&amp;gt;
    &amp;lt;/TasksContext&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 3단계: 트리 안에서 context 사용하기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1779872627624&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// TaskList.js

import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskList() {
  const tasks = useContext(TasksContext);
  return (
    &amp;lt;ul&amp;gt;
      {tasks.map(task =&amp;gt; (
        &amp;lt;li key={task.id}&amp;gt;
          &amp;lt;Task task={task} /&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useContext(TasksDispatchContext);
  let taskContent;
  if (isEditing) {
    taskContent = (
      &amp;lt;&amp;gt;
        &amp;lt;input
          value={task.text}
          onChange={e =&amp;gt; {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} /&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; setIsEditing(false)}&amp;gt;
          Save
        &amp;lt;/button&amp;gt;
      &amp;lt;/&amp;gt;
    );
  } else {
    taskContent = (
      &amp;lt;&amp;gt;
        {task.text}
        &amp;lt;button onClick={() =&amp;gt; setIsEditing(true)}&amp;gt;
          Edit
        &amp;lt;/button&amp;gt;
      &amp;lt;/&amp;gt;
    );
  }
  return (
    &amp;lt;label&amp;gt;
      &amp;lt;input
        type=&quot;checkbox&quot;
        checked={task.done}
        onChange={e =&amp;gt; {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      /&amp;gt;
      {taskContent}
      &amp;lt;button onClick={() =&amp;gt; {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}&amp;gt;
        Delete
      &amp;lt;/button&amp;gt;
    &amp;lt;/label&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;✔️&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 하나의 파일로 합치기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;반드시 이런 방식으로 작성하지 않아도 되지만, reducer와 context를 모두 하나의 파일에 작성하면 컴포넌트들을 조금 더 정리 가능&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&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&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Reducer를 같은 파일로 옮기고 TasksProvider 컴포넌트를 새로 선언&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;ㄴ 해당 컴포넌트는 모든 것을 하나로 묶는 역할을 함&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Reducer로 state를 관리&lt;/li&gt;
&lt;li&gt;두 context를 모두 하위 컴포넌트에 제공&lt;/li&gt;
&lt;li&gt;children을 prop으로 받기 때문에 JSX를 전달 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779872889875&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    &amp;lt;TasksContext value={tasks}&amp;gt;
      &amp;lt;TasksDispatchContext value={dispatch}&amp;gt;
        {children}
      &amp;lt;/TasksDispatchContext&amp;gt;
    &amp;lt;/TasksContext&amp;gt;
  );
}&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;TasksContext.js에서 context를 사용하기 위한 use 함수들도 내보낼 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1779872930752&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}&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;이 함수를 사용하여 컴포넌트에서 context를 읽을 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1779872962644&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const tasks = useTasks();
const dispatch = useTasksDispatch();&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;TasksProvider는 tasks를 화면의 한 부분으로 tasks를 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useTasks로 tasks를 읽을 수 있고, useTasksDispatch로 컴포넌트들에서 tasks를 업데이트할 수 있음&lt;/p&gt;</description>
      <category>React/공식 문서</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/24</guid>
      <comments>https://fromfavor.tistory.com/24#entry24comment</comments>
      <pubDate>Wed, 27 May 2026 18:10:42 +0900</pubDate>
    </item>
    <item>
      <title>섹션 12. 사이드 이펙트와 컴포넌트 최적화</title>
      <link>https://fromfavor.tistory.com/23</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;useId&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 접근성(Web Accessibility) : 모든 사용자가 어떤 신체적, 기술적 조건에 구애받지 않고 웹사이트의 모든 정보에 동등하게 접근하고 이해할 수 있도록 보장하는 것&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;lt;input&amp;gt;의 id 속성과 &amp;lt;label&amp;gt;의 htmlFor 속성을 동일한 값으로 매칭하여 참조관계 생성&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;li&gt;라벨 텍스트를 클릭해도 인풋창에 자동으로 포커스 --&amp;gt; UX 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useId 훅 : 컴포넌트 내부에 고유하고 독립적인 id 값을 생성하기 위한 리액트 내장 훅&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; 컴포넌트가 여러 번 렌더링되어도 리액트가 알아서 서버와 클라이언트 모두에서 겹치지 않는 안전한 고유 ID 문자열을 뱉어줌&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779346236353&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import Checkbox from &quot;./components/Checkbox&quot;;

export default function App() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Checkbox /&amp;gt;
      &amp;lt;Checkbox /&amp;gt;
      
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779346252740&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Checkbox.tsx

import { useId } from &quot;react&quot;;

export default function Checkbox() {

  const uuid = useId();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input type=&quot;checkbox&quot; id={uuid} /&amp;gt;
      &amp;lt;label htmlFor={uuid}&amp;gt;사과&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;할 일 관리 앱(useId)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드&lt;/p&gt;
&lt;pre id=&quot;code_1779346758870&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Checkbox.tsx

type CheckboxProps = Omit&amp;lt;React.ComponentPropsWithoutRef&amp;lt;&quot;input&quot;&amp;gt;, &quot;type&quot;&amp;gt; &amp;amp; {
  type?: &quot;checkbox&quot;;
  parentClassName: string;
};
export default function Checkbox(props: CheckboxProps) {
  const { parentClassName, children, ...rest } = props;
  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className={parentClassName}&amp;gt;
        &amp;lt;input {...rest} /&amp;gt;
        &amp;lt;label&amp;gt;{children}&amp;lt;/label&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;useId 사용 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779346832009&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Checkbox.tsx

import { useId } from &quot;react&quot;;

type CheckboxProps = Omit&amp;lt;React.ComponentPropsWithoutRef&amp;lt;&quot;input&quot;&amp;gt;, &quot;type&quot;&amp;gt; &amp;amp; {
  type?: &quot;checkbox&quot;;
  parentClassName: string;
};
export default function Checkbox(props: CheckboxProps) {
  const { parentClassName, children, ...rest } = props;
  const uuid = useId();
  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className={parentClassName}&amp;gt;
        &amp;lt;input id={uuid} {...rest} /&amp;gt;
        &amp;lt;label htmlFor={uuid}&amp;gt;{children}&amp;lt;/label&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useEffect 이론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트의 주된 목적 : 컴포넌트에서 반환하는 JSX 요소를 바탕으로 화면을 렌더링하는 것&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;사이드 이펙트(Side Effect) : 화면 렌더링을 제외한 다른 작업들&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) API 통신, 로컬&amp;amp;세션 스토리지 작업, 외부 라이브러리 연동&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;b&gt;useEffect(effect함수, 의존성배열([]))&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 배열에 따라 effect 함수를 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;effect 함수를 호출하는 세 가지 상황&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트 생성(=마운트)&lt;/li&gt;
&lt;li&gt;컴포넌트 삭제(=언마운트)&lt;/li&gt;
&lt;li&gt;컴포넌트 변경(=업데이트)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useEffect 실습&lt;/h3&gt;
&lt;pre id=&quot;code_1779347853535&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Counter.tsx

import { useEffect, useState } from &quot;react&quot;;

export default function Count() {
  const [count, setCount] = useState(0);

  // 의존성 배열이 비어있다면) 컴포넌트가 생성될 때만 호출됨 
  useEffect(() =&amp;gt; {
    console.log(&quot;Count 컴포넌트 생성&quot;);

    // useEffect 내부 return) 컴포넌트가 삭제될 때만 호출됨
    return () =&amp;gt; {
      console.log(&quot;Count 컴포넌트 삭제&quot;);
    }
  }, []);

  // 의존성 배열에 상태 변경을 감지하고 싶은 값을 넣는다면) 컴포넌트 상태가 변경될 때만 호출됨 
  useEffect(() =&amp;gt; {console.log(&quot;Count Change : &quot; + count)}, [count]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Count: {count} &amp;lt;/h1&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount((count) =&amp;gt; count + 1)}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;할 일 관리 앱(useEffect)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침을 해도 값을 유지하게 하는 방법 : 데이터베이스 사용 or 로컬 세션 스토리지 or 쿠키&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;기존 코드&lt;/p&gt;
&lt;pre id=&quot;code_1779348536089&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Todo.tsx

import { useEffect, useState } from &quot;react&quot;;
import TodoEditor from &quot;./TodoEditor&quot;;
import TodoHeader from &quot;./TodoHeader&quot;;
import TodoList from &quot;./TodoList&quot;;

export default function Todo() {
  const [todos, setTodos] = useState&amp;lt;Todo[]&amp;gt;([]);
  const addTodo = (text: string) =&amp;gt; {
    setTodos((todos) =&amp;gt; [
      ...todos,
      {
        id: Date.now(),
        text,
        completed: false,
      },
    ]);
  };
  const toggleTodo = (id: number) =&amp;gt; {
    setTodos((todos) =&amp;gt;
      todos.map((todo) =&amp;gt;
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
  const deleteTodo = (id: number) =&amp;gt; {
    setTodos((todos) =&amp;gt; todos.filter((todo) =&amp;gt; todo.id !== id));
  };
  const modifyTodo = (id: number, text: string) =&amp;gt; {
    setTodos((todos) =&amp;gt;
      todos.map((todo) =&amp;gt; (todo.id === id ? { ...todo, text } : todo))
    );
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;todo&quot;&amp;gt;
        &amp;lt;TodoHeader /&amp;gt;
        {/* 할 일 등록  */}
        &amp;lt;TodoEditor addTodo={addTodo} /&amp;gt;
        {/* 할 일 목록  */}
        &amp;lt;TodoList
          todos={todos}
          toggleTodo={toggleTodo}
          deleteTodo={deleteTodo}
          modifyTodo={modifyTodo}
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&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;useEffect 사용 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779348688499&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Todo.tsx

import { useEffect, useState } from &quot;react&quot;;
import TodoEditor from &quot;./TodoEditor&quot;;
import TodoHeader from &quot;./TodoHeader&quot;;
import TodoList from &quot;./TodoList&quot;;

export default function Todo() {
  const [todos, setTodos] = useState&amp;lt;Todo[]&amp;gt;(JSON.parse(localStorage.getItem(&quot;todos&quot;) || &quot;[]&quot;));
  const addTodo = (text: string) =&amp;gt; {
    setTodos((todos) =&amp;gt; [
      ...todos,
      {
        id: Date.now(),
        text,
        completed: false,
      },
    ]);
  };
  const toggleTodo = (id: number) =&amp;gt; {
    setTodos((todos) =&amp;gt;
      todos.map((todo) =&amp;gt;
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
  const deleteTodo = (id: number) =&amp;gt; {
    setTodos((todos) =&amp;gt; todos.filter((todo) =&amp;gt; todo.id !== id));
  };
  const modifyTodo = (id: number, text: string) =&amp;gt; {
    setTodos((todos) =&amp;gt;
      todos.map((todo) =&amp;gt; (todo.id === id ? { ...todo, text } : todo))
    );
  };

  useEffect(() =&amp;gt; {
    // console.log(&quot;todos 변경됨&quot;);
    localStorage.setItem(&quot;todos&quot;, JSON.stringify(todos))
  }, [todos]);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div className=&quot;todo&quot;&amp;gt;
        &amp;lt;TodoHeader /&amp;gt;
        {/* 할 일 등록  */}
        &amp;lt;TodoEditor addTodo={addTodo} /&amp;gt;
        {/* 할 일 목록  */}
        &amp;lt;TodoList
          todos={todos}
          toggleTodo={toggleTodo}
          deleteTodo={deleteTodo}
          modifyTodo={modifyTodo}
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 리렌더링 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리렌더링 : React에서 컴포넌트가 다시 렌더링되는 것&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트의 정의한 상태가 변경되었을 때&lt;/li&gt;
&lt;li&gt;컴포넌트로 전달하는 props가 변경되었을 때&lt;/li&gt;
&lt;li&gt;부모 컴포넌트가 리렌더링 되었을 때&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React.memo&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 리렌더링 : 화면에 실질적인 데이터 변화가 없음에도 불구하고 렌더링이 발생하는 현상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 부모 컴포넌트가 리렌더링되면, 자식 컴포넌트는 props의 변경 여부와 상관 없이 무조건 다시 호출됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&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;React.memo : 함수형 컴포넌트를 메모이제이션(memoization)하여 컴포넌트 자체를 메모리에 저장해두는 도구&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;React.memo 작동 방식&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트가 새로운 props를 받았을 때, 이전 props와 새로운 props를 얕은 비교&lt;/li&gt;
&lt;li&gt;두 props가 동일하다면, 컴포넌트 함수를 다시 실행하지 않고 메모리에 저장해둔 이전 렌더링 결과를 그대로 재사용&lt;/li&gt;
&lt;li&gt;props가 변경되었다면, 컴포넌트 함수를 실행하여 화면을 다시 그림&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779349704432&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ChildA.tsx

import React from &quot;react&quot;;
import ChildB from &quot;./ChildB&quot;;

export default React.memo(function ChildA() {
  console.log(&quot;ChildA&quot;);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;ChildA Component&amp;lt;/h1&amp;gt;
      &amp;lt;ChildB /&amp;gt;
    &amp;lt;/&amp;gt;
  );
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React.memo가 풀리는 순간&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React.memo를 통해 컴포넌트를 메모리에 저장해두었더라도, 아래 조건이 만족되면 메모리가 해제되고 리렌더링이 수행됨&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트 자체의 상태가 변경될 때&lt;/li&gt;
&lt;li&gt;컴포넌트로 전달되는 props의 값이 변경될 때&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useCallback&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback 훅 : 컴포넌트 내부의 함수 자체를 메모이제이션하여, 리렌더링 시 함수가 불필요하게 새로 생성되는 것을 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 리액트 컴포넌트는 리렌더링될 때마다 내부의 함수들을 매번 처음부터 다시 만듦&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; 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;&lt;b&gt;useCallback(callback함수, 의존성배열([]))&lt;/b&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;/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;/li&gt;
&lt;li&gt;이후, 컴포넌트가 아무리 리렌더링되어도 처음에 저장해둔 똑같은 함수를 계속 가져다 씀&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&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;/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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useMemo&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo 훅 : 컴포넌트 내부에서 발생하는 복잡한 연산의 결과 값을 메모이제이션하여 재사용하는 훅&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;b&gt;useMemo(() =&amp;gt; { return ... }, 의존성배열([]))&lt;/b&gt;&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;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;배열의 특정 상태값이 들어있는 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;평소에는 메모리에 있는 값을 재사용하다가, 배열 안의 상태값이 변경되는 순간에만 연산 함수를 다시 실행하여 메모리를 업데이트&amp;nbsp;&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;lazy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 상단에 정적으로 선언된 컴포넌트는, 당장 화면에 보이지 않더라도 초기 번들 파일에 무조건 포함됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&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;코드 분할 : 거대한 하나의 자바스크립트 번들 파일을 여러 개의 작은 조각 파일로 쪼개는 기술&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 당장 필요한 첫 화면 코드만 먼저 다운로드하고, 나머지 페이지나 무거운 컴포넌트들은 &lt;b&gt;그것이 실제로 필요해지는 순간&lt;/b&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;React에서는 컴포넌트 수준에서 코드를 분할할 수 있도록 React.lazy 함수를 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;특정 컴포넌트를 필요할 때 비동기적으로 로딩 가능&amp;nbsp; &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;/p&gt;
&lt;pre id=&quot;code_1779382655315&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ChildA from &quot;./components/ChildA&quot;;
import ChildB from &quot;./components/ChildB&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;React.lazy 사용 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779382707823&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ChildA = React.lazy(() =&amp;gt; import(&quot;./components/ChildA&quot;));
const ChildB = React.lazy(() =&amp;gt; import(&quot;./components/ChildB&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779382828608&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ChildA = lazy(() =&amp;gt; import(&quot;./components/ChildA&quot;));
const ChildB = lazy(() =&amp;gt; import(&quot;./components/ChildB&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Suspense&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React.lazy 함수의 문제점 : 컴포넌트가 실제 렌더링되는 순간에 서버에서 자바스크립트 파일을 다운로드(import)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--&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;Suspense 컴포넌트 : 자식 컴포넌트가 로딩(비동기 다운로드)되는 동안 화면이 끊기지 않도록, 임시 대체 UI를 띄워주는 React 내장 컴포넌트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ &lt;span style=&quot;color: #ee2323;&quot;&gt;로딩 상태를 관리하기 위해 컴포넌트 내부에 별도의 isLoading 같은 상태 변수나 if-else 분기 처리 해줄 필요 X&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;fallback 속성을 사용해 대체 UI 작성 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ HTML/JSX 형태 모두 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779383882736&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.js

import React, { Suspense, useState } from &quot;react&quot;;
// import ChildA from &quot;./components/ChildA&quot;;
// import ChildB from &quot;./components/ChildB&quot;;

const ChildA = React.lazy(() =&amp;gt; import(&quot;./components/ChildA&quot;));
const ChildB = React.lazy(() =&amp;gt; import(&quot;./components/ChildB&quot;));

export default function App() {
  const [isShow, setIsShow] = useState(false);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsShow((isShow) =&amp;gt; !isShow)}&amp;gt;toggle&amp;lt;/button&amp;gt;
      {isShow &amp;amp;&amp;amp; (
        &amp;lt;&amp;gt;
          &amp;lt;Suspense fallback={&amp;lt;h1&amp;gt;ChildA Loading...&amp;lt;/h1&amp;gt;}&amp;gt;
            &amp;lt;ChildA /&amp;gt;
          &amp;lt;/Suspense&amp;gt;
          &amp;lt;Suspense fallback={&amp;lt;h1&amp;gt;ChildB Loading...&amp;lt;/h1&amp;gt;}&amp;gt;
            &amp;lt;ChildB /&amp;gt;
          &amp;lt;/Suspense&amp;gt;
        &amp;lt;/&amp;gt;
      )}
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;error-boundary&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 컴포넌트 트리 중 단 한 곳에서라도 자바스크립트 런타임 에러가 발생하면, &lt;b&gt;에러가 발생한 컴포넌트뿐만 아니라 부모 트리 전체의 렌더링을 중단&lt;/b&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;Error Boundary : 에러가 발생해도 전체 앱이 죽지 않고, 에러가 난 그 컴포넌트 위치에만 대체 UI를 출력하여 서비스의 나머지 기능은 정상 작동하도록 보&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;b&gt;npm i react-error-boundary&lt;/b&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;에러가 발생해도 전체 트리가 멈추는 것이 아니라, 해당 구역만 대체 UI로 교체됨&lt;/p&gt;
&lt;pre id=&quot;code_1779384608765&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { lazy, Suspense, useState } from &quot;react&quot;;
import { ErrorBoundary } from &quot;react-error-boundary&quot;;
const ChildA = lazy(() =&amp;gt; import(&quot;./components/ChildA&quot;));
const ChildB = lazy(() =&amp;gt; import(&quot;./components/ChildB&quot;));

export default function App() {
  const [isShow, setIsShow] = useState(false);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsShow((isShow) =&amp;gt; !isShow)}&amp;gt;토글&amp;lt;/button&amp;gt;
      {isShow &amp;amp;&amp;amp; (
        &amp;lt;&amp;gt;
          &amp;lt;Suspense fallback={&amp;lt;h1&amp;gt;ChildA Loading..&amp;lt;/h1&amp;gt;}&amp;gt;
            &amp;lt;ChildA /&amp;gt;
          &amp;lt;/Suspense&amp;gt;
          &amp;lt;ErrorBoundary fallback={&amp;lt;div&amp;gt;Something went worng&amp;lt;/div&amp;gt;}&amp;gt;
            &amp;lt;Suspense fallback={&amp;lt;h1&amp;gt;ChildB Loading..&amp;lt;/h1&amp;gt;}&amp;gt;
              &amp;lt;ChildB /&amp;gt;
            &amp;lt;/Suspense&amp;gt;
          &amp;lt;/ErrorBoundary&amp;gt;
        &amp;lt;/&amp;gt;
      )}
    &amp;lt;/&amp;gt;
  );
}&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;fallback 속성 대신 &lt;b&gt;FallbackComponent&lt;/b&gt;를 사용하게 되면 동적으로 활용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄴ 리액트가 해당 컴포넌트의 props로 error와 resetErrorBoundary를 넘겨줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779385667135&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx

import { lazy, Suspense, useState } from &quot;react&quot;;
import { ErrorBoundary } from &quot;react-error-boundary&quot;;
const ChildA = lazy(() =&amp;gt; import(&quot;./components/ChildA&quot;));
const ChildB = lazy(() =&amp;gt; import(&quot;./components/ChildB&quot;));

function Fallback({
  error,
  resetErrorBoundary,
}: {
  error: Error;
  resetErrorBoundary: () =&amp;gt; void;
}) {
  return (
    &amp;lt;div role=&quot;alert&quot;&amp;gt;
      &amp;lt;p&amp;gt;Something went wrong: &amp;lt;/p&amp;gt;
      &amp;lt;pre style={{ color: &quot;red&quot; }}&amp;gt;{error.message}&amp;lt;/pre&amp;gt;
      &amp;lt;button onClick={resetErrorBoundary}&amp;gt;retry&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default function App() {
  const [isShow, setIsShow] = useState(false);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setIsShow((isShow) =&amp;gt; !isShow)}&amp;gt;토글&amp;lt;/button&amp;gt;
      {isShow &amp;amp;&amp;amp; (
        &amp;lt;&amp;gt;
          &amp;lt;Suspense fallback={&amp;lt;h1&amp;gt;ChildA Loading..&amp;lt;/h1&amp;gt;}&amp;gt;
            &amp;lt;ChildA /&amp;gt;
          &amp;lt;/Suspense&amp;gt;
          &amp;lt;ErrorBoundary FallbackComponent={Fallback}&amp;gt;
            &amp;lt;Suspense fallback={&amp;lt;h1&amp;gt;ChildB Loading..&amp;lt;/h1&amp;gt;}&amp;gt;
              &amp;lt;ChildB /&amp;gt;
            &amp;lt;/Suspense&amp;gt;
          &amp;lt;/ErrorBoundary&amp;gt;
        &amp;lt;/&amp;gt;
      )}
    &amp;lt;/&amp;gt;
  );
}&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;인자로 받는 2가지 매개변수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;error
&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;resetErrorBoundary
&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;</description>
      <category>React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지</category>
      <author>favor</author>
      <guid isPermaLink="true">https://fromfavor.tistory.com/23</guid>
      <comments>https://fromfavor.tistory.com/23#entry23comment</comments>
      <pubDate>Fri, 22 May 2026 02:48:32 +0900</pubDate>
    </item>
  </channel>
</rss>