<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>gamzamandu 님의 블로그</title>
    <link>https://gamzamandu.tistory.com/</link>
    <description>면접을 망쳤습니다. 반성문을 쓰고 다시 가보겠습니다.</description>
    <language>ko</language>
    <pubDate>Fri, 12 Jun 2026 04:53:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gamzamandu</managingEditor>
    <image>
      <title>gamzamandu 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8568509/attach/9b3162e3ca284c819ad7d93748b7d2a8</url>
      <link>https://gamzamandu.tistory.com</link>
    </image>
    <item>
      <title>OpenAPI만 주면 API 요청 함수를 생성해준다고?</title>
      <link>https://gamzamandu.tistory.com/6</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;[서론]&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[1. 시간이 없다 시간이]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;졸업작품전 프로젝트를 제출해야 하고 남은 시간은 한 달. 코엑스 전시 건으로 한 주가 지나갔으니 2주 정도의 개발시간이 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ustock.naver.com/?schedule=toBeIPOList&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;네이버 페이 비상장&lt;/a&gt;을 레퍼런스로 한 &quot;교내 프로젝트를 상장시키는 증권거래소, 깃허브 활동 내역이 뉴스가 된다!&quot;를 혼자 만들어야 한다. 증권 시장의 시스템(IPO, 공모청약, 주식거래, 유동성)의 기능을 구현해야 했기 때문에 만들어야 할 것들도 많고 그만큼 프론트엔드에 연결해야 할 API도 많다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMFAlM/dJMcagMACtp/FYOa5ZMgB1DrNIDrnVGv91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMFAlM/dJMcagMACtp/FYOa5ZMgB1DrNIDrnVGv91/img.png&quot; data-alt=&quot;좌(부산소마고 증권거래소), 우(네이버 페이 비상장)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMFAlM/dJMcagMACtp/FYOa5ZMgB1DrNIDrnVGv91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMFAlM%2FdJMcagMACtp%2FFYOa5ZMgB1DrNIDrnVGv91%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;3840&quot; height=&quot;2160&quot; data-origin-width=&quot;3840&quot; data-origin-height=&quot;2160&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;그것을 해결해줄 한 가지의 빛을 보았으니 그것이 Orval이다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[본론]&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;[1. Orval이 뭐예요??]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orval은 OpenAPI 스펙을 통해 Typscript로 API함수, 타입, 목데이터까지 생성해 주는 라이브러리이다. 노가다성으로 반복되는 CRUD API 보일러플레이트를 생성해 주며, 무려 React Query 훅까지 생성해 준다.&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;2314&quot; data-origin-height=&quot;1508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vZtcK/dJMcagseyNI/fOROO9QVd9zJGU3Yq0FzBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vZtcK/dJMcagseyNI/fOROO9QVd9zJGU3Yq0FzBK/img.png&quot; data-alt=&quot;좌(OpenAPI 스펙)을 토대로 (우)보일러플레이트 코드를 생성하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vZtcK/dJMcagseyNI/fOROO9QVd9zJGU3Yq0FzBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvZtcK%2FdJMcagseyNI%2FfOROO9QVd9zJGU3Yq0FzBK%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;2314&quot; height=&quot;1508&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1508&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;좌(OpenAPI 스펙)을 토대로 (우)보일러플레이트 코드를 생성하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[2. 본론 : 도입했을 때 어때]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[1. Pure UI 영역에 집중할 수 있다.]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orval이 API 레이어를 책임져주다 보니, 컴포넌트 코드에는 오직 상태 관리와 가시적인 영역을 위한 코드만 남는다. API&amp;nbsp;호출&amp;nbsp;레이어가&amp;nbsp;자동&amp;nbsp;생성되면서,&amp;nbsp;컴포넌트에서는&amp;nbsp;UI와&amp;nbsp;상태&amp;nbsp;관리에&amp;nbsp;더&amp;nbsp;집중할&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[2. 생산성이 압도적으로 올라간다.]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 함수들을 직접 구현할 필요가 없어지다 보니 프론트의 주요 작업이 크게 날아간다. 직접 구현해야 할 영역은 낙관적 업데이트(그마저도 Orval의 함수로 구현하면 된다)와 SSE 정도다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE나 WebSocket 같은 스트리밍 통신은 OpenAPI 표준으로 명확히 표현하기 어려워 &amp;nbsp;Orval 측에서도 지원하지 않는 것 같다. Orval은 REST API 도구라고 생각하면 좋다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[3. Orval에 종속되는 거 아니야?]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다. Orval은 &quot;스펙 &amp;rarr; 타입 + 훅&quot; 변환기 한 단계일 뿐이고, 실질적인 제어권은 개발자한테 있다.&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;예를 들면 baseAPI를 오버라이딩해서 인프라 코드를 우리 손에 둘 수 있다. &lt;b&gt;&lt;/b&gt;Orval은 기본적으로 단순 baseFetch만 쓰는데, config를 통해 이를 커스텀 함수로 교체할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// orval.config.ts
override: {
  mutator: { path: 'src/shared/api/index.ts', name: 'baseFetch' }
}
&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;Orval이 코드를 생성할 때, 모든 fetch 호출부에 직접 fetch()를 박는 대신 이 경로의 baseFetch를 import해서 쓴다. 함수 위치와 이름만 정해주면 Orval이 그 시그니처에 맞춰 생성해 주는 식이다.&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 class=&quot;javascript&quot;&gt;&lt;code&gt;// src/features/admin-panel/model/useAdminStocksQuery.ts
export function usePendingStocksQuery() {
  return useListByStatusSuspense({ status: 'PENDING' }, { query: { refetchInterval: 30_000 } })
}

// src/features/stock-team/model/useAddTeamMember.ts
export function useAddTeamMember(ticker: string) {
  return useMutation({
    mutationFn: ({ userId, shareRatio }) =&amp;gt; addTeamMember(ticker, { userId, shareRatio }),
    onSuccess: () =&amp;gt; queryClient.invalidateQueries({ queryKey: getGetMyStocksQueryKey() }),
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트는 생성된 함수명과 시그니처를 직접 모른다. 도메인 훅 이름(usePendingStocksQuery, useAddTeamMember)만 안다. 생성기를 교체해도 도메인 훅 내부만 고치면 된다.&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;[4. Orval쓰면 API 코드 난잡한 거 아니야?]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그렇지 않다. 오히려 반대다. 내 프로젝트 백엔드의 API 엔드포인트는 총 124개였는데, 이것을 인간이 직접 관리하는 것이 더 불안할 수 있다. 사람이 124개의 API 함수를 직접 타이핑하다 보면 오탈자나 타입 실수가 섞여 들어오기 마련이다. Orval은 스펙에서 코드를 생성하기 때문에 오탈자가 끼어들 여지가 없고, 백엔드 구조가 그대로 매핑되기 때문에 별도로 파악할 필요도 없다.&amp;nbsp;&lt;br /&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;Orval이 생성한 코드도 체계적으로 관리하기 위해&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;single: 모든 API를 한 파일 또는 한 묶음으로 생성하는 방식&lt;/li&gt;
&lt;li&gt;tags: 태그 기준으로 파일을 나누되, 비교적 덜 세분화된 구조로 생성&lt;/li&gt;
&lt;li&gt;tags-split: 태그별로 디렉터리까지 분리해서 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 프로젝트에서는 tags-split 모드를 사용했는데, 컨트롤러별 디렉터리로 격리된다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;src/api/
  ├── admin-stock-controller/
  ├── stock-controller/
  └── ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 스펙 구조가 그대로 매핑되고, mode: 'tags-split' 옵션 하나만 바꾸면 구조 자체도 재배치할 수 있다.&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;[5. API가 변경되면 어떻게 마이그레이션 하는데?]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orval의 API 함수는 백엔드의 OpenAPI에 따라 자동으로 생성된다. API 스펙이 변경되면 타입 에러가 발생하기 때문에, 컴파일 단계에서 빠르게 문제를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[6. 목데이터도 연결하기 편하다.]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orval는 mock: true 옵션 하나로 MSW&amp;nbsp;기반&amp;nbsp;mock&amp;nbsp;핸들러와&amp;nbsp;샘플&amp;nbsp;데이터를&amp;nbsp;자동&amp;nbsp;생성해 준다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[결론]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주라는&amp;nbsp;시간&amp;nbsp;안에&amp;nbsp;124개의&amp;nbsp;API를&amp;nbsp;연결하면서도&amp;nbsp;UI&amp;nbsp;품질을&amp;nbsp;유지할&amp;nbsp;수&amp;nbsp;있었던&amp;nbsp;건&amp;nbsp;Orval&amp;nbsp;덕분이었다.&amp;nbsp;새로운&amp;nbsp;도구를&amp;nbsp;빠르게&amp;nbsp;도입해&amp;nbsp;실행력을&amp;nbsp;확보하면서도,&amp;nbsp;Orval이&amp;nbsp;종속되지&amp;nbsp;않는&amp;nbsp;구조를&amp;nbsp;제공해준&amp;nbsp;덕분에&amp;nbsp;확장성을&amp;nbsp;잃지&amp;nbsp;않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;FSD와의&amp;nbsp;궁합도&amp;nbsp;좋았다.&amp;nbsp;Orval이&amp;nbsp;생성한&amp;nbsp;코드는&amp;nbsp;src/api/에&amp;nbsp;격리되고,&amp;nbsp;인프라&amp;nbsp;코드인&amp;nbsp;baseFetch는&amp;nbsp;FSD의&amp;nbsp;shared&amp;nbsp;레이어에&amp;nbsp;자연스럽게&amp;nbsp;안착했다.&amp;nbsp;features의&amp;nbsp;도메인&amp;nbsp;훅이&amp;nbsp;api를&amp;nbsp;참조하는&amp;nbsp;방향도&amp;nbsp;FSD의&amp;nbsp;단방향&amp;nbsp;의존성&amp;nbsp;원칙과&amp;nbsp;맞아떨어졌다.&amp;nbsp;자동생성&amp;nbsp;코드와&amp;nbsp;수동&amp;nbsp;코드의&amp;nbsp;경계가&amp;nbsp;구조적으로&amp;nbsp;명확하게&amp;nbsp;나뉘었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;백엔드 스펙이 그대로 코드로 매핑되다 보니 프론트와 백엔드 사이의 간극도 줄어들 것 같다. 이 프로젝트는 내가 모든 영역을 담당했기 때문에 직접 체감하진 못했지만, 실제 협업 상황이라면 API 명세가 변경될 때마다 타입 에러로 즉시 드러나기 때문에 별도 커뮤니케이션 없이도 변경점을 빠르게 파악할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Orval은 단순히 코드를 줄여주는 도구라기보단, API 레이어라는 인프라 관심사를 위임함으로써 개발자가 진짜 집중해야 할 영역에 더 많은 시간을 쓸 수 있게 해주는 도구라고 느꼈다.&lt;/p&gt;</description>
      <category>개발 공부</category>
      <category>Mock</category>
      <category>MSW</category>
      <category>orval</category>
      <author>gamzamandu</author>
      <guid isPermaLink="true">https://gamzamandu.tistory.com/6</guid>
      <comments>https://gamzamandu.tistory.com/6#entry6comment</comments>
      <pubDate>Sun, 7 Jun 2026 13:46:53 +0900</pubDate>
    </item>
    <item>
      <title>React Suspense는 Wakeable을 Throw한다.</title>
      <link>https://gamzamandu.tistory.com/4</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;[서론]&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1. [Suspense의 등장]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Suspense는 비동기 데이터를 다루는 보일러플레이트를 해결하기 위해 나왔다. 데이터를 받아오는 동안 로딩을 띄우고, 에러가 나오면 그에 맞는 UI를 띄우는 작업을 말이다. 아래 예시를 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244109&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function UserProfile({ userId }) {
  const [user, setUser]       = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState(null);

  useEffect(() =&amp;gt; {
    setLoading(true);
    fetchUser(userId)
      .then(data  =&amp;gt; { setUser(data);   setLoading(false); })
      .catch(err  =&amp;gt; { setError(err);   setLoading(false); });
  }, [userId]);

  if (loading) return &amp;lt;Spinner /&amp;gt;; // 핵심로직
  if (error)   return &amp;lt;ErrorCard error={error} /&amp;gt;; // 핵심로직
  return &amp;lt;UserCard user={user} /&amp;gt;; // 핵심로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 핵심로직에 비해 그 위에 loading/error 관리 코드가 컴포넌트를 덮고 있다. 컴포넌트가 10개면 이 패턴을 10개를 만들어야 한다는 의미이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Suspense는 이 상태관리의 책임을 외부로 위임&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컴포넌트는 핵심로직에 집중&lt;/b&gt;할 수 있도록 하기 위해 만들어졌다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2.&amp;nbsp;[Suspense의&amp;nbsp;사용]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 React 19 버전식 Suspense 예제이다. Suspense는 Children에 필요한 코드와 데이터를 로딩할 때까지 Fallback을 보여주게 된다. Children이 지연되면 가장 가까운 Boundery의 Suspense가 활성화된다. use()에 대해 궁금할 텐데 일단 여기선 비동기 작업(Promise)의 결과를 마치 동기 코드처럼 처리해주고 있다고 이해하라.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244110&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { use } from &quot;react&quot;;

function UserProfile({ userId }) {
  const user = use(fetchUser(userId));
  return &amp;lt;UserCard user={user} /&amp;gt;;     // 핵심로직만!
}

function App() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;Spinner /&amp;gt;}&amp;gt;
      &amp;lt;ErrorBoundary fallback={&amp;lt;ErrorCard error={error} /&amp;gt;}&amp;gt;
        &amp;lt;UserProfile userId={1} /&amp;gt;
      &amp;lt;/ErrorBoundary&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의문이 생긴다. 어떻게 Suspense는 비동기 상태를 읽어들이는 것일까? 아무 비동기 상태나 되는 것인가?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇지 않다. React 공식문서에 따르면 &amp;ldquo;Suspense가 가능한 데이터만이 Suspense 컴포넌트를 활성화합니다.&amp;rdquo;라고 되어있다.&lt;br /&gt;따라서 React 개발팀에서 정해둔 기준이 있는 것이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;(코드는 마법이 아니다.)&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 기준을 소개해보겠다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[본론]&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. [동작원리에 대하여]&lt;/b&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1.1. [Suspense는 Wakeable을 throw하는 것으로 통신한다]&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Suspense는 하나의 통신에 기반한다. React 내부에서 Suspense가 비동기 요청과 통신하는 방법은 Wakeable을 throw하는 것이다.&lt;br /&gt;&lt;br /&gt;컴포넌트가&amp;nbsp;아직&amp;nbsp;준비되지&amp;nbsp;않았을&amp;nbsp;때&amp;nbsp;Wakeable을&amp;nbsp;throw하면,&amp;nbsp;React가&amp;nbsp;가로채서&amp;nbsp;가장&amp;nbsp;가까운&amp;nbsp;Suspense&amp;nbsp;경계의&amp;nbsp;fallback을&amp;nbsp;보여준다.&amp;nbsp;그리고&amp;nbsp;Wakeable이&amp;nbsp;resolve되면&amp;nbsp;children을&amp;nbsp;다시&amp;nbsp;렌더링한다.&lt;br /&gt;&lt;br /&gt;무슨&amp;nbsp;말인지&amp;nbsp;모르겠을&amp;nbsp;테지만&amp;nbsp;일단&amp;nbsp;쭉&amp;nbsp;읽어보아라.&amp;nbsp;아무&amp;nbsp;비동기&amp;nbsp;요청이나&amp;nbsp;처리하는&amp;nbsp;마법은&amp;nbsp;없으니&amp;nbsp;Suspense가&amp;nbsp;처리할&amp;nbsp;요청을&amp;nbsp;React팀에서&amp;nbsp;정의한&amp;nbsp;것이다.&amp;nbsp;단순한&amp;nbsp;비동기&amp;nbsp;요청과는&amp;nbsp;차이를&amp;nbsp;둔&amp;nbsp;Wakeable&amp;nbsp;개념을&amp;nbsp;소개해보겠다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;[Thenable, Promise 그리고 Wakeable]&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;각&amp;nbsp;개념은&amp;nbsp;서로&amp;nbsp;계층적인&amp;nbsp;관계가&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1. Thenable&lt;/b&gt;&lt;br /&gt;Thenable은 JS에서 가장 넓은(loose한) 개념인데, `then()` 메서드를 가진 객체라고 생각하면 다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;{ then(onFulfill, onReject) { ... } } 구조를 가졌다면 모두 Thenable이다. 상태의 정의는 개발자의 몫이나, Promise와 호환을 보장하는 레벨이라고 이해하자.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. Promise&lt;/b&gt;&lt;br /&gt;Promise는 Thenable의 특수한 경우라 할 수 있겠다. Promise/A+ 스펙을 준수하는 객체이다. 상태(pending &amp;rarr; fulfilled/rejected)가 한 번 정해지면 불변이고, then() 콜백은 항상 microtask 큐로 비동기 실행되는 엄격한 규칙을 따른다는 것이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. Wakeable&lt;/b&gt;&lt;br /&gt;Wakeable은&amp;nbsp;React가&amp;nbsp;Suspense&amp;nbsp;상태를&amp;nbsp;제어하기&amp;nbsp;위해&amp;nbsp;만든&amp;nbsp;Promise의&amp;nbsp;최소&amp;nbsp;인터페이스이다.&amp;nbsp;&lt;br /&gt;네이티브&amp;nbsp;Promise와는&amp;nbsp;달리,&amp;nbsp;React가&amp;nbsp;비동기&amp;nbsp;요청의&amp;nbsp;주체에게&amp;nbsp;&quot;지금&amp;nbsp;준비된&amp;nbsp;거야?&quot;라고&amp;nbsp;질문(ping)할&amp;nbsp;수&amp;nbsp;있어야&amp;nbsp;한다.&amp;nbsp;&lt;br /&gt;Thenable과&amp;nbsp;Promise는&amp;nbsp;&quot;언제&amp;nbsp;완료될지&quot;&amp;nbsp;React가&amp;nbsp;모르나,&amp;nbsp;Wakeable은&amp;nbsp;React가&amp;nbsp;주도권을&amp;nbsp;가질&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;설계한&amp;nbsp;것이다.&lt;br /&gt;&lt;br /&gt;Wakeable은&amp;nbsp;React에&amp;nbsp;전달만&amp;nbsp;되면&amp;nbsp;되기&amp;nbsp;때문에&amp;nbsp;가벼운&amp;nbsp;인터페이스를&amp;nbsp;유지한다.&amp;nbsp;이로&amp;nbsp;얻는&amp;nbsp;장점이&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;&amp;nbsp;성능: 네이티브 Promise는 스펙상 콜백을 항상 마이크로태스크 큐에 넣는다.&lt;br /&gt;반면 React Flight 같이 성능이 중요한 영역에서 Wakeable(가짜 Promise)를 이용해서 비용을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;유연성: 네이티브 Promise는 생성 시점에서 실행자가 즉시 실행된다. 반면 Wakeable은 커스텀하여 제어할 수 있다. React에게 주도권을 쥐어주기 위한 용도이니 훨씬 React에 유용하다.&lt;/li&gt;
&lt;li&gt;가벼움: Wakeable은 ping을 할 수 있는 최소 인터페이스이다. reconciler가 throw된 객체에서 `then(onFulfill, onReject)` 콜백만 연결하면 된다. 나머지 Promise의 메서드는 신경 쓰지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;설명을&amp;nbsp;들으면&amp;nbsp;&quot;난&amp;nbsp;Suspense를&amp;nbsp;쓸&amp;nbsp;때&amp;nbsp;Wakeable한&amp;nbsp;Promise를&amp;nbsp;호출해서&amp;nbsp;쓰지&amp;nbsp;않았는데&quot;라는&amp;nbsp;생각이&amp;nbsp;들&amp;nbsp;수도&amp;nbsp;있는데,&amp;nbsp;React가&amp;nbsp;당신이&amp;nbsp;만든&amp;nbsp;Promise를&amp;nbsp;Wakeable&amp;nbsp;객체로&amp;nbsp;취급(duck&amp;nbsp;typing)했기&amp;nbsp;때문이다.&amp;nbsp;이&amp;nbsp;과정에&amp;nbsp;대해&amp;nbsp;알아보자.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;1.2. [React는 throw된 값이 Wakeable인지 판별한다]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React는&amp;nbsp;컴포넌트를&amp;nbsp;렌더링할&amp;nbsp;때&amp;nbsp;fiber&amp;nbsp;트리를&amp;nbsp;위에서&amp;nbsp;아래로&amp;nbsp;내려가며&amp;nbsp;하나씩&amp;nbsp;처리한다.&amp;nbsp;이&amp;nbsp;내려가는&amp;nbsp;과정을&amp;nbsp;begin&amp;nbsp;phase라&amp;nbsp;부른다.&amp;nbsp;Suspense를&amp;nbsp;만나면&amp;nbsp;그&amp;nbsp;안으로&amp;nbsp;들어가&amp;nbsp;children을&amp;nbsp;렌더링하기&amp;nbsp;시작한다.&amp;nbsp;이&amp;nbsp;상태에서&amp;nbsp;컴포넌트가&amp;nbsp;무언가를&amp;nbsp;throw하면&amp;nbsp;workloop의&amp;nbsp;try...catch가&amp;nbsp;이것을&amp;nbsp;잡는다.&amp;nbsp;그리고&amp;nbsp;이것이&amp;nbsp;Suspense를&amp;nbsp;위한&amp;nbsp;Wakeable인지,&amp;nbsp;진짜&amp;nbsp;에러인지&amp;nbsp;조사한다.&amp;nbsp;throw는&amp;nbsp;원래&amp;nbsp;에러를&amp;nbsp;던지기&amp;nbsp;위해&amp;nbsp;만든&amp;nbsp;것이기&amp;nbsp;때문이다.&lt;br /&gt;&lt;br /&gt;실제 코드를 보면 알 수 있다. React는 instanceof Promise로 판단하지 않는다. then이 붙어있으면 Wakeable로 취급한다.&lt;br /&gt;반대로 then이 없는 것이 throw되면 에러로 취급해 ErrorBoundary 경로로 흘러간다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244111&quot; class=&quot;cs&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// ReactFiberThrow.js
function throwException(root, returnFiber, sourceFiber, value, rootRenderLanes) {
  sourceFiber.flags |= Incomplete;

  if (
    value !== null &amp;amp;&amp;amp;
    typeof value === 'object' &amp;amp;&amp;amp;
    typeof value.then === 'function'   // &amp;larr; 이 조건 하나가 전부
  ) {
    // Wakeable로 취급 &amp;rarr; Suspense 처리 경로
    const wakeable = value;
    // ...
  } else {
    // 일반 에러 &amp;rarr; ErrorBoundary 경로
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;참고로 Duck Typing은 &quot;오리처럼 걷고, 오리처럼 꽥꽥 소리를 낸다면 오리다&quot;라는 의미이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;개발자들은 귀여운 거 좋아한다.&lt;/s&gt;&lt;br /&gt;&lt;br /&gt;Wakeable로&amp;nbsp;판별됐다.&amp;nbsp;지금&amp;nbsp;React는&amp;nbsp;Suspense&amp;nbsp;안쪽&amp;nbsp;깊은&amp;nbsp;곳,&amp;nbsp;throw가&amp;nbsp;발생한&amp;nbsp;지점에&amp;nbsp;있다.&amp;nbsp;이제&amp;nbsp;이&amp;nbsp;throw를&amp;nbsp;처리할&amp;nbsp;Suspense를&amp;nbsp;찾아야&amp;nbsp;한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1.3. [가장 가까운 Suspense Boundary를 찾아 ShouldCapture를 세운다]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;getNearestSuspenseBoundaryToCapture()가&amp;nbsp;throw가&amp;nbsp;발생한&amp;nbsp;fiber의&amp;nbsp;return&amp;nbsp;포인터를&amp;nbsp;타고&amp;nbsp;조상을&amp;nbsp;거슬러&amp;nbsp;올라가며&amp;nbsp;가장&amp;nbsp;가까운&amp;nbsp;Suspense&amp;nbsp;fiber를&amp;nbsp;직접&amp;nbsp;찾아낸다.&amp;nbsp;이로&amp;nbsp;인해&amp;nbsp;여러&amp;nbsp;Suspense가&amp;nbsp;중첩되어&amp;nbsp;있어도&amp;nbsp;가장&amp;nbsp;안쪽&amp;nbsp;경계가&amp;nbsp;먼저&amp;nbsp;잡힌다.&amp;nbsp;(여기서&amp;nbsp;fiber라는&amp;nbsp;개념이&amp;nbsp;생소할&amp;nbsp;수&amp;nbsp;있는데&amp;nbsp;React는&amp;nbsp;모든&amp;nbsp;것을&amp;nbsp;fiber로&amp;nbsp;표현한다고&amp;nbsp;생각하라.&amp;nbsp;당신이&amp;nbsp;만든&amp;nbsp;Component도&amp;nbsp;모두&amp;nbsp;Fiber화&amp;nbsp;되어있다.&amp;nbsp;부모-자식&amp;nbsp;컴포넌트는&amp;nbsp;fiber의&amp;nbsp;return&amp;nbsp;포인터로&amp;nbsp;연결되어&amp;nbsp;있다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Suspense&amp;nbsp;fiber를&amp;nbsp;찾으면&amp;nbsp;그&amp;nbsp;fiber에&amp;nbsp;직접&amp;nbsp;ShouldCapture&amp;nbsp;플래그를&amp;nbsp;세운다.&amp;nbsp;&quot;이&amp;nbsp;Suspense가&amp;nbsp;이번&amp;nbsp;suspend를&amp;nbsp;처리해야&amp;nbsp;한다&quot;는&amp;nbsp;접수&amp;nbsp;신호다.&amp;nbsp;바로&amp;nbsp;fallback을&amp;nbsp;렌더링하지&amp;nbsp;않는&amp;nbsp;이유가&amp;nbsp;있다.&amp;nbsp;지금은&amp;nbsp;아직&amp;nbsp;begin&amp;nbsp;phase&amp;nbsp;도중이라&amp;nbsp;자식&amp;nbsp;평가가&amp;nbsp;끝나지&amp;nbsp;않은&amp;nbsp;상태다.&amp;nbsp;이&amp;nbsp;시점에&amp;nbsp;fallback&amp;nbsp;렌더링을&amp;nbsp;확정하면&amp;nbsp;불완전한&amp;nbsp;상태에서&amp;nbsp;진행하게&amp;nbsp;된다.&amp;nbsp;실제&amp;nbsp;확정은&amp;nbsp;이후&amp;nbsp;complete&amp;nbsp;phase에서&amp;nbsp;이루어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244111&quot; class=&quot;actionscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);

if (suspenseBoundary !== null) {
  markSuspenseBoundaryShouldCapture(
    suspenseBoundary, returnFiber, sourceFiber, root, rootRenderLanes
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;1.4. [Wakeable이 끝나면 알려줄 리스너를 등록한다]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Suspense가&amp;nbsp;처리할&amp;nbsp;요청이&amp;nbsp;예약됐다.&amp;nbsp;이제&amp;nbsp;언제&amp;nbsp;그&amp;nbsp;요청이&amp;nbsp;끝날지&amp;nbsp;알아야&amp;nbsp;한다.&amp;nbsp;React가&amp;nbsp;주기적으로&amp;nbsp;상태를&amp;nbsp;확인하는&amp;nbsp;것이&amp;nbsp;아니라,&amp;nbsp;Promise&amp;nbsp;측에서&amp;nbsp;상태를&amp;nbsp;알려주는&amp;nbsp;구조다.&amp;nbsp;옵저버&amp;nbsp;패턴이다.&lt;br /&gt;&lt;br /&gt;이를 위해 두 개의 리스너를 등록한다. ping은 &quot;스케줄링할 작업이 생겼다는 신호&quot;를 보내는 것이고, retry는 &quot;이 특정한 Suspense가 대상이라는 정보&quot;다. React가 &amp;lt;언제를 결정하는 스케줄링 레이어&amp;gt;와 &amp;lt;어디를 결정하는 렌더링 타겟 레이어&amp;gt;를 명확히 나누었기 때문이다.&lt;br /&gt;&lt;br /&gt;스케줄링 레이어는 우선순위에 따라 렌더링 순서를 조정할 뿐이다. 대상이 작업 순서에 오면 그때 타겟 레이어를 확인하여 무엇을 해야 할지 정보를 확인한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244111&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;if (suspenseBoundary.mode &amp;amp; ConcurrentMode) {
  attachPingListener(root, wakeable, rootRenderLanes);  // 언제
}
attachRetryListener(suspenseBoundary, root, wakeable);  // 어디&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;[ping]&lt;/b&gt;&lt;br /&gt;attachPingListener()의 역할은 React 스케줄러에게 &quot;이 root에서 대기 중이던 Suspense 작업이 끝났으니, 해당 lanes 우선순위로 리렌더링을 스케줄링해라&quot;라는 신호를 보낸다.&lt;br /&gt;&lt;br /&gt;코드를&amp;nbsp;보면&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;한&amp;nbsp;가지&amp;nbsp;사실은&amp;nbsp;Cache가&amp;nbsp;있다.&amp;nbsp;한번&amp;nbsp;Listen한&amp;nbsp;Wakeable를&amp;nbsp;중복&amp;nbsp;등록하지&amp;nbsp;않게&amp;nbsp;WeakMap을&amp;nbsp;만들어놨다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777904244111&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;function attachPingListener(root, wakeable, lanes) {
  let pingCache = root.pingCache;
  // ... (WeakMap으로 중복 방지)

  const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
  wakeable.then(ping, ping); // resolve든 reject든 ping 호출
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서&amp;nbsp;주목할&amp;nbsp;점은&amp;nbsp;resolve&amp;nbsp;callback과&amp;nbsp;reject&amp;nbsp;callback&amp;nbsp;모두&amp;nbsp;ping으로&amp;nbsp;설정해둔&amp;nbsp;점이다.&amp;nbsp;Suspense의&amp;nbsp;관점에서는&amp;nbsp;성공했건&amp;nbsp;실패했건&amp;nbsp;중요하지&amp;nbsp;않다.&amp;nbsp;&quot;Promise가&amp;nbsp;끝나면&amp;nbsp;다시&amp;nbsp;렌더링을&amp;nbsp;시도해야&amp;nbsp;한다&quot;는&amp;nbsp;것이다.&amp;nbsp;성공인지&amp;nbsp;실패인지는&amp;nbsp;다음&amp;nbsp;렌더링의&amp;nbsp;역할이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;[retry]&lt;/b&gt;&lt;br /&gt;ping이&amp;nbsp;&quot;언제&amp;nbsp;다시&amp;nbsp;시작할지&quot;라면,&amp;nbsp;retry는&amp;nbsp;&quot;어디서부터&amp;nbsp;다시&amp;nbsp;시작할지&quot;다.&amp;nbsp;1.3에서&amp;nbsp;ShouldCapture를&amp;nbsp;세운&amp;nbsp;Suspense&amp;nbsp;fiber를&amp;nbsp;retry가&amp;nbsp;기억해둔다.&amp;nbsp;Promise가&amp;nbsp;resolve된&amp;nbsp;이후&amp;nbsp;실제로&amp;nbsp;동작하는데,&amp;nbsp;자세한&amp;nbsp;동작은&amp;nbsp;1.6에서&amp;nbsp;다루겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Wakeable 판별, Boundary 탐색, ShouldCapture 세팅, 리스너 등록이 모두 끝났다. 지금까지가 전부 begin phase 도중 일어난 일이다. React가 children을 렌더링하러 내려가다가 throw가 터졌고, 그 자리에서 처리할 수 있는 것들을 해뒀다. 이제 begin phase에서 할 수 없었던 것, 즉 경로 정리와 fallback 렌더링 확정을 위해 complete phase로 넘어가야 한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1.5. [ShouldCapture를 DidCapture로 전환하고 fallback을 렌더링한다]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;begin phase에서 내려가던 렌더링이 throw로 중단됐다. 이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;complete phase&lt;/b&gt;가 시작된다. React는 begin phase에서 내려간 경로를 올라오며 마무리하는데, throw가 발생했으니 정상적인 완료가 아니다. workloop는 completeUnitOfWork를 호출하고, 이것이 Stack Unwinding의 시작이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. [Stack Unwinding]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;completeUnitOfWork는&amp;nbsp;throw가&amp;nbsp;발생한&amp;nbsp;fiber부터&amp;nbsp;위로&amp;nbsp;올라가며&amp;nbsp;경로상의&amp;nbsp;모든&amp;nbsp;fiber에&amp;nbsp;Incomplete를&amp;nbsp;마킹한다.&amp;nbsp;&quot;이&amp;nbsp;작업은&amp;nbsp;중단되었음.&amp;nbsp;자식이&amp;nbsp;다&amp;nbsp;못&amp;nbsp;그려졌기&amp;nbsp;때문.&quot;이라는&amp;nbsp;표시다.&amp;nbsp;이&amp;nbsp;과정에서&amp;nbsp;미완성&amp;nbsp;노드들이&amp;nbsp;실제&amp;nbsp;DOM에&amp;nbsp;반영되는&amp;nbsp;것을&amp;nbsp;막을&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;올라오다가&amp;nbsp;1.3에서&amp;nbsp;ShouldCapture를&amp;nbsp;세워둔&amp;nbsp;Suspense&amp;nbsp;fiber에&amp;nbsp;도달한다.&amp;nbsp;이것이&amp;nbsp;Suspense의&amp;nbsp;complete&amp;nbsp;phase다.&amp;nbsp;이&amp;nbsp;순간&amp;nbsp;ShouldCapture를&amp;nbsp;DidCapture로&amp;nbsp;전환하고&amp;nbsp;멈춘다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ShouldCapture가 &quot;throw 잡았어, 내가 처리할게&quot;라는 접수 신호였다면, DidCapture는 &quot;경로 정리까지 다 끝났어, 이제 fallback 렌더링해도 돼&quot;라는 수습 완료 신호다. ShouldCapture는 begin phase 도중 세워진 것이라 자식 평가가 끝나지 않은 불완전한 상태였다. DidCapture는 complete phase에서 경로를 전부 정리한 뒤 세워지는 것이라 이제 안전하게 fallback 렌더링을 확정할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. [fallback 렌더링]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;workloop는&amp;nbsp;반환된&amp;nbsp;Suspense부터&amp;nbsp;다시&amp;nbsp;begin&amp;nbsp;phase를&amp;nbsp;시작한다.&amp;nbsp;DidCapture&amp;nbsp;플래그를&amp;nbsp;확인하고,&amp;nbsp;children&amp;nbsp;대신&amp;nbsp;fallback&amp;nbsp;브랜치로&amp;nbsp;진입한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244112&quot; class=&quot;aspectj&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const didSuspend = (workInProgress.flags &amp;amp; DidCapture) !== 0;

if (didSuspend) {
  return mountSuspenseFallbackChildren(...); // fallback 렌더링
} else {
  return mountSuspensePrimaryChildren(...);  // children 렌더링
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;DidCapture는 일회용이다. fallback을 렌더링하고 나면 제거된다. 이후 retry로 재렌더링이 트리거될 때 DidCapture가 없으므로 children 브랜치로 정상 진입한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;[children은 fiber 트리에서 사라지지 않는다]&lt;/b&gt;&lt;br /&gt;fallback이&amp;nbsp;화면에&amp;nbsp;나타나는&amp;nbsp;동안&amp;nbsp;children은&amp;nbsp;DOM에서&amp;nbsp;제거되지만&amp;nbsp;fiber&amp;nbsp;트리에서는&amp;nbsp;살아있다.&amp;nbsp;Suspense는&amp;nbsp;내부적으로&amp;nbsp;children을&amp;nbsp;Offscreen&amp;nbsp;컴포넌트로&amp;nbsp;감싸서&amp;nbsp;숨긴&amp;nbsp;채로&amp;nbsp;유지한다.&amp;nbsp;마치&amp;nbsp;`display:&amp;nbsp;none`처럼&amp;nbsp;화면에서만&amp;nbsp;안&amp;nbsp;보이는&amp;nbsp;것이지,&amp;nbsp;존재&amp;nbsp;자체가&amp;nbsp;사라진&amp;nbsp;게&amp;nbsp;아니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1.6. [Promise가 resolve되면 retry가 children을 다시 렌더링한다]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;fallback이&amp;nbsp;화면에&amp;nbsp;나타났다.&amp;nbsp;Promise가&amp;nbsp;resolve되면&amp;nbsp;ping이&amp;nbsp;스케줄러를&amp;nbsp;깨우고,&amp;nbsp;스케줄러가&amp;nbsp;적절한&amp;nbsp;시점에&amp;nbsp;렌더링을&amp;nbsp;다시&amp;nbsp;시작한다.&amp;nbsp;이때&amp;nbsp;1.4에서&amp;nbsp;등록해둔&amp;nbsp;retry가&amp;nbsp;&quot;어디서부터&amp;nbsp;다시&amp;nbsp;시작할지&quot;를&amp;nbsp;알려준다.&amp;nbsp;React는&amp;nbsp;root&amp;nbsp;전체를&amp;nbsp;처음부터&amp;nbsp;다시&amp;nbsp;렌더링하지&amp;nbsp;않는다.&amp;nbsp;retry가&amp;nbsp;가리키는&amp;nbsp;Suspense&amp;nbsp;fiber부터&amp;nbsp;reconciling을&amp;nbsp;재시작한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;재시도에서&amp;nbsp;children&amp;nbsp;컴포넌트가&amp;nbsp;다시&amp;nbsp;실행된다.&amp;nbsp;이번엔&amp;nbsp;캐시에&amp;nbsp;데이터가&amp;nbsp;있으므로&amp;nbsp;throw&amp;nbsp;없이&amp;nbsp;정상적으로&amp;nbsp;값을&amp;nbsp;반환한다.&amp;nbsp;DidCapture가&amp;nbsp;없으므로&amp;nbsp;children&amp;nbsp;브랜치로&amp;nbsp;정상&amp;nbsp;진입하고,&amp;nbsp;DOM에서&amp;nbsp;fallback이&amp;nbsp;제거되고&amp;nbsp;children이&amp;nbsp;나타난다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자&amp;nbsp;여기까지&amp;nbsp;Suspense의&amp;nbsp;동작원리에&amp;nbsp;대해&amp;nbsp;알아보았다.&amp;nbsp;이제&amp;nbsp;서론에서&amp;nbsp;간단히&amp;nbsp;설명했던&amp;nbsp;Suspense의&amp;nbsp;사용에&amp;nbsp;동작원리를&amp;nbsp;더해보자.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;2. [동작원리로&amp;nbsp;알아보는&amp;nbsp;Suspense&amp;nbsp;사용법]&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2.1. [서론의 코드가 동작하는 이유]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서론에서 이런 코드를 소개하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244112&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { use } from &quot;react&quot;;

function UserProfile({ userId }) {
  const user = use(fetchUser(userId));
  return &amp;lt;UserCard user={user} /&amp;gt;;     // 핵심로직만!
}

function App() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;Spinner /&amp;gt;}&amp;gt;
      &amp;lt;ErrorBoundary fallback={&amp;lt;ErrorCard error={error} /&amp;gt;}&amp;gt;
        &amp;lt;UserProfile userId={1} /&amp;gt;
      &amp;lt;/ErrorBoundary&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;use()에 대해 자세히 소개해보겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서&amp;nbsp;Wakeable은&amp;nbsp;React가&amp;nbsp;비동기&amp;nbsp;요청의&amp;nbsp;주도권을&amp;nbsp;갖기&amp;nbsp;위해&amp;nbsp;설계한&amp;nbsp;인터페이스라고&amp;nbsp;했다.&amp;nbsp;그런데&amp;nbsp;개발자&amp;nbsp;입장에서&amp;nbsp;Wakeable을&amp;nbsp;직접&amp;nbsp;다루려면&amp;nbsp;pending인지&amp;nbsp;확인하고,&amp;nbsp;pending이면&amp;nbsp;throw하고,&amp;nbsp;resolve됐으면&amp;nbsp;값을&amp;nbsp;꺼내는&amp;nbsp;분기&amp;nbsp;처리를&amp;nbsp;매번&amp;nbsp;직접&amp;nbsp;작성해야&amp;nbsp;한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그&amp;nbsp;주도권을&amp;nbsp;개발자가&amp;nbsp;편하게&amp;nbsp;쓸&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;React&amp;nbsp;19에서&amp;nbsp;공식&amp;nbsp;API로&amp;nbsp;노출한&amp;nbsp;것이&amp;nbsp;use()다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;use()는&amp;nbsp;Promise를&amp;nbsp;받아서&amp;nbsp;아직&amp;nbsp;pending이면&amp;nbsp;throw하고,&amp;nbsp;resolve됐으면&amp;nbsp;값을&amp;nbsp;반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244113&quot; class=&quot;actionscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;function use(promise) {
  if (promise.status === 'pending')  throw promise;        // &amp;rarr; Suspense로
  if (promise.status === 'rejected') throw promise.reason; // &amp;rarr; ErrorBoundary로
  return promise.value;                                    // &amp;rarr; 정상 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자 이제 이걸 알았으면, 동작을 전개해 보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;fetchUser(userId)가&amp;nbsp;pending&amp;nbsp;Promise를&amp;nbsp;반환하면&amp;nbsp;use()가&amp;nbsp;그것을&amp;nbsp;throw한다.&amp;nbsp;1.2번에서&amp;nbsp;봤듯이&amp;nbsp;React는&amp;nbsp;throw된&amp;nbsp;값에&amp;nbsp;then이&amp;nbsp;붙어있으면&amp;nbsp;Wakeable로&amp;nbsp;판별한다.&amp;nbsp;Promise는&amp;nbsp;then을&amp;nbsp;가지고&amp;nbsp;있으니&amp;nbsp;당연히&amp;nbsp;Wakeable로&amp;nbsp;취급된다.&amp;nbsp;React는&amp;nbsp;가장&amp;nbsp;가까운&amp;nbsp;Suspense&amp;nbsp;경계의&amp;nbsp;fallback을&amp;nbsp;보여준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;fetchUser가&amp;nbsp;resolve되면&amp;nbsp;ping과&amp;nbsp;retry가&amp;nbsp;React를&amp;nbsp;깨운다.&amp;nbsp;컴포넌트가&amp;nbsp;다시&amp;nbsp;실행되고&amp;nbsp;이번엔&amp;nbsp;use()가&amp;nbsp;resolved된&amp;nbsp;값,&amp;nbsp;즉&amp;nbsp;유저&amp;nbsp;데이터를&amp;nbsp;반환한다.&amp;nbsp;user&amp;nbsp;변수에&amp;nbsp;담기고, 비로소 &amp;lt;UserCard&amp;nbsp;user={user}&amp;nbsp;/&amp;gt;가&amp;nbsp;화면에&amp;nbsp;나타난다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서론의 코드가 동작한 이유는 use()가 pending/resolved/rejected 분기를 처리하고 Wakeable를 throw했기에 가능한 일이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예제 코드에는 사실 한 가지 문제가 있다. fetchUser(userId)가 컴포넌트 안에서 호출되고 있으니, 렌더링 때마다 Promise가 생긴다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무한루프가 발생하는 것이다. 이것을 해결하고 넘어가 보자.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2.2. [무한루프를 막는 방법]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해결책은&amp;nbsp;단순하다.&amp;nbsp;렌더링마다&amp;nbsp;새&amp;nbsp;Promise를&amp;nbsp;만들지&amp;nbsp;않으면&amp;nbsp;된다.&amp;nbsp;같은&amp;nbsp;요청에&amp;nbsp;대해서는&amp;nbsp;항상&amp;nbsp;같은&amp;nbsp;Promise&amp;nbsp;객체를&amp;nbsp;반환해야&amp;nbsp;한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 Promise를 컴포넌트 바깥에서 만드는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244113&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const userPromise = fetchUser(1); // 컴포넌트 바깥에서 한 번만 생성

function UserProfile() {
  const user = use(userPromise); // 항상 같은 Promise
  return &amp;lt;UserCard user={user} /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그러나 이 방식은 userId가 바뀔 때 대응이 안 된다. 임시방편으로 useMemo로 개선할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1777904244113&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;function UserProfile({ userId }) {
  const userPromise = useMemo(() =&amp;gt; fetchUser(userId), [userId]);
  const user = use(userPromise);
  return &amp;lt;UserCard user={user} /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;userId가 같으면 같은 Promise를 유지하고, 바뀌면 새로 만든다. 무한루프는 막힌다.&lt;br /&gt;여기서&amp;nbsp;useMemo를&amp;nbsp;임시방편이라고&amp;nbsp;소개한&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;이유가&amp;nbsp;있는데,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. React는 캐싱의 영속을 보장하지 않는다.&lt;/b&gt;&lt;br /&gt;React&amp;nbsp;useMemo&amp;nbsp;공식&amp;nbsp;문서에&amp;nbsp;&quot;You&amp;nbsp;may&amp;nbsp;rely&amp;nbsp;on&amp;nbsp;useMemo&amp;nbsp;as&amp;nbsp;a&amp;nbsp;performance&amp;nbsp;optimization,&amp;nbsp;not&amp;nbsp;as&amp;nbsp;a&amp;nbsp;semantic&amp;nbsp;guarantee.&quot;라고&amp;nbsp;적혀있다.&amp;nbsp;즉,&amp;nbsp;React는&amp;nbsp;메모리&amp;nbsp;확보를&amp;nbsp;위해&amp;nbsp;언제든&amp;nbsp;useMemo에&amp;nbsp;저장된&amp;nbsp;값을&amp;nbsp;삭제할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;설계되어&amp;nbsp;있다.&amp;nbsp;React&amp;nbsp;설계&amp;nbsp;원칙상&amp;nbsp;useMemo는&amp;nbsp;값의&amp;nbsp;유지를&amp;nbsp;보장(Guarantee)하지&amp;nbsp;않으므로,&amp;nbsp;비동기&amp;nbsp;데이터의&amp;nbsp;무결성을&amp;nbsp;담보하기에는&amp;nbsp;구조적으로&amp;nbsp;위험하다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. useMemo가 컴포넌트 생명주기에 완전히 종속되어 있음. 확장의 어려움.&lt;/b&gt;&lt;br /&gt;그러나&amp;nbsp;useMemo는&amp;nbsp;컴포넌트&amp;nbsp;인스턴스에&amp;nbsp;묶여있다.&amp;nbsp;컴포넌트가&amp;nbsp;언마운트되면&amp;nbsp;캐시가&amp;nbsp;사라지고,&amp;nbsp;다른&amp;nbsp;컴포넌트에서&amp;nbsp;같은&amp;nbsp;userId로&amp;nbsp;요청하면&amp;nbsp;새로&amp;nbsp;fetch한다.&amp;nbsp;거기에&amp;nbsp;&quot;오래된&amp;nbsp;데이터를&amp;nbsp;다시&amp;nbsp;가져오고&amp;nbsp;싶다&quot;는&amp;nbsp;요구사항이&amp;nbsp;생기는&amp;nbsp;순간&amp;nbsp;직접&amp;nbsp;구현해야&amp;nbsp;할&amp;nbsp;것들이&amp;nbsp;폭발적으로&amp;nbsp;늘어난다.&amp;nbsp;stale&amp;nbsp;처리,&amp;nbsp;탭&amp;nbsp;포커스시&amp;nbsp;재검증,&amp;nbsp;네트워크&amp;nbsp;재연결시&amp;nbsp;갱신...&amp;nbsp;결국&amp;nbsp;직접&amp;nbsp;구현하다&amp;nbsp;보면&amp;nbsp;TanStack&amp;nbsp;Query를&amp;nbsp;다시&amp;nbsp;만들게&amp;nbsp;된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 실무에서는 useSuspenseQuery를 쓴다. queryKey를 기반으로 Promise를 컴포넌트 외부의 QueryClient에 캐싱하고, stale 처리와 재검증까지 알아서 해준다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId], // 이 키로 Promise를 캐싱
    queryFn: () =&amp;gt; fetchUser(userId),
  });
  return &amp;lt;UserCard user={user} /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;data가 항상 정의되어 있음이 보장된다. 로딩 상태는 Suspense가, 에러 상태는 ErrorBoundary가 처리하기 때문이다. 서론에서 보여준 보일러플레이트 문제를 라이브러리 수준에서 완전히 해결한 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2.3.&amp;nbsp;[Suspense를&amp;nbsp;트리거하는&amp;nbsp;다른&amp;nbsp;방법들]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지는 use()로 Promise를 throw해서 Suspense를 트리거했다. 서론에서 &quot;Suspense는 Children에 필요한 코드와 데이터를 로딩할 때까지 Fallback을 보여주게 된다.&quot;라고 말했는데 데이터를 기다리는 방법은 알아봤으니, 이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;코드를 기다리는 방법&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React가 처음부터 공식 지원한 방식, React.lazy다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777904244114&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { lazy, Suspense } from 'react';

// 이 컴포넌트의 코드는 실제로 렌더링될 때까지 로드되지 않음
const HeavyEditor = lazy(() =&amp;gt; import('./HeavyEditor'));

function App() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;에디터 로딩 중...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;HeavyEditor /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React.lazy의&amp;nbsp;동작은&amp;nbsp;use()와&amp;nbsp;동일한&amp;nbsp;개념이다.&amp;nbsp;그전에&amp;nbsp;import()에&amp;nbsp;대해&amp;nbsp;짚고&amp;nbsp;넘어가자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;import()는 JavaScript 언어 표준(ES2020)에 추가된 문법인데, 평소에 쓰는 static import와 달리 함수처럼 호출하고 Promise를 반환한다. 번들러(Webpack, Vite)는 import() 구문을 보고 해당 파일을 별도 청크로 분리한다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;static import&amp;nbsp;&amp;nbsp;&amp;rarr; 빌드 타임에 하나의 번들로 합쳐짐&lt;br /&gt;import()&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;런타임에&amp;nbsp;필요할&amp;nbsp;때&amp;nbsp;별도&amp;nbsp;파일로&amp;nbsp;요청&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;React.lazy는&amp;nbsp;이&amp;nbsp;import()가&amp;nbsp;반환하는&amp;nbsp;Promise를&amp;nbsp;받아서&amp;nbsp;Wakeable로&amp;nbsp;throw하는&amp;nbsp;래퍼&lt;/b&gt;다.&amp;nbsp;컴포넌트&amp;nbsp;코드가&amp;nbsp;아직&amp;nbsp;로드되지&amp;nbsp;않았으면&amp;nbsp;Promise를&amp;nbsp;throw한다.&amp;nbsp;React가&amp;nbsp;이것을&amp;nbsp;Wakeable로&amp;nbsp;판별하고,&amp;nbsp;가장&amp;nbsp;가까운&amp;nbsp;Suspense&amp;nbsp;경계의&amp;nbsp;fallback을&amp;nbsp;보여준다.&amp;nbsp;다운로드가&amp;nbsp;완료되면&amp;nbsp;ping과&amp;nbsp;retry가&amp;nbsp;React를&amp;nbsp;깨우고&amp;nbsp;컴포넌트를&amp;nbsp;렌더링한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서도 알 수 있듯이 Suspense는 하나의 통신에 기반하고, 방식은 Wakeable을 throw하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;[결론]&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;어떻게 Suspense는 비동기 상태를 읽어 들이는 것일까? 아무 비동기 상태나 되는 것인가? &amp;nbsp;아무 비동기 상태나 되는 것이 아니다.&amp;nbsp;&lt;b&gt;Suspense는&amp;nbsp;Wakeable을&amp;nbsp;throw하는&amp;nbsp;것&amp;nbsp;하나에&amp;nbsp;기반한다&lt;/b&gt;.&amp;nbsp;use()든,&amp;nbsp;React.lazy든,&amp;nbsp;TanStack&amp;nbsp;Query의&amp;nbsp;useSuspenseQuery든&amp;nbsp;결국&amp;nbsp;내부에서&amp;nbsp;하는&amp;nbsp;일은&amp;nbsp;같다.&amp;nbsp;Wakeable을&amp;nbsp;throw하고,&amp;nbsp;React가&amp;nbsp;그것을&amp;nbsp;잡아&amp;nbsp;fallback을&amp;nbsp;보여주고,&amp;nbsp;resolve되면&amp;nbsp;다시&amp;nbsp;렌더링한다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발 공부</category>
      <category>react</category>
      <category>suspense</category>
      <category>Thenable</category>
      <category>use()</category>
      <category>Wakeable</category>
      <author>gamzamandu</author>
      <guid isPermaLink="true">https://gamzamandu.tistory.com/4</guid>
      <comments>https://gamzamandu.tistory.com/4#entry4comment</comments>
      <pubDate>Sat, 2 May 2026 12:13:18 +0900</pubDate>
    </item>
    <item>
      <title>JS 배열을 순회하는 함수들의 내부구조를 뜯어보자</title>
      <link>https://gamzamandu.tistory.com/3</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[서론]&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;전제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 코드는 그냥 실행되지 않는다. 브라우저와 Node.js 환경에서 JS는 V8 엔진 위에서 동작한다. V8은 JS 코드를 단순히 해석하는 데 그치지 않고, 실행 중에 코드를 분석하고 최적화한다. 같은 코드라도 V8이 어떻게 판단하느냐에 따라 실행 속도가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 글은 그 최적화 방식을 따라가며 &lt;b&gt;forEach, map, filter, reduce의 내부구조를 이해&lt;/b&gt;하는 것을 목표로 한다.&amp;nbsp;이 네 가지 함수는 &lt;b&gt;배열 전체를 순회하며 콜백을 실행한다는 공통 구조&lt;/b&gt;를 가지면서도, &lt;b&gt;내부에서 하는 일이 각자 다르다&lt;/b&gt;. Elements Kind가 무엇인지, 이터레이터 메서드가 왜 이런 형태로 설계됐는지, 그리고 각 함수가 내부에서 실제로 어떻게 다르게 동작하는지를 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서 다루는 내용은 &lt;b&gt;V8&amp;nbsp;엔진&amp;nbsp;기준&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpiderMonkey(Firefox),&amp;nbsp;JavaScriptCore(Safari)&amp;nbsp;같은&amp;nbsp;다른&amp;nbsp;엔진은&amp;nbsp;내부&amp;nbsp;구현이&amp;nbsp;다를&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;다만&amp;nbsp;배열을&amp;nbsp;효율적으로&amp;nbsp;처리하기&amp;nbsp;위한&amp;nbsp;핵심&amp;nbsp;원리는&amp;nbsp;엔진을&amp;nbsp;막론하고&amp;nbsp;유사하다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[본론]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부구조를 다루기 앞서, 아래 문제를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100,000개 요소로 완전히 채워진 배열에서, 동일한 더하기 연산 x+1을 map()과 forEach()로 실행할 때 어느 쪽이 더 빠를까?&lt;/p&gt;
&lt;pre id=&quot;code_1777441578342&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const arr = Array(100000).fill(0);

arr.map(x =&amp;gt; x+1); 
arr.forEach(x =&amp;gt; x+1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach 방식이 18.907ms, Map 방식이 41.168ms로 forEach가 약 2배 정도 더 빠르다. 내부적으로 어떤 차이가 있길래 이런 문제가 발생할까?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. [Elements Kind]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V8은 배열을 연산할 때 단순히 인덱스를 조회하는 것이 아니라, 배열의 요소들이 어떤 종류들인지 파악하는 Element Kind 과정을 거친다. 이 작업을 잘 이해하면 특정 상황에서 배열의 처리 속도가 달라지는 이유를 알 수 있다.&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;[배열은 6가지 조합으로 분류된다.]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V8은 배열을 두 가지 축으로 분류한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 첫번째 축은 &lt;b&gt;배열에 구멍(Hole)이 있는지 여부&lt;/b&gt;이다. 구멍이 없으면 Packed, 있으면 Holey이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 두번째 축은 &lt;b&gt;요소의 타입&lt;/b&gt;이다. 작은 정수면 SMI(Small Integer), 부동소수점이면 Double, 그 외 문자열은 Elements로 분류된다.&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개), 두 번째 축(3개)의 조합으로 배열은 6가지로 분류된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brD9Ik/dJMcagFmW2c/WXBv3dzMRek0xUKDO3TqA1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brD9Ik/dJMcagFmW2c/WXBv3dzMRek0xUKDO3TqA1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brD9Ik/dJMcagFmW2c/WXBv3dzMRek0xUKDO3TqA1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrD9Ik%2FdJMcagFmW2c%2FWXBv3dzMRek0xUKDO3TqA1%2Fimg.jpg&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;960&quot; height=&quot;540&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1777466455991&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1, 2, 3]        // PACKED_SMI_ELEMENTS
[1.1, 2.2, 3.3]  // PACKED_DOUBLE_ELEMENTS
[1, 'a', {}]     // PACKED_ELEMENTS

[1, , 3]         // HOLEY_SMI_ELEMENTS
[1.1, , 3.3]     // HOLEY_DOUBLE_ELEMENTS
[1, , 'a']       // HOLEY_ELEMENTS&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;V8이 이렇게 분류한데는 이유가 있을 것이다. 그 이유를 설명하겠다.&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;[첫 번째 축, Packed와 Holey]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 축에 소개된 Hole에 대한 개념을 다루겠다. 아래 코드를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 배열을 모두 arr [1]으로 읽으면 undefined가 나온다. 겉보기엔 모두 undefined로 동일해 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1777466641096&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const holey         = [1, , 3];       // hole
const withUndefined = [1, undefined, 3]; // 명시적 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;in 연산자로 &quot;1번 인덱스에 값이 존재하는가&quot;를 물어보면 대답이 달라진다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;대답은 &quot;holey에서는 값 자체가 존재하지 않는다&quot;이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777466677848&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1 in holey          // false &amp;mdash; 인덱스 1에 값 자체가 없음
1 in withUndefined  // true  &amp;mdash; 인덱스 1에 undefined라는 값이 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 같은 인덱스인데 []와 in의 결과가 다른 걸까.&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;in은 &amp;lt;이 인덱스에 값이 존재하는가&amp;gt;라는 물음이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hole은 그 자리에 값을 할당한 적이 없으니 false, 명시적으로 undefined를 넣은 건 값이 존재하니 true다.&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;arr[1]은 &amp;lt;값을 달라&amp;gt;는 요청이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V8은 인덱스 1을 확인하고, 없으면 프로토타입 체인을 타고 올라가고, 거기도 없으면 undefined를 반환한다. hole이든 명시적 undefined든 최종적으로 undefined가 나오니 구분이 안 된다.&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;여기서 hole의 정체가 드러난다. &lt;b&gt;hole은 &quot;undefined가 저장된 셀&quot;이 아니라 &quot;값 자체가 할당되지 않은 셀&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;이 차이가 V8에게 중요한 이유는, hole이 있는 배열을 순회할 때 JS 스펙상 각 인덱스마다 HasProperty 체크를 수행해야 하기 때문이다. 값이 없으면 프로토타입 체인을 타고 올라가 찾아봐야 한다는 보장을 지켜야 한다. V8은 이 가능성을 배제할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;(프로토타입에 대한 설명은 다음에 자세히 다뤄보겠다.)&lt;/s&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;반면 Packed 배열은 모든 인덱스에 값이 보장되므로 HasProperty 체크 자체가 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 V8은 Packed와 Holey를 구분한다. hole이 없는 배열에서 불필요한 탐색 비용을 없애기 위해서다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;앞서 설명했듯 V8은 요소의 타입을 세 가지로 구분하고 있다.&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. SMI(Small Integer)는 작은 정수이다. V8은 이 범위의 정수를 포인터 안에 직접 집어넣는다. 별도의 힙 할당이 없고, &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;2. Double은 부동 소수점이다. 배열 내부에서 64bit Float로 저장된다. 힙 할당은 없으나 &lt;b&gt;SMI보다 두 배의 메모리 공간을 차지&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;3. Element는 그 외의 모든 것이다. 문자열, 객체가 여기에 속한다. SMI과 Double과는 다르게, &lt;b&gt;각 요소를 가리키는 포인터를 저장&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;/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;먼저, 같은 배열을 ForEach로 순회했을 때 비용이다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[SMI]&amp;nbsp;forEach&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;0.758ms&lt;br /&gt;[Double]&amp;nbsp;forEach&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;1.183ms&lt;br /&gt;[Elements]&amp;nbsp;forEach&amp;nbsp;&amp;rarr;&amp;nbsp;0.687ms&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 차이가 없을 수 있다. map으로 바꿨을 때는?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[SMI] map &amp;rarr; 2.131ms&lt;br /&gt;[Double] map &amp;rarr; 5.302ms&lt;br /&gt;[Elements] map &amp;rarr; 1.948ms&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Double이 갑자기 튀어 오른다. forEach와 대비했을 때 4.5배 차이이다. 이유는 Map은 결과 배열을 새로 만들기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100,000개의 배열을 초기화할 때 SMI보다 두 배의 메모리를 쓰게 된다.&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;V8이 타입을 세 가지로 구분하는 이유가 바로 여기에 있다. 각 &lt;b&gt;타입에 맞는 메모리 레이아웃을 선택해서 읽기 비용을 줄이는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2. [Elements Kind의 특징]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 V8은 Kind가 구체적일수록 더 공격적으로 최적화하게 된다. 예로 PACKED_SMI_ELEMENTS은 모든 요소가 정수임이 보장되므로 타입 체크를 건너뛰고 연속된 메모리를 직접 읽는다. HOLEY_ELEMENTS는 매 인덱스마다 구멍이 있는지를 확인하고, 구멍이면 HasProperty 체크를 실행한다.&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;그럼 이 Elements Kind는 어느 시점에 결정이 될까?&lt;b&gt; Kind는 배열이 생성되는 순간 결정&lt;/b&gt;된다. 이후 &lt;b&gt;요소가 추가되거나 변경될 때 재평가&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1777468658300&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = [1, 2, 3];       // 생성 시점에 PACKED_SMI_ELEMENTS
const b = [1, 2, 'hello']; // 생성 시점에 PACKED_ELEMENTS
const c = [];              // 생성 시점에 PACKED_SMI_ELEMENTS (비어있어도)&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;중요한 점은 이 전환은 단방향이다. pop()으로&amp;nbsp;요소를&amp;nbsp;제거해서&amp;nbsp;배열이&amp;nbsp;다시&amp;nbsp;정수만&amp;nbsp;남아도&amp;nbsp;Kind는&amp;nbsp;되돌아오지&amp;nbsp;않는다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777468711953&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = [1, 2, 3];  // PACKED_SMI_ELEMENTS
a.push(1.5);           // PACKED_DOUBLE_ELEMENTS
a.push('x');           // PACKED_ELEMENTS
a.pop(); a.pop();      // 다시 [1, 2, 3]이 됐지만 &amp;mdash; 여전히 PACKED_ELEMENTS&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;V8은 Kind를 되돌리기 위해 배열 전체를 다시 스캔하느니, Kind를 유지하는 편이 낫다고 판단한다.&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;그럼 아래 arr의 Elements Kind는 어떻게 평가될까? 0으로 꽉 찬 정수 배열이니 PACKED_SMI_ELEMENTS이지 않을까?&lt;/p&gt;
&lt;pre id=&quot;code_1777468859704&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const arr = Array(100000).fill(0);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 HOLEY_SMI_ELEMENTS이다. Array(n)은 크기만 지정된 빈 배열을 만드는 순간 Holey로 출발하기 때문에, 이후 fill(0)으로 값을 전부 채워도 떠나간 Kind는 되돌아오지 않는다. 매정한 놈이다.&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;Elements Kind를 정리하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. V8은 배열의 hole 여부, 요소의 타입으로 배열을 6가지 종류로 분류한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 분류된 6가지 종류로 연산의 차이를 주어, 최적화를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Kind는 배열이 생성되는 순간 결정되고, 요소가 추가/변경될 때 재평가된다. 단, 이 전환은 단방향이라 한 번 강등되면 되돌아오지 않는다.&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;말했듯이 Array(100000).fill(0)은 HOLEY_SMI 배열이다. 이 배열을 순회하는 어느 함수든 HasProperty 비용을 치르게 된다.&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3. [이터레이터]&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본론으로&amp;nbsp;들어가기&amp;nbsp;전에&amp;nbsp;한&amp;nbsp;가지&amp;nbsp;질문을&amp;nbsp;먼저&amp;nbsp;던져야&amp;nbsp;한다.&amp;nbsp;forEach,&amp;nbsp;map,&amp;nbsp;filter,&amp;nbsp;reduce는&amp;nbsp;왜&amp;nbsp;이런&amp;nbsp;형태로&amp;nbsp;설계됐을까?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JS가&amp;nbsp;이&amp;nbsp;네&amp;nbsp;함수를&amp;nbsp;도입하기&amp;nbsp;전,&amp;nbsp;배열&amp;nbsp;순회는&amp;nbsp;전부&amp;nbsp;for&amp;nbsp;루프였다.&lt;/p&gt;
&lt;pre id=&quot;code_1777471979809&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const data = [1, 2, 3, 4, 5];

// 짝수만 골라서 2배로 &amp;mdash; for 루프
const result = [];
for (let i = 0; i &amp;lt; data.length; i++) {
  if (data[i] % 2 === 0) {
    result.push(data[i] * 2);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 코드에는 두 가지 관심사가 섞여 있다. i++, i &amp;lt; data.length 같은 &lt;b&gt;순회&amp;nbsp;로직&lt;/b&gt;과,&amp;nbsp;%&amp;nbsp;2&amp;nbsp;===&amp;nbsp;0,&amp;nbsp;*&amp;nbsp;2&amp;nbsp;같은&amp;nbsp;&lt;b&gt;비즈니스&amp;nbsp;로직&lt;/b&gt;이다.&amp;nbsp;&lt;br /&gt;코드가&amp;nbsp;길어질수록&amp;nbsp;이&amp;nbsp;둘이&amp;nbsp;뒤엉켜&amp;nbsp;읽기&amp;nbsp;어려워진다.&amp;nbsp;그리고&amp;nbsp;매번 개발자가 직접&amp;nbsp;순회를&amp;nbsp;제어해야&amp;nbsp;하므로&amp;nbsp;off-by-one&amp;nbsp;에러&amp;nbsp;같은&amp;nbsp;실수도&amp;nbsp;생긴다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ES5에서 forEach, map, filter, reduce가 도입된 건 이 문제를 해결하기 위해서다. 핵심 아이디어는 &lt;b&gt;순회 제어권을 개발자에서 V8런타임으로 위임하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 예시를 보자. 개발자는&amp;nbsp;&lt;b&gt;&quot;무엇을&amp;nbsp;할지&quot;만&amp;nbsp;콜백&lt;/b&gt;으로&amp;nbsp;넘긴다.&amp;nbsp;&lt;b&gt;&quot;어떻게 순회할지&quot;는 V8런타임&lt;/b&gt;이&amp;nbsp;책임진다.&amp;nbsp;&lt;br /&gt;이&amp;nbsp;패턴을&amp;nbsp;내부&amp;nbsp;이터레이터라고&amp;nbsp;한다.&amp;nbsp;소비자(콜백)가&amp;nbsp;순회를&amp;nbsp;제어하지&amp;nbsp;않고,&amp;nbsp;함수&amp;nbsp;내부에서&amp;nbsp;순회가&amp;nbsp;이루어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1777472088346&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data.filter(x =&amp;gt; x % 2 === 0).map(x =&amp;gt; x * 2);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(반대 개념인 외부 이터레이터는 ES6의 Symbol.iterator와 for...of다. 소비자가 직접 next()를 호출하며 순회를 제어한다.)&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;이터레이터&amp;nbsp;방식이&amp;nbsp;단순히&amp;nbsp;가독성만&amp;nbsp;높이는&amp;nbsp;게&amp;nbsp;아니다.&amp;nbsp;순회&amp;nbsp;제어권이&amp;nbsp;함수&amp;nbsp;안에&amp;nbsp;있으므로,&amp;nbsp;앞서&amp;nbsp;다룬&amp;nbsp;Elements&amp;nbsp;Kind&amp;nbsp;같은&amp;nbsp;배열&amp;nbsp;상태를&amp;nbsp;함수가&amp;nbsp;직접&amp;nbsp;활용해&amp;nbsp;최적화할&amp;nbsp;수&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;네&amp;nbsp;함수의&amp;nbsp;내부를&amp;nbsp;뜯어볼&amp;nbsp;준비가&amp;nbsp;됐다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4. [각 함수의 내부구조]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach, map, filter, reduce는 배열의 이터레이터 메서드다. 배열을 순회하면서 각 요소에 콜백을 실행한다는 공통점이 있지만, 순회 외에 각자가 하는 일이 다르다.&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;네&amp;nbsp;함수의&amp;nbsp;순회&amp;nbsp;비용을&amp;nbsp;나란히&amp;nbsp;놓아보자.&amp;nbsp;배열은&amp;nbsp;도입부와&amp;nbsp;동일한&amp;nbsp;Array(100000).fill(0)이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;forEach&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;0.732ms&lt;br /&gt;map&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;2.078ms&lt;br /&gt;filter(전부통과)&amp;nbsp;&amp;rarr;&amp;nbsp;5.157ms&lt;br /&gt;filter(전부탈락)&amp;nbsp;&amp;rarr;&amp;nbsp;0.734ms&lt;br /&gt;reduce&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;0.650ms&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수치가&amp;nbsp;제각각이다.&amp;nbsp;같은&amp;nbsp;HOLEY_SMI&amp;nbsp;배열을&amp;nbsp;순회하는데&amp;nbsp;왜&amp;nbsp;이렇게&amp;nbsp;다를까.&amp;nbsp;각&amp;nbsp;함수가&amp;nbsp;순회&amp;nbsp;외에&amp;nbsp;무엇을&amp;nbsp;더&amp;nbsp;하는지가&amp;nbsp;다르기&amp;nbsp;때문이다.&lt;/p&gt;
&lt;div&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[forEach]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach는 각 인덱스에서 콜백을 실행하고 끝이다. 결과를 저장하지 않고, 새 배열도 만들지 않는다. 반환값은 항상 undefined다. &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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[map]&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach와 구조가 거의 같지만 한 가지가 다르다. &lt;b&gt;결과를 담을 배열을 새로 만들어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const result = new Array(len);       // ① 결과 배열 미리 할당
for (let k = 0; k &amp;lt; len; k++) {
  if (k in arr) {
    result[k] = fn(arr[k], k, arr); // ② 콜백 실행 + 결과 저장
  }
}
return result;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 배열 100,000개짜리를 할당하는 비용만 약 1.3ms다. forEach(0.732ms)와 map(2.078ms)의 차이가 거기서 온다. &lt;br /&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;Elements Kind&lt;/b&gt; &amp;rarr; HOLEY_SMI 배열이라 매 인덱스마다 HasProperty 체크가 붙는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 동작&lt;/b&gt; &amp;rarr; map은 거기에 결과 배열 할당 비용이 얹힌다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach가 빠른 이유는 HOLEY 체크를 덜 해서가 아니다. 둘 다 똑같이 체크한다. map이 느린 건 &lt;b&gt;결과 배열을 새로 만들기 때문이다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여기서 Elements Kind가 다시 개입한다. map이 만드는 결과 배열의 Kind는 입력 배열의 Kind를 따른다. SMI 배열을 순회하면 결과도 SMI 배열로 할당되고, Double 배열을 순회하면 결과도 Double 배열로 할당된다. Double은 요소 하나당 SMI의 두 배 메모리를 쓰기 때문에, 100,000개짜리 결과 배열을 만드는 비용도 두 배로 뛴다. 앞서 측정한 [Double] map &amp;rarr; 5.302ms가 [SMI] map &amp;rarr; 2.131ms의 두 배를 넘는 이유가 여기에 있다. forEach는 결과 배열을 만들지 않으로 [Double] forEach &amp;rarr; 1.183ms에서 이 차이가 나타나지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[filter]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter가 유독 느린 이유는 결과 배열의 최종 크기를 순회 전에 알 수 없다는 데 있다. map은 입력과 출력 크기가 항상 같으니 미리 할당할 수 있다. filter는 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V8은 이 문제를 이렇게 해결한다. 일단 원본과 같은 크기로 결과 배열을 할당하고, 조건을 통과한 것만 채운 뒤, 마지막에 실제 크기로 줄인다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;filter(전부 통과) &amp;rarr; 5.157ms   &amp;larr; 원본 크기 할당 + 전체 복사
filter(전부 탈락) &amp;rarr; 0.734ms   &amp;larr; 원본 크기 할당 + 복사 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 통과시킬 때가 가장 느리다. 반대로 전부 탈락시키면 복사 비용이 없어 forEach 수준으로 내려간다. &lt;b&gt;filter는 결과가 적을수록 빠르다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter에는 한 가지 특성이 더 있다. &lt;b&gt;map과 달리 hole&lt;/b&gt;을 제거한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #eaecf0;&quot;&gt;&lt;code&gt;const holey = [1, , 3, , 5];

holey.map(x =&amp;gt; x);        // [1, empty, 3, empty, 5] &amp;mdash; hole 유지
holey.filter(x =&amp;gt; true);  // [1, 3, 5]               &amp;mdash; hole 사라짐&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;map은&amp;nbsp;hole을&amp;nbsp;만나면&amp;nbsp;그&amp;nbsp;자리를&amp;nbsp;비워두고&amp;nbsp;결과&amp;nbsp;배열에도&amp;nbsp;그대로&amp;nbsp;전파한다.&amp;nbsp;filter는&amp;nbsp;조건을&amp;nbsp;통과한&amp;nbsp;요소만&amp;nbsp;순서대로&amp;nbsp;쌓으므로&amp;nbsp;hole이&amp;nbsp;결과에&amp;nbsp;포함될&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;&lt;b&gt;filter의&amp;nbsp;결과&amp;nbsp;배열은&amp;nbsp;항상&amp;nbsp;PACKED로&amp;nbsp;출발&lt;/b&gt;한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;앞서 Elements Kind의 전환은 단방향이라고 했다. 한 번 HOLEY가 되면 되돌아오지 않는다고. filter는 그 원칙의 실용적인 우회로다. HOLEY 배열을 의도치 않게 들고 있다면 filter(x =&amp;gt; x != null) 한 번으로 hole을 걷어내고 PACKED 배열을 새로 얻을 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[reduce]&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduce의&amp;nbsp;순회&amp;nbsp;비용은&amp;nbsp;forEach와&amp;nbsp;거의&amp;nbsp;같다.&amp;nbsp;새&amp;nbsp;배열을&amp;nbsp;만들지&amp;nbsp;않고,&amp;nbsp;반환값은&amp;nbsp;누산기&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;reduce는 누산기를 어떻게 쓰냐에 따라 비용이 크게 달라진다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;number 누산기는 타입이 처음부터 끝까지 고정된다. V8 입장에서는 누산기가 사실상 PACKED_SMI나 PACKED_DOUBLE처럼 동작하는 셈이다. 타입이 보장되니 콜백 안의 연산을 타입 특화 코드로 컴파일할 수 있고, 매 iteration마다 타입 체크를 건너뛴다.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;array 누산기에 push를 쓰는 건 매 iteration마다 배열 쓰기 연산이 추가된다. spread로 새 배열을 만드는 건 매 iteration마다 배열 전체를 복사하는 O(n&amp;sup2;) 패턴이다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[결론 : V8최적화의 산물]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지&amp;nbsp;살펴본&amp;nbsp;것처럼,&amp;nbsp;forEach,&amp;nbsp;map,&amp;nbsp;filter,&amp;nbsp;reduce는&amp;nbsp;배열을&amp;nbsp;순회한다는&amp;nbsp;공통점&amp;nbsp;아래에서&amp;nbsp;각자&amp;nbsp;다른&amp;nbsp;구조로&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;V8은 배열을 Elements Kind로 분류해 순회 경로를 결정한다. 같은 코드라도 배열이 어떻게 생성됐는지, 어떤 타입의 요소를 담고 있는지에 따라 V8이 선택하는 실행 경로가 달라진다. 그 위에서 각 함수는 순회 외에 하는 일이 다르다. map은 결과 배열을 만들고, filter는 크기를 모른 채 할당하고 줄이고, reduce는 누산기 하나만 들고 돈다. forEach는 그 모든 비용이 없다.&lt;/p&gt;</description>
      <category>개발 공부</category>
      <category>elements kind</category>
      <category>filter</category>
      <category>forEach</category>
      <category>hole</category>
      <category>iteration</category>
      <category>Javascript</category>
      <category>Map</category>
      <category>Reduce</category>
      <category>v8</category>
      <author>gamzamandu</author>
      <guid isPermaLink="true">https://gamzamandu.tistory.com/3</guid>
      <comments>https://gamzamandu.tistory.com/3#entry3comment</comments>
      <pubDate>Wed, 29 Apr 2026 23:58:14 +0900</pubDate>
    </item>
    <item>
      <title>TSC는 Type Narrowing을 어떻게 처리하는 걸까</title>
      <link>https://gamzamandu.tistory.com/2</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[서론 : TSC에 대한 소개 그리고 Type Narrowing]&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TSC는 무엇인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TSC는 TypeScript Complier로, &lt;b&gt;TypesScript 코드를 JavaScript로 변환해 주는 컴파일러&lt;/b&gt;이다. TSC는 타입 검사를 통해 코드 안정성을 보장하면서도 런타임에서 실행 가능한 JS 코드를 생성한다. TSC는 tsconfig.json를 기반으로 소스 파일들의 의존성 그래프를 구축하고 전체 컴파일 콘텍스트를 관리하는 Program을 실행하게 된다.&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;TSC는 크게 소스코드를 키워드/연산자 단위로 토큰화하는 &lt;b&gt;Scanner&lt;/b&gt;, 토큰화된 SyntaxKind를 AST(추상구문트리)로 변환하는 &lt;b&gt;Parser&lt;/b&gt;, AST Node를 토대로 타입정보(선언과 참조)를 매핑하는 &lt;b&gt;Binder&lt;/b&gt;, 이렇게 만들어진 Symbol Table을 토대로 타입 일치 여부를 검사하는 Type &lt;b&gt;Checker&lt;/b&gt;, 마지막으로 JS소스와 .d.ts 파일을 생성하는 &lt;b&gt;Emitter&lt;/b&gt; 단계로 나뉜다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Type&amp;nbsp;Narrowing는 무엇인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Type Narrowing에 대해 간략하게 소개한다면, TypeScript에서 타입 내로잉(Narrowing)은 유니언 타입(union type)처럼 여러 가능한 타입 중 하나로 범위를 좁혀 더 구체적인 타입으로 만드는 과정이라고 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1777354247370&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function processInput(input: string | number) {
  if (typeof input === &quot;string&quot;) {
    // input은 이제 string 타입으로 좁혀짐
    return input.toUpperCase();
  } else {
    // input은 이제 number 타입으로 좁혀짐
    return input.toFixed(2);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시를 보면 ProcessInput의 인자인 input은 string과 number를 가질 수 있는 union type이다. typeof, in, instanceof 같은 type guard를 통해 string과 number를 구분하며 타입이 자동으로 좁혀진다. (이래서 타입 좁히기라고 부른다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;본문의 요지는 이것이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;TSC에서 Type Narrowing는 어떻게 처리하는 것일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Type의 유효성을 검증하고, 필요한 경우 더 구체적인 타입으로 좁혀나가는(Narrowing) 작업이 수행되는 곳은 Type Checker 단계이다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Type Checker 단계에서 타입을 검사하는 방법을 더 파보도록 하자.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[본론 : TSC는&amp;nbsp;Type&amp;nbsp;Narrowing을&amp;nbsp;어떻게&amp;nbsp;처리하는&amp;nbsp;걸까]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 Type Narrowing을 다루는 소스코드를 Type Checker 과정까지 전개해보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1777362523147&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let x: string | number = 123;
if (typeof x === 'string') {
  x.toUpperCase();
}&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;1. [Scanner &amp;amp; Parser]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명했듯이, 소스코드는 Scanner 단계에서 토큰 단위로 분해되고 Parser 단계에서 AST(추상구문트리)로 변환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;AST는 소스코드에서 세미콜론이나 괄호 같은 사소한 문법 요소는 생략해 핵심 구조만 남겨 해석을 유연하게 할 수 있게 도와준다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;TypeScript의 AST로 변환한다면 다음과 같아진다.&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;422&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AnYWw/dJMcafTXzr0/BAbn7OYLmlveehWGsB7Slk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AnYWw/dJMcafTXzr0/BAbn7OYLmlveehWGsB7Slk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AnYWw/dJMcafTXzr0/BAbn7OYLmlveehWGsB7Slk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAnYWw%2FdJMcafTXzr0%2FBAbn7OYLmlveehWGsB7Slk%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;422&quot; height=&quot;719&quot; data-origin-width=&quot;422&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. [Binder]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 이제 Binder에서 AST의 정보를 토대로 Symbol Table를 생성해 보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AST Node를 순회하면서 VariableDeclaration(변수선언) 노드를 발견한다. 해당 노드의 Identifier(&quot;x&quot;) 식별자의 이름을 마주한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 스코프에 식별자의 이름을 키로 한 메타데이터를 Symbol Table 안에 구성하게 된다. 이 정보를 토대로 Checker가 x의 타입은 string | number였군. 이렇게 알 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 중요한 지점이 나온다. 그럼 &amp;lt;Type Narrowing은 어떻게 이루어지는 것인가?&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 제어 흐름을 바뀌는 지점들을 저장하기 위해, 실행 경로를 분석하여 &lt;b&gt;각 지점의 타입을 추론하는 Control Flow Analysis&lt;/b&gt;(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;과정을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;제어 흐름 분석)이라고 한다. 이 과정 덕분에 TSC가 타입이 분기되는 지점에서도 빈틈없이 타입을 추적할 수 있는 것이다.&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;다시 TSC를 전개하러 가보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binder는 AST를 조회하다가 if문, typeof, instanceof 같은 타입의 범위를 좁히는 지점을 만나면 Flow Graph를 생성하게 된다.&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 Flow Node가 구성되는 것이다.&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;2048&quot; data-origin-height=&quot;974&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qxn0L/dJMcaaE8lyX/AkygNqlNTDKG3bLQ89Fndk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qxn0L/dJMcaaE8lyX/AkygNqlNTDKG3bLQ89Fndk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qxn0L/dJMcaaE8lyX/AkygNqlNTDKG3bLQ89Fndk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqxn0L%2FdJMcaaE8lyX%2FAkygNqlNTDKG3bLQ89Fndk%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;2048&quot; height=&quot;974&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;974&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;좀 더 복잡적인 상황에 대한 Flow Node가 궁금하다면 참고하라.&lt;/p&gt;
&lt;table style=&quot;text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;조건문&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;IfStatement&lt;/td&gt;
&lt;td&gt;test&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건의&lt;span&gt;&amp;nbsp;&lt;/span&gt;true/false&lt;span&gt;&amp;nbsp;&lt;/span&gt;분기 생성.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;조건 연산자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ConditionalExpression&lt;/td&gt;
&lt;td&gt;? :&lt;span&gt;&amp;nbsp;&lt;/span&gt;삼항 연산자. 분기 생성.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;반복문&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;WhileStatement,&lt;span&gt;&amp;nbsp;&lt;/span&gt;ForStatement&lt;/td&gt;
&lt;td&gt;루프 진입/탈출 시 타입 재계산 필요.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;논리 연산자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;BinaryExpression&lt;/td&gt;
&lt;td&gt;&amp;amp;&amp;amp;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;||&lt;span&gt;&amp;nbsp;&lt;/span&gt;연산자. 단락 평가(Short-circuit)로 인한 타입 내로잉.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;타입 가드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;BinaryExpression&lt;/td&gt;
&lt;td&gt;typeof,&lt;span&gt;&amp;nbsp;&lt;/span&gt;instanceof&lt;span&gt;&amp;nbsp;&lt;/span&gt;비교식. Flow Node의 핵심.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;분기점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SwitchStatement&lt;/td&gt;
&lt;td&gt;case&lt;span&gt;&amp;nbsp;&lt;/span&gt;별로 타입 흐름 분기.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tlprc/dJMcahjXUGm/b9FZs1uxuZ2wOiMZ8KJfM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tlprc/dJMcahjXUGm/b9FZs1uxuZ2wOiMZ8KJfM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tlprc/dJMcahjXUGm/b9FZs1uxuZ2wOiMZ8KJfM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftlprc%2FdJMcahjXUGm%2Fb9FZs1uxuZ2wOiMZ8KJfM0%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;1024&quot; height=&quot;487&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. [Checker]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 코드의 구조를 위한 Symbol Table과 타입의 흐름을 위한 Flow Node가 구성되었다. 이것을 Checker가 해석만 하면 될 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Checker가 AST를 다시 순회하며 타입 검사를 수행할 때, 각 노드에서 다음과 같은 과정을 거친다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1. [근본 타입 확인]&lt;/b&gt;&lt;br /&gt;Checker는 현재 방문 중인 식별자(x)를 만났을 때, Symbol Table을 조회하여 x가 string | number로 정의되어 있다는 기본 정보를 먼저 확보한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;2. [타입 내로잉]&lt;/b&gt;&lt;br /&gt;Checker가 if (typeof x === 'string') 블록 안으로 들어간다. (이때 Checker는 Flow Graph를 참조한다.)&lt;br /&gt;Flow Graph는 해당 블록이 typeof x === 'string' 조건이 true인 경로임을 알려준다. Checker는 이 정보를 바탕으로, Symbol Table에 있던 원래 타입(string | number)을 해당 블록 안에서만 string으로 좁힌다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. [타입 안전성 검증]&lt;/b&gt;&lt;br /&gt;이제 블록 내부의 x.toUpperCase()를 만난다.&lt;br /&gt;Checker는&amp;nbsp;좁혀진&amp;nbsp;타입(string)을&amp;nbsp;기준으로&amp;nbsp;.toUpperCase()&amp;nbsp;메서드가&amp;nbsp;존재하는지&amp;nbsp;검사한다.&lt;br /&gt;&lt;br /&gt;string 타입에는 해당 메서드가 존재하므로, 타입 체커는 이를 &quot;안전함&quot;으로 통과시킨다. 만약 블록 밖에서 호출했다면, 여전히 string | number로 남아있어 에러를 뱉었을 것.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[결론 : Type Narrowing이 가능한 건 Control Flow Analysis 덕분]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 살펴본 것처럼, 다양한 분기 조건에서도 타입을 정확하게 추론하는 이유는 제어 흐름 분석(Control Flow Analysis, CFA) 덕분이다.&lt;br /&gt;&lt;br /&gt;TSC는 Binder를 통해 변수의 선언과 범위를 담은 Symbol Table을 구축하고, 코드의 실행 경로마다 타입이 어떻게 변하는지 추적하는 Flow Graph를 설계하고 Type Checker가 이 두 가지를 결합한 것이다.&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;사실 이 과정을 통해 Type Narrowing 뿐만 아니라. Type Assertion이 어떻게 동작하는지도 이해할 수 있다.&lt;/p&gt;</description>
      <category>개발 공부</category>
      <category>AST</category>
      <category>Type Narrowing</category>
      <category>TypeScript</category>
      <category>Typescript Complier</category>
      <author>gamzamandu</author>
      <guid isPermaLink="true">https://gamzamandu.tistory.com/2</guid>
      <comments>https://gamzamandu.tistory.com/2#entry2comment</comments>
      <pubDate>Tue, 28 Apr 2026 17:40:05 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 API 호출수를 최대 80% 줄인 이야기</title>
      <link>https://gamzamandu.tistory.com/1</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;[서론 : 프로젝트에 대한 소개]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트는 &quot;납득 가능한 외래어 순화 인공지능 서비스&quot;라고 설명할 수 있겠다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;나랏말싸미_(2).gif&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qFoBO/dJMcabKNCJN/sqaApVQWG3Ski38X4SPPQ0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qFoBO/dJMcabKNCJN/sqaApVQWG3Ski38X4SPPQ0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qFoBO/dJMcabKNCJN/sqaApVQWG3Ski38X4SPPQ0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/qFoBO/dJMcabKNCJN/sqaApVQWG3Ski38X4SPPQ0/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;1080&quot; data-filename=&quot;나랏말싸미_(2).gif&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI시대가 되면서 우리나라에는 외래어가 물밀듯이 들어오고 있다. 최근 설문에 따르면 &quot;신문&amp;middot;방송에서 사용하는 말의 의미를 몰라 곤란하다&amp;rdquo;는 응답이 89%에 달할 만큼 외래어 사용이 증가하고 있다. 국민들의 알 권리가 저하되기 때문에 정부에서는 신문이나 공문서에서 우리말 사용을 지향하는 &quot;공공언어&quot; 사용을 권장한다. 그러나 &quot;공공언어&quot;를 만드는 주체인 국립국어원이 제안한 다듬은 말은 &quot;다듬은 말이 맥락에 맞지 않아 사용하기 힘들다.&quot;, &quot;다듬은 말이 납득이 되지 않는다.&quot;라는 반응으로 사용되지 않는다.&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;br /&gt;해당 인공지능에 대한 연구가 궁금하다면 팀의 AI 책임자인 권민재 군이 쓴 논문을 구경해 주시길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777252949138&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;인공지능 기반 외래어 감지 및 순화어 추천 | DBpia&quot; data-og-description=&quot;권민재 | 한국정보기술진흥원 학술지 | 2026.3&quot; data-og-host=&quot;www.dbpia.co.kr&quot; data-og-source-url=&quot;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&quot; data-og-url=&quot;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cltMfq/dJMb9eTSulB/CKv83gfnNsIziCspHtBHI0/img.jpg?width=329&amp;amp;height=465&amp;amp;face=0_0_329_465,https://scrap.kakaocdn.net/dn/IMylX/dJMb84X1BO9/QStByXppaVVKLpYJUMeXN0/img.jpg?width=329&amp;amp;height=465&amp;amp;face=0_0_329_465,https://scrap.kakaocdn.net/dn/TLH9z/dJMb887bFqy/dV2knedRumw9K6RjrsI2lk/img.png?width=2160&amp;amp;height=380&amp;amp;face=0_0_2160_380&quot;&gt;&lt;a href=&quot;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cltMfq/dJMb9eTSulB/CKv83gfnNsIziCspHtBHI0/img.jpg?width=329&amp;amp;height=465&amp;amp;face=0_0_329_465,https://scrap.kakaocdn.net/dn/IMylX/dJMb84X1BO9/QStByXppaVVKLpYJUMeXN0/img.jpg?width=329&amp;amp;height=465&amp;amp;face=0_0_329_465,https://scrap.kakaocdn.net/dn/TLH9z/dJMb887bFqy/dV2knedRumw9K6RjrsI2lk/img.png?width=2160&amp;amp;height=380&amp;amp;face=0_0_2160_380');&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;인공지능 기반 외래어 감지 및 순화어 추천 | DBpia&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;권민재 | 한국정보기술진흥원 학술지 | 2026.3&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.dbpia.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[본론]&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;0. 무슨 일을 담당했는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트에서 나는 기획, PM, 디자인, 프론트, 데이터 수집 쪽을 담당하였고, 본 글에서는 &lt;b&gt;&amp;lt;순화 API 호출을 줄이는 과정&amp;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;b&gt;&amp;lt;문서편집과 문서의 외래어를 순화해주는 화면&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 문서편집기의 외래어는 하이라이트된다.&lt;br /&gt;2. 오른쪽 사이드바에는 감지된 외래어와 그 외래어에 대한 다듬은 말이 추천된다.&lt;br /&gt;3. 다듬은 말에 대한 제안을 승인하면 문서편집기에서 그 단어가 다듬을 말로 변경된다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. API 호출이 멈추질 않는다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 초안은 문서에디터가 수정되면 순화 요청을 보내는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1777254472871&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(()=&amp;gt;{
	순화API호출(documentContent)
},[documentContent])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 이렇게 하면 한 글자 수정할 때마다 순화 요청이 날아가는 참극이 발생할 것이기에 &quot;디바운스&quot; 도입을 선택했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;디바운스(Debounce)는 프로그래밍에서 연속적으로 발생하는 이벤트를 그룹화해 불필요한 호출을 방지하는 기술이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. useEffect를 활용하여 디바운스를 구현해 보자.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect는 컴포넌트 렌더링 후 부수적인 효과를 실행하는 Hook인데, 내부적으로는 React가 Fiber 노드의 updateQueue에 Effect를 저장해 두고, DOM 커밋 단계 이후 다음 이벤트 루프에서 실행된다. 이 함수는 하나의 useEffect 버전이 존재해야 하므로 새로운 버전의 변경이 호출되면, 이전 버전의 useEffect는 제거된다. 이때 제거된 useEffect에는 제거될 때 액션을 취할 수 있는데, 여러분들이 잘 아는 Clean-up이다. 단순히 컴포넌트가 Unmount 될 때 트리거된다고 생각할 수 있지만, 모든 것이 React Fiber로 이루어진 React세계의 관점에서는 &lt;b&gt;Component가 unmount 되면서 (Component를 React에 등록한 Fiber의 UpdateQueue로 등록되어 있던) useEffect 인스턴스가 자체가 unmount 되기 때문에 Clean-up이 동작하는 것이지 Component의 unmount만 관심사로 둔 것이 아니다.&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;이걸 활용하면 디바운스를 쉽게 구현할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777254634755&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 timeout = setTimeout(() =&amp;gt; {
    순화API호출(documentContent);
  }, 3000);

  return () =&amp;gt; clearTimeout(timeout);
}, [documentContent]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서가 변경되면 useEffect 안에서는 setTimeout으로 3000ms 뒤에 콜백함수가 실행된다. 여기서 새로 변경이 되면 기존 useEffect 인스턴스는 Clean-up을 통해 setTimeout이 해제됨으로 실행되지 않는다. 결론적으로 언제나 3000ms 후 아무 변경도 하지 않아야 API가 호출되는 디바운스를 만들 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여기서 3000ms로 잡은 이유는 Google Docs나 Notion 같은 문서편집기가 대게 2~3초의 디바운스를 적용하는 데에서 착안했다.&lt;br /&gt;&lt;s&gt;써봤을 때도 가장 좋다.&lt;/s&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 아직도 불필요한 API 호출은 존재한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바운스를 도입했음에도 아직 불필요한 호출은 존재한다. 말하자면 이런 상황이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;수많은 문장들 중에 한 문장만 수정했는데, 변경하지 않은 문장들까지 순화 요청이 되고 있어요.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐? 변경이 안 됐는데 요청에 포함되고 있다고? 변경을 추적해야겠군. 뭐? 변경을 추적한다고? 이거 완전 Git이잖아.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wflDK/dJMcadIwZKs/5NpZNtFtv4EYfIIZc3SlKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wflDK/dJMcadIwZKs/5NpZNtFtv4EYfIIZc3SlKK/img.png&quot; data-alt=&quot;버전 관리의 정수 Git&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wflDK/dJMcadIwZKs/5NpZNtFtv4EYfIIZc3SlKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwflDK%2FdJMcadIwZKs%2F5NpZNtFtv4EYfIIZc3SlKK%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;347&quot; height=&quot;145&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;버전 관리의 정수 Git&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 변경점만 찾아내는 코드를 만들어보자.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경점을 찾아내기 위해서는 먼저 &lt;b&gt;변경의 단위를 정의하는 것&lt;/b&gt;이 필요할 것이다. Git이 파일 단위로 변경사항을 추적하듯, 이 시스템에서도 문장을 추적의 기본 단위로 삼았다. 고맙게도 문서편집기 구현에 사용한 CKEditor 같은 경우는 내용을 블록 단위로 분리하는 구조를 가지고 있다. 해당 블록을 변경의 단위로 삼았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;예를 들면 h1으로 쓴 블록과 p로 쓴 블록이 분리되어 있기 때문에 해당 블록을 한 문장의 단위로 가정하겠다는 것이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경의 단위를 정했겠다. 이제 버전을 관리해야 할 것이다. 버전 관리는 간단하게 2-State구조를 채택했다. 전 버전과 현재 버전만 관리하겠다는 것이다. 버전 비교를 위해서 좋은 방법도 많겠지만 &quot;deep-diff&quot; 라이브러리를 활용했다. diff(lhs, rhs) 한 번으로 문장이 변경됐는지, 생성됐는지, 제거됐는지, 유지됐는지 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1777258007897&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[
  {
    &quot;kind&quot;: &quot;E&quot;,
    &quot;path&quot;: [&quot;title&quot;],
    &quot;lhs&quot;: &quot;오늘은 날씨가 좋아&quot;,
    &quot;rhs&quot;: &quot;오늘은 날씨가 흐려&quot;
  },
  {
    &quot;kind&quot;: &quot;N&quot;       // 새로 추가
    &quot;path&quot;: [&quot;newField&quot;],
    &quot;rhs&quot;: &quot;추가된 필드&quot;
  }
]&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;5. 이전 버전의 응답이 지연돼서, 현재 버전과 충돌이 난다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 문서편집기의 최적화는 끝나지 않았다. 상황은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;v1에서 요청을 보낸다. 그동안 문서가 수집되어, v2 버전에서 요청이 호출되었다.&lt;br /&gt;그런데 v1의 응답값이 이제야 돌아오고 있다! v2의 수정사항과 충돌이 났다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교적 간단한 문제이다. 응답값이 버전에 대한 정보를 같이 반환해 주면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 &quot;순화 응답은 v1를 지칭하고 있습니다.&quot;&amp;nbsp;&lt;br /&gt;음 현재는 v3이기 때문에 v1는 적용하지 않고 폐기해야겠군.&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;6. 유저가 대용량 텍스트를 복붙 하면 어떻게 될 것인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바운스를 통해 장기적인 편집의 요청을 최소화했다. 하지만 단기적인 상황에서는 어떻게 할 것인가? 유저가 1000자의 텍스트를 복사/붙여 넣기를 한다면? 뭐라도 최적화하고 싶다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;[순화 요청이 필요함] --[서버로 전송]-&amp;gt; [외래어 감지] -&amp;gt; [외래어가 감지된 문장만 순화 AI 적용] -&amp;gt; [반환]&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 아이디어가 떠올랐다. &lt;b&gt;외래어 감지를 클라이언트 측에서 실행시키자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀의 외래어 감지 모델은 LSTM 경량 모델이다. (이것도 어떻게 했는지 궁금하다면 &lt;a title=&quot;여길&quot; href=&quot;https://www.dbpia.co.kr/journal/articleDetail?nodeId=NODE12579655&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여길&lt;/a&gt; 구경하길 바란다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래어 감지 모델은 클라이언트 측으로 이전시키면&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서버의 LSTM(TensorFlow.py)을 클라이언트 단에서 TenserFlow.js로 실행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(모델을 Tensor Flow SavedModel로 변환 후 tfjs_converter 사용한 뒤 브라우저에서 호출했습니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여담) 외래어 감지 모델은 정말 경량 모델이라 실행되지 않는 컴퓨터는 없을 것이긴 하나, &lt;br /&gt;클라이언트 환경을 체크해서 외래어 감지 모델의 실행을 백엔드 측에서 실행해 주는 Fail-Safe 대안을 마련해 두는 것도 좋을 것 같다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7. 개선된 성능을 측정해 보자.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론상 개선된다는 것을 알았지만, 개선된 버전이 얼마나 효율적으로 API를 줄였는지 측정하고 싶다. 현업의 개발자라면 A/B테스트를 통해서 실제 시나리오를 검증하고 정량화할 수 있으나, 사용자가 없는 가난한 개발자라면 실제상황을 재현해야 할 것이다.&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;목표는 A상황과 B상황 모두 동일한 상황에서 API 호출수를 측정하고 싶다. 인간이 아무리 노력한들 똑같은 문서 편집을 두 번 재현하는 능력은 무리이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;클라이언트의 클릭, 입력 이벤트를 모조리 기록할 수 없을까?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사하게도 이미 이것을 한 개발자가 있다. 클라이언트의 텍스트 입력과 같은 사용자 행동을 JSON으로 저장해 주고 실행까지 해주는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://rety.verou.me/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;rety 라이브러리&lt;/a&gt;가 있다. 이것을 이용해 문서편집 시나리오 10개 정도를 저장하고 개선 전 버전(디바운스까지 적용)과 개선 후 버전(모든 개선 적용)에 실행시켰다. 대용량 텍스트 복사/붙여 넣기 같은 상황을 제외한 나머지 시나리오에서 모두 60% 이상의 감소를 이끌어냈다.&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <category>debounce</category>
      <category>frontend</category>
      <author>gamzamandu</author>
      <guid isPermaLink="true">https://gamzamandu.tistory.com/1</guid>
      <comments>https://gamzamandu.tistory.com/1#entry1comment</comments>
      <pubDate>Mon, 27 Apr 2026 15:07:00 +0900</pubDate>
    </item>
  </channel>
</rss>