<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기술 블로그</title>
    <link>https://kjk5.tistory.com/</link>
    <description>개발 블로그</description>
    <language>ko</language>
    <pubDate>Fri, 26 Jun 2026 05:53:59 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jaegwan</managingEditor>
    <image>
      <title>기술 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/2950083/attach/eb26322b11af4802b838ee4e0c7dafc6</url>
      <link>https://kjk5.tistory.com</link>
    </image>
    <item>
      <title>React 대량 데이터 편집 모달에서 입력 지연 원인 추적기</title>
      <link>https://kjk5.tistory.com/154</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;551&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DISyn/dJMcadH4hW5/bFB5rCyZzIBnIdqI0pObx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DISyn/dJMcadH4hW5/bFB5rCyZzIBnIdqI0pObx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DISyn/dJMcadH4hW5/bFB5rCyZzIBnIdqI0pObx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDISyn%2FdJMcadH4hW5%2FbFB5rCyZzIBnIdqI0pObx0%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;553&quot; height=&quot;551&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;551&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;[React] 대규모 데이터 편집 시 발생하는 입력 지연 분석: Fiber 트리 순회와 브라우저 Reflow&lt;/h2&gt;
&lt;p&gt;React 환경에서 수천 건의 데이터를 동시에 다루는 편집 UI를 구현하다 보면, 단순한 최적화만으로는 해결되지 않는 성능 병목을 마주하곤 합니다. 최근 약 3,800건의 매출 데이터를 수정하는 모달에서 &lt;strong&gt;입력 시 약 5초간의 지연(Input Lag)&lt;/strong&gt;이 발생했던 사례를 통해, React 내부 동작과 브라우저 렌더링 엔진의 관점에서 그 원인을 분석하고 해결 과정을 기록합니다.&lt;/p&gt;
&lt;h3&gt;1. 현상 및 초기 진단&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;규모:&lt;/strong&gt; 약 3,822행 (행당 Input 및 SelectBox 등 10여 개의 컴포넌트 포함)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문제:&lt;/strong&gt; 특정 입력 필드에 타이핑 시 화면이 수 초간 프리징됨.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;초기 가설:&lt;/strong&gt; 검색 필터링 로직의 부하 혹은 부모 컴포넌트의 불필요한 리렌더링을 의심함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그러나 성능 측정 결과, 필터링 로직은 1ms 미만으로 수행되었으며, &lt;code&gt;onBlur&lt;/code&gt; 처리를 통해 부모의 리렌더링을 억제했음에도 지연 현상은 개선되지 않았습니다.&lt;/p&gt;
&lt;h3&gt;2. React.memo와 Fiber 트리 순회 비용&lt;/h3&gt;
&lt;p&gt;Chrome DevTools의 Performance 프로파일링을 통해 확인한 결과, JavaScript 실행 시간(Yellow Block)이 병목의 큰 비중을 차지하고 있었습니다. 특히 React 내부의 &lt;code&gt;recursivelyTraverseMutationEffects&lt;/code&gt; 함수가 수만 번 호출되는 것을 확인했습니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;분석 결과 비교&lt;/strong&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;최적화 단계&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;JS 실행 시간 (Performance)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;특이 사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;기존 (No Memo)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;4,366ms&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;모든 Fiber 노드를 전수 조사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;React.memo 적용 후&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;2,983ms&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;변경되지 않은 서브트리 스킵&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;React는 상태 변경 시 Fiber 트리를 순회하며 업데이트가 필요한 노드를 탐색합니다. 행당 컴포넌트가 복잡할수록(Hook 사용량이 많을수록) Fiber 노드의 수는 기하급수적으로 증가하며, &lt;code&gt;React.memo&lt;/code&gt;가 없을 경우 단 하나의 입력 변경에도 수만 개의 노드를 재귀적으로 방문하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; 적용 시, 변경되지 않은 행의 &lt;code&gt;subtreeFlags&lt;/code&gt;를 확인하여 해당 서브트리 전체를 건너뛰기 때문에 JS 실행 비용을 유의미하게 절감할 수 있었습니다. &lt;/p&gt;
&lt;h3&gt;3. 두 번째 병목: 브라우저 렌더링 엔진의 한계 (Reflow)&lt;/h3&gt;
&lt;p&gt;JavaScript 실행 시간을 단축했음에도 불구하고, 프로파일러상에는 여전히 &lt;strong&gt;보라색 블록(Layout/Reflow)&lt;/strong&gt;이 선명하게 남아 있었습니다. 이는 React의 영역을 넘어 브라우저 렌더링 엔진이 처리해야 할 물리적인 한계치에 도달했음을 의미합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DOM 노드 규모:&lt;/strong&gt; 3,822행 × 11개 셀 = &lt;strong&gt;약 10만 개의 DOM 노드&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;원인:&lt;/strong&gt; &lt;code&gt;input&lt;/code&gt;의 value가 변경될 때마다 브라우저는 DOM에 존재하는 10만 개의 노드를 대상으로 레이아웃을 재계산합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;한계:&lt;/strong&gt; &lt;code&gt;React.memo&lt;/code&gt;는 JavaScript 엔진의 부하를 줄여줄 뿐, 이미 DOM에 그려진 수많은 노드로 인한 브라우저의 Reflow 비용은 해결할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 종합 및 최종 해결책&lt;/h3&gt;
&lt;p&gt;이번 디버깅을 통해 파악한 성능 병목의 구조는 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;병목 레이어&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;원인&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;대응 방안&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;JavaScript (React)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Fiber 트리 재귀 순회 부하&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;React.memo&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;서브트리 스킵 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Browser (Rendering)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;10만 개 DOM 노드의 Reflow&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Virtualization&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;DOM 노드 수 자체를 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;결론적으로 &lt;strong&gt;가상화(Virtualization)&lt;/strong&gt;는 단순히 리렌더링 성능만을 위한 도구가 아닙니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fiber 트리의 규모를 물리적으로 축소&lt;/strong&gt;하여 React의 순회 비용을 최소화하고,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOM 노드 수를 수백 개 수준으로 유지&lt;/strong&gt;하여 브라우저의 레이아웃 계산 부하를 근본적으로 제거하는 가장 확실한 전략입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5. 마치며&lt;/h3&gt;
&lt;p&gt;대규모 데이터를 다루는 UI에서는 &amp;quot;느림&amp;quot;의 원인이 복합적으로 작용할 때가 많습니다. 이번 사례처럼 JS 실행 비용과 브라우저 렌더링 비용이 혼재된 경우, 단계별 측정을 통해 병목의 실체를 명확히 구분하는 과정이 필수적입니다. &lt;/p&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/154</guid>
      <comments>https://kjk5.tistory.com/154#entry154comment</comments>
      <pubDate>Fri, 20 Mar 2026 16:49:41 +0900</pubDate>
    </item>
    <item>
      <title>5,000건 대량 테이블의 '전체 선택' 지연 해결하기 (Virtualization)</title>
      <link>https://kjk5.tistory.com/153</link>
      <description>&lt;p&gt;정산 시스템의 데이터 수정 모달에서 최대 5,000건의 행을 테이블로 보여줘야 하는 기능을 구현했습니다. 개별 체크박스 토글은 문제가 없었으나, 헤더의 &lt;strong&gt;&amp;#39;전체 선택&amp;#39;&lt;/strong&gt;을 클릭할 때마다 화면이 수 초간 멈추는 성능 저하가 발생했습니다.&lt;/p&gt;
&lt;p&gt;지난 포스트에서 다룬 &lt;code&gt;Set&lt;/code&gt; 기반 최적화와 &lt;code&gt;React.memo&lt;/code&gt;만으로는 해결되지 않았던 이 문제를 &lt;strong&gt;가상화(Virtualization)&lt;/strong&gt;를 통해 해결한 과정을 정리합니다.&lt;/p&gt;
&lt;h3&gt;1. 병목 지점 분석: React.memo가 무력화되는 순간&lt;/h3&gt;
&lt;p&gt;개별 체크박스 토글은 &lt;code&gt;React.memo&lt;/code&gt;로 최적화가 가능합니다. 5,000개 행 중 상태가 변하는 행은 단 1개뿐이기 때문입니다. 하지만 &amp;#39;전체 선택&amp;#39;은 이야기가 다릅니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;전체 선택 시 발생하는 일&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;selectedSeqs&lt;/code&gt; 상태가 0개에서 5,000개로 변경됩니다.&lt;/li&gt;
&lt;li&gt;각 행의 &lt;code&gt;isSelected&lt;/code&gt; 프로퍼티가 전부 &lt;code&gt;false&lt;/code&gt; → &lt;code&gt;true&lt;/code&gt;로 바뀝니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;React.memo&lt;/code&gt;의 비교 함수가 작동하지만, &lt;strong&gt;모든 행의 상태가 바뀌었으므로 5,000개 행 전체를 리렌더링&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;React는 5,000개 &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;에 대한 Virtual DOM diff를 수행하고 실제 DOM을 업데이트합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결과적으로 데이터가 수천 건이 넘어가면 Virtual DOM을 비교하고 실제 DOM에 반영하는 과정 자체가 브라우저 메인 스레드에 큰 부담을 주게 됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 해결책: 가상화 (Virtualization)&lt;/h3&gt;
&lt;p&gt;가상화의 핵심 아이디어는 &lt;strong&gt;&amp;quot;눈에 보이는 영역(Viewport)만 렌더링하자&amp;quot;&lt;/strong&gt;는 것입니다. 5,000개의 데이터가 있더라도 사용자의 화면에 보이는 행은 고작 10~15개 내외입니다. 가상화를 적용하면 전체 선택을 눌러도 React는 현재 보이는 15개의 행만 비교하면 되므로 즉각적인 반응 속도를 얻을 수 있습니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;구현: @tanstack/react-virtual&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;TanStack Table과 궁합이 좋은 &lt;code&gt;@tanstack/react-virtual&lt;/code&gt;을 활용하여 가상 테이블을 구현했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { useVirtualizer } from &amp;#39;@tanstack/react-virtual&amp;#39;;

const VirtualSimpleTable = ({ data, columns, estimateRowHeight = 53 }) =&amp;gt; {
  const parentRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
  const { rows } = table.getRowModel();

  // 가상화 설정
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () =&amp;gt; parentRef.current,
    estimateSize: () =&amp;gt; estimateRowHeight,
    overscan: 5,
    measureElement: el =&amp;gt; el.getBoundingClientRect().height, // 동적 높이 대응
  });

  const gridTemplateColumns = table
    .getAllColumns()
    .map(col =&amp;gt; `${col.getSize()}fr`)
    .join(&amp;#39; &amp;#39;);

  return (
    &amp;lt;div ref={parentRef} style={{ maxHeight: 400, overflowY: &amp;#39;auto&amp;#39; }}&amp;gt;
      {/* 가상화 적용 시 내부 컨테이너 높이를 전체 데이터 높이만큼 확보 */}
      &amp;lt;div style={{ height: virtualizer.getTotalSize(), position: &amp;#39;relative&amp;#39; }}&amp;gt;
        {virtualizer.getVirtualItems().map(virtualRow =&amp;gt; {
          const row = rows[virtualRow.index];
          return (
            &amp;lt;div
              key={row.id}
              ref={virtualizer.measureElement}
              style={{
                position: &amp;#39;absolute&amp;#39;,
                top: 0,
                transform: `translateY(${virtualRow.start}px)`,
                display: &amp;#39;grid&amp;#39;,
                gridTemplateColumns,
              }}
            &amp;gt;
              {row.getVisibleCells().map(cell =&amp;gt; (
                &amp;lt;div key={cell.id}&amp;gt;
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                &amp;lt;/div&amp;gt;
              ))}
            &amp;lt;/div&amp;gt;
          );
        })}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. 주요 구현 포인트&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;1) Table 대신 Div + CSS Grid 사용&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;가상화 라이브러리는 각 행을 &lt;code&gt;absolute&lt;/code&gt; 포지션으로 배치합니다. 하지만 표준 &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; 태그 내에서 &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;에 &lt;code&gt;absolute&lt;/code&gt;를 적용하면 테이블 레이아웃 구조가 깨지게 됩니다. 따라서 레이아웃 유연성이 높은 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;와 &lt;code&gt;CSS Grid&lt;/code&gt; 조합으로 전환하여 컬럼 정렬과 가상화 배치를 동시에 잡았습니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;2) 동적 행 높이 대응 (measureElement)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;데이터 내용에 따라 행 높이가 달라질 수 있는 경우, &lt;code&gt;estimateSize&lt;/code&gt;만으로는 위치 계산이 부정확할 수 있습니다. &lt;code&gt;measureElement&lt;/code&gt;를 통해 실제 렌더링된 요소의 높이를 측정함으로써 스크롤 위치를 정확하게 유지할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4. React.memo vs 가상화 비교&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;React.memo (행 단위)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;가상화 (Virtualization)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;작동 원리&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;변경 없는 행 렌더링 스킵&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;보이는 행만 DOM에 렌더링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전체 선택 시&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;5,000개 전수 리렌더링 (느림)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;약 15개만 리렌더링 (매우 빠름)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;초기 렌더링&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;5,000개 DOM 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;약 15개 DOM 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;구현 난이도&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;낮음&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;중간 (CSS 레이아웃 수정 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt;는 &amp;#39;변경된 행만 다시 그리는&amp;#39; 훌륭한 전략이지만, 전체 선택처럼 &lt;strong&gt;모든 행의 상태가 동시에 변하는 케이스&lt;/strong&gt;에서는 힘을 쓰지 못합니다. &lt;/p&gt;
&lt;p&gt;데이터가 수천 건 이상이고 사용자와의 인터랙션이 잦은 테이블이라면, 처음부터 가상화를 고려하는 것이 성능과 사용자 경험(UX) 측면에서 훨씬 유리합니다. 특히 &lt;code&gt;@tanstack/react-virtual&lt;/code&gt;을 활용하면 기존 TanStack Table의 로직을 그대로 유지하면서 성능만 끌어올릴 수 있습니다.&lt;/p&gt;
&lt;hr&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/153</guid>
      <comments>https://kjk5.tistory.com/153#entry153comment</comments>
      <pubDate>Thu, 19 Mar 2026 10:05:48 +0900</pubDate>
    </item>
    <item>
      <title>체크박스 아이템 최적화</title>
      <link>https://kjk5.tistory.com/152</link>
      <description>&lt;h2&gt;[react] 대량의 체크박스(5,000건) 렌더링 최적화하기&lt;/h2&gt;
&lt;p&gt;프로젝트를 진행하다 보면 대량의 데이터를 다뤄야 하는 상황을 마주하곤 합니다. 최근 정산 데이터 수정 모달에서 약 5,000건의 데이터를 테이블로 렌더링하고, 각 행을 체크박스로 선택하는 기능을 구현했습니다.&lt;/p&gt;
&lt;p&gt;하지만 구현 후 테스트 과정에서 체크박스를 클릭할 때마다 &lt;strong&gt;1~2초 수준의 심각한 지연&lt;/strong&gt;이 발생했습니다. 이번 포스트에서는 배열 기반 상태 관리의 함정과 이를 &lt;code&gt;Set&lt;/code&gt; 및 &lt;code&gt;React.memo&lt;/code&gt;로 해결한 과정을 정리합니다.&lt;/p&gt;
&lt;h3&gt;1. 문제 상황과 원인 분석&lt;/h3&gt;
&lt;p&gt;처음에는 일반적인 방식인 배열(&lt;code&gt;Array&lt;/code&gt;)을 사용하여 선택된 아이템의 상태를 관리했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const [selectedItems, setSelectedItems] = useState&amp;lt;DataListEditItem[]&amp;gt;([]);

// 개별 선택 핸들러
const handleSelectRow = (item: DataListEditItem) =&amp;gt; {
  setSelectedItems(prev =&amp;gt;
    prev.some(selected =&amp;gt; selected.seq === item.seq)
      ? prev.filter(selected =&amp;gt; selected.seq !== item.seq)
      : [...prev, item],
  );
};

// 컬럼 정의 내 체크 여부 판단
cell: ({ row }) =&amp;gt; (
  &amp;lt;input
    type=&amp;quot;checkbox&amp;quot;
    checked={selectedItems.some(item =&amp;gt; item.seq === row.original.seq)}
  /&amp;gt;
),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;언뜻 보면 큰 문제가 없어 보이지만, 데이터가 5,000건 이상으로 늘어나면 &lt;strong&gt;시간 복잡도&lt;/strong&gt; 측면에서 병목이 발생합니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;시간복잡도로 보는 병목&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;체크박스 하나를 클릭할 때마다 다음과 같은 연산이 일어납니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;selectedItems&lt;/code&gt; 상태가 변경되어 전체 컴포넌트가 리렌더링됩니다.&lt;/li&gt;
&lt;li&gt;5,000개 행이 모두 다시 그려집니다.&lt;/li&gt;
&lt;li&gt;각 행은 &lt;code&gt;selectedItems.some()&lt;/code&gt;을 실행하여 체크 여부를 판단합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;Array.some()&lt;/code&gt;은 최악의 경우 배열 전체를 순회하는 &lt;strong&gt;O(s)&lt;/strong&gt; 연산입니다(s = 선택된 아이템 수). 이를 5,000개 행마다 반복하므로 전체 비용은 &lt;strong&gt;O(n × s)&lt;/strong&gt;가 됩니다. 만약 전체 선택 후 1개를 해제한다면 약 2,500만 번(5,000 × 5,000)의 비교 연산이 수행되는 셈입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 해결 단계 1: Set 기반으로 전환&lt;/h3&gt;
&lt;p&gt;첫 번째 개선은 조회 성능이 뛰어난 &lt;code&gt;Set&lt;/code&gt; 자료구조를 사용하는 것입니다. &lt;code&gt;Set.has()&lt;/code&gt;는 &lt;strong&gt;O(1)&lt;/strong&gt;의 시간 복잡도를 가지므로, 5,000행 전체를 확인해도 &lt;strong&gt;O(n)&lt;/strong&gt;으로 연산량이 급감합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const [selectedSeqs, setSelectedSeqs] = useState&amp;lt;Set&amp;lt;number&amp;gt;&amp;gt;(new Set());

const handleSelectRow = (item: DataListEditItem) =&amp;gt; {
  setSelectedSeqs(prev =&amp;gt; {
    const next = new Set(prev);
    if (next.has(item.seq)) next.delete(item.seq);
    else next.add(item.seq);
    return next;
  });
};

// 조회 시 O(1)
cell: ({ row }) =&amp;gt; (
  &amp;lt;input
    type=&amp;quot;checkbox&amp;quot;
    checked={selectedSeqs.has(row.original.seq)}
  /&amp;gt;
),
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;연산&lt;/th&gt;
&lt;th&gt;배열 (Before)&lt;/th&gt;
&lt;th&gt;Set (After)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;개별 선택/해제&lt;/td&gt;
&lt;td&gt;O(n × s) ≈ O(n²)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O(n)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;체크 여부 비교 (1행)&lt;/td&gt;
&lt;td&gt;O(s)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O(1)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;3. 해결 단계 2: 행 단위 React.memo 적용&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt;을 통해 연산 비용은 줄였지만, 여전히 상태가 변경될 때마다 5,000개의 행이 모두 리렌더링되는 근본적인 문제가 남아 있습니다. 이를 해결하기 위해 각 행을 &lt;code&gt;memo&lt;/code&gt;로 감싸고 &lt;strong&gt;커스텀 비교 함수&lt;/strong&gt;를 적용했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// SimpleTable 내부 Row 컴포넌트화
const MemoizedRow = memo(
  &amp;lt;T,&amp;gt;({ row, isSelected }: MemoizedRowProps&amp;lt;T&amp;gt;) =&amp;gt; (
    &amp;lt;tr&amp;gt;
      {row.getVisibleCells().map(cell =&amp;gt; (
        &amp;lt;td key={cell.id}&amp;gt;
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        &amp;lt;/td&amp;gt;
      ))}
    &amp;lt;/tr&amp;gt;
  ),
  (prev, next) =&amp;gt;
    prev.row.original === next.row.original &amp;amp;&amp;amp; 
    prev.isSelected === next.isSelected // 데이터와 선택 상태가 같으면 리렌더링 방지
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식을 통해 체크박스 하나를 클릭했을 때, 실제 변경이 일어난 &lt;strong&gt;단 1개의 행&lt;/strong&gt;만 다시 그려지도록 최적화했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;4. 최종 비교 및 정리&lt;/h3&gt;
&lt;p&gt;각 단계별 최적화 결과는 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;배열 기반&lt;/th&gt;
&lt;th&gt;Set 전환&lt;/th&gt;
&lt;th&gt;Set + Memo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;checked 비교&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(s)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;개별 클릭 시 리렌더링&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5,000개 행&lt;/td&gt;
&lt;td&gt;5,000개 행&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1개 행&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5,000건 체감 속도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1~2초 지연&lt;/td&gt;
&lt;td&gt;다소 개선&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;즉시 반응&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;데이터셋이 작을 때는 배열 기반으로도 충분하지만, 수천 건 이상의 인터랙티브한 테이블을 구현해야 한다면 &lt;strong&gt;&lt;code&gt;Set&lt;/code&gt;을 통한 조회 최적화&lt;/strong&gt;와 &lt;strong&gt;&lt;code&gt;memo&lt;/code&gt;를 통한 렌더링 최적화&lt;/strong&gt; 조합을 필수적으로 고려해야 합니다.&lt;/p&gt;
&lt;p&gt;특히 &lt;code&gt;Set&lt;/code&gt;으로의 전환은 코드 수정량이 적으면서도 O(n²)에서 O(n)으로 성능을 끌어올릴 수 있는 가장 효율적인 방법입니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/152</guid>
      <comments>https://kjk5.tistory.com/152#entry152comment</comments>
      <pubDate>Fri, 13 Mar 2026 12:17:44 +0900</pubDate>
    </item>
    <item>
      <title>초성 검색을 지원하는 Local Async SelectBox 구현하기</title>
      <link>https://kjk5.tistory.com/151</link>
      <description>&lt;p&gt;프로젝트 규모가 커짐에 따라 서버 부하를 줄이기 위해 전체 데이터를 클라이언트에서 관리하며 필터링해야 하는 경우가 많아집니다. 특히 한국어 서비스에서는 사용자의 검색 경험을 결정짓는 &amp;#39;초성 검색&amp;#39;과 UI의 안정성을 보장하는 &amp;#39;포탈(Portal) 렌더링&amp;#39;이 필수적입니다.&lt;/p&gt;
&lt;p&gt;이번 포스트에서는 &lt;code&gt;hangul-js&lt;/code&gt;를 활용하여 검색 효율을 높이고, 외부 환경에 영향을 받지 않는 독립적인 &lt;strong&gt;LocalAsyncSelectBox&lt;/strong&gt;를 구축한 과정을 정리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1. 커스텀 컴포넌트 구현의 필요성&lt;/h3&gt;
&lt;p&gt;기존 오픈소스 라이브러리들은 범용성을 위해 설계되었으나, 다음과 같은 기술적 제약이 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;한글 검색 최적화 미흡:&lt;/strong&gt; 단순 텍스트 매칭만 지원하여 초성 검색 등 한국어 특화 기능을 별도로 구현해야 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;레이아웃 간섭:&lt;/strong&gt; 드롭다운 UI가 부모 컨테이너의 &lt;code&gt;overflow: hidden&lt;/code&gt;이나 &lt;code&gt;z-index&lt;/code&gt; 설정에 영향을 받아 가려지는 현상 발생.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;낮은 예측 가능성:&lt;/strong&gt; 라이브러리 내부 로직에 의존할 경우 특이 케이스(Edge Case) 대응 시 디버깅 리소스가 증가함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 문제를 해결하기 위해 &lt;strong&gt;제어권을 완전히 확보한 커스텀 컴포넌트&lt;/strong&gt;를 제작하여 장기적인 유지보수 효율을 높이고자 했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;2. 핵심 구현 사항&lt;/h3&gt;
&lt;h4&gt;한글 초성 검색 로직 (&lt;code&gt;hangul-js&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;사용자가 &amp;#39;ㅅㄱ&amp;#39;만 입력해도 &amp;#39;사과&amp;#39;를 찾을 수 있도록 정규표현식과 &lt;code&gt;hangul-js&lt;/code&gt;를 조합했습니다. 입력값이 초성인지 여부를 판단하여 필터링 로직을 분기 처리했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const filterData = useCallback((keyword: string) =&amp;gt; {
  if (!keyword.trim() || keyword.length &amp;lt; minLength) {
    setOptions([]);
    setIsOpen(false);
    return;
  }

  const searchKeyword = keyword.toLowerCase();
  const isChosung = /^[ㄱ-ㅎ]+$/.test(searchKeyword); // 초성 여부 검사

  const filtered = data
    .filter(item =&amp;gt; {
      if (!item?.label) return false;
      const searchTarget = item.label.toLowerCase();

      if (isChosung) {
        // 초성 검색: 각 글자의 첫 자음을 분해하여 비교
        const targetChosung = Hangul.disassemble(searchTarget, true)
          .map(char =&amp;gt; char[0])
          .join(&amp;#39;&amp;#39;);
        return targetChosung.includes(searchKeyword);
      }

      // 일반 검색 및 한글 형태소 검색 지원
      return searchTarget.includes(searchKeyword) || Hangul.search(searchTarget, searchKeyword) !== -1;
    })
    .slice(0, 5); // 성능 최적화를 위한 노출 개수 제한

  setOptions(filtered);
  // ...이후 렌더링 제어
}, [data, minLength]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;ModalPortal과 뷰포트 좌표 계산&lt;/h4&gt;
&lt;p&gt;드롭다운이 레이아웃 구조에 구애받지 않도록 &lt;code&gt;ModalPortal&lt;/code&gt;을 통해 DOM 계층의 최상단에서 렌더링되도록 했습니다. 이때 부모 요소와의 정렬을 위해 &lt;code&gt;getBoundingClientRect()&lt;/code&gt;를 활용해 동적으로 좌표를 계산합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const updateDropdownPosition = useCallback(() =&amp;gt; {
  if (containerRef.current) {
    const rect = containerRef.current.getBoundingClientRect();
    setDropdownRect({
      top: rect.bottom + window.scrollY,
      left: rect.left + window.scrollX,
      width: rect.width,
    });
  }
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;3. 실무 적용 예시: 정산 파일 업로드 폼&lt;/h3&gt;
&lt;p&gt;구현된 컴포넌트는 &lt;code&gt;CalculateFileUploadForm&lt;/code&gt; 내에서 매체 및 상품 선택 섹션에 적용되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debounce 처리:&lt;/strong&gt; 300ms의 타이머를 설정해 연속적인 입력 시 불필요한 연산을 방지했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 무결성:&lt;/strong&gt; 입력 중에는 &lt;code&gt;onChange(null)&lt;/code&gt;를 호출하여 선택되지 않은 상태를 명확히 전달함으로써 잘못된 데이터가 제출되는 것을 방지했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;4. 마치며&lt;/h3&gt;
&lt;p&gt;컴포넌트 설계 시 가장 중점을 둔 것은 &lt;strong&gt;&amp;quot;확장성과 안정성&amp;quot;&lt;/strong&gt;입니다. 라이브러리의 불투명한 내부 로직에 의존하기보다 직접 제어 가능한 구조를 구축함으로써, 예기치 못한 UI 버그를 사전에 차단하고 일관된 사용자 경험을 제공할 수 있게 되었습니다.&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/151</guid>
      <comments>https://kjk5.tistory.com/151#entry151comment</comments>
      <pubDate>Mon, 9 Feb 2026 10:33:57 +0900</pubDate>
    </item>
    <item>
      <title>react의 실수하기 쉬운 안티패턴과 파훼법</title>
      <link>https://kjk5.tistory.com/150</link>
      <description>&lt;hr&gt;
&lt;h2&gt;1. 상태가 다른 상태를 바꾸는 패턴 (useEffect 안에서 setState)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [a, setA] = useState(0);
const [b, setB] = useState(0);

useEffect(() =&amp;gt; {
  setB(a * 2);
}, [a]);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;문제: 다른 값으로부터 계산 가능한 값을 굳이 상태로 두면 무한 루프 위험 + 중복 관리 필요&lt;/li&gt;
&lt;li&gt;해결: 계산은 상태로 두지 않고 직접 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [a, setA] = useState(0);
const b = a * 2; // 혹은 useMemo&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;2. 상태를 지나치게 잘게 나눈 경우&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [email, setEmail] = useState(&amp;#39;&amp;#39;);
const [isValid, setIsValid] = useState(false);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;문제: 서로 연결된 값이 따로 관리되면 동기화 문제 발생&lt;/li&gt;
&lt;li&gt;해결: 하나의 객체로 묶어서 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [form, setForm] = useState({ email: &amp;#39;&amp;#39;, isValid: false });

const handleChange = (email: string) =&amp;gt; {
  setForm({ email, isValid: validateEmail(email) });
};&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;3. useEffect로 데이터 가공하기&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  const filtered = list.filter(item =&amp;gt; item.active);
  setFilteredList(filtered);
}, [list]);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;문제: 단순히 계산 가능한 값을 effect와 state로 억지로 분리&lt;/li&gt;
&lt;li&gt;해결: 렌더링 과정에서 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const filteredList = useMemo(
  () =&amp;gt; list.filter(item =&amp;gt; item.active),
  [list]
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4. 렌더링 중에 비동기 호출&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const data = fetch(&amp;#39;/api/data&amp;#39;); // ❌ 렌더마다 실행&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;문제: 무한 호출 발생&lt;/li&gt;
&lt;li&gt;해결: useEffect 안에서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  fetch(&amp;#39;/api/data&amp;#39;).then(res =&amp;gt; setData(res));
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5. 조건부로 훅 호출&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;if (isOpen) {
  const [value, setValue] = useState(0); // ❌
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;문제: 훅은 항상 같은 순서로 실행돼야 함&lt;/li&gt;
&lt;li&gt;해결: 훅은 최상단에 두고 조건은 렌더링 단계에서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [value, setValue] = useState(0);
if (!isOpen) return null;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;6. props를 그대로 상태로 복사&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Child({ initialValue }) {
  const [value, setValue] = useState(initialValue); // ❌ 부모 값 변경시 동기화 안 됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;문제: 부모 값이 바뀌어도 동기화 안 됨&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;해결:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;props를 직접 사용&lt;/li&gt;
&lt;li&gt;정말 독립적인 값이어야 한다면 이름을 &lt;code&gt;defaultValue&lt;/code&gt;처럼 명확히&lt;/li&gt;
&lt;li&gt;필요하다면 의도적으로 &lt;code&gt;useEffect&lt;/code&gt;로 갱신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. 불필요한 리렌더&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;원인:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context에 큰 객체를 넣어서 전달&lt;/li&gt;
&lt;li&gt;매번 새로운 함수나 객체를 만들어서 props로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;해결:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;로 최적화&lt;/li&gt;
&lt;li&gt;Context는 작은 단위로 쪼개서 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/150</guid>
      <comments>https://kjk5.tistory.com/150#entry150comment</comments>
      <pubDate>Mon, 25 Aug 2025 14:34:18 +0900</pubDate>
    </item>
    <item>
      <title>[rn] prebuild + fastlane 같이 사용하기</title>
      <link>https://kjk5.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;expo의 장점과 eas 지출회피를 같이하기 위해 prebuild를 사용하고 있었으나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prebuild 시 ios 폴더가 초기화되는 현상 떄문에 fastlane을 적용하기 어려웠다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방법 1. expo config plugin&lt;/li&gt;
&lt;li&gt;방법 2. app.json 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법들은 prebuild시 유지해야할 항목을 보존하는데 도움을 줄 순 있으나 fastlane 관련 파일을 보존하긴 어려워&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prebuild와 복구 명령어를 통합하기로 했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;fastlane 폴더를 루트에서 백업&lt;br /&gt;bash&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/project-root&lt;br /&gt;┣ /fastlane-template/&lt;br /&gt;┃ ┣ Appfile&lt;br /&gt;┃ ┗ Fastfile&lt;br /&gt;2. expo prebuild 이후에 복사&lt;br /&gt;bash&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx expo prebuild --clean&lt;br /&gt;cp -R ../fastlane-template ios/fastlane&lt;br /&gt;prebuild 후 스크립트로 자동 복구 가능&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;//prebuild.sh

npx expo prebuild --clean
echo &quot;  Before fastlane copy: $(pwd)&quot;
cp -R ./fastlane-template/fastlane ./ios/fastlane
echo &quot;✅ fastlane 디렉토리 복원 완료&quot;
cd ios
fastlane beta
echo &quot;✅ iOS 빌드 완료&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754392464328&quot; class=&quot;ruby&quot; data-ke-language=&quot;ruby&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default_platform(:ios)

platform :ios do
  desc &quot;TestFlight 업로드&quot;
  lane :beta do
    # 코드사이닝 설정
    automatic_code_signing(
      use_automatic_signing: true,
      team_id: &quot;H4DJJ5V239&quot;,
      path: &quot;nocotine.xcodeproj&quot;
    )
    
    increment_build_number(
      # xcodeproj: &quot;nocotine.xcodeproj&quot;
      build_number: &quot;6&quot;  # 원하는 번호로 설정
    )

    build_app(
      workspace: &quot;nocotine.xcworkspace&quot;,  # workspace 다시 사용
      scheme: &quot;nocotine&quot;,
      export_method: &quot;app-store&quot;,
      configuration: &quot;Release&quot;,
      clean: true,
      include_bitcode: false
    )

   upload_to_testflight(
      api_key_path: &quot;./fastlane/keyCode.json&quot;, // 본인 키 Json
      skip_waiting_for_build_processing: true
    )3
  end
  
  desc &quot;수동 Archive 테스트&quot;
  lane :archive_only do
    # 코드사이닝 설정
    automatic_code_signing(
      use_automatic_signing: true,
      team_id: &quot;team id &quot;,// 본인 팀아이디
      path: &quot;nocotine.xcodeproj&quot;
    )
    
    increment_build_number(
      xcodeproj: &quot;nocotine.xcodeproj&quot;
    )
    
    gym(
      workspace: &quot;nocotine.xcworkspace&quot;,  # workspace 다시 사용
      scheme: &quot;nocotine&quot;,
      configuration: &quot;Release&quot;,
      clean: true,
      skip_archive: false,
      skip_package_ipa: true,  # IPA 생성 안 함, Archive만
      include_bitcode: false
    )
  end
end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754392620544&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//key.json

{
    &quot;key_id&quot;: &quot;&quot;,
    &quot;issuer_id&quot;: &quot;~~&quot;,// apple connect - 보안 - 통합에서 이슈어id 확인
    &quot;key&quot;: &quot;-----BEGIN PRIVATE KEY-----\n~~~~\n-----END PRIVATE KEY-----&quot;, //각 줄바꿈에 \n 삽입 // 관리자로 키 생성해야함
    &quot;duration&quot;: 1200,
    &quot;in_house&quot;: false
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/149</guid>
      <comments>https://kjk5.tistory.com/149#entry149comment</comments>
      <pubDate>Tue, 5 Aug 2025 20:17:06 +0900</pubDate>
    </item>
    <item>
      <title>expo prebuild로 프로덕션 빌드하기</title>
      <link>https://kjk5.tistory.com/148</link>
      <description>&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;npx&amp;nbsp;expo&amp;nbsp;prebuild&amp;nbsp;--clean&lt;br /&gt;cd&amp;nbsp;ios&lt;br /&gt;pod&amp;nbsp;install&lt;br /&gt;open&amp;nbsp;ios/nocotine.xcworkspace&lt;br /&gt;&lt;br /&gt;왼쪽 상단에서 &quot;nocotine&quot; &amp;rarr; &quot;nocotine&quot; (프로젝트, 파란색 아이콘) 클릭&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;245&quot; data-start=&quot;217&quot;&gt;&lt;b&gt;TARGETS &amp;gt; nocotine&lt;/b&gt; 선택&lt;/li&gt;
&lt;li data-end=&quot;439&quot; data-start=&quot;246&quot;&gt;&lt;b&gt;Signing &amp;amp; Capabilities&lt;/b&gt; 탭에서
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;439&quot; data-start=&quot;285&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;320&quot; data-start=&quot;285&quot;&gt;&lt;b&gt;Team&lt;/b&gt;: Apple Developer 계정 선택&lt;/li&gt;
&lt;li data-end=&quot;380&quot; data-start=&quot;324&quot;&gt;&lt;b&gt;Bundle Identifier&lt;/b&gt;: com.jaegwan.nocotine 맞는지 확인&lt;/li&gt;
&lt;li data-end=&quot;439&quot; data-start=&quot;384&quot;&gt;&lt;b&gt;Provisioning Profile&lt;/b&gt;: 자동으로 맞춰주거나, 직접 선택(Ad Hoc 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;559&quot; data-start=&quot;440&quot;&gt;&lt;b&gt;Build Settings &amp;gt; Versioning&lt;/b&gt;에서
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;559&quot; data-start=&quot;482&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;524&quot; data-start=&quot;482&quot;&gt;&lt;b&gt;Current Project Version&lt;/b&gt;: 빌드 넘버(예: 3)&lt;/li&gt;
&lt;li data-end=&quot;559&quot; data-start=&quot;528&quot;&gt;&lt;b&gt;Version&lt;/b&gt;: 마케팅 버전(예: 1.0.2)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;803&quot; data-start=&quot;665&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;758&quot; data-start=&quot;665&quot;&gt;&lt;b&gt;상단 Scheme에서 &quot;Any iOS Device (arm64)&quot; 선택&lt;/b&gt;&lt;br /&gt;(시뮬레이터 아님! &quot;Generic iOS Device&quot; 또는 실제 기기)&lt;/li&gt;
&lt;li data-end=&quot;803&quot; data-start=&quot;759&quot;&gt;&lt;b&gt;상단 메뉴&lt;/b&gt;에서
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;803&quot; data-start=&quot;779&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;803&quot; data-start=&quot;779&quot;&gt;Product &amp;gt; Archive 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;839&quot; data-start=&quot;805&quot; data-ke-size=&quot;size16&quot;&gt;빌드가 끝나면 &lt;b&gt;Organizer 창&lt;/b&gt;이 자동으로 뜸&lt;br /&gt;&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 data-end=&quot;915&quot; data-start=&quot;880&quot;&gt;&lt;b&gt;Organizer 창&lt;/b&gt;에서 방금 빌드한 아카이브 선택&lt;/li&gt;
&lt;li data-end=&quot;943&quot; data-start=&quot;916&quot;&gt;&lt;b&gt;Distribute App&lt;/b&gt; 버튼 클릭&lt;/li&gt;
&lt;li data-end=&quot;1114&quot; data-start=&quot;944&quot;&gt;배포 방식 선택:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1114&quot; data-start=&quot;960&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1015&quot; data-start=&quot;960&quot;&gt;&lt;b&gt;App Store Connect&lt;/b&gt;: 앱스토어 업로드(Transporter 없이 직접 가능)&lt;/li&gt;
&lt;li data-end=&quot;1054&quot; data-start=&quot;1019&quot;&gt;&lt;b&gt;Ad Hoc&lt;/b&gt;: 테스트 기기에 직접 설치용 ipa 생성&lt;/li&gt;
&lt;li data-end=&quot;1086&quot; data-start=&quot;1058&quot;&gt;&lt;b&gt;Development&lt;/b&gt;: 개발자 디바이스용&lt;/li&gt;
&lt;li data-end=&quot;1114&quot; data-start=&quot;1090&quot;&gt;&lt;b&gt;Enterprise&lt;/b&gt;: 기업 배포용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1149&quot; data-start=&quot;1115&quot;&gt;&lt;b&gt;배포 타입 선택 후, Xcode 안내에 따라 진행&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1204&quot; data-start=&quot;1150&quot;&gt;마지막 단계에서 &lt;b&gt;Export&lt;/b&gt;하면&lt;br /&gt;&amp;rarr; .ipa 파일이 원하는 경로에 생성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/148</guid>
      <comments>https://kjk5.tistory.com/148#entry148comment</comments>
      <pubDate>Fri, 25 Jul 2025 12:25:36 +0900</pubDate>
    </item>
    <item>
      <title>expo - ios 빌드 및 출시</title>
      <link>https://kjk5.tistory.com/147</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;0. eas build는 github repo를 기반으로 빌드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.json 파일 점검하여&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ios:{buildNumber} 등 체크&lt;br /&gt;1. ipa 생성&lt;/p&gt;
&lt;pre id=&quot;code_1753344517501&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g eas-cli

eas login

eas build:configure # (최초 1회)

eas build -p ios --profile production # 필요시 --clear-cache&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;br /&gt;&lt;br /&gt;빌드가 완료되면 EAS 에 접속 해 ipa 를 내려받는다.&lt;br /&gt;&lt;br /&gt;2. apple connect에 전송&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/149Um/btsPxYyxxAc/oFUY8izV2eXhcteP2hltH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/149Um/btsPxYyxxAc/oFUY8izV2eXhcteP2hltH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/149Um/btsPxYyxxAc/oFUY8izV2eXhcteP2hltH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F149Um%2FbtsPxYyxxAc%2FoFUY8izV2eXhcteP2hltH1%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;1352&quot; height=&quot;876&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;876&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;3. 심사 제출&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/147</guid>
      <comments>https://kjk5.tistory.com/147#entry147comment</comments>
      <pubDate>Fri, 25 Jul 2025 00:09:28 +0900</pubDate>
    </item>
    <item>
      <title>Type Guard와 every 메서드의 한계</title>
      <link>https://kjk5.tistory.com/129</link>
      <description>&lt;h2&gt;문제 상황: Type Guard와 &lt;code&gt;every&lt;/code&gt; 메서드의 한계&lt;/h2&gt;
&lt;p&gt;TypeScript는 코드의 타입 안전성을 보장하기 위해 컴파일 시점에 타입을 검사한다. 이 과정에서 &lt;strong&gt;타입 가드&lt;/strong&gt;와 &lt;strong&gt;타입 단언&lt;/strong&gt;을 활용해 컴파일러가 타입을 정확하게 추론하도록 돕는다. 하지만 때로는 타입 가드를 사용했음에도 TypeScript가 배열의 타입을 확신하지 못하는 경우가 생긴다.&lt;/p&gt;
&lt;h3&gt;예시 코드&lt;/h3&gt;
&lt;p&gt;다음 예제에서 &lt;code&gt;result&lt;/code&gt; 배열이 &lt;code&gt;FileSystemFileEntry&lt;/code&gt; 또는 &lt;code&gt;FileSystemDirectoryEntry&lt;/code&gt;로만 구성되어 있는지를 &lt;code&gt;every&lt;/code&gt; 메서드를 통해 확인하고자 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (
    result.length &amp;gt; 0 &amp;amp;&amp;amp;
    result.every(entry =&amp;gt; isFileSystemFileEntry(entry) || isFileSystemDirectoryEntry(entry))
) {
    resolve(result); // 오류 발생: TypeScript는 여전히 result의 타입을 알 수 없음
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기서 TypeScript는 &lt;code&gt;result&lt;/code&gt;가 &lt;code&gt;(FileSystemFileEntry | FileSystemDirectoryEntry)[]&lt;/code&gt; 타입임을 보장하지 않는다. 비록 &lt;code&gt;every&lt;/code&gt;로 각 요소의 타입을 확인했지만, &lt;strong&gt;컴파일러는 여전히 &lt;code&gt;result&lt;/code&gt; 배열 전체가 안전한 타입인지 확신하지 못한다&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;왜 발생하는가?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;every&lt;/code&gt; 메서드는 배열의 모든 요소가 조건을 만족하는지 여부만을 반환한다. 즉, &lt;code&gt;true&lt;/code&gt; 또는 &lt;code&gt;false&lt;/code&gt; 값을 반환할 뿐이며, &lt;strong&gt;이 정보만으로는 컴파일러가 &lt;code&gt;result&lt;/code&gt; 전체 배열의 타입을 자동으로 강제할 수 없다&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;타입 추론의 한계&lt;/h3&gt;
&lt;p&gt;타입 가드가 특정 요소의 타입을 좁히는 데는 효과적이지만, &lt;strong&gt;&lt;code&gt;every&lt;/code&gt;와 같은 메서드는 배열 자체의 타입을 좁히지 않는다&lt;/strong&gt;. TypeScript는 &lt;code&gt;every&lt;/code&gt;의 결과가 &lt;code&gt;true&lt;/code&gt;일 때, 개별 요소들이 특정 타입임을 알 수 있을지라도 배열 전체의 타입은 추론하지 않는다. 예를 들어, &lt;code&gt;result&lt;/code&gt;가 &lt;code&gt;FileSystemEntry[]&lt;/code&gt; 타입으로 선언되어 있다면 &lt;code&gt;every&lt;/code&gt; 이후에도 여전히 &lt;code&gt;FileSystemEntry[]&lt;/code&gt; 타입으로 인식하게 된다.&lt;/p&gt;
&lt;p&gt;TypeScript 컴파일러가 불완전하다고 느낄 수도 있지만, 사실 TypeScript는 의도적으로 배열의 전체 타입을 자동으로 추론하지 않도록 설계된 것이다. 그 이유는 TypeScript의 타입 시스템이 정적 타입 검사를 수행하기 때문이다. 즉, 컴파일 시점에만 타입을 검사하며, 런타임에서의 변화는 감지하지 않는다.&lt;/p&gt;
&lt;p&gt;왜 TypeScript는 every의 결과로 타입을 강제하지 않는가?&lt;br&gt;every 메서드는 배열의 모든 요소가 특정 조건을 만족하는지를 검사하지만, 이때 반환되는 값은 오직 true나 false이다. 이렇듯 every는 단순히 배열이 조건을 만족하는지 아닌지를 알려주는 용도로 사용된다. TypeScript는 런타임에서의 조건을 기반으로 배열의 전체 타입을 좁히는 작업을 하지 않는다.&lt;/p&gt;
&lt;p&gt;예를 들어, TypeScript가 result.every(...)의 결과가 true라고 해서, result의 타입을 자동으로 (FileSystemFileEntry | FileSystemDirectoryEntry)[]로 확신한다고 가정해보자. 만약 배열이 변경되거나, 이후 코드에서 다른 타입의 요소가 추가된다면, TypeScript는 컴파일 시점에서 이 변경을 미리 감지할 수 없게 된다. 따라서 컴파일 시점의 타입 검사에서는 every의 검사 결과로 배열 전체 타입을 확신하지 않도록 하고, 대신 개발자가 타입을 직접 좁히거나 명시적으로 지정하도록 하는 것이다.&lt;/p&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;every&lt;/code&gt;를 사용할 때 TypeScript가 배열의 타입을 확신하게 하려면, &lt;strong&gt;타입 가드를 통해 배열을 다시 선언&lt;/strong&gt;해줄 필요가 있다. 이를 해결하는 방법은 크게 두 가지로 나뉜다.&lt;/p&gt;
&lt;h3&gt;1. 타입 단언 사용&lt;/h3&gt;
&lt;p&gt;타입 단언을 사용하여 TypeScript에게 &lt;code&gt;result&lt;/code&gt;가 안전한 타입임을 명시할 수 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resolve(result as (FileSystemFileEntry | FileSystemDirectoryEntry)[]);&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. &lt;code&gt;filter&lt;/code&gt;와 타입 가드 조합 사용&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;filter&lt;/code&gt;와 타입 가드를 조합하여 컴파일러가 배열의 타입을 안전하게 추론하도록 만들 수 있다. 이를 통해 타입 단언을 피하면서도 원하는 타입으로 제한할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const validEntries = result.filter(
    (entry): entry is FileSystemFileEntry | FileSystemDirectoryEntry =&amp;gt;
        isFileSystemFileEntry(entry) || isFileSystemDirectoryEntry(entry)
);

resolve(validEntries); // TypeScript는 validEntries가 (FileSystemFileEntry | FileSystemDirectoryEntry)[] 타입임을 확신한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;여기서 &lt;code&gt;filter&lt;/code&gt; 메서드는 새로운 배열을 반환하며, 이 배열이 &lt;code&gt;FileSystemFileEntry&lt;/code&gt;와 &lt;code&gt;FileSystemDirectoryEntry&lt;/code&gt;만 포함하고 있다고 TypeScript가 확신할 수 있도록 돕는다. 따라서 타입 단언 없이도 배열의 타입을 안전하게 추론할 수 있다.&lt;/p&gt;
&lt;h2&gt;결론&lt;/h2&gt;
&lt;p&gt;TypeScript는 개별 요소에 대한 타입 가드가 사용되더라도 배열 전체 타입을 자동으로 강제하지는 않는다. 이런 경우 &lt;code&gt;filter&lt;/code&gt;와 타입 가드를 조합하여 컴파일러가 타입을 추론하도록 돕거나, 타입 단언을 통해 안전성을 명시해줄 수 있다. 이러한 이해를 통해 TypeScript의 타입 시스템을 더욱 효과적으로 활용할 수 있을 것이다.&lt;/p&gt;</description>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/129</guid>
      <comments>https://kjk5.tistory.com/129#entry129comment</comments>
      <pubDate>Mon, 14 Oct 2024 11:45:18 +0900</pubDate>
    </item>
    <item>
      <title>타입스크립트에서 배열 요소 타입 가드: 왜 작동하지 않는가?</title>
      <link>https://kjk5.tistory.com/128</link>
      <description>&lt;h1&gt;타입스크립트에서 배열 요소 타입 가드: 왜 작동하지 않는가?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 정적 타입 기반의 언어로, 타입을 명확히 정의하고 코드의 안정성을 높이는 데 중요한 역할을 한다. 그중에서도 &lt;b&gt;타입 가드(Type Guard)&lt;/b&gt;는 변수가 특정 조건을 만족할 때 해당 변수의 타입을 좁히는 기능을 담당한다. 하지만 &lt;b&gt;배열 요소&lt;/b&gt;에 타입 가드를 적용할 때는 예상치 못한 오류가 발생할 수 있다. 이번 글에서는 &lt;b&gt;배열 요소에 타입 가드가 제대로 작동하지 않는 이유&lt;/b&gt;와 &lt;b&gt;이를 해결하는 방법&lt;/b&gt;을 살펴보겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타입 가드란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 가드는 변수의 타입을 안전하게 좁히기 위해 사용하는 조건문이다. 대표적인 예로 &lt;code&gt;typeof&lt;/code&gt;, &lt;code&gt;instanceof&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt; 연산자가 있다. 이들은 특정 조건을 만족할 때 타입스크립트가 변수를 더욱 구체적인 타입으로 추론하도록 돕는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입 가드 예시&lt;/h3&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;function isString(value: any): value is string {
    return typeof value === &quot;string&quot;;
}

function printLength(value: any) {
    if (isString(value)) {
        // 타입스크립트는 여기서 value가 string임을 알 수 있다.
        console.log(value.length);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시에서 &lt;code&gt;isString&lt;/code&gt; 함수는 &lt;code&gt;value&lt;/code&gt;가 문자열(&lt;code&gt;string&lt;/code&gt;)인지 확인하는 타입 가드다. 이 타입 가드를 통해 조건문 안에서는 &lt;code&gt;value&lt;/code&gt;가 &lt;code&gt;string&lt;/code&gt; 타입으로 안전하게 좁혀져 사용될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 요소에 타입 가드를 적용할 때의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 요소를 다룰 때는 &lt;b&gt;배열의 동적 특성&lt;/b&gt; 때문에 타입 가드가 예상대로 작동하지 않는 경우가 발생한다. 예를 들어 배열에서 특정 요소를 인덱스로 접근할 때 타입스크립트는 해당 요소의 타입을 &lt;b&gt;완전히 좁히지 못하는 경우&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황 예시&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const fileEntries: FileSystemEntry[] = getFileEntries(); // 여러 FileSystemEntry 객체가 들어있는 배열
if (isFileSystemFileEntry(fileEntries[i])) {
    fileList.dcmFiles.push(fileEntries[i]);  // 오류 발생
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;isFileSystemFileEntry&lt;/code&gt; 타입 가드는 배열 요소가 &lt;code&gt;FileSystemFileEntry&lt;/code&gt;인지 확인해준다. 그러나 타입스크립트는 &lt;code&gt;fileEntries[i]&lt;/code&gt;의 타입을 &lt;code&gt;FileSystemFileEntry&lt;/code&gt;로 확실하게 좁히지 못하고, 여전히 &lt;code&gt;FileSystemEntry&lt;/code&gt; 타입으로 취급할 수 있다. 이로 인해 &quot;fileEntries[i]&quot;를 &lt;code&gt;FileSystemFileEntry&lt;/code&gt;에 할당할 수 없다는 오류가 발생하게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입스크립트가 배열 요소를 좁히지 못하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제가 발생하는 주된 이유는 배열이 &lt;b&gt;동적 데이터 구조&lt;/b&gt;이기 때문이다. 배열 요소는 인덱스로 접근할 수 있으며, 이때 &lt;b&gt;배열의 길이가 동적으로 변할 수 있다&lt;/b&gt;고 타입스크립트는 가정한다. 즉, 배열에서 특정 인덱스를 통해 접근하는 요소가 언제든 바뀔 수 있기 때문에, 타입스크립트는 타입을 안전하게 좁히는 데 주저하게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법: 배열 요소를 변수에 할당하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위한 가장 간단한 방법은 배열 요소를 &lt;b&gt;변수에 할당한 후&lt;/b&gt; 타입 가드를 적용하는 것이다. 이를 통해 타입스크립트는 해당 변수를 &lt;b&gt;정적으로 처리&lt;/b&gt;할 수 있게 되며, 타입을 안전하게 좁히게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결된 코드 예시&lt;/h3&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;const fileEntry = fileEntries[i]; // 배열 요소를 먼저 변수에 할당
if (isFileSystemFileEntry(fileEntry)) {
    fileList.dcmFiles.push(fileEntry);  // 문제 해결
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 배열 요소를 변수에 할당하면, 타입스크립트는 &lt;code&gt;fileEntry&lt;/code&gt;를 &lt;code&gt;FileSystemFileEntry&lt;/code&gt;로 좁히는 것을 확실하게 할 수 있다. 이로 인해 더 이상 타입 관련 오류가 발생하지 않으며, 타입스크립트는 안전하게 해당 요소를 &lt;code&gt;fileList.dcmFiles&lt;/code&gt;에 추가할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트에서 배열 요소를 다룰 때 타입 가드가 예상대로 작동하지 않는 이유는 &lt;b&gt;배열의 동적 특성&lt;/b&gt; 때문이다. 배열의 요소는 언제든 바뀔 수 있는 가능성이 있기 때문에, 타입스크립트는 타입을 안전하게 좁히는 것을 주저할 수 있다. 이러한 문제를 해결하려면 &lt;b&gt;배열 요소를 변수에 먼저 할당한 후&lt;/b&gt; 타입 가드를 적용하는 방법이 효과적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 적용하면 배열 요소에 타입 가드를 성공적으로 사용할 수 있으며, 코드의 안정성과 가독성을 동시에 높일 수 있다.&lt;/p&gt;</description>
      <author>jaegwan</author>
      <guid isPermaLink="true">https://kjk5.tistory.com/128</guid>
      <comments>https://kjk5.tistory.com/128#entry128comment</comments>
      <pubDate>Mon, 30 Sep 2024 19:33:55 +0900</pubDate>
    </item>
  </channel>
</rss>