<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>사색의 공간</title>
    <link>https://junhee1203.tistory.com/</link>
    <description>안녕하세요 !</description>
    <language>ko</language>
    <pubDate>Sat, 11 Jul 2026 17:40:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Jun_Hee</managingEditor>
    <image>
      <title>사색의 공간</title>
      <url>https://tistory1.daumcdn.net/tistory/7130412/attach/3f0e4666e8ca4177a7a223f3c28c73ed</url>
      <link>https://junhee1203.tistory.com</link>
    </image>
    <item>
      <title>리액트 딥다이브(4)</title>
      <link>https://junhee1203.tistory.com/30</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. useImperativeHandle&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 컴포넌트가 자식 컴포넌트에게 ref를 전달함으로써 자식 컴포넌트의 특정 함수나 값에 접근할 수 있도록 하는 훅이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 부모는 자식의 내부 기능을 직접 건드릴 수 없지만,&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;useImperativeHandle&lt;/b&gt;&lt;/span&gt;을 사용하면 &lt;b&gt;특정 기능만 공개&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1765104938476&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Child.tsx
// 리액트 19버전부터는 forwardRef 생략이 가능하다.
export const ChildInput = forwardRef((props, ref) =&amp;gt; {
  const inputRef = useRef();

  // 부모에게 노출할 기능을 지정
  useImperativeHandle(ref, () =&amp;gt; ({
    focusInput: () =&amp;gt; {
      inputRef.current.focus();
    }
  }));

  return &amp;lt;input ref={inputRef} placeholder=&quot;여기에 포커스!&quot; /&amp;gt;;
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1765105017813&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Parent.tsx
const Parent = () =&amp;gt; {
  const childRef = useRef();

  const handleClick = () =&amp;gt; {
    childRef.current.focusInput(); // 자식의 함수 호출
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;ChildInput ref={childRef} /&amp;gt;
      &amp;lt;button onClick={handleClick}&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;p data-ke-size=&quot;size16&quot;&gt;이 훅은 리액트의 철학과 위배되는 훅이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;리액트의 철학 중 하나는 부모 -&amp;gt; 자식으로 데이터의 단방향 흐름이다.&lt;/b&gt;&lt;/span&gt;&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;gt; 부모를 살펴봐야 되는 문제를 야기시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;또한, 선언적으로 상태에 따라 UI가 변화되는 방식이 아닌 명령형으로 UI의 변화를 야기시킨다는 문제도 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1765105520275&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 선언적 방식
// visible이라는 상태와 Modal의 관계를 표현하고 있다.
const [visible, setVisible] = useState(false);

return (
  &amp;lt;&amp;gt;
    &amp;lt;button onClick={() =&amp;gt; setVisible(true)}&amp;gt;Open&amp;lt;/button&amp;gt;
    &amp;lt;Modal visible={visible} /&amp;gt;
  &amp;lt;/&amp;gt;
);

// 명령형
modalRef.current.open();&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;/p&gt;
&lt;pre id=&quot;code_1765105893835&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Child.tsx
useImperativeHandle(ref, () =&amp;gt; ({
  open: () =&amp;gt; {},
  close: () =&amp;gt; {}
}));

// Parent.tsx
type ModalHandle = {
  open: () =&amp;gt; void;
  close: () =&amp;gt; void;
};

const ref = useRef&amp;lt;ModalHandle&amp;gt;(null);
ref.current.open();&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;자식이 어떠한 동작을 넣을지 부모에서 추론이 안되므로 자식의 내부 구조를 보고 부모 ref의 타입을 지정해줘야 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위에 코드에서 자식이 open 동작을 삭제하면 ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부모는 ModalHandle 타입에 의존하고 있기 때문에 컴파일 단계에서 타입 오류가 발생하지는 않지만,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;런타임에서 ref.current.open(); 이 코드를 실행할 때 참조 오류가 날 것이다.&amp;nbsp;&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;b&gt;그래서 결론은 가급적 useImperativeHandle을 사용하는 것을 지양하는 것이 맞다. 웬만하면 state로 표현하고 선언적으로 UI를 관리하는 것이 리액트 철학에 더욱 가깝다고 생각한다.&lt;/b&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;5.&amp;nbsp; HOC&lt;/h2&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;우선 리액트 팀도 거의 사용하지 않는다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.legacy.reactjs.org/docs/higher-order-components.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.legacy.reactjs.org/docs/higher-order-components.html?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765107730441&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;고차 컴포넌트 &amp;ndash; React&quot; data-og-description=&quot;A JavaScript library for building user interfaces&quot; data-og-host=&quot;ko.legacy.reactjs.org&quot; data-og-source-url=&quot;https://ko.legacy.reactjs.org/docs/higher-order-components.html?utm_source=chatgpt.com&quot; data-og-url=&quot;https://ko.legacy.reactjs.org/docs/higher-order-components.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WdHE4/hyZOOSLx2s/w7ApnwT53g3sCJWhZB13Fk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://ko.legacy.reactjs.org/docs/higher-order-components.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.legacy.reactjs.org/docs/higher-order-components.html?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WdHE4/hyZOOSLx2s/w7ApnwT53g3sCJWhZB13Fk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;고차 컴포넌트 &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A JavaScript library for building user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.legacy.reactjs.org&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;HOC + 복잡한 props 타입 / 래핑(wrapping) 구조는 &lt;b&gt;타입스크립트 + 컴포넌트 유지보수&lt;/b&gt; 관점에서 오히려 불편하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HOC 구현해본 사람들이라면 한 번쯤 경험해봤겠지만,&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;아래처럼 로그인 인증한 회원만 접근하게 하는 HOC가 있다고 해보자&lt;/p&gt;
&lt;pre id=&quot;code_1765107874052&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function withAuth(WrappedComponent) {
  return function ProtectedComponent(props) {
    const { isLoggedIn } = useAuth();

    if (!isLoggedIn) {
      return &amp;lt;div&amp;gt;로그인이 필요합니다.&amp;lt;/div&amp;gt;;
    }

    return &amp;lt;WrappedComponent {...props} /&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;위의 코드는 아래 Composition을 이용한 방식으로 대체가 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1765107974327&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function LoginBoundary({ children }) {
  const { isLoggedIn } = useAuth();

  if (!isLoggedIn) {
    return &amp;lt;div&amp;gt;로그인이 필요합니다.&amp;lt;/div&amp;gt;;
  }

  return children;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1765108109600&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const MyComponent = () =&amp;gt; {
  return (
    &amp;lt;LoginBoundary&amp;gt;
      &amp;lt;Resolved /&amp;gt;
    &amp;lt;/LoginBoundary&amp;gt;
  );
};

const Resolved = () =&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;/p&gt;</description>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/30</guid>
      <comments>https://junhee1203.tistory.com/30#entry30comment</comments>
      <pubDate>Sun, 7 Dec 2025 20:51:58 +0900</pubDate>
    </item>
    <item>
      <title>리액트 딥다이브(3)</title>
      <link>https://junhee1203.tistory.com/29</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. useState&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 버튼을 눌렀을 때 의도대로 state가 변경되지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1764498197845&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

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

export default function Page() {
  const [, triggerRender] = useState();

  let state = &quot;hi&quot;;

  const handleButtonClick = () =&amp;gt; {
    state = &quot;hello&quot;;
    triggerRender();
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{state}&amp;lt;/h1&amp;gt;
      &amp;lt;button onClick={handleButtonClick}&amp;gt;Change State&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;p data-ke-size=&quot;size16&quot;&gt;triggerRaender 함수를 이용하여 버튼을 누르면 리렌더링은 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;그러나, 리렌더링이 일어난다는 것은 함수가 다시 실행되는 것이므로 함수 내의 state 값은 다시 'hi'가 되기 때문이다.&lt;/b&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;useState의 동작방식을 곰곰히 생각해보면 컴포넌트가 아무리 리렌더링 되어도 (함수가 다시 실행되어도) state 값이 초기값으로 초기화되지 않는다는 것이다 .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;이것이 가능한 이유는 클로저를 이용했기 때문이다.&lt;/span&gt;&lt;/b&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;useState는 함수를 인자로 받을 수가 있다. 전달한 함수의 리턴값은 useState의 초기값이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 컴포넌트가 아무리 리렌더링 되어도 초기에 전달한 함수는 더 이상 실행되지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1764498742320&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

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

export default function Page() {
  // 마운트시에만 useState의 인자로 전달한 콜백함수가 실행된다.
  // 리렌더링이 일어나도 더 이상 실행되지 않는다.
  const [state, setState] = useState(() =&amp;gt; {
    return Number(window.localStorage.getItem(&quot;cacheKey&quot;));
  });

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{state}&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

/////////////////////////////////////

&quot;use client&quot;;

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

export default function Page() {
  // 마운트뿐만 아니라 리렌더링이 발생하면 계속 useState 내부 코드가 실행된다.
  // 리렌더링이 되어도 state는 초기화 되지 않기에 불필요한 사이드 이펙트를 발생시킨다.
  const [state, setState] = useState(
    Number(window.localStorage.getItem(&quot;cacheKey&quot;))
  );

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{state}&amp;lt;/h1&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;위의 첫 번째 방식처럼 구현한다면 첫 렌더링시에만 로컬스토리지에 접근하고 그 이후 렌더링부터는 로컬스토리지에 접근하지 않는다.&lt;br /&gt;그리고, 이러한 방법을 &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;lazy initialization (게으른 초기화)&lt;/span&gt;&lt;/b&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;2. useEffect&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect에 대한 흔한 오해 중 하나가 생명주기를 위한 훅이라는 것이다.&amp;nbsp;&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;사실, useEffect는 생명주기보다 컴포넌트가 렌더링 된 이후에 어떠한 부수효과(Side Effect)를 일으키는 훅이라고 보는 것이 옳다.&lt;/b&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;특히, useEffect에서 가장 중요한 점은 클린업 함수를 포함하여 어느 타이밍에 실행되냐는 것이다.&amp;nbsp;&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴포넌트가 마운트 될 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 렌더 단계를 통해 가상돔의 설계도를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 커밋 단계를 통해 실제 DOM의 설계도를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. DOM의 설계도가 완성되었다면 Ref를 연결한다 (callbackRef 실행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 브라우저가 DOM 설계도를 보고 화면을 그린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 컴포넌트가 그려지면 useEffect에 전달한 콜백함수가 &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;비동기&lt;/span&gt;&lt;/b&gt;로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴포넌트가 리렌더링 될 때 (의존성 배열로 정의한 값이 변경되었다고 가정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 렌더 단계를 통해 이전의 가상돔과 현재의 가상돔에서 달라진 차이점을 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 커밋 단계를 통해 달라진 차이점을 반영한 실제 DOM의 설계도를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. DOM의 설계도가 완성되었다면 Ref를 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 브라우저가 DOM의 설계도를 보고 달라진 부분만 다시 그린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 컴포넌트가 그려지면 useEffect에 정의한 클린업 함수가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. useEffect에서 정의한 클린업 함수가 실행된 후 useEffect에서 정의한 콜백 함수가 비동기로 실행된다.&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: #ef5369;&quot;&gt;&lt;b&gt;여기서 가장 중요한 점은 클린업 함수는 리렌더링 직전이 아니라 리렌더링 이후에 실행된다는 것이다.&lt;/b&gt;&lt;/span&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;size18&quot;&gt;컴포넌트가 언마운트 될 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. DOM과 연결된 Ref가 해제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. useEffect에서 정의한 클린업 함수가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위의 과정들은 아래 코드를 통해 확인이 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1764501477870&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;
import { useEffect, useState } from &quot;react&quot;;

export default function Page() {
  const [count, setCount] = useState(0);
  const [isMounted, setMounted] = useState(true);
  // --- 1. 렌더 단계-- -
  // state/props가 변경되면 가장 먼저 실행
  console.log(&quot;1. Render Start&quot;, count);

  useEffect(() =&amp;gt; {
    // --- 2. 커밋 단계 (useEffect) ---
    console.log(&quot;3. useEffect&quot;, count);
    return () =&amp;gt; {
      console.log(&quot;Cleanup: useEffect&quot;, count);
    };
  }, [count]);

  const refCallback = (element: HTMLElement | null) =&amp;gt; {
    // --- 2-1. 커밋 단계 (ref 할당) ---
    // DOM이 업데이트 된 직후 호출
    if (element) {
      console.log(&quot;2. Ref Assigned&quot;, count, element);
    } else {
      console.log(&quot;Ref is null (Unmounted)&quot;, count);
    }
  };

  const handleRerender = () =&amp;gt; {
    console.log(&quot;======= RERENDER =======&quot;);
    setCount((c) =&amp;gt; c + 1);
  };
  const handleUnmount = () =&amp;gt; {
    console.log(&quot;======= UNMOUNT =======&quot;);
    setMounted(false);
  };

  if (!isMounted) {
    return &amp;lt;p&amp;gt;Unmounted!&amp;lt;/p&amp;gt;;
  }

  return (
    &amp;lt;div
      style={{ padding: &quot;20px&quot;, border: &quot;1px solid #ddd&quot;, borderRadius: &quot;8px&quot; }}
    &amp;gt;
      &amp;lt;button onClick={handleRerender}&amp;gt;Rerender ({count})&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={handleUnmount} style={{ marginLeft: &quot;10px&quot; }}&amp;gt;
        Unmount
      &amp;lt;/button&amp;gt;
      &amp;lt;div
        ref={refCallback}
        style={{
          marginTop: &quot;10px&quot;,
          padding: &quot;10px&quot;,
          border: &quot;1px solid black&quot;,
        }}
      &amp;gt;
        Check the console logs!
      &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;위 코드의 결과에서 한 가지 주목할만한 점은, 클린업 함수가 리렌더링 이후에 실행이 되어도 이전 state를 참고해 실행된다는 점이다.&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;3. useRef&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef는 일반적으로 DOM에 접근할 때 사용되지만, 리렌더링을 발생시키지 않고 원하는 상태값을 얻을 수 있는 것에 착안하면 아래와 같은 트릭도 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1764502746620&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;
import { useEffect, useRef, useState } from &quot;react&quot;;

function usePrevious(value: any) {
  const ref = useRef(null);
  useEffect(() =&amp;gt; {
    ref.current = value;
  }, [value]);
  return ref.current;
}

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

  // 0 undefined
  // 1 0
  // 2 1
  // 3 2
  return (
    &amp;lt;div
      style={{ padding: &quot;20px&quot;, border: &quot;1px solid #ddd&quot;, borderRadius: &quot;8px&quot; }}
    &amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Increment&amp;lt;/button&amp;gt;
      &amp;lt;p&amp;gt;Current: {count}&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;Previous: {previousCount}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;</description>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/29</guid>
      <comments>https://junhee1203.tistory.com/29#entry29comment</comments>
      <pubDate>Sun, 30 Nov 2025 20:43:38 +0900</pubDate>
    </item>
    <item>
      <title>리액트 딥다이브(2)</title>
      <link>https://junhee1203.tistory.com/28</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;리액트에서의 렌더링 vs 브라우저에서의 렌더링 vs SSR&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 리액트를 공부했을 때 렌더링이라는 용어에 대해 매우 혼란스러웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;내가 아는 렌더링은 브라우저에 단순히 UI 그리는 렌더링인데, 렌더링이라는 용어를 사용하는 곳마다 그 의미가 달랐다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 기회에 한 번 정리해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 브라우저에서의 렌더링&lt;/p&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;2. 리액트에서 렌더링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트의 props와 state를 기반으로 가상돔을 만들고, 재조정을 통해 변경된 state를 추적하고 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;3. SSR (Server Side Rendering)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 HTML을 완성키는 과정&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;리액트에서 렌더링 과정&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 리액트에서 렌더링 정의를&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;컴포넌트의 props와 state를 기반으로 가상돔을 만들고, 재조정을 통해 변경된 state를 추적하고 UI에 반영하는 과정&lt;/b&gt;&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;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;1. 렌더 단계&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;2. 커밋 단계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 단계는 변경된 state가 있다면 DOM을 조작하여 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;/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;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 굳이 불필요한 곳에 memo를 덕지덕지 붙여가며 리렌더링 최적화를 할 필요가 없다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM을 직접 조작하는 비용이 훨씬 높지 단순히 객체 덩어리를 비교하는 작업은 큰 비용이 들지 않기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리액트의 함수형 컴포넌트에서 렌더링이 일어나는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. useState()의 두 번째 배열 요소인 setter가 실행되는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. useReducer()의 두 번째 배열 요소인 dispatch가 실행되는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. key props가 변경되는 경우&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;리액트에서 key란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;리액트에서 Key는 가상돔 트리 안에서 형제 노드들끼리 식별할 수 있게 하는 고유한 ID이다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 배열을 map 메서드로 UI를 그릴 때 key를 사용해봤을 것이다. map 메서드로 나열한 UI들은 리액트 트리내에서 서로 형제관계에 있기 때문에 key로 이들을 구별할 수 있게 해준다.&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;책에서는 key를 활용하면 강제로 리렌더링을 일으키는 것이 가능하다고 하는데, 사실 나는 이게 좀 표현이 애매하다고 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;실제로 key가 변경되면 리액트는 새로운 컴포넌트라고 인식하게 된다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;즉, key가 변경되면 리렌더링이 아니라 언마운트 되었다가 새로운 컴포넌트가 마운트 되는 것이다. (리마운트)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 들어보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const QUESTIONS = [
  {
    id: &quot;q-1&quot;,
    title: &quot;당신의 주요 목표는 무엇인가요?&quot;,
    options: [&quot;건강&quot;, &quot;커리어&quot;, &quot;돈&quot;, &quot;사랑&quot;, &quot;성장&quot;],
  },
  {
    id: &quot;q-2&quot;,
    title: &quot;관심 있는 분야를 선택해주세요.&quot;,
    options: [&quot;운동&quot;, &quot;자기계발&quot;, &quot;주식&quot;, &quot;음악&quot;, &quot;여행&quot;],
  },
  {
    id: &quot;q-3&quot;,
    title: &quot;선호하는 콘텐츠 형태는 무엇인가요?&quot;,
    options: [&quot;영상&quot;, &quot;글&quot;, &quot;팟캐스트&quot;, &quot;뉴스레터&quot;],
  },
];

function FadingText({ message }: { message: string }) {
  return (
    &amp;lt;AnimateText.Wrapper&amp;gt;
      &amp;lt;Top.Root
        title={
          &amp;lt;Top.TitleParagraph size={22}&amp;gt;
            &amp;lt;AnimateText motion={Motion.fade.inOut()}&amp;gt;{message}&amp;lt;/AnimateText&amp;gt;
          &amp;lt;/Top.TitleParagraph&amp;gt;
        }
      /&amp;gt;
    &amp;lt;/AnimateText.Wrapper&amp;gt;
  );
}

export default function SurveyPage() {
  const [step, nextStep] = useReducer(
    (s) =&amp;gt; Math.min(s + 1, QUESTIONS.length),
    0
  );

  const currentQuestion = QUESTIONS[step];

  return (
    &amp;lt;&amp;gt;
      {/* key가 변경될 때마다 컴포넌트가 리마운트 되며 애니메이션이 매번 새로 실행됨 */}
      &amp;lt;FadingText key={current.id} message={current.title} /&amp;gt;
      &amp;lt;List&amp;gt;
        {currentQuestion.options.map((option) =&amp;gt; (
          &amp;lt;ListRow
            key={option}
            left={&amp;lt;Txt&amp;gt;{option}&amp;lt;/Txt&amp;gt;}
            onClick={nextStep}
          /&amp;gt;
        ))}
      &amp;lt;/List&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;</description>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/28</guid>
      <comments>https://junhee1203.tistory.com/28#entry28comment</comments>
      <pubDate>Sun, 23 Nov 2025 20:52:45 +0900</pubDate>
    </item>
    <item>
      <title>리액트 딥다이브(1)</title>
      <link>https://junhee1203.tistory.com/27</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JSX에 대한 고찰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 JSX는 기본적으로 JSXElement, JSXAttributes, JSXChildren, JSXStrings 라는 4가지 구조적 단위를 기반으로 구성되어 있고 각 의미들에 대해 자세히 설명하고 있다. 그런데, 그다지 중요한 내용들은 아닌 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSX에 대해 몇 가지 알아야 될 중요한 점은 다음과 같다고 본다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저는 JSX를 이해할 수 없으므로 반드시 &lt;b&gt;바벨과 같은 도구를 통해 JS로 트랜스파일&lt;/b&gt;을 거쳐야 된다.&lt;/li&gt;
&lt;li&gt;JSX는 당연하게도 주로 리액트에서 사용하지만, 트랜스파일만 가능하다면 다른 곳에서도 사용될 수 있다.&lt;/li&gt;
&lt;li&gt;JSX는 자바스크립트 내부에서 표현하기 까다로웠던 XML 스타일의 트리 구문을 작성하는데 도움을 주는 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;문법적 설탕&lt;/b&gt;&lt;/span&gt;이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 위의 3번에서 리액트가 왜 JSX를 선택했는지가 드러난다.&lt;br /&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;리액트의 철학은 절차가 아닌 관계에 집중하는 선언형 프로그래밍에 있다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;이 철학을 가장 잘 드러내는 것이 바로 JSX로, &lt;b&gt;UI 간의 관계를 직관적으로 표현하기에 탁월한 문법&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre id=&quot;code_1763216986751&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 내부를 보지 않아도 유저의 프로필 안에 유저 사진, 정보가 있다는 것이 관계적으로 드러난다
&amp;lt;UserProfile&amp;gt;
  &amp;lt;Avatar /&amp;gt;
  &amp;lt;UserInfo /&amp;gt;
&amp;lt;/UserProfile&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1763217263517&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 코드만 봐도 users 배열의 각 객체가 UserCard 컴포넌트 하나로 매핑된다는 사실이 바로 읽힌다.
// JSX를 이용하면 데이터 구조와 UI 구조의 관계를 드러내어 선언적으로 프로그래밍할 수 있다 
const UserList = ({ users }) =&amp;gt; (
  &amp;lt;section&amp;gt;
    {users.map(user =&amp;gt; (
      &amp;lt;UserCard
        key={user.id}
        name={user.name}
        email={user.email}
      /&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;JSX 트랜스파일링 결과, 브라우저 렌더링 과정, 가상돔에 관한 내용은 이전 블로그 글인 가상돔 만들기에서 보았으므로 여기서 서술하는 것은 스킵한다.&lt;/p&gt;
&lt;figure id=&quot;og_1763217668713&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[React] 바닐라JS로 가상돔(VirtualDOM) 만들기&quot; data-og-description=&quot;현대 프론트엔드 프레임워크를 대표하는 기술은 단연코 리액트일 것이다. 물론, 리액트는 공식적으로 라이브러리지만 사실상 프레임워크라 해도 어색하지 않다.&amp;nbsp;일부 시각에서는, 바닐라JS보&quot; data-og-host=&quot;junhee1203.tistory.com&quot; data-og-source-url=&quot;https://junhee1203.tistory.com/17&quot; data-og-url=&quot;https://junhee1203.tistory.com/17&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Dfy0J/hyZMwFDjnJ/RNhQkRzdAKkSlsbg9fujr1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/mnE3F/hyZNQCJuFJ/FoHLw2HyWbWXehWL08ZnDK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/K5x9V/hyZNWwbq45/0o0ZqiUhMbckKF3QUUqt70/img.png?width=1482&amp;amp;height=468&amp;amp;face=0_0_1482_468&quot;&gt;&lt;a href=&quot;https://junhee1203.tistory.com/17&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://junhee1203.tistory.com/17&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Dfy0J/hyZMwFDjnJ/RNhQkRzdAKkSlsbg9fujr1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/mnE3F/hyZNQCJuFJ/FoHLw2HyWbWXehWL08ZnDK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/K5x9V/hyZNWwbq45/0o0ZqiUhMbckKF3QUUqt70/img.png?width=1482&amp;amp;height=468&amp;amp;face=0_0_1482_468');&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] 바닐라JS로 가상돔(VirtualDOM) 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;현대 프론트엔드 프레임워크를 대표하는 기술은 단연코 리액트일 것이다. 물론, 리액트는 공식적으로 라이브러리지만 사실상 프레임워크라 해도 어색하지 않다.&amp;nbsp;일부 시각에서는, 바닐라JS보&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;junhee1203.tistory.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;h2 data-ke-size=&quot;size26&quot;&gt;2. 리액트 파이버의 도입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 15까지는 트리 구조인 가상 DOM의 노드는 단순히 React.createElement의 결과로 생성된 &lt;b&gt;평범한 자바스크립트 객체&lt;/b&gt;였다.&lt;br /&gt;재조정(Reconciliation) 과정은 이전 가상 DOM과 새로운 가상 DOM을 &lt;b&gt;객체 단위로 비교(diff)&lt;/b&gt; 하는 방식으로 동작했다.&lt;br /&gt;따라서 이 비교 과정이 &lt;b&gt;동기적으로 한 번에 수행&lt;/b&gt;되었기 때문에, 렌더링 중 사용자 입력이나 애니메이션 같은 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;UI 이벤트가 발생해도 즉시 처리할 수 없는 문제&lt;/b&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;b&gt;React 16에서 새로운 아키텍처인 Fiber&lt;/b&gt;가 도입되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;Fiber는 가상 DOM의 노드지만 이전과 달리 평범한 객체가 아니라&amp;nbsp;작업 단위로 쪼개어 관리할 수 있는 특별한 객체이다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 React는 렌더링을 &lt;b&gt;작은 단위로 나누어 수행하고&lt;/b&gt;, 필요할 때 &lt;b&gt;작업을 일시 중단하거나 재개&lt;/b&gt;할 수 있게 되었다.&lt;br /&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;pre id=&quot;code_1763259356452&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;UserList /&amp;gt;
      &amp;lt;ChatWindow /&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;이 구조는 내부적으로 다음과 같은 파이버 트리로 관리된다.&lt;/p&gt;
&lt;pre id=&quot;code_1763259422603&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;App (Fiber)
 ├─ Header (Fiber)
 ├─ UserList (Fiber)
 └─ ChatWindow (Fiber)&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;만약 UserList의 업데이트가 매우 크고 오래 걸리면, 이제 React는 파이버를 통해 Header랑 ChatWindow는 우선 렌더링하고,&lt;br /&gt;UserList는 나중에 렌더링을 하며 스케줄링을 조절할 수 있는 것이다.&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;3. 파이버를 이용한 렌더링 전략&lt;/h2&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;&amp;nbsp;React가 루트부터 전체 가상 DOM 트리를 순회하며 diff 계산을 함.&lt;/li&gt;
&lt;li&gt;이 작업은 &lt;b&gt;하나의 커다란 동기 함수 호출&lt;/b&gt; (스택 재귀 기반).&lt;/li&gt;
&lt;li&gt;중간에 멈출 수 없음 &amp;rarr; 브라우저가 100ms 이상 멈춰도 React는 끝까지 계산함.&lt;/li&gt;
&lt;/ol&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;파이버의 도입 이후 리액트는 렌더링 전략을 &lt;b&gt;&quot;한번에 이전과 달라진 모든 것을 계산하는 방식&quot;&lt;/b&gt;에서 &quot;&lt;b&gt;파이버라는 노드 하나하나가 작업 단위가 되며, 부분적으로 쪼개어 이전과 달라진 점을 계산하는 방식&quot;&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;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 data-end=&quot;770&quot; data-start=&quot;730&quot;&gt;루트에서부터 Fiber 노드를 하나씩 처리&lt;/li&gt;
&lt;li data-end=&quot;815&quot; data-start=&quot;771&quot;&gt;&lt;b&gt;Fiber 하나를 처리할 때마다&lt;/b&gt; &amp;ldquo;지금 브라우저가 바쁜가?&amp;rdquo;를 확인&lt;/li&gt;
&lt;li data-end=&quot;859&quot; data-start=&quot;816&quot;&gt;만약 브라우저가 바쁘지 않은 상태라면 &amp;rarr; 다음 Fiber 처리&lt;/li&gt;
&lt;li data-end=&quot;965&quot; data-start=&quot;860&quot;&gt;만약, 브라우저가 사용자 입력/애니메이션 등 &lt;b&gt;더 중요한 일이 있다면&lt;/b&gt; &amp;rarr;&lt;br /&gt;React는 현재 작업을 &lt;b&gt;일시 중단 &lt;/b&gt;하고, 나중에 이어서 처리&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;b&gt;즉 이것은, CPU의 스케줄링과 매우 유사한 방식이다.&lt;/b&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;4. 파이버로 인해 발생한 오해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이버를 알아보다 보니 한 가지 의문점이 들었다. 필자가 알기로는 분명 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;리액트는 상태의 변화를 일괄적으로 모으고 하나의 스냅샷으로 한 번에 배치처리&lt;/b&gt;&lt;/span&gt;하는 것으로 알고 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1763260391199&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 배치처리의 유명한 예시
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () =&amp;gt; {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };
  console.log(count) //마운트시 : 0 , 클릭시 3이 아니라 1이 출력된다

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;{count}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;+3&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Fiber는 렌더링을 이전과 달리&amp;nbsp;&lt;b&gt;세밀한 작업 단위로 쪼개어 처리한다&lt;/b&gt;고 했다.&lt;br /&gt;이 점이 React의 업데이트가 한 번에 &lt;b&gt;배치 처리(batch update)&lt;/b&gt; 된다는 특성과 다소 상충되는 것처럼 보였고, 알아보니&lt;br /&gt;사실 두 개념은 서로 &lt;b&gt;다른 단계에서 작동하는 것&lt;/b&gt;을 알게되었다.&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;279&quot; data-ke-size=&quot;size16&quot;&gt;React의 렌더링 과정은 크게 두 단계로 나뉜다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;368&quot; data-start=&quot;311&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;339&quot; data-start=&quot;311&quot;&gt;&lt;b&gt;렌더 단계(Render Phase)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;368&quot; data-start=&quot;340&quot;&gt;&lt;b&gt;커밋 단계(Commit Phase)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;526&quot; data-start=&quot;370&quot; data-ke-size=&quot;size16&quot;&gt;이름만 보면 렌더 단계에서 화면을 그리는 과정이라고 오해하기 쉽지만, 실제로 렌더 단계는 &lt;b&gt;재조정(Reconciliation)&lt;/b&gt; 과정에 해당한다.&lt;br /&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;즉, 이전 가상 DOM과 새로운 가상 DOM을 비교하여 &amp;ldquo;무엇이 어떻게 달라졌는가&amp;rdquo;를 계산하는 준비 단계&lt;/b&gt;&lt;b&gt;다&lt;/b&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;528&quot; data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;커밋 단계&lt;/b&gt;는 렌더 단계에서 계산된 결과를 바탕으로 &lt;b&gt;실제 DOM을 조작하고, 변경된 내용을 화면에 반영하는 단계&lt;/b&gt;다.&lt;br /&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;우리가 일반적으로 &amp;lsquo;렌더링되어 화면에 그려진다&amp;rsquo;고 생각하는 일은 바로 이 커밋 단계에서 일어난다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;528&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;847&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fiber는 커밋 단계가 아닌&amp;nbsp;렌더 단계에 관여한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;847&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;React는 Fiber를 통해 렌더링 작업을 작은 단위로 나누어 처리하며, 필요할 경우 브라우저의 상태에 따라 &lt;b&gt;작업을 일시 중단하거나 나중에 재개&lt;/b&gt;할 수 있다. 즉, Fiber는 &amp;ldquo;렌더 단게에서 diff 계산을 효율적으로 수행하기 위한 내부 스케줄링 메커니즘&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-end=&quot;847&quot; data-start=&quot;668&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1059&quot; data-start=&quot;849&quot; data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;배치 처리(batch update)&lt;/b&gt; 는 Fiber와 달리 &lt;b&gt;커밋 단계&lt;/b&gt;에서 이루어진다. Fiber가 렌더 단계에서 계산해낸 여러 변경 사항(diff)을 모두 모아, React는 커밋 단계에서 이들을 &lt;b&gt;한 번에 DOM에 반영&lt;/b&gt;한다.&lt;br /&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;그래서 React는 &amp;ldquo;계산은 잘게 나누어 처리하지만, 실제 화면 반영은 한 번에 수행&amp;rdquo;하는 구조를 갖게 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1256&quot; data-start=&quot;1061&quot; data-ke-size=&quot;size16&quot;&gt;결국 Fiber와 배치 처리는 서로 상충되는 개념이 아니라, &lt;b&gt;React의 렌더링을 효율적이고 부드럽게 만드는 두 축&lt;/b&gt;이다.&lt;br /&gt;Fiber는 &amp;lsquo;어떻게 계산할 것인가&amp;rsquo; 를, 배치 처리는 &amp;lsquo;언제 반영할 것인가&amp;rsquo; 를 다루며, 이 둘이 함께 동작함으로써 React는 &lt;b&gt;성능과 반응성을 모두 잡은 렌더링 모델&lt;/b&gt;을 완성하게 된 것이다.&lt;/p&gt;</description>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/27</guid>
      <comments>https://junhee1203.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 16 Nov 2025 11:51:39 +0900</pubDate>
    </item>
    <item>
      <title>[후기] 2025 카카오페이 채용연계형 개발자 인턴십 코딩 테스트 후기</title>
      <link>https://junhee1203.tistory.com/23</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;카카오페이 채용연계형 개발자 인턴십&lt;b&gt;[Front-End 모집]&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: #ef6f53;&quot;&gt;&lt;b&gt; 훗날, 카카오페이뿐만 아니라 카카오 계열 인턴에 지원하실 때 이 글이 조금이나마 도움이 되길 바랍니다.&lt;/b&gt;&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;1️⃣ 채용 절차&lt;/h2&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;b&gt;서류 접수&lt;/b&gt;: 2/17 (월) ~ 2/23 (일)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서류 합격자 발표&lt;/b&gt;: 2/26 (수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과제 전형&lt;/b&gt;: 3/1 (토) ~ 3/2 (일)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과제 전형 합격자 발표&lt;/b&gt;: 3/5 (수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인터뷰&lt;/b&gt;: 3/7 (금) ~ 3/25 (화)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 합격자 발표&lt;/b&gt;: 3/26 (수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입사&lt;/b&gt;: 4/7 (월)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;근무 기간&lt;/b&gt;: 6개월&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;2️⃣ 서류 전형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유 양식의 이력서를 필수로 제출해야 되며, 자기소개서는 선택이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 자기소개서도 제출했습니다. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;어학 성적 및 자격증은 아무 것도 제출하지 않았는데 합격한 것으로 보아 이 부분에서는 걱정 안 하셔도 될 것 같습니다.&lt;/b&gt;&lt;/span&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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;1. 카카오페이 채용 연계형 인턴십에 지원한 동기는 무엇이고, 참여를 통해 본인이 이루고 싶은 것은 무엇인가요?&lt;/span&gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;2. &lt;span data-token-index=&quot;0&quot;&gt;카카오페이 서비스를 사용해보고 사용자 입장에서 수정이 되었으면 하는 UI/UX 하나만 정하여 어떻게 수정이 되면 더 잘 사용할 수 있을지 이야기해주세요&lt;/span&gt; &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;3️⃣ &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;과제 전형&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;운이 좋게도 서류 전형에 합격하여 과제 전형 안내를 받았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBfjXj/btsMz7rmZok/utY1O8oKobZTn2d12ZFcs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBfjXj/btsMz7rmZok/utY1O8oKobZTn2d12ZFcs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBfjXj/btsMz7rmZok/utY1O8oKobZTn2d12ZFcs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBfjXj%2FbtsMz7rmZok%2FutY1O8oKobZTn2d12ZFcs1%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;838&quot; height=&quot;216&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;개인적으로 서류에 합격한다면 어떠한 과제가 주어질지 기대가 컸습니다. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;그러나, 결국은 코딩테스트였습니다.&lt;/b&gt;&lt;/span&gt; 처음부터 채용 절차에 과제 전형보다 코딩 테스트로 안내하는 것이 어떨까 하는 아쉬움이 들었습니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시험 플랫폼&lt;/b&gt;: 프로그래머스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제 수&lt;/b&gt;: 2 문제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시험 시간&lt;/b&gt;: 2시간&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응시 언어&lt;/b&gt;: 모집 분야와 관계없이 본인이 원하는 언어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;난이도는 어려웠습니다.&lt;/b&gt;&lt;/span&gt; 원래 카카오 코딩 테스트는 문제 수를 5~7 문제로 하고 쉬운 문제도 출제하는 것으로 알았는데 문제 수가 2 문제밖에 되지 않아서인지 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;백준 기준으로 실버1 ~ 골드 급의 문제들로만 출제 되었습니다.&lt;/b&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;/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;이 글이 카카오 계열 준비하시는 분들께 도움이 되길 바라며, 코딩 테스트 결과가 나오면 추가적으로 아래에 공유드리겠습니다. 감사합니다 !&amp;nbsp;&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;b&gt;저는 탈락했습니다   오픈 채팅 톡방을 확인하니 대부분 1.5 솔 ~ 2솔 분들이 합격하였습니다 ! &lt;br /&gt;비록 탈락하였지만, 넛지헬스케어 코딩테스트 탈락 이후 틈틈히 코테 준비를 안 했다면 1솔도 못 했을 거라 생각합니다. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앞으로도 꾸준히 코테 준비를 해야겠습니다  &lt;/b&gt;&lt;/p&gt;</description>
      <category>일상</category>
      <category>인턴</category>
      <category>카카오</category>
      <category>카카오페이</category>
      <category>코딩테스트</category>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/23</guid>
      <comments>https://junhee1203.tistory.com/23#entry23comment</comments>
      <pubDate>Sun, 2 Mar 2025 19:06:04 +0900</pubDate>
    </item>
    <item>
      <title>[React] TanStack-Query 의 useMutation 훅과 낙관적(optimistic) 업데이트 구현하기</title>
      <link>https://junhee1203.tistory.com/22</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TanStack-Query&lt;/b&gt;는 비동기 데이터를 효율적으로 상태 관리 할 수 있도록 강력한 여러 기능을 제공하는 라이브러리다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 대표적인 훅 중 하나로 &lt;b&gt;useQuery&lt;/b&gt; 가 있다.&lt;span style=&quot;color: #ef5369;&quot;&gt; &lt;b&gt;useQuery는 서버의 데이터를 조회하는 훅으로, GET 요청에 사용된다.&lt;/b&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;b&gt;useMutation&lt;/b&gt; 에 대해서 알아본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mutation의 사전적 정의는 변경, 변이이다.&lt;/b&gt; &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;따라서, useMutation 은 흔히 POST, DELETE, PUT 등 서버의 데이터를 변경하는 요청에 사용된다.&lt;/b&gt;&lt;/span&gt; 또한, 해당 훅을 이용하면 &lt;b&gt;낙관적 업데이트&lt;/b&gt;를 손쉽게 구현할 수 있다. 직접 서버를 생성하고 todo 앱을 만들어 하나하나 살펴보자.&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;1️⃣ 프로젝트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 디렉토리 구조는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1739864094005&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├─&amp;nbsp;client
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;App.css
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;App.jsx
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;main.jsx
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Buton.jsx
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;util.js
└─&amp;nbsp;server
&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;app.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ Server&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;express&lt;/b&gt;, &lt;b&gt;cors&lt;/b&gt; 라이브러리를 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739864154327&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i express cors&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739864181341&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//app.js
const express = require('express');
const cors = require('cors');

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());

const PORT = 3000;

const todoList = [
  { id: 1, content: '밥 먹기' },
  {
    id: 2,
    content: '손 씻기',
  },
  {
    id: 3,
    content: '산책 하기',
  },
];

app.get('/', (req, res) =&amp;gt; {
  res.json(todoList);
});

app.listen(PORT, () =&amp;gt; {
  console.log(`Server running on http://localhost:${PORT}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ Client&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vite&lt;/b&gt;를 이용해 리액트 프로젝트를 생성하고 &lt;b&gt;TanStack Query&lt;/b&gt; 라이브러리를 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739864362237&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm create vite@latest
npm i @tanstack/react-query&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;b&gt;QueryClient&lt;/b&gt; 객체를 생성하고, &lt;b&gt;QeuryClientProvider&lt;/b&gt; 를 통해 전역으로 주입한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739864922177&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/main.jsx
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

createRoot(document.getElementById('root')).render(
  &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/QueryClientProvider&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;b&gt;useQuery&lt;/b&gt; 훅을 이용하여, 서버에 저장되어 있는 &lt;b&gt;todoList&lt;/b&gt;를 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;Button&lt;/b&gt; 컴포넌트에 &lt;b&gt;newTodo&lt;/b&gt; 상태와, &lt;b&gt;clearInput&lt;/b&gt; 함수를 props로 전달한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739864955238&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/App.jsx
import { useQuery } from '@tanstack/react-query';
import './App.css';
import { fetchTodo } from './util';
import Button from './Button';
import { useState } from 'react';

function App() {
 const [newTodo, setNewTodo] = useState('');

  const { data, isFetching } = useQuery({
    queryKey: ['todo'],
    queryFn: fetchTodo,
  });

  const handleInput = (e) =&amp;gt; {
    setNewTodo(e.target.value);
  };

  const clearInput = () =&amp;gt; {
    setNewTodo('');
  };

  if (isFetching) return &amp;lt;div&amp;gt;Fetching...&amp;lt;/div&amp;gt;;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;TodoList&amp;lt;/h1&amp;gt;
      &amp;lt;ul&amp;gt;
        {data?.map((todoList) =&amp;gt; (
          &amp;lt;li key={todoList.id}&amp;gt;{todoList.content}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
      &amp;lt;input
        type=&quot;text&quot;
        placeholder=&quot;추가할 할 일을 입력하세요&quot;
        onChange={handleInput}
        value={newTodo}
      /&amp;gt;
      &amp;lt;Button text=&quot;추가&quot; newTodo={newTodo} clearInput={clearInput} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1739864969797&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* client/src/App.css */
div {
  display: flex;
  flex-direction: column;
  align-items: center;
}

input {
  font-size: 16px;
}&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;Button 컴포넌트는 &lt;b&gt;useMutation&lt;/b&gt; 로직이 포함될 컴포넌트이다.&lt;/p&gt;
&lt;pre id=&quot;code_1739874303466&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/Button.jsx
const Button = ({ text,newTodo,clearInput }) =&amp;gt; {
  return &amp;lt;button&amp;gt;{text}&amp;lt;/button&amp;gt;;
};

export default Button;&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;b&gt;useQuery&lt;/b&gt; 훅의 &lt;b&gt;queryFn&lt;/b&gt;에 할당한 함수는 따로 모듈로 분리했다.&lt;/p&gt;
&lt;pre id=&quot;code_1739864979219&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/util.js
export const fetchTodo = async () =&amp;gt; {
  const response = await fetch('http://localhost:3000');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json(); 
};&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1913&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yyOkS/btsMnzH2xUS/yOlOIr0OA6k1Q0jUUyMTL1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yyOkS/btsMnzH2xUS/yOlOIr0OA6k1Q0jUUyMTL1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yyOkS/btsMnzH2xUS/yOlOIr0OA6k1Q0jUUyMTL1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/yyOkS/btsMnzH2xUS/yOlOIr0OA6k1Q0jUUyMTL1/img.gif&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;1913&quot; height=&quot;659&quot; data-origin-width=&quot;1913&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ useMutation 사용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, useMutation 훅은 다음과 같이 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1739877681551&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { mutate, isPending, isError, isSuccess } = useMutation({
  mutationFn: () =&amp;gt; {},
  onSuccess: () =&amp;gt; {},
  onError: () =&amp;gt; {},
  onSettled: () =&amp;gt; {},
  onMutate: () =&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;useMutation에 할당하는 각각의 프로퍼티에 대해 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;mutationFn&lt;/b&gt;: 구조 분해 할당으로 받아온 mutate 함수를 통해 실행할 mutation 함수를 정의. 반드시 Promise를 반환하는 함수여야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onSuccess&lt;/b&gt;: mutate 가 성공했을 때 실행되는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onError&lt;/b&gt;: mutate가 실패했을 때 실행되는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onSettled&lt;/b&gt;: mutate가 성공하든 실패하든 완료되었을 때 실행되는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;onMutate&lt;/b&gt;: mutate가 시작되기 전에 실행되는 함수. 낙관적 업데이트를 구현할 때 유용.&lt;/li&gt;
&lt;/ul&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;b&gt;mutate&lt;/b&gt;: mutationFn에 정의한 함수. 즉, mutation을 실행하는 함수&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;isPending&lt;/b&gt;: mutate가 진행 중인지 여부&lt;/li&gt;
&lt;li&gt;&lt;b&gt;isError&lt;/b&gt;: mutate가 실패했는지 여부&lt;/li&gt;
&lt;li&gt;&lt;b&gt;isSuccess&lt;/b&gt;: mutate가 성공했는지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 자주 사용되는 프로퍼티와 반환값만 살펴보았지만 이 외에도 다양한 프로퍼티와 반환값이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적인 정보는 아래 공식 사이트를 참고하면 된다.&lt;/p&gt;
&lt;figure id=&quot;og_1739888839284&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;useMutation | TanStack Query React Docs&quot; data-og-description=&quot;tsx const { data, error, isError, isIdle, isPending, isPaused, isSuccess, failureCount, failureReason, mutate, mutateAsync, reset, status, submittedAt, variables, } = useMutation( { mutationFn, gcTime...&quot; data-og-host=&quot;tanstack.com&quot; data-og-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation&quot; data-og-url=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b88BRk/hyYjEpuAIO/Hy7wqdfOPa6YetfAwOMelk/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704,https://scrap.kakaocdn.net/dn/bJosnO/hyYfU1Sl5u/crwJT6MIGtzoFwZhJUWevk/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704&quot;&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b88BRk/hyYjEpuAIO/Hy7wqdfOPa6YetfAwOMelk/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704,https://scrap.kakaocdn.net/dn/bJosnO/hyYfU1Sl5u/crwJT6MIGtzoFwZhJUWevk/img.png?width=3000&amp;amp;height=1704&amp;amp;face=0_0_3000_1704');&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;useMutation | TanStack Query React Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;tsx const { data, error, isError, isIdle, isPending, isPaused, isSuccess, failureCount, failureReason, mutate, mutateAsync, reset, status, submittedAt, variables, } = useMutation( { mutationFn, gcTime...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tanstack.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;항상 프로그래밍을 하면서 느끼는 것이, 이론만 보는 것은 크게 와닿지 않는다. 직접 코드를 구현해보고 결과가 어떻게 되는지 확인하는 것이 이해의 폭을 넓힐 수 있다.&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;먼저, 서버가 POST 요청에 응답할 수 있도록 핸들러를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739889401334&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//server/app.js
/*
생략
*/

const todoList = [
  { id: 1, content: '밥 먹기' },
  {
    id: 2,
    content: '손 씻기',
  },
  {
    id: 3,
    content: '산책 하기',
  },
];

app.post('/', (req, res) =&amp;gt; {
  setTimeout(() =&amp;gt; {
    todoList.push({
      id: todoList.length + 1,
      content: req.body.content,
    });
    res.status(201).json(req.body);
  }, 3000);
});

app.listen(PORT, () =&amp;gt; {
  console.log(`Server running on http://localhost:${PORT}`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일부러 setTimeout을 통해 3초 뒤에 응답하도록 하였다&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;이제 POST 요청을 하는 함수를 클라이언트 util.js 에 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;POST 요청을 하는 함수는, useMutation 훅의 mutationFn 프로퍼티에 대입될 예정이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739889580598&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/util.js
/*
생략
*/

export const addTodo = async (newTodo) =&amp;gt; {
  const response = await fetch('http://localhost:3000', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ content: newTodo }),
  });
  if (!response.ok) {
    throw new Error('Failed to add todo');
  }

  return response.json();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Button 컴포넌트에서 useMutation 훅을 사용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1739889701502&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/Button.jsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { addTodo } from './util';

const Button = ({ text, newTodo, clearInput }) =&amp;gt; {
  const queryClient = useQueryClient();
  const { mutate, isPending } = useMutation({
    mutationFn: addTodo,
    onSuccess: () =&amp;gt; {
      queryClient.invalidateQueries(['todo']);
      clearInput();
    },
    onError: (error) =&amp;gt; {
      console.error(error.message);
    },
  });

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; mutate(newTodo)}&amp;gt;{text}&amp;lt;/button&amp;gt;
      {isPending &amp;amp;&amp;amp; &amp;lt;div&amp;gt;할 일 목록을 추가 중입니다...&amp;lt;/div&amp;gt;}
    &amp;lt;/&amp;gt;
  );
};

export default Button;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;util.js 에서 구현한 addTodo 함수를 mutationFn 프로퍼티의 값으로 사용하였다.&amp;nbsp;&lt;br /&gt;따라서 구조 분해 할당으로 받은 mutate 함수를 실행하면 addTodo 함수가 실행된다.&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;onSuccess 에 할당한 콜백 함수를 살펴보면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1739903667464&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onSuccess: () =&amp;gt; {
      queryClient.invalidateQueries(['todo']);
      clearInput();
    },&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;queryClient.invalidateQueries&lt;/b&gt; 메서드는 매개변수로 할당한 &lt;b&gt;queryKey&lt;/b&gt;의 모든 쿼리를 &lt;b&gt;stale&lt;/b&gt; 상태로 바꾸고 해당 queryKey에 등록된 queryFn 함수를 통해 &lt;b&gt;서버로 데이터를 재요청&lt;/b&gt; 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;쉽게 말하면, POST 요청을 통해 서버의 데이터가 바뀌었으니, 변경된 데이터를 다시 요청하여 서버와 클라이언트의 상태를 동기화하는 것이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, clearInput 함수를 통해 input에 입력한 값을 초기화 해준다.&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbdT4/btsMmX34xje/44f5Uvy2hcKsAKDVaIZndK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbdT4/btsMmX34xje/44f5Uvy2hcKsAKDVaIZndK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbdT4/btsMmX34xje/44f5Uvy2hcKsAKDVaIZndK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/lbdT4/btsMmX34xje/44f5Uvy2hcKsAKDVaIZndK/img.gif&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;991&quot; height=&quot;394&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 서버에서 요청이 들어오면 3초 후에 응답을 하기 때문에, 3초 동안 &lt;b&gt;isPending&lt;/b&gt;이 &lt;b&gt;true&lt;/b&gt;가 되어 로딩 UI가 나오는 것이다.&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;3️⃣ 낙관적 업데이트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;낙관적 업데이트(Optimistic Update)&lt;/b&gt;는 UI를 업데이트할 때 &lt;b&gt;서버의 응답을 기다리지 않고&lt;/b&gt;, &lt;b&gt;요청이 성공할 것이라고 가정&lt;/b&gt;하여 클라이언트에서 미리 UI를 업데이트 하는 패턴이다. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;즉, 데이터 변경 요청(POST, PUT, DELETE 등)이 성공할 것이라고 &quot;낙관적으로&quot; 생각하고, 성공 응답 전에 화면을 업데이트 하는 방법이다.&lt;/b&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;현재, POST 요청을 보내면 서버에서는 3초 간의 딜레이가 발생한 후, 응답을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;낙관적 업데이트를 구현하면 이러한 딜레이를 무시한 채, 즉각적으로 화면에 반영하여 UX를 크게 향샹 시킬 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMutation 훅의 &lt;b&gt;onMutate&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;먼저, onMutate 프로퍼티가 사용되면 mutate 함수가 실행될 때 다음과 같은 순서로 작동된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;mutate 함수 실행&lt;/li&gt;
&lt;li&gt;onMutate에 할당한 콜백 함수 실행&lt;/li&gt;
&lt;li&gt;mutationFn에 할당한 콜백 함수 실행. 지금 예제에서는 addTodo 함수 실행&lt;/li&gt;
&lt;li&gt;addTodo 함수가 성공하면 onSuccess에 할당한 함수 실행&lt;/li&gt;
&lt;li&gt;addTodo 함수가 실패하면 onSuccess에 실패한 함수 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1739895589872&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//client/src/Button.jsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { addTodo } from './util';

const Button = ({ text, newTodo, clearInput }) =&amp;gt; {
  const queryClient = useQueryClient();
  const { mutate, isPending } = useMutation({
    mutationFn: addTodo,
    onMutate: async (newTodo) =&amp;gt; {
      // 혹시 모를 진행 중인 쿼리가 있을 경우를 대비
      await queryClient.cancelQueries({ queryKey: ['todo'] });

      const previousTodos = queryClient.getQueryData(['todo']);

      queryClient.setQueryData(['todo'], (oldData) =&amp;gt; {
        const newTodoItem = {
          id: oldData.length + 1,
          content: newTodo,
        };

        return [...oldData, newTodoItem];
      });

      clearInput();

      return { previousTodos };
    },
    onSuccess: () =&amp;gt; {
      queryClient.invalidateQueries(['todo']);
    },
    onError: (error, _, context) =&amp;gt; {
      console.error(error.message);
      // 이전 상태로 롤백
      queryClient.setQueryData(['todo'], context.previousTodos);
    },
  });

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; mutate(newTodo)}&amp;gt;{text}&amp;lt;/button&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

export default Button;&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;onMutate 의 콜백 함수를 자세히 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;onMutate 의 콜백 함수는 mutate 함수에 할당하는 변수를 그대로 받을 수 있다.&lt;/b&gt; 여기서는 newTodo가 된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739899368662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onMutate: async (newTodo) =&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;b&gt;먼저, cancelQuries 메서드를 통해 혹시라도 네트워크 요청 중인 'todo' 쿼리가 있다면 취소한다.&lt;/b&gt; 일종의 방어적 프로그래밍 기법이며 TanStack Query에서 낙관적 업데이트를 구현할 때 &lt;b&gt;추천하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre id=&quot;code_1739899391454&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await queryClient.cancelQueries({ queryKey: ['todo'] });&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;그리고, 이전 'todo' queryKey에 캐싱되어 있는 데이터를 &lt;b&gt;getQueryData&lt;/b&gt; 메서드를 통해 가져오고 &lt;b&gt;previousTodos&lt;/b&gt; 변수에 할당한다. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;해당 변수는 혹시라도 mutation 함수가 실패했을 때, 낙관적 업데이트를 취소하고 이전 상태로 되돌리기 위해 사용된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739899409277&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const previousTodos = queryClient.getQueryData(['todo']);&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;b&gt;setQueryData&lt;/b&gt; 메서드를 통해 'todo' queryKey에 캐싱된 데이터에&amp;nbsp;&lt;b&gt;newTodoItem을&lt;/b&gt;&amp;nbsp;추가하여 변경한다. setQueryData 메서드로 전달하는 콜백 함수 내의 &lt;b&gt;oldData&lt;/b&gt; 는 'todo' queryKey에 캐싱된 데이터를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;참고로, setQeuryData 로 캐싱된 데이터를 직접 변경하면 TanStack Query는 네트워크 요청 없이 변경된 데이터로 해당 queryKey를 사용하는 컴포넌트를 리렌더링 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739899434986&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;queryClient.setQueryData(['todo'], (oldData) =&amp;gt; {
        const newTodoItem = {
          id: oldData.length + 1,
          content: newTodo,
        };

        return [...oldData, newTodoItem];
      });&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;마지막으로, clearInput() 함수를 통해 input 값을 비우고 {previousTodos} 를 반환하며 onMutate의 콜백 함수의 실행을 종료한다.&lt;/p&gt;
&lt;pre id=&quot;code_1739899477996&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;onMutate: async (newTodo) =&amp;gt; {
     // 생략
      clearInput();
      return { previousTodos };
    },&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;b&gt;onMutate를 통해 반환된 {previousTodos} 는 onSuccess, onError 콜백 함수의 세 번째 인자로 전달할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 현재 onError 의 콜백 함수를 보면 세 번째 인자인 context로 받은 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgvXoN/btsMnN7saSX/KAcrna596gvPOPdyVmicC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgvXoN/btsMnN7saSX/KAcrna596gvPOPdyVmicC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgvXoN/btsMnN7saSX/KAcrna596gvPOPdyVmicC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgvXoN%2FbtsMnN7saSX%2FKAcrna596gvPOPdyVmicC1%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;577&quot; height=&quot;536&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;onError 콜백 함수는 setQueryData 메서드를 통해 이전에 캐싱된 데이터로 롤백하는 로직을 담고 있다.&lt;/b&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/54Kaw/btsMnQweMJ1/D6rf2phmgRxqfK8KqWbUe0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/54Kaw/btsMnQweMJ1/D6rf2phmgRxqfK8KqWbUe0/img.gif&quot; data-alt=&quot;무언가 이상하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/54Kaw/btsMnQweMJ1/D6rf2phmgRxqfK8KqWbUe0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/54Kaw/btsMnQweMJ1/D6rf2phmgRxqfK8KqWbUe0/img.gif&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;808&quot; height=&quot;502&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;무언가 이상하다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히 보면 이상하게 동작되는 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;할 일을 계속 추가하는 도중 낙관적 업데이트가 된 UI들이 사라지고 이전에 입력했던 할 일들이 하나씩 화면에 렌더링 되는 현상이 발생했다.&amp;nbsp;&lt;/b&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;처음에 ㅇ 입력 -&amp;gt; 낙관적 업데이트 반영&lt;/li&gt;
&lt;li&gt;두 번째에 ㄴ 입력 -&amp;gt; 낙관적 업데이트 반영&lt;/li&gt;
&lt;li&gt;세 번째에 ㄹ 입력 -&amp;gt; 낙관적 업데이트 반영&lt;/li&gt;
&lt;li&gt;네 번째에 ㅇ 입력 -&amp;gt; 낙관적 업데이트 반영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처음에 입력한 ㅇ에 대한 POST 요청이 3초가 지나서 서버가 todoList에 새로운 todo를 push하고 클라이언트에 응답&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Mutation 함수가 성공했기에 onSucess 콜백 함수가 실행되어 queryClient.invalidateQueries(['todo']) 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'todo' queryKey에 등록된 queryFn 즉, fetchTodo 함수가 실행되어 처음에 입력한 ㅇ까지 push된 todoList 전송&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;처음에 입력한 ㅇ까지 반영된 todoList가 화면에 리렌더링 되어 지금까지 반영된 낙관적 업데이트 모두 삭제&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;그 다음 입력한 ㄴ,ㄹ,ㅇ 도 입력한 순간을 기준으로 3초 뒤 위와 같은 이유로 화면에 리렌더링&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;결론은, 낙관적 업데이트를 위해 &lt;span style=&quot;text-align: left;&quot;&gt;queryClient.invalidateQueries(['todo']) 를 실행하면 안되는 것이다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;따라서, onSucess의 콜백 함수를 삭제하였다. 애초에 낙관적 업데이트는 서버가 정상적으로 작동할 것이라는 가정으로 행해지는 업데이트기에 지금은 굳이 mutation 함수가 성공했을 때 추가적으로 할 작업이 없기 때문이다.&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 style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;onSuccess 콜백 함수를 삭제한 후 결과는 다음과 같다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1UeVI/btsMmNAs45s/Mx0MEqO6Z6tM3q48yuUejk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1UeVI/btsMmNAs45s/Mx0MEqO6Z6tM3q48yuUejk/img.gif&quot; data-alt=&quot;잘 작동한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1UeVI/btsMmNAs45s/Mx0MEqO6Z6tM3q48yuUejk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/1UeVI/btsMmNAs45s/Mx0MEqO6Z6tM3q48yuUejk/img.gif&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;834&quot; height=&quot;546&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잘 작동한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&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;마지막으로 mutation 함수가 실패했을 때의 상황을 만들어 onError 콜백 함수가 제대로 작용하는지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 포트 번호는 &lt;b&gt;3000&lt;/b&gt;이지만 일부러 &lt;b&gt;3001&lt;/b&gt;로 수정하여 잘못된 요청을 보내게 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1739902957693&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const addTodo = async (newTodo) =&amp;gt; {
  const response = await fetch('http://localhost:3001', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ content: newTodo }),
  });
  if (!response.ok) {
    throw new Error('Failed to add todo');
  }

  return response.json();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2GLnM/btsMnFaIzL0/5NDrIVtospKQ9NFXwRWLU1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2GLnM/btsMnFaIzL0/5NDrIVtospKQ9NFXwRWLU1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2GLnM/btsMnFaIzL0/5NDrIVtospKQ9NFXwRWLU1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/2GLnM/btsMnFaIzL0/5NDrIVtospKQ9NFXwRWLU1/img.gif&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;967&quot; height=&quot;474&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 낙관적 업데이트를 수행했음에도, 요청이 실패하여 다시 이전 상태로 되돌아간 것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>React</category>
      <category>react</category>
      <category>tanstack query</category>
      <category>useMutation</category>
      <category>낙관적 업데이트</category>
      <category>리액트</category>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/22</guid>
      <comments>https://junhee1203.tistory.com/22#entry22comment</comments>
      <pubDate>Wed, 19 Feb 2025 03:43:57 +0900</pubDate>
    </item>
    <item>
      <title>[후기] 넛지헬스케어 코딩 테스트 후기</title>
      <link>https://junhee1203.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 부스트캠프 수료 후, 1월까지 리팩토링 기간을 거치고 현재 취준에 들어갔습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 하나가 &lt;b&gt;넛지헬스케어&lt;/b&gt;고 서류전형 통과 후 코딩 테스트를 보게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고로, 넛지헬스케어는 캐시워크라는 서비스를 운영하는 회사입니다.&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739258177228&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;캐시워크 - 돈버는 만보기 | 캐시워크 - 돈버는 만보기&quot; data-og-description=&quot;걷기만 해도 돈이 되는 신기한 만보기 캐시워크를 사용해보세요!&quot; data-og-host=&quot;cashwalk.com&quot; data-og-source-url=&quot;https://cashwalk.com/&quot; data-og-url=&quot;https://cashwalk.com/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://cashwalk.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cashwalk.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;캐시워크 - 돈버는 만보기 | 캐시워크 - 돈버는 만보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;걷기만 해도 돈이 되는 신기한 만보기 캐시워크를 사용해보세요!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cashwalk.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;넛지헬스케어 코딩 테스트 후기에 관해 검색되는 글이 전혀 없어서 어떤 식으로 출제되는지 전혀 감을 잡지 못한 채 테스트에 임했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;훗날 넛지헬스케어 코딩 테스트에 응시하시는 분들께 도움을 드리고자 간략한 후기를 작성해보겠습니다.&lt;/b&gt;&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;1️⃣ 플랫폼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;코드 시그널&lt;/b&gt;&lt;/span&gt;이라는 외국 플랫폼을 통해 테스트를 보게 됩니다. 외국에서는 유명한 플랫폼인 거 같은데 저는 처음 들어본 플랫폼이었습니다. &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;외국 플랫폼이다 보니 문제는 모두 영어로 출제됩니다. &lt;/span&gt;&lt;/b&gt;이 부분은 당연히 인지하고 있었지만, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;가뜩이나 시간도 짧은데 &lt;/span&gt;문제의 지문이 생각보다 길어서 해석하는데 매우 애를 먹었습니다.&lt;/b&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;테스트 하기 전, 코드 시그널에서 연습을 하고 싶었는데 사이트 내에 어디로 들어가야 알고리즘 연습을 할 수 있는지 알 수가 없어서 LeetCode로 연습을 조금 했습니다. 사실상 연습은 알고리즘 문제를 푸는 것보다 영어 문제 읽는 연습을 했습니다.&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;2️⃣ 난이도&lt;/h2&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;b&gt;시험 시간: 70분&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제 수: 4문제&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;언어: 자유 (필자는 JS로 응시)&lt;/b&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: #ef5369;&quot;&gt;&lt;b&gt;시험 시간과 관계없이 난이도만 본다면 대기업 코딩테스트보다는 확실히 쉬운 편입니다&lt;/b&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;그런데, 시간이 70분밖에 주어지지 않고 무엇보다 문제가 영어로 되어 있다 보니 4문제를 다 해결하는 것은 쉽지 않아 보입니다. 혹시나 해서 말씀드리면 화면이 공유되고 있기 때문에 gpt로 지문 해석하는 것은 불가능합니다.&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;그리고, JS로 응시하시는 분들은 입력에 관해서 궁금하실 겁니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;다행히, 프로그래머스처럼 함수만 완성하면 됩니다.&lt;/b&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;/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번 문제는 단순한 구현 문제였습니다. 해석에 문제만 없다면 매우 쉽습니다.&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;2번 문제는 재귀로 해결했습니다. 그러나, 지문을 해석하고 이해하는데 애를 많이 먹었습니다. 재귀 말고 다른 방법도 있을 거 같긴 합니다.&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;3번은 시간상 손도 못 댔습니다. 대충 보니 이차원 배열 관련된 문제인 것 같았습니다. 2번도 지문이 긴 편이었는데 2번보다도 지문이 길었고 작동 예시가 영상으로도 첨부 되어 있던 기괴한 문제였습니다. 보자마자 바로 스킵...&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;4번 문제는 완전 탐색으로 풀었는데 시간초과가 나왔습니다. 문제는 어렵지는 않았는데 시간 초과를 해결하지 못했습니다. 나중에 gpt한테 물어보니 투 포인터 or 슬라이딩 윈도우를 쓰라고 하던데, 투 포인터는 생각도 못했고 슬라이딩 윈도우는 뭔지 몰라서 어차피 시간이 충분해도 시간 초과는 해결 못했을 것 같습니다.&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;3️⃣ 점수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;각 문제마다 점수가 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 케이스, 시간 제한까지 모두 통과하면 &lt;b&gt;300점&lt;/b&gt;입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽히 통과하지 못하면 &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;저는 다음과 같이 점수를 받았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번: 300&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번: 300&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번: 0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번: 140&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;총점: 740/1200&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;4️⃣ 소감&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번 문제를 통해, 시간 초과를 해결할 수 있는 여러 알고리즘을 연습해야겠다고 생각이 들었습니다.&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;재귀 문제도 중간에 계속 실수를 하여, 시간을 많이 낭비하기도 했습니다. 아직 실력이 부족한 것 같습니다.&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;/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;&lt;b&gt;저는 탈락했습니다   아무래도 마지막 문제의 시간 초과를 해결하지 못한 것이 컸던 것 같습니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>일상</category>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/21</guid>
      <comments>https://junhee1203.tistory.com/21#entry21comment</comments>
      <pubDate>Tue, 11 Feb 2025 16:55:57 +0900</pubDate>
    </item>
    <item>
      <title>[React] TanStack-Query의 useInfiniteQuery 훅을 이용하여 무한스크롤 구현하기</title>
      <link>https://junhee1203.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TanStack-Query&lt;/b&gt; 는 다양한 기능을 제공하며, 그 중 &lt;b&gt;무한스크롤&lt;/b&gt; 기능도 제공한다. TanStack-Query에서 제공하는 &lt;b&gt;useInfinityQuery&lt;/b&gt; 훅을 사용하면 쉽게 무한스크롤 기능을 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 TanStack-Query를 이용하여 무한스크롤을 구현하는 과정을 정리해보겠다.&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;1️⃣ Mock Data&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API나 서버를 사용하기 번거로우니 클라이언트 자체에서 &lt;b&gt;Mock Data&lt;/b&gt; 를 만들어 사용하자.&lt;/p&gt;
&lt;pre id=&quot;code_1736418416834&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mockData.js
const mockData = Array.from({ length: 100 }, (_, i) =&amp;gt; i + 1).map(
  (num) =&amp;gt; `${num} 데이터입니다.`
);

export function fetchMockData(pageParam) {
  const MAX_PAGE_PARAM = 10;

  return new Promise((resolve, reject) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      if (pageParam &amp;lt;= MAX_PAGE_PARAM) {
        resolve(mockData.slice((pageParam - 1) * 10, pageParam * 10));
      } else {
        reject(new Error('페이지 번호가 최대 페이지 번호를 초과했습니다.'));
      }
    }, 2000);
  });
}&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;&lt;b&gt;mockData&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;['1번 데이터입니다', '2번 데이터입니다', ... , '100번 데이터입니다'] 배열&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fetchMockData 함수&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pageParam 매개변수를 받아서 mockData 배열을 slice 메서드로 10개씩 쪼갠다.&lt;/li&gt;
&lt;li&gt;pageParam === 1 =&amp;gt; ['1번 데이터입니다', ... '10번 데이터입니다']&lt;/li&gt;
&lt;li&gt;pageParam === 2 =&amp;gt; ['11번 데이터입니다', ... '20번 데이터입니다']&lt;/li&gt;
&lt;li&gt;쪼개진 배열은 Promise의 resolve 값으로 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;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ 화면 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetchMockData 함수를 사용하여 mockData를 화면에 렌더링 해보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736419459857&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import './App.css';
import { useEffect, useState } from 'react';
import { fetchMockData } from './mockData';

function App() {
  const [items, setItems] = useState(null);

  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
      const fetchedData = await fetchMockData(1);
      setItems(fetchedData);
    };

    fetchData();
  }, []);

  return (
    &amp;lt;div className=&quot;main-container&quot;&amp;gt;
      {items?.map((item, index) =&amp;gt; (
        &amp;lt;div key={index}&amp;gt;{item}&amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1736419481831&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* App.css */
.main-container {
  width: 100%;
  background-color: gray;
}

.list-container {
  width: 100%;
  height: 15vh;
  color: azure;
  font-size: 24px;
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SCcAe/btsLITHtKHQ/QDLpMxFL2DVK6xytaLtSF0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SCcAe/btsLITHtKHQ/QDLpMxFL2DVK6xytaLtSF0/img.gif&quot; data-alt=&quot;새로고침을 하니 2초 후 전달 받은 데이터가 화면에 렌더링 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SCcAe/btsLITHtKHQ/QDLpMxFL2DVK6xytaLtSF0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/SCcAe/btsLITHtKHQ/QDLpMxFL2DVK6xytaLtSF0/img.gif&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;1920&quot; height=&quot;879&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로고침을 하니 2초 후 전달 받은 데이터가 화면에 렌더링 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣ useInfinityQuery 훅&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재까지는 &lt;b&gt;fetchMockData&lt;/b&gt; 함수의 매개변수에 1을 전달했으므로 1번부터 10번 데이터까지만 표시된다. 만약 매개변수를 2로 변경하면 기존의 1번부터 10번 데이터는 삭제되고, 11번부터 20번 데이터가 화면에 표시될 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;그러나, 무한스크롤의 핵심은 이전 데이터는 그대로 화면에 유지되며 새로운 데이터를 계속 추가하여 화면에 렌더링 하는 것이다.&lt;/b&gt;&lt;/span&gt; &lt;b&gt;TanStack-Query&lt;/b&gt; 에서 제공하는 &lt;b&gt;uesInfiniteQuery&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;우선, useInfiniteQuery 훅에서는 &lt;b&gt;Page&lt;/b&gt;라는 개념이 등장한다. 이때, &lt;b&gt;Page&lt;/b&gt;를 흔히 우리가 생각하는 책 한 장 한 장의 페이지로 생각하면 혼란스러울 수 있다. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;여기서의 Page는 일종의 데이터 덩어리 혹은 단위 즉, Chunk라고 생각하는 것이 맞다.&lt;/b&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;현재, 무한스크롤을 구현하기 위해서는 useInfinteQuery 함수를 다음과 같이 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1736436881421&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['mockData'],
    queryFn: ({ pageParam }) =&amp;gt; fetchMockData(pageParam),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) =&amp;gt;
      allPages.length &amp;lt;= 10 ? allPages.length + 1 : undefined,
  });&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;useInfitieQuery에 할당한 객체 내의 옵션에 대해 먼저 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TanStack-Query에 대한 기초 지식이 있다고 가정하고 queryKey에 대한 내용은 생략하겠다.&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;b&gt;queryFn&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useInfiniteQuery 는 useQuery와 달리 queryFn에 &lt;b&gt;pageParam&lt;/b&gt; 을 매개변수로 하는 콜백함수를 넣음&lt;/li&gt;
&lt;li&gt;그리고, 그 pageParam은 API 함수(fetchMockData)의 매개변수로 할당하는 것이 일반적&lt;/li&gt;
&lt;li&gt;개발자가 직접 설정하지 않는 이상 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;pageParam의 초기값은&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;undefined&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;컴포넌트가 마운트 되면 초기 pageParam 값으로 자동으로 한 번 실행.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;initialPageParam&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: #ef5369;&quot;&gt;&lt;b&gt;queryFn 의 초기 pageParam 값&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;즉, 위의 코드에서 queryFn이 처음 실행될 때 pageParam의 값은 1이 되므로, fetchMockData(1) 함수가 실행됨.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;getNextPageParam&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;lastPage&lt;/b&gt;, &lt;b&gt;allPages&lt;/b&gt; 를 매개변수로 하는 콜백함수를 넣음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; lastPage&lt;/b&gt;는 가장 마지막 queryFn을 통해 받아온 데이터 덩어리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;allPages&lt;/b&gt;는 지금까지 받아온 모든 데이터 덩어리의 배열&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;해당 함수의 리턴값은 다음 queryFn이 실행될 때 pageParam의 값으로 할당 됨.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;현재 mockData의 최대 pageParam은 10이었으므로 삼항연산자를 통해 queryFn의 pageParam이 최대 10까지 들어가도록 설정&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;data&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;pageParam&lt;/b&gt;, &lt;b&gt;pages&lt;/b&gt; 프로퍼티를 갖는 객체를 반환&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: left;&quot;&gt;pageParam은 &lt;/span&gt;지금까지 실행된 queryFn의 pageParam 값을 순서대로 기록한 배열&lt;/b&gt;&lt;/span&gt; (queryFn의 pageParam과 다름)&lt;/li&gt;
&lt;li&gt;pages는 지금까지 받아온 모든 데이터 즉, allPages랑 같음.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;fetchNextPage&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: #ef5369;&quot;&gt;&lt;b&gt;getNexPageParam 함수를 실행시켜 변화된 pageParam 값으로 queryFn을 실행시키는 함수&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;pageParam이 undefined면 queryFn 실행 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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;hasNextPage&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 받아올 데이터가 존재하는지의 여부를 나타내는 boolean 값&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;isFetchingNextPage&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 받아오고 있는지 여부를 나타내는 boolean 값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1367&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5blLu/btsLIDkz6bd/r5LbZixAjQ6p2fxzIR91X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5blLu/btsLIDkz6bd/r5LbZixAjQ6p2fxzIR91X0/img.png&quot; data-alt=&quot;대략적인 흐름도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5blLu/btsLIDkz6bd/r5LbZixAjQ6p2fxzIR91X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5blLu%2FbtsLIDkz6bd%2Fr5LbZixAjQ6p2fxzIR91X0%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;1367&quot; height=&quot;560&quot; data-origin-width=&quot;1367&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대략적인 흐름도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백문이불어일견 테스트 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1736443891138&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.jsx
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

createRoot(document.getElementById('root')).render(
  &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/QueryClientProvider&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1736440575117&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import './App.css';
import { fetchMockData } from './mockData';
import { useInfiniteQuery } from '@tanstack/react-query';

function App() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ['mockData'],
      queryFn: ({ pageParam }) =&amp;gt; fetchMockData(pageParam),
      initialPageParam: 1,
      getNextPageParam: (lastPage, allPages) =&amp;gt;
        allPages.length &amp;lt; 10 ? allPages.length + 1 : undefined,
    });

  console.log(data)

  // data.pages가 이차원 배열이므로 flat 메서드 이용
  const allItems = data?.pages?.flat();

  return (
    &amp;lt;div className=&quot;main-container&quot;&amp;gt;
      {allItems?.map((item, index) =&amp;gt; (
        &amp;lt;div className=&quot;list-container&quot; key={index}&amp;gt;
          {item}
        &amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nmx3Y/btsLI2YK19u/ln6LjGpigsrwoH2SdmzlG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nmx3Y/btsLI2YK19u/ln6LjGpigsrwoH2SdmzlG0/img.png&quot; data-alt=&quot;console.log(data) 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nmx3Y/btsLI2YK19u/ln6LjGpigsrwoH2SdmzlG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNmx3Y%2FbtsLI2YK19u%2Fln6LjGpigsrwoH2SdmzlG0%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;567&quot; height=&quot;166&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;console.log(data) 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAJaKF/btsLI3i5PgF/nxw3tZGcsBQQXi2KgTiC1K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAJaKF/btsLI3i5PgF/nxw3tZGcsBQQXi2KgTiC1K/img.gif&quot; data-alt=&quot;화면에 정상적으로 렌더링 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAJaKF/btsLI3i5PgF/nxw3tZGcsBQQXi2KgTiC1K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cAJaKF/btsLI3i5PgF/nxw3tZGcsBQQXi2KgTiC1K/img.gif&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;1920&quot; height=&quot;879&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;화면에 정상적으로 렌더링 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 fetchNextPage 함수와 hasNextPage, isFetchingNextPage 값을 사용하여 UI 표현 및 데이터를 추가적으로 받아오자.&lt;/p&gt;
&lt;pre id=&quot;code_1736441179623&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx

function App() {
 // 생략
 return (
    &amp;lt;div className=&quot;main-container&quot;&amp;gt;
      {allItems?.map((item, index) =&amp;gt; (
        &amp;lt;div className=&quot;list-container&quot; key={index}&amp;gt;
          {item}
        &amp;lt;/div&amp;gt;
      ))}
      {data &amp;amp;&amp;amp; hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage &amp;amp;&amp;amp; (
        &amp;lt;button onClick={fetchNextPage}&amp;gt;데이터 추가&amp;lt;/button&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&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;button 태그를 추가하고 onClick 이벤트 핸들러에 fetchNextPage 함수를 할당했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;데이터 추가 버튼은 초기 데이터를 받아온 후, 다음 데이터를 가져올 수 있는 상태이면서 데이터 로딩 중이 아닐 때만 표시되도록 하여, UI의 어색함을 해소했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wb3Lt/btsLIaDctvL/EtDqtKUelNnqb2LPTbxHt1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wb3Lt/btsLIaDctvL/EtDqtKUelNnqb2LPTbxHt1/img.gif&quot; data-alt=&quot;이전 데이터를 유지하며 추가적으로 데이터를 받아오고 화면에 렌더링 한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wb3Lt/btsLIaDctvL/EtDqtKUelNnqb2LPTbxHt1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/wb3Lt/btsLIaDctvL/EtDqtKUelNnqb2LPTbxHt1/img.gif&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;1920&quot; height=&quot;879&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이전 데이터를 유지하며 추가적으로 데이터를 받아오고 화면에 렌더링 한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNFozI/btsLJu1w5sa/7pHCNpPowgQwrOt3dvrKf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNFozI/btsLJu1w5sa/7pHCNpPowgQwrOt3dvrKf0/img.png&quot; data-alt=&quot;30 데이터까지 받아온 후 console.log(data) 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNFozI/btsLJu1w5sa/7pHCNpPowgQwrOt3dvrKf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNFozI%2FbtsLJu1w5sa%2F7pHCNpPowgQwrOt3dvrKf0%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;593&quot; height=&quot;245&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;30 데이터까지 받아온 후 console.log(data) 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣ 무한스크롤 구현&lt;/h2&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;이를 위해 &lt;b&gt;IntersectionObserver&lt;/b&gt;를 이용한다.&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt; IntersectionObserver는 내가 원하는 요소가 현재 뷰포트에서 보이는지 안 보이는지 추적할 수 있는 객체이다.&lt;/b&gt;&lt;/span&gt; 리액트에서는 &lt;span style=&quot;color: #333e4c; text-align: start;&quot;&gt;&lt;b&gt;react-intersection-observer&lt;/b&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: #333e4c; text-align: start;&quot;&gt;먼저, &lt;span style=&quot;color: #333e4c; text-align: start;&quot;&gt;react-intersection-observer&lt;/span&gt; 라이브러리를 설치한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736445536858&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i react-intersection-observer&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;아래와 같이 &lt;b&gt;useInView&lt;/b&gt; 훅을 호출한다.&lt;/p&gt;
&lt;pre id=&quot;code_1736445603013&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [ref, inView] = useInView();&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;useRef 훅을 사용해보았다면 ref의 의미를 바로 파악할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;r&lt;/b&gt;&lt;b&gt;ef는 내가 추적할 태그(요소)의 ref 속성으로 넣어주면 된다. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;inView는 추적할 요소가 화면에 보이면 true, 보이지 않으면 false가 되는 boolean 값이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 이제 기존의 button 태그 대신 보이지 않는 div 태그를 만들어 useInView 훅을 이용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1736445811150&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import { useEffect } from 'react';
import './App.css';
import { fetchMockData } from './mockData';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';

function App() {
 // 생략
  const [ref, inView] = useInView();
  
  console.log(inView)

  return (
    &amp;lt;div className=&quot;main-container&quot;&amp;gt;
      {allItems?.map((item, index) =&amp;gt; (
        &amp;lt;div className=&quot;list-container&quot; key={index}&amp;gt;
          {item}
        &amp;lt;/div&amp;gt;
      ))}
      {data &amp;amp;&amp;amp; hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage &amp;amp;&amp;amp; &amp;lt;div ref={ref}&amp;gt;&amp;lt;/div&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbbhmH/btsLHOtCMJR/wecTtBnceJ5FNjF1CUGKg1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbbhmH/btsLHOtCMJR/wecTtBnceJ5FNjF1CUGKg1/img.gif&quot; data-alt=&quot;스크롤이 아래로 내려가니 inView의 값이 true가 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbbhmH/btsLHOtCMJR/wecTtBnceJ5FNjF1CUGKg1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bbbhmH/btsLHOtCMJR/wecTtBnceJ5FNjF1CUGKg1/img.gif&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;1920&quot; height=&quot;879&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스크롤이 아래로 내려가니 inView의 값이 true가 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최하단의 숨겨진 div 태그가 뷰포트에 나타나는 순간에 inView가 true가 되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 inView가 true가 될 때, &lt;b&gt;fetchNextPage&lt;/b&gt; 함수를 실행하면 무한스크롤이 완성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;useEffect&lt;/b&gt; 훅을 이용하자.&lt;/p&gt;
&lt;pre id=&quot;code_1736446069059&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import { useEffect } from 'react';
import './App.css';
import { fetchMockData } from './mockData';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';

function App() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ['mockData'],
      queryFn: ({ pageParam }) =&amp;gt; fetchMockData(pageParam),
      initialPageParam: 1,
      getNextPageParam: (lastPage, allPages) =&amp;gt;
        allPages.length &amp;lt; 10 ? allPages.length + 1 : undefined,
    });

  const [ref, inView] = useInView();

  useEffect(() =&amp;gt; {
    if (inView) fetchNextPage();
  }, [inView, fetchNextPage]);

  const allItems = data?.pages?.flat();

  return (
    &amp;lt;div className=&quot;main-container&quot;&amp;gt;
      {allItems?.map((item, index) =&amp;gt; (
        &amp;lt;div className=&quot;list-container&quot; key={index}&amp;gt;
          {item}
        &amp;lt;/div&amp;gt;
      ))}
      {data &amp;amp;&amp;amp; hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage &amp;amp;&amp;amp; &amp;lt;div ref={ref}&amp;gt;&amp;lt;/div&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oazo5/btsLHTaUE4P/uPmVWlrZrxBMBTggeGy841/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oazo5/btsLHTaUE4P/uPmVWlrZrxBMBTggeGy841/img.gif&quot; data-alt=&quot;무한스크롤이 제대로 작동한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oazo5/btsLHTaUE4P/uPmVWlrZrxBMBTggeGy841/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/Oazo5/btsLHTaUE4P/uPmVWlrZrxBMBTggeGy841/img.gif&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;1920&quot; height=&quot;879&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;무한스크롤이 제대로 작동한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, useEffect를 사용하지 않고 useInView 훅의 매개변수를 할당해 다음과 같이 구현할 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1736446288468&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import './App.css';
import { fetchMockData } from './mockData';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';

function App() {
	// 생략
  const [ref] = useInView({
    triggerOnce: true,
    onChange: (inView) =&amp;gt; {
      if (inView) fetchNextPage();
    },
  });

  return (
    // 생략
}

export default App;&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;5️⃣ 스켈레톤 UI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 스켈레톤 UI도 구현해서 적용해보자. 현재는 fetch 요청이 발생하면 2초 후에 데이터를 받아온다.&amp;nbsp; 2초의 시간 동안 화면에 어떠한 변화도 없으니 UX가 매우 떨어지기 마련이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 스켈레톤 UI를 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1736447206546&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import './Skeleton.css';

const Skeleton = () =&amp;gt; {
  return (
    &amp;lt;&amp;gt;
      {[...Array(10)].map((_, index) =&amp;gt; (
        &amp;lt;div className=&quot;skeleton-container&quot; key={index}&amp;gt;&amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/&amp;gt;
  );
};

export default Skeleton;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1736447235971&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* Skeleton.css */
.skeleton-container{
  color: gray;
  height: 15vh;
}&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;이제 isFetchinNextPage 가 true일 때, 스켈레돈 UI를 화면에 렌더링 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1736447279230&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import './App.css';
import { fetchMockData } from './mockData';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import Skeleton from './skeleton';

function App() {
  // 생략

  return (
    &amp;lt;div className=&quot;main-container&quot;&amp;gt;
      {allItems?.map((item, index) =&amp;gt; (
        &amp;lt;div className=&quot;list-container&quot; key={index}&amp;gt;
          {item}
        &amp;lt;/div&amp;gt;
      ))}
      {isFetchingNextPage &amp;amp;&amp;amp; &amp;lt;Skeleton/&amp;gt;}
      {data &amp;amp;&amp;amp; hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage &amp;amp;&amp;amp; &amp;lt;div ref={ref}&amp;gt;&amp;lt;/div&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZeVzI/btsLHxeDsvs/K60tapyU4HD3gYr7z3waa1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZeVzI/btsLHxeDsvs/K60tapyU4HD3gYr7z3waa1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZeVzI/btsLHxeDsvs/K60tapyU4HD3gYr7z3waa1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dZeVzI/btsLHxeDsvs/K60tapyU4HD3gYr7z3waa1/img.gif&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;1920&quot; height=&quot;879&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 실행되고 데이터를 불러올 오는 사이에 스켈레톤 UI가 렌더링 되어 UX가 전보다 크게 향상된 것을 체감할 수 있다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <category>react</category>
      <category>tanstack-query</category>
      <category>useInfiniteQuery</category>
      <category>리액트</category>
      <category>리액트 쿼리</category>
      <category>무한스크롤</category>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/20</guid>
      <comments>https://junhee1203.tistory.com/20#entry20comment</comments>
      <pubDate>Fri, 10 Jan 2025 03:42:39 +0900</pubDate>
    </item>
    <item>
      <title>[React] 비동기 데이터 상태 관리와 TanStack-Query 라이브러리</title>
      <link>https://junhee1203.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대게, 프론트엔드에서 &lt;b&gt;상태관리 하는 데이터&lt;/b&gt;를 두 가지로 구분할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 일반 데이터&lt;/b&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;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;/li&gt;
&lt;li&gt;사용자 입력, UI 상태(모달 열림/닫힘, 탭 선택 등), 선택된 아이템 등.&lt;/li&gt;
&lt;li&gt;비교적 단순하며, 비동기 작업이 필요하지 않음.&lt;/li&gt;
&lt;li&gt;useState, useReducer 훅을 이용하여 상태 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 비동기 데이터&lt;/b&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;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;외부 API, 데이터베이스 등에서 가져오거나 저장하는 데이터.&lt;/li&gt;
&lt;li&gt;일반적으로 서버와 통신해야 하므로 비동기 작업(Promise, async/await)이 필요함.&lt;/li&gt;
&lt;li&gt;단순할 경우 useEffect, fetch 를 통하여 상태 관리&lt;/li&gt;
&lt;li&gt;복잡하면 SWR, TanStack Query 등 라이브러리를 이용하여 상태 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;프론트엔드에서 외부 API를 사용하여 데이터를 받아오는 경우는 일반적이므로 비동기 데이터를 상태 관리 하는 역량은 필수적이라고 할 수 있다.&lt;/b&gt;&lt;/span&gt; 이번 글에서는 비동기 데이터를 상태 관리하는 방법과 대표적인 라이브러리인&amp;nbsp;&lt;b&gt;TanStack Query&lt;/b&gt;에 대해 간략히 알아보겠다. 개인적으로, &lt;b&gt;TanStack Query&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;b&gt;TanStack Query&lt;/b&gt;의 예전 이름은 &lt;b&gt;React Query&lt;/b&gt; 였고, 실제로 지금도 많은 사람들이 &lt;b&gt;React Query&lt;/b&gt; 라는 이름에 더욱 익숙하다. 필자 역시 그렇다. 과거의 리액트만 지원했던 &lt;b&gt;React Query&lt;/b&gt;가 이제는 Svelt, Vue 등 다양한 SPA 프레임워크를 지원하게 된 이후, 이름을 &lt;b&gt;TanStack Query&lt;/b&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;1️⃣ 초기 세팅&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  서버 세팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 외부 API를 받아오기 위해 로컬에서 서버를 세팅해주자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 루트 디렉토리 하위에 server, client 디렉토리를 분리해 주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1734252759293&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├─&amp;nbsp;client
└─&amp;nbsp;server&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;server 디렉토리로 이동한 후, 관련 라이브러리를 설치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1734252840641&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd /server
npm i express cors&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;server 디렉토리 하위에 app.js 파일을 만들고 다음과 같이 세팅해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1734252896267&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ./server/app.js
const express = require('express');
const app = express();
const cors = require('cors');
const port = 3000;

// 미들웨어 설정
app.use(express.json()); // JSON 파싱
app.use(express.urlencoded({ extended: true })); // URL-encoded 파싱
app.use(cors()); // cors 방지

const todoList = [
  {
    id: 1,
    content: '밥 먹기',
  },
  {
    id: 2,
    content: '공부하기',
  },
  {
    id: 3,
    content: '잠 자기',
  },
];

app.get('/', (req, res) =&amp;gt; {
  res.send(todoList);
});

app.listen(port, () =&amp;gt; {
  console.log(`서버가 http://localhost:${port} 에서 실행 중입니다`);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node 명령어로 서버를 실행시켜준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  클라이언트 세팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 클라이언트 초기 세팅을 하자. &lt;b&gt;Vite&lt;/b&gt;를 이용하여 리액트 프로젝트를 생성하겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1734253147595&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd ../client
npm create vite@latest
npm i&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;초기 Vite로 리액트 세팅을 했을 때 생성된 불필요한 코드와 파일들을 삭제해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 편의상 &lt;b&gt;StrictMode&lt;/b&gt; 를 해제하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 App.jsx 코드를 다음과 같이 입력하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1734253252225&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from 'react';
import './App.css';

function App() {
  const [todoList, setTodoList] = useState([]);

  useEffect(() =&amp;gt; {
    const fetchTodoList = async (url) =&amp;gt; {
      const response = await fetch(url);
      const todoList = await response.json();
      setTodoList(todoList);
    };

    fetchTodoList('http://localhost:3000');
  }, []);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;TodoList&amp;lt;/h1&amp;gt;
      &amp;lt;ul&amp;gt;
        {todoList.map((todo) =&amp;gt; (
          &amp;lt;li key={todo.id}&amp;gt;{todo.content}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;&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;이제 npm run dev 명령어로 클라이언트 서버를 실행시켜주면 다음과 같이 화면이 렌더링 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qsb3r/btsLjaiUC5L/CnvmYrgZPtREKcK3cx82K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qsb3r/btsLjaiUC5L/CnvmYrgZPtREKcK3cx82K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qsb3r/btsLjaiUC5L/CnvmYrgZPtREKcK3cx82K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqsb3r%2FbtsLjaiUC5L%2FCnvmYrgZPtREKcK3cx82K0%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;543&quot; height=&quot;268&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 클라이언트 코드를 보면, &lt;b&gt;useEffect&lt;/b&gt; 와 &lt;b&gt;fetch&lt;/b&gt;의 조합으로 비동기 데이터를 클라이언트로 받아온 뒤, &lt;b&gt;useState&lt;/b&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;2️⃣ useFetch 커스텀 훅&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허나, 위의 단순한 코드는 대표적으로 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;두 가지 문제점&lt;/b&gt;&lt;/span&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;서버로부터 데이터를 받아오는 사이에 보여줄 UI가 존재하지 않음. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;즉, 로딩 UI가 존재하지 않음.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;만약, 데이터를 받아오는 것에 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;실패한다면 보여줄 UI가 존재하지 않음.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백분이 불어일견, 한번 잘못된 URL로 요청을 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1734253991455&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  useEffect(() =&amp;gt; {
    const fetchTodoList = async (url) =&amp;gt; {
      const response = await fetch(url);
      const todoList = await response.json();
      setTodoList(todoList);
    };

    fetchTodoList('http://localhost:3001'); // 포트 번호를 3001로 수정
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDLI3H/btsLjGIo8vN/JRanmniHmWB8L7zvVRTUX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDLI3H/btsLjGIo8vN/JRanmniHmWB8L7zvVRTUX1/img.png&quot; data-alt=&quot;화면에 아무 것도 보이지 않는다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDLI3H/btsLjGIo8vN/JRanmniHmWB8L7zvVRTUX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDLI3H%2FbtsLjGIo8vN%2FJRanmniHmWB8L7zvVRTUX1%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;621&quot; height=&quot;263&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;화면에 아무 것도 보이지 않는다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 정상적인 URL로 요청하지만 데이터를 받아오는 시간, 즉 로딩 시간을 길게 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;서버에서 요청을 받으면 2초 후에 데이터를 전송하도록 코드를 수정하겠다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734254184690&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./server/app.js
app.get('/', (req, res) =&amp;gt; {
  setTimeout(() =&amp;gt; {
    res.send(todoList);
  },2000);
});&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVwMQO/btsLjdNsYy4/TzajXp0kYTUfpUIIUeiyJk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVwMQO/btsLjdNsYy4/TzajXp0kYTUfpUIIUeiyJk/img.gif&quot; data-alt=&quot;새로고침 하면 2초 동안 아무 것도 보이지 않다가 2초 후 리스트가 화면에 렌더링 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVwMQO/btsLjdNsYy4/TzajXp0kYTUfpUIIUeiyJk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dVwMQO/btsLjdNsYy4/TzajXp0kYTUfpUIIUeiyJk/img.gif&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;516&quot; height=&quot;288&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로고침 하면 2초 동안 아무 것도 보이지 않다가 2초 후 리스트가 화면에 렌더링 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;위의 두 문제를 해결하는 방법은 간단하다. 로딩 중이나 에러가 발생했을 때 보여줄 UI를 만들면 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 다음과 같은 &lt;b&gt;useFetch 커스텀 훅&lt;/b&gt;으로 해결 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 아래 &lt;b&gt;useFetch&lt;/b&gt; 로직에는 몇 가지 개선점이 있지만, 대략적인 흐름 차원에서만 보면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1734512162339&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from 'react';

export const useFetch = (url) =&amp;gt; {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true); // 로딩 중임을 나타낼 state
  const [error, setError] = useState(null); // 에러를 나타낼 state

  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
      try {
        setIsLoading(true);
        setError(null);

        const response = await fetch(url);

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        setData(data);
        setIsLoading(false);
      } catch (error) {
        setError(error.message);
        setIsLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, isLoading, error };
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1734515897138&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import './App.css';
import { useFetch } from './useFetch';

function App() {
  const {
    data: todoList,
    isLoading,
    error,
  } = useFetch('http://localhost:3000');

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;TodoList&amp;lt;/h1&amp;gt;
      {isLoading &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Loading...&amp;lt;/span&amp;gt;}
      {error &amp;amp;&amp;amp; &amp;lt;span&amp;gt;{error}&amp;lt;/span&amp;gt;}
      {!isLoading &amp;amp;&amp;amp; !error &amp;amp;&amp;amp; (
        &amp;lt;ul&amp;gt;
          {todoList?.map((todo) =&amp;gt; (
            &amp;lt;li key={todo.id}&amp;gt;{todo.content}&amp;lt;/li&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&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;아래는 위의 useFetch를 적용했을 때의 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKB0KZ/btsLnklunSk/4xmxoeRBOb77PSkJ2rKSsk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKB0KZ/btsLnklunSk/4xmxoeRBOb77PSkJ2rKSsk/img.gif&quot; data-alt=&quot;정상적인 URL로 요청했을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKB0KZ/btsLnklunSk/4xmxoeRBOb77PSkJ2rKSsk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cKB0KZ/btsLnklunSk/4xmxoeRBOb77PSkJ2rKSsk/img.gif&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;612&quot; height=&quot;309&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정상적인 URL로 요청했을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOtiJf/btsLn7Z2PDY/xc3MGBmkOZ7GkaXHpJXkCK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOtiJf/btsLn7Z2PDY/xc3MGBmkOZ7GkaXHpJXkCK/img.gif&quot; data-alt=&quot;잘못된 URL로 fetch 요청을 했을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOtiJf/btsLn7Z2PDY/xc3MGBmkOZ7GkaXHpJXkCK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bOtiJf/btsLn7Z2PDY/xc3MGBmkOZ7GkaXHpJXkCK/img.gif&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;606&quot; height=&quot;280&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잘못된 URL로 fetch 요청을 했을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;이전의 결과와 다르게 로딩 UI를 넣음으로써, 보다 개선된 UX를 제공할 수 있다.&lt;/b&gt;&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;3️⃣ TanStack Query 도입&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위의 예시에서 useFetch 훅을 사용하여 데이터 이외에 isLoading, error 라는 추가적인 상태를 직접 관리해 주었다.&lt;/b&gt; 그러나, 개발자 입장에서 관리해야될 상태가 늘어나는 것은 여간 좋은 일이 아니다. &lt;b&gt;TanStack Query&lt;/b&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;이번에는 useFetch 훅을 사용하지 않고 TanStack Query를 사용하여 수동으로 상태 관리를 하지 않고 위의 예시를 리팩토링 해보자.&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_1734538455741&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm i @tanstack/react-query&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;main.jsx에서 &lt;b&gt;queryClient 인스턴스&lt;/b&gt;를 생성하고 &lt;b&gt;QueryClientProvider&lt;/b&gt; 이름의&lt;b&gt; Context 컴포넌트&lt;/b&gt;로 App 컴포넌트를 감싸고 props로 생성한 인스턴스를 전달한다. 이때, &lt;b&gt;queryClient 인스턴스는 TanStack Query의 모든 쿼리와 캐시를 관리&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;참고로, TanStack Query에서 '쿼리'는 서버에 특정 데이터를 요청하고 가져오는 작업 즉, 패칭 작업이라고 생각하면 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734538401056&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.jsx
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient(); // 인스턴스 생성

createRoot(document.getElementById('root')).render(
 // Context 컴포넌트로 감싸기
  &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/QueryClientProvider&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.jsx 로 돌아가서 &lt;b&gt;useQuery 훅&lt;/b&gt;을 사용하여 다음과 같이 비동기 데이터를 처리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1734539206035&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.jsx
import { useQuery } from '@tanstack/react-query';
import './App.css';

function App() {
  const fetchTodos = async () =&amp;gt; {
    const response = await fetch('http://localhost:3000');
    return response.json();
  };

  const {
    isLoading,
    data: todoList,
    isError,
    error,
  } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;TodoList&amp;lt;/h1&amp;gt;
      {isLoading &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Loading...&amp;lt;/span&amp;gt;}
      {isError &amp;amp;&amp;amp; &amp;lt;span&amp;gt;{error.message}&amp;lt;/span&amp;gt;}
      {!isLoading &amp;amp;&amp;amp; !error &amp;amp;&amp;amp; (
        &amp;lt;ul&amp;gt;
          {todoList?.map((todo) =&amp;gt; (
            &amp;lt;li key={todo.id}&amp;gt;{todo.content}&amp;lt;/li&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}

export default App;&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;하나씩 살펴보면, 위의 코드에서 useQuery는 &lt;b&gt;queryKey&lt;/b&gt;, &lt;b&gt;queryFn&lt;/b&gt; 이름의 프로퍼티를 갖는 객체를 매개변수로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;queryKey 는 쿼리를 식별하는 역할을 한다.&lt;/b&gt;&lt;/span&gt; 현재 예제에서는 큰 의미를 갖지 않는다. 후에 좀 더 자세히 다루겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;queryFn 은 데이터를 가져오는 함수를 프로퍼티의 value로 받는다.&lt;/b&gt;&lt;/span&gt; 위의 예시에서는 fetchTodos 함수를 value로 받았다.&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: #ef5369;&quot;&gt;&lt;b&gt;useQuery는 isLoading, data, isError, error 등등 여러 프로퍼티를 갖는 하나의 객체를 반환한다.&lt;/b&gt;&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;isLoading&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;첫 번째 데이터 로딩 시에만 true&lt;/li&gt;
&lt;li&gt;데이터가 없고 처음으로 fetch하는 중일 때만 true&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;data&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;성공적으로 받아온 데이터&lt;/li&gt;
&lt;li&gt;아직 데이터를 받아오지 않았다면 undefined&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;isError&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;에러가 발생했을 때 true&lt;/li&gt;
&lt;li&gt;fetch 실패, 데이터 파싱 오류 등이 발생했을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;error&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;구체적인 에러 정보를 담은 객체&lt;/li&gt;
&lt;li&gt;isError가 true일 때만 값이 존재&lt;/li&gt;
&lt;li&gt;주로 error.message로 에러 메시지 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 그 외로 &lt;b&gt;isFetching&lt;/b&gt;, &lt;b&gt;isSuccess&lt;/b&gt;, &lt;b&gt;status&lt;/b&gt; 등 다양한 프로퍼티가 있다.&amp;nbsp; 워낙 많은 프로퍼티를 제공하기에 보통 위의 코드처럼 필요한 프로퍼티만 &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;이제 &lt;b&gt;TanStack&lt;/b&gt; &lt;b&gt;Query&lt;/b&gt; 라이브러리를 적용한 결과를 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s5zrN/btsLnnvSgRQ/xWVoMKEIuJ7MlBo8DMN3w0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s5zrN/btsLnnvSgRQ/xWVoMKEIuJ7MlBo8DMN3w0/img.gif&quot; data-alt=&quot;잘 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s5zrN/btsLnnvSgRQ/xWVoMKEIuJ7MlBo8DMN3w0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/s5zrN/btsLnnvSgRQ/xWVoMKEIuJ7MlBo8DMN3w0/img.gif&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;549&quot; height=&quot;313&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;잘 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 잘못된 URL로 요청을 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bar2El/btsLn0Ae9An/vHP7cKlrehVvPm1bOKZY21/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bar2El/btsLn0Ae9An/vHP7cKlrehVvPm1bOKZY21/img.gif&quot; data-alt=&quot;시간이 조금 걸리지만 에러 메세지가 나온다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bar2El/btsLn0Ae9An/vHP7cKlrehVvPm1bOKZY21/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bar2El/btsLn0Ae9An/vHP7cKlrehVvPm1bOKZY21/img.gif&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;536&quot; height=&quot;301&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시간이 조금 걸리지만 에러 메세지가 나온다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;시간이 조금 걸리는 이유는 기본적으로 TanStack Query는 데이터 요청에 실패했을 시, 1초 간격으로 3번의 재시도를 한다.&lt;/b&gt;&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;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;1초 후 첫 번째 retry&lt;/li&gt;
&lt;li&gt;1초 후 두 번째 retry&lt;/li&gt;
&lt;li&gt;1초 후 세 번째 retry&lt;/li&gt;
&lt;li&gt;최종 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 기본 설정을 바꾸고 싶으면 main.jsx에서 queryClient 인스턴스를 다음과 같이 생성하면된다.&lt;/p&gt;
&lt;pre id=&quot;code_1734540360499&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0, // 재시도 x
    },
  },
});

createRoot(document.getElementById('root')).render(
  &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/QueryClientProvider&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RyUVy/btsLnRwvL2v/SZTwUENmg20GDI5Umlm8K0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RyUVy/btsLnRwvL2v/SZTwUENmg20GDI5Umlm8K0/img.gif&quot; data-alt=&quot;재시도를 더 이상 하지 않고 서버에서 설정한 시간인 2초가 지난 후 에러메세지가 출력된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RyUVy/btsLnRwvL2v/SZTwUENmg20GDI5Umlm8K0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/RyUVy/btsLnRwvL2v/SZTwUENmg20GDI5Umlm8K0/img.gif&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;656&quot; height=&quot;273&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;재시도를 더 이상 하지 않고 서버에서 설정한 시간인 2초가 지난 후 에러메세지가 출력된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고로, retry뿐만 아니라 다양한 기본 옵션들을 수정할 수 있으니 참고하자.&amp;nbsp;&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;useFetch를 사용했을 때와 비교해보자. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;확실히 TanStack Query를 사용하니 useState를 통해 isLoading, error 등 부가적인 상태를 관리할 필요가 전혀 없어졌다. 심지어, useQuery 훅 하나만으로 여러 상태들을 제공해주며 기존 useFetch 커스텀 훅에는 존재하지 않았던 재시도 기능까지 자동적으로 지원을 해준다. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지만 살펴보아도 &lt;b&gt;TanStack Query&lt;/b&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;&lt;b&gt;TansStack Query&lt;/b&gt;는 이외에도 &lt;b&gt;캐싱&lt;/b&gt;, &lt;b&gt;리패칭&lt;/b&gt;, &lt;b&gt;무한스크롤&lt;/b&gt; 등 다양한 기능들을 제공한다. 위의 &lt;b&gt;TanStack Query&lt;/b&gt;의 예시는 아주 단순한 예시일 뿐이다.&lt;/p&gt;</description>
      <category>React</category>
      <category>react query</category>
      <category>tanstack query</category>
      <category>리액트</category>
      <category>리액트 쿼리</category>
      <category>상태 관리</category>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/19</guid>
      <comments>https://junhee1203.tistory.com/19#entry19comment</comments>
      <pubDate>Thu, 19 Dec 2024 02:09:16 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 옵저버(Observer) 패턴을 이용한 상태 관리</title>
      <link>https://junhee1203.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;흔히, &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;디자인 패턴&lt;/b&gt;&lt;/span&gt;이라 하면 객체지향 언어의 원조인 자바에서 많이 활용된다. 디자인 패턴의 종류는 매우 많으며 쓰임새 또한 다양하다. 하지만 자바라는 언어 자체가 백엔드 언어이다 보니, 자바스크립트로 디자인 패턴을 공부하여도 &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;이번 포스팅에서는 프론트엔드에서도 유용한 디자인 패턴 중 하나인 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;옵저버 패턴&lt;/b&gt;&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;&lt;b&gt;1️⃣ 옵저버 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵저버 패턴은 크게 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;주체(Subject)&lt;/b&gt;&lt;/span&gt;와 &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;옵저버(Observer)&lt;/span&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;&lt;b&gt;주체(Subject)&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;옵저버들을 관리하는 객체이다.&lt;/b&gt;&lt;/span&gt; 옵저버는 원하는 주체와 결합할 수 있으며 주체는 결합된 옵저버들을 관리한다.&lt;/li&gt;
&lt;li&gt;주체는 옵저버를 추가하거나 제거할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;주체는 상태를 가지고 있으며, 상태가 변경되면 관리 중인 모든 옵저버에게 상태변화를 알린다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵저버(Observer)&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;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;옵저버는 관찰자라는 영어 직역처럼, 주체의 상태를 관찰한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;주체의 상태가 변경되면, 옵저버는 &lt;b&gt;변경된 상태를 전달받아 적절한 업데이트&lt;/b&gt; 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beTKeg/btsJT6PdsIP/9diGRcpPOjPKigHgKOEnZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beTKeg/btsJT6PdsIP/9diGRcpPOjPKigHgKOEnZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beTKeg/btsJT6PdsIP/9diGRcpPOjPKigHgKOEnZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeTKeg%2FbtsJT6PdsIP%2F9diGRcpPOjPKigHgKOEnZk%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;1008&quot; height=&quot;620&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이유는 아래와 같다.&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;(독립성)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;옵저버 를 추가하거나 제거해도 주체의 코드를 변경할 필요가 없다. &lt;b&gt;(유연성)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;주체는 상태 관리와 옵저버 의 목록만 관리하면 된다.&lt;/b&gt; &lt;b&gt;각 옵저버는 상태 변경에 대한 업데이트만 하면 된다.&lt;/b&gt;&lt;/span&gt; &lt;b&gt;(단일 책임 원칙 준수)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;주체는 상태가 변경되면 반드시 관리 중인 옵저버에게 상태 변경을 알려야 된다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2️⃣ 옵저버 패턴 추상화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 추상화를 해보자. 주체와 관찰자 객체의 멤버를 추상화 해보면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tQ8ld/btsJSOa8mmD/BkNKoX1oUhCuR9eyyy32Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tQ8ld/btsJSOa8mmD/BkNKoX1oUhCuR9eyyy32Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tQ8ld/btsJSOa8mmD/BkNKoX1oUhCuR9eyyy32Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtQ8ld%2FbtsJSOa8mmD%2FBkNKoX1oUhCuR9eyyy32Pk%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;927&quot; height=&quot;638&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &lt;b&gt;주체 (Subject)&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;Property
&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;observers : 관리 중인 옵저버들의 모음&amp;nbsp;&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;getState : 주체의 현재 상태값을 리턴&lt;/li&gt;
&lt;li&gt;setState : 주체의 현재 상태를 새로운 상태로 변경&lt;/li&gt;
&lt;li&gt;addObserver : 옵저버 추가&lt;/li&gt;
&lt;li&gt;removeObserver : 옵저버 제거&lt;/li&gt;
&lt;li&gt;notify : 상태가 변경되면 옵저버들에게 변경된 상태 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관찰자 (Observer)&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;Method
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;update : 상태가 변경되면 내부적으로 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 직접 구현해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1727863470955&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// core.js
export class Subject {
  constructor(initialstate = null) {
    this.state = initialstate;
    this.observers = [];
  }

  getState() {
    return this.state;
  }

  setState(newState) {
    this.state = newState;
    this.notify(this.state);
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    const index = this.observers.indexOf(observer);

    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notify() {
    for (const observer of this.observers) {
      observer.update(this.state);
    }
  }
}

export class Observer {
  update(newState){}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;update 메서드는 옵저버마다 다르기에 추상 클래스에서는 구현하지 않았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화한 클래스를 이용하여 구체화 해보자. 실생활의 예시로 생각해보면 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;주체는 유튜브 채널&lt;/b&gt;&lt;/span&gt;이 될 수 있고, &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;옵저버는 채널의 구독자&lt;/span&gt;&lt;/b&gt;가 될 수 있다. 유튜브 채널에서 영상을 새로 업로드하면 구독자들에게 알림을 보내는 시스템을 그대로 모방한 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1727875651158&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { Subject,Observer } from &quot;./core.js&quot;;

class YouTubeChannel extends Subject {
  constructor(initialstate){
    super(initialstate)
  }
}

class Subscriber extends Observer {
  constructor(name){
    this.name = name;
  }

  update(newState){
    console.log(`${this.name}님, ${newState}가 업로드 되었습니다 !! `)
  }
}

const myYouTube = new YouTubeChannel();

const subscriber1 = new Subscriber('subscriber1');
const subscriber2 = new Subscriber('subscriber2');

myYouTube.addObserver(subscriber1)
myYouTube.addObserver(subscriber2)


myYouTube.setState('New video')&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sQdIU/btsJUWE8NCk/GaSrqJ8iSKnvZkkDEH9ALK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sQdIU/btsJUWE8NCk/GaSrqJ8iSKnvZkkDEH9ALK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sQdIU/btsJUWE8NCk/GaSrqJ8iSKnvZkkDEH9ALK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsQdIU%2FbtsJUWE8NCk%2FGaSrqJ8iSKnvZkkDEH9ALK%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;477&quot; height=&quot;56&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주체인 myYouTube의 상태만 변경해주면 옵저버들인 subscriber 에게 상태 변화를 알려주어 update 메서드를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3️⃣ 클라이언트 상태 관리에 적용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 옵저버 패턴을 활용하여 아주 간단한 todo 앱을 만들어 클라이언트 단에서 어떻게 상태 관리를 할 수 있는지 알아보자. 먼저 기본적인 HTML 구조를 만들자.&lt;/p&gt;
&lt;pre id=&quot;code_1727877836110&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.html
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;

&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
  &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
  &amp;lt;script type=&quot;module&quot; src=&quot;main.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;TODO 리스트&amp;lt;span class=&quot;count&quot;&amp;gt;(count)&amp;lt;/span&amp;gt;&amp;lt;/h1&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;input type=&quot;text&quot; id=&quot;new-todo&quot; placeholder=&quot;새로운 할 일&quot;&amp;gt;
    &amp;lt;ul class=&quot;todo-list&quot;&amp;gt;
    &amp;lt;/ul&amp;gt;
    &amp;lt;button id=&quot;add-todo&quot;&amp;gt;추가&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/emwNEH/btsJUbbXdaX/RYApwbNAUqTKfRE5rZynA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/emwNEH/btsJUbbXdaX/RYApwbNAUqTKfRE5rZynA1/img.png&quot; data-alt=&quot;렌더링 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/emwNEH/btsJUbbXdaX/RYApwbNAUqTKfRE5rZynA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FemwNEH%2FbtsJUbbXdaX%2FRYApwbNAUqTKfRE5rZynA1%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;986&quot; height=&quot;288&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;렌더링 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵저버 패턴을 클라이언트 상황에 맞게 구체화 해보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727878064699&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { Subject, Observer } from './core.js';

class TodoModel extends Subject {
  constructor(initialstate) {
    super(initialstate);
  }

  addTodo(todoValue) {
    if (!todoValue.trim()) return;
    const newTodo = {
      id: this.state.length + 1,
      value: todoValue,
    };
    const newTodoList = [...this.state, newTodo];
    this.setState(newTodoList);
  }
}

class TodoList extends Observer {
  update(newState) {
    const $ul = document.querySelector('.todo-list');
    $ul.innerHTML = `
    ${newState.map((todo) =&amp;gt; `&amp;lt;li&amp;gt;${todo.value}&amp;lt;/li&amp;gt;`).join('')}
    `;
  }
}

class TodoCount extends Observer {
  update(newState) {
    const $count = document.querySelector('.count');
    $count.innerHTML = `
    (${newState.length})
    `;
  }
}

const todoModel = new TodoModel([
  { id: 1, value: '밥 먹기' },
  { id: 2, value: '샤워 하기' },
]);

const todoList = new TodoList();
const todoCount = new TodoCount();

todoList.update(todoModel.getState());
todoCount.update(todoModel.getState());&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주체 (Subject) : todoModel&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 옵저버 (Observer) : todoList, todoCount&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;todoModel 에 초기 상태값을 할당하고, 각 옵저버들의 update 메서드를 직접 호출하여 초기 상태값을 기반으로 화면에 렌더링 하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6cUEj/btsJTLdIBqJ/0EgvCpI2vu7FeXTGv8X5uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6cUEj/btsJTLdIBqJ/0EgvCpI2vu7FeXTGv8X5uk/img.png&quot; data-alt=&quot;렌더링 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6cUEj/btsJTLdIBqJ/0EgvCpI2vu7FeXTGv8X5uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6cUEj%2FbtsJTLdIBqJ%2F0EgvCpI2vu7FeXTGv8X5uk%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;952&quot; height=&quot;293&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;렌더링 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;그런데, 직접 옵저버를 조작하는 것은 옵저버 패턴과 맞지 않다.&lt;/b&gt;&lt;/span&gt; 주체의 추상 클래스에 intialize 메서드를 추가하여 초기 상태값으로 관찰자에게 알림을 보내도록 구현해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1727880544316&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// core.js
export class Subject {
  /*
  생략
  */
   notify() {
    for (const observer of this.observers) {
      observer.update(this.state);
    }
  }
  
  initialize() {
    this.notify();
  }
}

export class Observer {
 // 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727880569438&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { Subject, Observer } from './core.js';

class TodoModel extends Subject {
  // 생략
}

class TodoList extends Observer {
  // 생략
}

class TodoCount extends Observer {
  // 생략
}

const todoModel = new TodoModel([
  { id: 1, value: '밥 먹기' },
  { id: 2, value: '샤워 하기' },
]);

const todoList = new TodoList();
const todoCount = new TodoCount();

todoModel.addObserver(todoList)
todoModel.addObserver(todoCount)

todoModel.initialize() // 초기화&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;input 태그에서 입력을 받고 추가 버튼을 클릭하면 상태가 변경되게 구현하자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727882042648&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { Subject, Observer } from './core.js';

/*
생략
*/

const todoList = new TodoList();
const todoCount = new TodoCount();

todoModel.addObserver(todoList);
todoModel.addObserver(todoCount);

function getElement(selector) {
  return document.querySelector(selector);
}

function appendTodo() {
  const $input = getElement('#new-todo');
  const inputValue = $input.value;
  todoModel.addTodo(inputValue);
  $input.value = '';
}

function addClickHandler(element) {
  element.addEventListener('click', appendTodo);
}

todoModel.initialize();
addClickHandler(getElement('#add-todo'));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;상태만 변경해주니 옵저버인 list와 count가 변경된 상태에 맞게 업데이트 하는 것을 확인할 수 있다.&lt;/b&gt;&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;&lt;b&gt;4️⃣ 모듈화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 main.js의 하나의 파일 안에 다양한 기능이 들어있고, 코드도 복잡해졌기에 모듈화를 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더 구조는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1727885232842&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;components
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;todoCount.js
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;todoList.js
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;core
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;observer.js
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;subject.js
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;model
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;todoModel.js
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;utils
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;domUtil.js
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;eventHandler.js
├─&amp;nbsp;index.html
├─&amp;nbsp;main.js
└─&amp;nbsp;package.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈화를 하면서 로직이 조금 수정된 부분만 살펴보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1727885365054&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { TodoCount } from './src/components/todoCount.js';
import { TodoList } from './src/components/todoList.js';
import { TodoModel } from './src/model/todoModel.js';
import { getElement } from './src/utils/domUtil.js';
import { addClickHandler, appendTodo } from './src/utils/eventHandler.js';

const todoModel = new TodoModel([
  { id: 1, value: '밥 먹기' },
  { id: 2, value: '샤워 하기' },
]);

const todoList = new TodoList();
const todoCount = new TodoCount();

todoModel.addObserver(todoList);
todoModel.addObserver(todoCount);

todoModel.initialize();
addClickHandler(getElement('#add-todo'), appendTodo(todoModel));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727885391694&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/utils/eventHandler.js
import { getElement } from './domUtil.js';

export function appendTodo(todoModel) {
  return () =&amp;gt; {
    const $input = getElement('#new-todo');
    const inputValue = $input.value;
    todoModel.addTodo(inputValue);
    $input.value = '';
  };
}

export function addClickHandler(element, handler) {
  element.addEventListener('click', handler);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈화로 인해 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;appendTodo&lt;/b&gt;&lt;/span&gt;, &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;addClickHandler&lt;/b&gt; &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;&lt;b&gt;5️⃣ 추가 기능 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 삭제 기능을 추가로 구현해보자. 현재 로직에 맞게 옵저버 패턴을 이용해서 구현하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 TodoList 템플릿에 삭제 버튼을 넣어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1727888297780&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/components/todoList.js
import { Observer } from '../core/observer.js';

export class TodoList extends Observer {

  update(newState) {
    const $ul = document.querySelector('.todo-list');
    $ul.innerHTML = `
    ${newState
      .map(
        (todo) =&amp;gt;
        `&amp;lt;li data-id=${todo.id}&amp;gt;
        ${todo.value}
        &amp;lt;button&amp;gt;삭제&amp;lt;/button&amp;gt;
        &amp;lt;/li&amp;gt;`
      )
      .join('')}
    `;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlfz7V/btsJU1TUL2r/wpePN8XevunNT71oD3NlIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlfz7V/btsJU1TUL2r/wpePN8XevunNT71oD3NlIK/img.png&quot; data-alt=&quot;렌더링 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlfz7V/btsJU1TUL2r/wpePN8XevunNT71oD3NlIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdlfz7V%2FbtsJU1TUL2r%2FwpePN8XevunNT71oD3NlIK%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;875&quot; height=&quot;347&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;렌더링 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TodoModel 에 &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;deleteTodo&lt;/span&gt; &lt;/b&gt;메서드를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727888426087&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/model/todoModel.js
import { Subject } from '../core/subject.js';

export class TodoModel extends Subject {
  constructor(initialstate) {
    super(initialstate);
  }

  addTodo(todoValue) {
   // 생략
  }

  deleteTodo(id) {
    const deletingIndex = this.state.findIndex((item) =&amp;gt; item.id == id);
    if (deletingIndex == -1) return;
    this.state.splice(deletingIndex, 1);
    this.setState(this.state);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 핸들러인 removeTodo 함수를 구현한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727888595857&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/utils/eventHandler.js
import { getElement } from './domUtil.js';

export function appendTodo(todoModel) {
// 생략
}

export function removeTodo(todoModel) {
  return (e) =&amp;gt; {
    if (e.target.tagName == 'BUTTON') {
      const $li = e.target.closest('li');
      const deletingId = $li.dataset.id;
      todoModel.deleteTodo(deletingId);
    }
  };
}

export function addClickHandler(element, handler) {
  element.addEventListener('click', handler);
}&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;main에 핸들러를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727888626995&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { TodoCount } from './src/components/todoCount.js';
import { TodoList } from './src/components/todoList.js';
import { TodoModel } from './src/model/todoModel.js';
import { getElement } from './src/utils/domUtil.js';
import { addClickHandler, appendTodo, removeTodo } from './src/utils/eventHandler.js';

const todoModel = new TodoModel([
  { id: 1, value: '밥 먹기' },
  { id: 2, value: '샤워 하기' },
]);

const todoList = new TodoList();
const todoCount = new TodoCount();

todoModel.addObserver(todoList);
todoModel.addObserver(todoCount);

todoModel.initialize();
addClickHandler(getElement('#add-todo'), appendTodo(todoModel));
addClickHandler(getElement('.todo-list'), removeTodo(todoModel));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 작동하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6️⃣ 관심사 분리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 자세히 생각해보면 옵저버 패턴에서 옵저버는 상태 변화에만 관심을 가져하는데 TodoList, TodoCount 는 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;현재 옵저버임에도 상태 변화뿐만 아니라 컴포넌트의 UI와 강하게 결합되어 있다.&lt;/b&gt;&lt;/span&gt; update 메서드 내에서 DOM 조작을 하고 있기 때문이다. &lt;b&gt;따라서, 옵저버의 역할만 할 수 있게 &lt;span style=&quot;color: #ef6f53;&quot;&gt;View 클래스&lt;/span&gt;를 도입하여 역할을 나누자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 구조를 다음과 같이 재구성 하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1727893116018&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├─&amp;nbsp;src
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;core
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;observer.js
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;subject.js
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;model
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;todoModel.js
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;utils
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;domUtil.js
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;eventHandler.js
│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;views
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;├─&amp;nbsp;todoCountView.js
│&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;todoListView.js
│&amp;nbsp;&amp;nbsp;└─&amp;nbsp;observers
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;todoCount.js
│&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;todoList.js
├─&amp;nbsp;index.html
├─&amp;nbsp;main.js
└─&amp;nbsp;package.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;observers 폴더로 옵저버를 따로 분류하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1727893780161&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/observers/todoList.js
import { Observer } from '../core/observer.js';

export class TodoListObserver extends Observer {
  constructor(view) {
    super();
    this.view = view;
  }

  update(newState) {
    this.view.render(newState);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727893785446&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/observers/todoCount.js
import { Observer } from '../core/observer.js';

export class TodoCountObserver extends Observer {
  constructor(view) {
    super();
    this.view = view;
  }

  update(newState) {
    this.view.render(newState);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 옵저버의 역할은 똑같다. &lt;b&gt;생성자 함수로 state와 관련된 view를 받는다. 그리고 상태가 변경되면 update 메서드를 통해 view를 렌더링 해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;view 는 다음과 같이 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1727893903371&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/views/todoListView.js
export class TodoListView {
  constructor() {
    this.rootElement = document.querySelector('.todo-list');
  }

  render(state) {
    this.rootElement.innerHTML = `
    ${state
      .map(
        (todo) =&amp;gt;
          `&amp;lt;li data-id=${todo.id}&amp;gt;
        ${todo.value}
        &amp;lt;button&amp;gt;삭제&amp;lt;/button&amp;gt;
        &amp;lt;/li&amp;gt;`
      )
      .join('')}
    `;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727893927921&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/views/todoCountView.js
export class TodoCountView {
  constructor() {
    this.rootElement = document.querySelector('.count');
  }

  render(state) {
    this.rootElement.innerHTML = `&amp;lt;span&amp;gt;(${state.length})&amp;lt;/span&amp;gt;`;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;rootElement&lt;/b&gt;&lt;/span&gt;는 뷰의 최상위 요소이다. state가 변경되면 뷰는 정해진 형식으로 렌더링 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727894145713&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
import { TodoCountObserver } from './src/observers/todoCount.js';
import { TodoListObserver } from './src/observers/todoList.js';
import { TodoModel } from './src/model/todoModel.js';
import { getElement } from './src/utils/domUtil.js';
import { addClickHandler, appendTodo, removeTodo } from './src/utils/eventHandler.js';
import { TodoCountView } from './src/views/todoCountView.js';
import { TodoListView } from './src/views/todoListView.js';

const todoModel = new TodoModel([
  { id: 1, value: '밥 먹기' },
  { id: 2, value: '샤워 하기' },
]); 

const todoCountView = new TodoCountView()
const todoListView = new TodoListView()

const todoListObserver = new TodoListObserver(todoListView);
const todoCountObserver = new TodoCountObserver(todoCountView);

todoModel.addObserver(todoListObserver);
todoModel.addObserver(todoCountObserver);

todoModel.initialize();

addClickHandler(getElement('#add-todo'), appendTodo(todoModel));
addClickHandler(getElement('.todo-list'), removeTodo(todoModel));&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;현재 코드에서 아쉬운 점은 main 에서 이벤트 핸들러를 등록하는 것보다, view 클래스에서 이벤트 등록 메서드를 추가하는 것이 더욱 바람직해 보인다. 사실, 리팩토링은 원래 끝이 없고 계속 진행하면 포스팅의 주제와 맞지 않기에 이번 포스팅은 여기까지만 다루겠다.&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: #ef5369;&quot;&gt;&lt;b&gt;Redux는 옵저버 패턴을 기반으로 구현&lt;/b&gt;&lt;/span&gt;되어 있다. 옵저버 패턴을 사용함으로써 주체에 옵저버를 등록하기만 하면 &lt;b&gt;주체의 상태가 변경될 때 옵저버는 내부적으로 알아서 업데이트를 수행&lt;/b&gt;하고, 코드의 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;유지보수성&lt;/b&gt;&lt;/span&gt;과 &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;확장성&lt;/b&gt;&lt;/span&gt;을 높일 수 있다. 이러한 구조 덕분에, 복잡한 애플리케이션에서도 상태 변화에 따른 UI 업데이트나 로직 처리가 간결해지고, 변화에 유연하게 대응할 수 있다.&lt;/p&gt;</description>
      <category>JavaScript</category>
      <category>javascript</category>
      <category>디자인패턴</category>
      <category>옵저버</category>
      <category>옵저버패턴</category>
      <author>Jun_Hee</author>
      <guid isPermaLink="true">https://junhee1203.tistory.com/18</guid>
      <comments>https://junhee1203.tistory.com/18#entry18comment</comments>
      <pubDate>Thu, 3 Oct 2024 04:17:40 +0900</pubDate>
    </item>
  </channel>
</rss>