<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tkddn_dev.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 26 Mar 2026 07:43:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tkddn_dev.log</title>
            <url>https://velog.velcdn.com/images/tkddn_dev8430/profile/252c722b-81cc-4ae3-acfb-561e11bc6aa3/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tkddn_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tkddn_dev8430" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js <Image />는 실제로 무엇을 최적화할까]]></title>
            <link>https://velog.io/@tkddn_dev8430/Next.js-Image-%EB%8A%94-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@tkddn_dev8430/Next.js-Image-%EB%8A%94-%EC%8B%A4%EC%A0%9C%EB%A1%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 26 Mar 2026 07:43:07 GMT</pubDate>
            <description><![CDATA[<p>이전 업무에서 일부 페이지를 Next.js 기반으로 마이그레이션하면서, 기존에 사용하던 <code>&lt;img&gt;</code> 태그를 <code>&lt;Image /&gt;</code> 컴포넌트로 교체한 경험이 있었다.</p>
<p>Next.js는 이미지 처리 과정에서 기본적으로 몇 가지 최적화 기능을 제공한다.</p>
<ul>
<li>Lazy Loading</li>
<li>포맷 변환 (원본 → WebP / AVIF)</li>
<li>Responsive Image 생성</li>
</ul>
<p>마이그레이션 당시에는 이미지가 화면에서 언제 노출되는지를 기준으로 우선순위를 다르게 적용했다.</p>
<p>첫 화면에 바로 보이는 배너 이미지는 <code>priority</code>를 사용해 먼저 요청되도록 했고, 리스트 내부 썸네일은 기본 lazy loading 동작을 유지해 초기 렌더링 비용을 줄이고자 했다.</p>
<p>이번에는 Next.js가 제공하는 이미지 최적화 기능을 하나씩 정리하면서, 어떤 요소가 실제 체감 성능에 가장 큰 영향을 주는지도 함께 확인해보려고 한다.</p>
<br/>

<h2 id="성능-비교해보기">성능 비교해보기</h2>
<p>총 4가지 경우를 두고, 같은 이미지를 렌더링이 어떻게 이뤄지고 있는지 확인해보았다.
앞으로 정리할 테스트는 아래 링크를 통해서 직접 볼 수 있다.
<a href="https://test-image-improvement.vercel.app/">https://test-image-improvement.vercel.app/</a></p>
<h3 id="a-기본-img">A. 기본 <code>&lt;img/&gt;</code></h3>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/12765085-23c2-48ca-9a5e-6ffe4287d0de/image.png' width='1000px'/>
</div>

<ul>
<li>viewport 포함 여부와 관계없이 모든 이미지 리소스가 초기 렌더링 시점에 동시에 요청된다.</li>
<li>이미지의 타입이 jpeg로 원본 이미지 포멧을 따라간다.</li>
</ul>
<br/>

<h3 id="b-lazyloading-적용">B. LazyLoading 적용</h3>
<p>A 와 동일한 리소스를 활용하고 대신 img 태그의 <code>loading</code> 속성을 <code>lazy</code> 로 설정하여 Lazy Loading 되도록 설정한 후 다시 확인해보았다.</p>
<pre><code class="language-tsx">&lt;img src=&quot;image.jpg&quot; alt=&quot;...&quot; loading=&quot;lazy&quot; /&gt;
&lt;iframe src=&quot;video-player.html&quot; title=&quot;...&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;</code></pre>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/eb047a62-b89b-4c0f-a003-3ed14f2503b2/image.gif' width='1000px'/>
</div>

<p>초기에는 렌더링시에 바로 노출되는 이미지들만 로드한다. 이후 스크롤을 내려 하단에 위치한 이미지 리소스가 필요한 경우에 로드되는 것을 확인할 수 있다.</p>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/c159a125-1eba-4e41-92c7-e12c417842dc/image.png' width='1000px'/>
</div>

<ul>
<li><p>초기 리소스 로드, 이후 Lazy Load 되는 리소스가 로드되는 두가지 시점이 타임라인에 찍혀있는 것을 볼 수 있다.</p>
</li>
<li><p>타입은 여전히 jpeg 원본을 활용하고 있다.</p>
  <aside>

<p>  여기서 /redirect로 찍힌 요청의 경우 같은 이미지로 테스트를 하고 있기 떄문에 같은 이미지라도 서로 다른 요청으로 인식 시키기 위해서 들어간 실험적인 요소다.</p>
  </aside>


</li>
</ul>
<br/>

<h3 id="c-nextjs-image-컴포넌트-기본-활용">C. Next.js Image 컴포넌트 기본 활용</h3>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/246844f0-a76e-414b-b1ab-37b2742827de/image.png' width='1000px'/>
</div>

<p><img src="" alt=""></p>
<ul>
<li>현재 테스트에서는 viewport 안에 있는 이미지들이 먼저 요청되는 모습을 볼 수 있었다.</li>
<li>이미지 타입이 jpeg 원본 이미지 포멧에서 avif 이미지 포멧으로 변경되었다.</li>
<li><code>&lt;Image /&gt;</code> 역시 기본적으로 lazy loading이 적용되며, viewport 밖 이미지는 즉시 요청되지 않는다.</li>
</ul>
<p>Next.js 의 <code>&lt;Image /&gt;</code>는 원본 파일 자체를 직접 변경하는 방식이 아니라, 요청 시점에 최적화된 이미지를 별도 endpoint를 통해 전달하는 구조로 동작한다.</p>
<p>실제 Network 탭을 보면 원본 이미지 URL이 그대로 사용되지 않고, <code>_next/image</code> 경로를 통해 다시 요청되는 것을 확인할 수 있다.</p>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/c121bf7a-8da1-41de-a12c-957c03339da9/image.png' width='1000px'/>
</div>

<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/659077ba-ce8b-4776-956b-a2f40bdd219c/image.png' width='1000px'/>
</div>

<p>w(width), q(quality) 같은 쿼리도 추가되어서 이미지 최적화가 적용된 것을 볼 수 있다.</p>
<br/>

<h3 id="d-nextjs-image-컴포넌트의-priority-활용">D. Next.js Image 컴포넌트의 priority 활용</h3>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/a9d21f7a-72db-4884-8fc4-59a2000d7849/image.png' width='1000px'/>
</div>

<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/9dc95909-c17d-4c2c-b2bc-eeb854e4d129/image.png' width='1000px'/>
</div>

<ul>
<li><code>priority</code>가 적용된 이미지는 lazy loading이 비활성화되고 preload 대상으로 먼저 요청된다.</li>
<li>이미지 포맷이나 품질 자체는 달라지지 않지만, 초기 렌더링 시점에 요청되는 리소스가 앞당겨진다. 상단 이미지(C)와 비교했을 때 이미지 사이즈가 커진 것을 볼 수 있었다.</li>
<li>preload 시점과 선택된 width 후보에 따라 일부 이미지의 초기 전송량이 더 크게 나타날 수 있었고, 실제로는 LCP도 오히려 길어지는 결과가 나타났다.<br/>

</li>
</ul>
<h3 id="e-nextjs-image-컴포넌트의-size-적용해주기">E. Next.js Image 컴포넌트의 size 적용해주기</h3>
<pre><code class="language-TypeScript">&lt;Image
  src={...}
  sizes=&quot;(max-width: 768px) 100vw, 33vw&quot;
/&gt;</code></pre>
<div align='center'>
  <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/828ee434-da9c-40b5-9ba1-a8b279d06892/image.png' width='1000px'/>
</div>

<div align='center'>
  <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/d1913193-dda4-4f4b-a36f-20344b3c2e10/image.png' width='1000px'/>
</div>
- C케이스와 비교했을 때, 요청 수는 동일했지만 전송량이 크게 감소했다.
- sizes를 지정하면 브라우저가 srcset 후보 중 현재 레이아웃에 더 적절한 크기를 선택할 수 있다.
- 같은 이미지라도 더 작은 width 후보를 선택하면서 초기 전송 비용 차이가 크게 나타났다.

<br />

<h2 id="정리">정리</h2>
<p>이번 최적화 케이스들을 비교하면서 Next.js 공식 문서도 함께 살펴보았는데, 이번에 확인한 옵션 외에도 이미지 품질, 크기, placeholder 등 다양한 설정이 제공되고 있었다.</p>
<table>
<thead>
<tr>
<th>케이스</th>
<th>초기 요청 수</th>
<th>전송량</th>
<th>LCP</th>
</tr>
</thead>
<tbody><tr>
<td>A. <code>&lt;img&gt;</code> eager</td>
<td>24개</td>
<td>4,588 kB</td>
<td>28.47s</td>
</tr>
<tr>
<td>B. <code>&lt;img&gt;</code> lazy</td>
<td>8개</td>
<td>1,827 kB</td>
<td>13.06s</td>
</tr>
<tr>
<td>C. <code>&lt;Image /&gt;</code></td>
<td>8개</td>
<td>1,198 kB</td>
<td>8.44s</td>
</tr>
<tr>
<td>D. <code>&lt;Image priority /&gt;</code></td>
<td>8개</td>
<td>1,198 kB</td>
<td>11.33s</td>
</tr>
<tr>
<td>E. <code>&lt;Image + sizes /&gt;</code></td>
<td>8개</td>
<td>378 kB</td>
<td>3.3s</td>
</tr>
</tbody></table>
<p>가장 먼저 체감되는 차이는 lazy loading 여부에서 나타났고, 가장 큰 전송량 감소는 sizes 설정에서 확인할 수 있었다.</p>
<p><code>&lt;img&gt;</code> 태그에서도 loading=&quot;lazy&quot;만으로 초기 요청 수를 줄일 수 있었지만, <code>&lt;Image /&gt;</code>는 여기에 포맷 변환과 <code>_next/image</code> 기반 최적화 경로까지 함께 제공한다는 점에서 차이가 있었다.</p>
<p>반면 priority는 첫 화면에서 반드시 빠르게 보여야 하는 이미지에는 유효했지만, 여러 이미지에 동시에 적용할 경우 기대한 만큼 성능이 개선되지 않을 수도 있었다.
특히 이번 비교에서는 sizes 없이 priority만 적용했을 때 LCP가 오히려 길어지는 결과도 확인할 수 있었다.</p>
<p>결국 <code>&lt;Image /&gt;</code>는 단순히 태그를 교체하는 것보다, 어떤 이미지를 어떤 크기로 먼저 보여줄지까지 함께 설계해야 효과가 커지는 기능에 가까웠다.
특히 sizes를 함께 지정했을 때 responsive image 최적화 효과가 가장 분명하게 나타나는 것을 확인할 수 있었다.</p>
<h4 id="무조건-image-컴포넌트가-좋은가">무조건 Image 컴포넌트가 좋은가..?</h4>
<p>물론 모든 이미지에 <code>&lt;Image /&gt;</code>가 필요한 것은 아니다. 이미 충분히 작은 리소스나 SVG처럼 포맷 최적화 이득이 크지 않은 경우에는 기본 <code>&lt;img&gt;</code> 태그가 더 단순한 선택이 될 수 있다.
특히 반복적으로 사용하는 정적 이미지라면, 처음부터 WebP 같은 가벼운 포맷으로 준비해 <code>&lt;img&gt;</code> 태그로 직접 사용하는 편이 오히려 불필요한 최적화 단계를 줄이는 방법이 될 수 있다.</p>
<br/>

<h2 id="마치며">마치며</h2>
<p>이번 내용을 정리하면서 lazy loading과 priority를 적용할 때의 판단이 크게 잘못되지 않았다는 점을 다시 확인할 수 있었다 😩</p>
<p>특히 Network 타임라인으로 리소스 요청 시점을 직접 비교해보니, 어떤 최적화가 실제로 먼저 체감되는지 더 직관적으로 이해할 수 있었다.</p>
<p>또한, <code>&lt;Image /&gt;</code>를 사용하는 경우뿐 아니라, 기본 <code>&lt;img&gt;</code> 태그를 선택하는 기준도 함께 정리할 수 있었다.</p>
<br/>

<h2 id="reference">Reference</h2>
<ul>
<li><a href="https://nextjs.org/docs/app/api-reference/components/image?utm_source=chatgpt.com">Next.js Image Component 공식 문서</a></li>
<li><a href="https://vercel.com/academy/nextjs-foundations/advanced-image-optimization?utm_source=chatgpt.com">Next.js Image Optimization 가이드 (Vercel Academy)</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading?utm_source=chatgpt.com">MDN Lazy Loading 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TanStack Query vs SWR — 어떤 기준으로 선택할까]]></title>
            <link>https://velog.io/@tkddn_dev8430/TanStack-Query-vs-SWR-%EC%96%B4%EB%96%A4-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%EC%84%A0%ED%83%9D%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@tkddn_dev8430/TanStack-Query-vs-SWR-%EC%96%B4%EB%96%A4-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%EC%84%A0%ED%83%9D%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 16 Mar 2026 17:15:58 GMT</pubDate>
            <description><![CDATA[<p>서버 상태라는 개념이 주목받기 시작하면서 TanStack Query에 대한 관심도 자연스럽게 높아졌다. 나 역시 여러 프로젝트에서 TanStack Query를 활용해왔는데, 돌아보면 &quot;왜 TanStack Query인가?&quot;라는 질문을 스스로에게 제대로 던진 적이 없었다. 그냥 레퍼런스가 많고, 팀원들이 익숙하고, 기능이 많으니까 — 라는 이유로 선택해왔던 것 같다. </p>
<p>비슷한 문제를 푸는 SWR이라는 라이브러리가 있다는 건 알고 있었다. 하지만 &quot;TanStack Query가 더 좋다&quot;는 막연한 인식만 있었을 뿐, 실제로 어떻게 다른지, 혹은 SWR이 더 나은 선택이었을 상황이 있었는지는 따져본 적이 없었다.</p>
<p>그래서 두 라이브러리를 여러가지에서 알아보고 앞으로 의식적으로 기술 스택을 선택하기 위한 기준을 마련하기 위해서 정리해보았다.</p>
<h2 id="철학">철학</h2>
<h3 id="tanstack-query">TanStack Query</h3>
<ul>
<li>기존에 서버 데이터를 다루는 방식은 <code>state</code>와 <code>useEffect</code>의 조합이었다. 일반적인 클라이언트 상태를 표현하고 관리하는 데는 효과적이지만, 비동기 상태 즉 서버 상태를 관리하기에는 어색한 부분이 있었다.</li>
<li>TanStack Query는 이 문제를 해결하기 위해 <strong>&#39;서버 상태 라이브러리&#39;</strong> 로 정의한다. 서버와 클라이언트 사이의 비동기 작업을 관리하는 역할을 담당하며, Redux나 Zustand 같은 클라이언트 상태 라이브러리와는 명확히 역할을 구분한다.</li>
<li>그리고 캐싱, 백그라운드 업데이트, Mutation, DevTools까지 — 비동기 데이터와 관련한 복잡한 상황을 처음부터 고려하여 설계된 라이브러리다.</li>
</ul>
<h3 id="swr">SWR</h3>
<ul>
<li>반면 SWR은 데이터 캐싱에 초점을 맞춘 라이브러리다.</li>
<li>SWR은 캐시에서 먼저 데이터를 반환하고(stale) 이후 요청을 보내 최신 데이터로 업데이트하는(revalidate) 플로우를 단 하나의 훅으로 단순화한다.</li>
<li>Vercel에서 만든 라이브러리인 만큼, Next.js와의 궁합이 좋다는 제품적인 특징도 존재한다.</li>
</ul>
<h2 id="핵심-기능-비교">핵심 기능 비교</h2>
<h3 id="21-캐시-키-설계와-무효화-전략">2.1 캐시 키 설계와 무효화 전략</h3>
<p>캐싱은 두 라이브러리 모두 지원하지만, <strong>어떻게 캐시를 식별하고 무효화하는지</strong>에서 차이가 난다.</p>
<p>SWR은 캐시 키로 <strong>문자열</strong>을 사용한다. </p>
<p>보통 fetch URL이 그대로 키가 되기 때문에 직관적이고 간단하다.</p>
<pre><code class="language-tsx">// SWR — 문자열 기반 캐시 키
useSWR(&#39;/api/todos?status=done&#39;, fetcher)
useSWR(&#39;/api/todos?status=pending&#39;, fetcher)

// todos 관련 캐시 전체 무효화 → 패턴 매칭을 직접 구현해야 함
// 문자열 관련 매칭 로직을 많이 사용
mutate(key =&gt; typeof key === &#39;string&#39; &amp;&amp; key.startsWith(&#39;/api/todos&#39;), undefined, { revalidate: true })</code></pre>
<p>반면 TanStack Query는 캐시 키로 <strong>배열</strong>을 사용한다. 배열을 사용하면 계층적인 키를 만들 수 있어 캐시 무효화가 훨씬 강력해진다.</p>
<p>실제 앱에서는 &quot;유저 정보를 수정했으니 유저 관련 캐시를 전부 날려야 하는&quot; 상황이 자주 생긴다. TanStack Query의 계층적 키 설계는 이런 케이스를 훨씬 명확하게 처리할 수 있다.</p>
<pre><code class="language-tsx">// TanStack Query — 배열 기반 캐시 키
useQuery({ queryKey: [&#39;todos&#39;, { status: &#39;done&#39; }],    queryFn: fetchTodos })
useQuery({ queryKey: [&#39;todos&#39;, { status: &#39;pending&#39; }], queryFn: fetchTodos })

// todos 관련 캐시 전체 무효화 → prefix 하나로 끝
queryClient.invalidateQueries({ queryKey: [&#39;todos&#39;] })</code></pre>
<p>또한 TanStack Query는 데이터의 신선도를 <strong><code>staleTime</code></strong>과 <strong><code>gcTime</code></strong> 두 개의 개념으로 분리해서 관리한다.</p>
<pre><code class="language-tsx">useQuery({
  queryKey: [&#39;user&#39;],
  queryFn: fetchUser,
  staleTime: 1000 * 60 * 5,  // 5분간 fresh → 이 안에선 refetch 안 함
  gcTime:    1000 * 60 * 10, // 10분 후 메모리에서 제거
})</code></pre>
<p><strong><code>staleTime</code></strong>- 가져온 데이터가 얼마나 오래 &quot;신선한&quot; 상태로 유지되는지를 결정한다. 이 기간 동안에는 컴포넌트가 다시 마운트되거나 refetch가 트리거되더라도 TanStack Query는 네트워크 요청을 보내지 않는다.</p>
<p><strong><code>gcTime</code></strong>- 비활성 캐시가 메모리에서 제거되기까지의 시간을 제어한다. 반면 SWR은 <code>staleTime</code>이나 조건부 자동 재검증 개념이 없다.</p>
<p>캐싱된 데이터의 refetching 시점을 구체적으로 제어하고 싶다면 TanStack Query가 더 적합하다.</p>
<aside>

<h3 id="💡-캐시-키는-어떻게-비교-되는거지">💡 캐시 키는 어떻게 비교 되는거지?</h3>
<p><strong>SWR</strong></p>
<p>예시 코드에 드러나있듯이 문자열 기반 키 비교를 수행하기 때문에 JS의 일반적인 문자열 비교 연산을 많이 수행한다. 라이브러리 차원에서 키 비교를 하지 않기 때문에 개발자가 직접 키 매칭 로직을 작성해야한다.</p>
<p><strong>Tanstack Query</strong></p>
<p><code>hashKey</code> 함수를 통해 전달받은 쿼리키를 문자열로 직렬화해 캐시 키로 활용한다. 이때 쿼리키 배열 안에 <strong>객체가 포함된 경우</strong>, 해당 객체의 프로퍼티를 정렬한 뒤 직렬화하기 때문에 객체 내부 키의 순서가 달라도 동일한 캐시로 취급된다.</p>
<pre><code class="language-tsx">// packages/query-core/src/utils.ts
export function hashKey(queryKey: QueryKey | MutationKey): string {
  return JSON.stringify(queryKey, (_, val) =&gt;
    isPlainObject(val)
      ? Object.keys(val)
          .sort()                    // ← 객체 키 정렬
          .reduce((result, key) =&gt; {
            result[key] = val[key]
            return result
          }, {} as any)
      : val,
  )
}</code></pre>
<p>캐시 키를 조회할때는 hashkey와 일치 여부를 판단하는 것이 아니라 부분적으로 일치하는지 prefix 매칭을 수행한다. 그래서 부분적인 캐시 키를 통해서 여러 개의 쿼리들을 invalidate 할 수 있게 된다.</p>
<pre><code class="language-tsx">// [&#39;todos&#39;]를 prefix로 가지는 모든 캐시 키가 무효화된다.
queryClient.invalidateQueries({ queryKey: [&#39;todos&#39;] })</code></pre>
</aside>

<hr>
<h3 id="22-번들-사이즈">2.2 번들 사이즈</h3>
<p>SWR은 gzip 기준 약 4.2KB의 매우 가벼운 라이브러리다.</p>
<p>TanStack Query는 풍부한 기능만큼 11.4KB로 조금 더 무겁다.</p>
<table>
<thead>
<tr>
<th></th>
<th>SWR</th>
<th>TanStack Query</th>
</tr>
</thead>
<tbody><tr>
<td>번들 사이즈 (gzip)</td>
<td>~4.2KB</td>
<td>~13KB</td>
</tr>
<tr>
<td>DevTools</td>
<td>비공식 커뮤니티</td>
<td><strong>공식 내장</strong></td>
</tr>
<tr>
<td>프레임워크 지원</td>
<td>React 전용</td>
<td>React, Vue, Svelte, Solid, Angular</td>
</tr>
</tbody></table>
<p>하지만 13KB가 엄청난 병목을 일으킬 만큼의 용량이 아니기 때문에 크게 유의미한 비교는 아닌 것 같다.</p>
<hr>
<h3 id="23-mutation과-optimistic-update">2.3 Mutation과 Optimistic Update</h3>
<p>SWR은 별도의 <code>useMutation</code> 훅이 없다. <strong><code>mutate</code></strong> 함수 하나가 캐시 업데이트와 재검증을 동시에 담당한다. v2부터 <code>useSWRMutation</code>이 추가됐다고 하지만, 뮤테이션 이후 캐시 무효화는 여전히 수동으로 처리해야 한다.</p>
<pre><code class="language-tsx">const toggleTodo = async () =&gt; {
  mutate(`/api/todos/${id}`, { ...todo, done: !todo.done }, false) // 낙관적 업데이트
  try {
    await updateTodo(id, { done: !todo.done })
    mutate(`/api/todos/${id}`) // 성공 후 재검증
  } catch {
    mutate(`/api/todos/${id}`, todo, false) // 실패 시 수동 롤백
  }
  // mutation 로딩 상태는 useState로 따로 관리해야 함
}</code></pre>
<p>TanStack Query는 <strong><code>useMutation</code></strong> 훅이 명시적으로 분리되어 있고, <code>onMutate</code> / <code>onSuccess</code> / <code>onError</code> / <code>onSettled</code> 라이프사이클 콜백을 제공한다.</p>
<pre><code class="language-tsx">const mutation = useMutation({
  mutationFn: (newData) =&gt; updateTodo(id, newData),
  onMutate: async (newData) =&gt; {
    await queryClient.cancelQueries({ queryKey: [&#39;todos&#39;, id] })
    const snapshot = queryClient.getQueryData([&#39;todos&#39;, id])
    queryClient.setQueryData([&#39;todos&#39;, id], (old) =&gt; ({ ...old, ...newData }))
    return { snapshot }
  },
  onError: (err, newData, context) =&gt; {
    queryClient.setQueryData([&#39;todos&#39;, id], context.snapshot) // 자동 롤백
  },
  onSettled: () =&gt; {
    queryClient.invalidateQueries({ queryKey: [&#39;todos&#39;, id] })
  },
})

// 로딩·에러 상태가 훅에서 바로 제공됨
const { isPending, isError } = mutation</code></pre>
<h3 id="swr은-언제-써보면-좋을까">SWR은 언제 써보면 좋을까?</h3>
<p>솔직히 비교하면서도 TanStack Query가 더 좋아 보였던 건 사실이다. 비동기 작업과 관련된 다양한 케이스를 처음부터 고려해 설계된 라이브러리라는 것이 많이 드러났던 것 같다.</p>
<p>그럼에도 SWR을 쓰면 좋은 상황들은 있는 것 같다.</p>
<p><strong>단순한 앱에서의 키 관리</strong>가 편리하다. TanStack Query를 쓰다 보면 쿼리키를 어떻게 설계할지 고민하게 되는 순간이 온다. 키가 다른 쿼리의 무효화에도 영향을 줄 수 있기 때문에 쿼리키 팩토리 패턴 같은 것까지 고려해야 하는 상황이 생긴다. 이 복잡도는 정교한 캐시 제어를 위한 필연적인 과정인 것 같다. 반면 SWR은 URL이 곧 캐시 키이기 때문에, 정교한 무효화 전략이 필요 없는 단순한 앱이라면 오히려 이 단순함이 장점이 되는 것 같다.</p>
<p><strong>러닝커브가 상대적으로 낮다</strong>. TanStack Query는 <code>useQuery</code>와 <code>useMutation</code> 각각의 사용법과 내부 옵션을 파악해야 한다. SWR은 <code>mutate</code> 하나로 처리하기 때문에 배우는 데 드는 비용이 적다. 다만 이 단순함의 이면에는 mutation 상태(<code>isPending</code>, <code>isError</code>)를 라이브러리가 제공하지 않아 직접 관리해야 한다는 점이 있긴 하다.</p>
<p>결국 두 라이브러리의 선택 기준은 앱의 복잡도에 있는 것 같다.</p>
<p>단순한 read-heavy 앱이라면 <strong>SWR의 단순함이 오히려 강점</strong>이 되고, mutation이 복잡하거나 정교한 캐시 무효화가 필요하다면 TanStack Query의 구조화된 설계가 필요한 상황이라고 생각한다.</p>
<h3 id="마치며">마치며</h3>
<p>상당히 닮은 두 라이브러리를 비교해보면서 써보지 않았던 SWR에 대해서 알게되어 새로웠다. 그러면서 tanstack query가 비동기와 관련한 작업을 코드레벨에서 부터 잘 설계한 라이브러리라고 느껴져 새삼 인기가 많은 이유를 알게 된 것 같다. 앞으로 두 라이브러리를 가지고 어떤 것을 사용할지 판단할 때 적합한 판단을 내릴 수 있을 것 같다.</p>
<h3 id="reference">Reference</h3>
<ul>
<li>TanStack Query 공식 문서 <a href="https://tanstack.com/query/latest/docs/framework/react/overview">https://tanstack.com/query/latest/docs/framework/react/overview</a></li>
<li>TanStack Query 소스 코드 — utils.ts <a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts">https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts</a></li>
<li>SWR 공식 문서 <a href="https://swr.vercel.app/">https://swr.vercel.app</a></li>
<li>LogRocket — SWR vs TanStack Query <a href="https://blog.logrocket.com/swr-vs-tanstack-query-react/">https://blog.logrocket.com/swr-vs-tanstack-query-react/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Multi-Product AI Agent 개발기 — 여러 서비스를 분석하고 PR 생성 자동화 에이전트]]></title>
            <link>https://velog.io/@tkddn_dev8430/Multi-Product-AI-Agent%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/Multi-Product-AI-Agent%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 13 Mar 2026 07:05:48 GMT</pubDate>
            <description><![CDATA[<p><strong>unified agent</strong>는 여러 개의 개인 프로덕트를 하나의 관점에서 분석하고,
개선 우선순위를 판단한 뒤 실제 코드 수정과 PR 생성까지 이어지도록 설계한 자동화 에이전트입니다.</p>
<p>수익화를 목적으로 개인 프로젝트로 <a href="https://qr-generator.cc/">QR Generator</a>와 <a href="https://convertkits.org/">Convert Image</a>라는 두 개의 툴 서비스를 운영하면서, 아래와 같은 한계와 문제들이 있었습니다.</p>
<ul>
<li><strong>마케팅 경험의 부재:</strong> 유지보수와 개선 방향을 잡기 위해 GA4, GSC 데이터를 세팅했지만, 데이터를 통해 어떤 인사이트를 도출하고 다음 방향성을 정해야 하는지에 대한 경험이 부족했습니다.</li>
<li><strong>관리 부담:</strong> 두 서비스의 GSC, GA4 데이터를 각각 확인하고 전략을 세우는 데 상당한 시간이 소요되었습니다.</li>
<li><strong>의사결정의 모호함:</strong> 어떤 프로덕트에 리소스를 더 집중해야 할지 데이터 기반으로 우선순위를 정하기 어려웠습니다.</li>
<li><strong>실행의 병목:</strong> 리포트에서 &quot;메타 타이틀을 수정하세요&quot;라는 인사이트를 얻더라도, 실제 파일 수정과 PR 생성까지 이어지는 과정이 번거로워 실행이 미뤄지는 문제가 있었습니다.</li>
</ul>
<p>단순히 데이터를 보여주는 도구가 아니라, <strong>스스로 판단하고 코드를 수정해 PR까지 올리는 에이전트</strong>가 필요했습니다.</p>
<p>아래에서는 에이전트를 구성하는 전체 구현 과정보다는, 실제로 설계하면서 중요하게 판단했던 의사결정 지점들을 중심으로 기록해두었습니다.
<br/></p>
<h2 id="프로젝트-구조-결정하기-monorepo-vs-multi-repo">프로젝트 구조 결정하기: Monorepo vs Multi-Repo</h2>
<p>에이전트를 설계할 때 가장 먼저 마주한 고민은 &quot;에이전트가 두 프로젝트를 어떤 방식으로 바라보게 할 것인가?&quot;였습니다. 모노레포를 고려하기도 했지만, 최종적으로는 <strong>Multi-Repo 구조를 유지하되 이를 상위 계층에서 통합 관리하는 방식</strong>을 선택했습니다.</p>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/3f561ea6-dfd4-4bdf-869b-c79da86ba046/image.png' width='700px' />
</div>

<ul>
<li><strong>코드의 독립성:</strong> 두 프로젝트가 공유하는 코드가 없어 결합도를 높일 이유가 없었고, 모노레포를 도입하려면 기존 프로젝트 구조를 변경해야 했습니다.</li>
<li><strong>데이터 중심의 통합:</strong> 에이전트에게 필요한 것은 코드 전체가 아니라 각 프로덕트의 데이터와 PR 생성 권한이었습니다.</li>
<li><strong>확장성:</strong> 이 구조 덕분에 기존 프로젝트 설정을 거의 건드리지 않고도 새로운 프로덕트를 언제든 관리 대상에 추가할 수 있었습니다.</li>
</ul>
</br>

<h2 id="agent의-단계별-설계">Agent의 단계별 설계</h2>
<p>핵심은 분석에서 실행까지 이어지는 파이프라인을 만드는 것이었습니다.</p>
<ol>
<li><p><strong>Analysis (분석)</strong>
 에이전트는 먼저 각 프로덕트의 GSC(Search Console), GA4(Analytics) 지표를 수집합니다. 수집된 로우 데이터는 <strong>통합 분석 모듈</strong>을 통해 가공됩니다. 이 모듈은 단순히 데이터를 나열하는 것이 아니라, 각 서비스의 트래픽, 사용자 참여도, SEO 순위, 수익 데이터를 사전에 정의한 목표 지표와 비교합니다.</p>
<p> 특히 서비스마다 중요도가 다르다는 점을 반영하기 위해 <strong>가중치 기반의 Health Score</strong>를 계산하도록 했습니다. 이를 통해 어떤 서비스의 성장이 정체되어 있는지, 어디에 리소스를 우선 투입해야 하는지를 보다 객관적으로 판단할 수 있게 했습니다.</p>
<p> Gemini AI는 이렇게 가공된 지표 분석 결과와 트렌드 데이터를 결합해 아래와 같은 구조화된 <strong>마크다운 리포트</strong>를 생성합니다.</p>
<ul>
<li><p><strong>Executive Summary</strong>: 전체 프로덕트 상태 요약</p>
</li>
<li><p><strong>Health Score</strong>: 트래픽, SEO, 수익률을 가중 합산한 건강 지표</p>
</li>
<li><p><strong>Action Plan</strong>: 하이라이트된 개선 과제 (🔴 긴급, 🟡 중요, 🟢 건의)</p>
<p>그리고 이 리포트를 기반으로 아래 과정을 따라 실제 기능 개선을 수행합니다.</p>
</li>
</ul>
</li>
</ol>
<div align='center'>
  <img src="https://velog.velcdn.com/images/tkddn_dev8430/post/06be5c8e-2378-4e44-9fd3-487ae97d6072/image.png" width="100%" />
</div>


<ol>
<li><p><strong>Action Extraction (추출)</strong>
마크다운 리포트에서 실행 가능한 액션을 구조화된 데이터로 변환합니다. 이 단계에서는 <strong>데이터 파싱 전략</strong>으로 성능을 확보하고, 복잡한 문맥만 <code>LLM Fallback</code>으로 처리하는 하이브리드 방식을 채택했습니다.</p>
<p>기존에는 전체 리포트를 LLM으로만 파싱했는데, 불필요한 토큰 소모가 컸고 특히 파일 경로를 잘못 짚는 환각(Hallucination)이 자주 발생했습니다. <code>Regex-First</code> 방식으로 전환한 뒤에는 약 90%의 전형적인 수정 액션을 거의 즉시 추출할 수 있었고, 비용도 거의 0에 가깝게 줄일 수 있었습니다.</p>
</li>
<li><p><strong>Action Validation (검증)</strong>
AI가 제안한 액션이 안전한지 검증합니다. 사전에 정의된 파일과 태그(Meta Title, Description 등)만 수정할 수 있도록 하는 <strong>화이트리스트 방식</strong>을 적용해 시스템 안정성을 확보했습니다.</p>
<p>여기서 화이트리스트(Whitelist) 방식이란, &quot;허용된 것 외에는 모두 금지&quot;하는 보안 원칙입니다. 예를 들어 <code>.env</code> 같은 민감 파일은 접근 자체를 차단하고, <code>layout.tsx</code> 같은 특정 SEO 관련 파일과 <code>update_meta_title</code> 같은 특정 액션 타입만 통과시키도록 제한했습니다. 이 덕분에 <code>rm -rf</code> 같은 파괴적인 명령이나 예상하지 못한 파일 수정은 구조적으로 발생할 수 없도록 설계할 수 있었습니다.</p>
</li>
<li><p><strong>Repository Dispatch (실행)</strong>
초기 버전에서는 에이전트가 직접 모든 코드를 클론해 수정했지만, 서비스 수가 늘어나면서 속도 문제가 발생했습니다. 이를 해결하기 위해 GitHub <code>Repository Dispatch</code> 이벤트를 활용해 각 프로덕트가 자신의 워크플로우 안에서 파일 수정과 PR 생성을 비동기적으로 처리하도록 구조를 바꿨습니다.</p>
</li>
</ol>
<pre><code class="language-yaml"># 프로덕트(Receiver) 측의 GitHub Action 예시
on:
  repository_dispatch:
    types: [seo_update_event]

jobs:
  update_meta:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Apply AI Actions
        run: python update_script.py &#39;${{ github.event.client_payload.actions }}&#39;</code></pre>
<div align='center'>
  <img src="https://velog.velcdn.com/images/tkddn_dev8430/post/421bb44c-97b7-4aa6-acf1-a9ccb14de3f0/image.png" width="100%" />
</div>

<br />

<h2 id="spec-기반으로-코드를-수정하도록-하기">Spec 기반으로 코드를 수정하도록 하기</h2>
<p>에이전트 개발 과정에서 또 하나 중요하게 본 점은, 코드 수정 과정이 항상 일정한 단계와 형식을 따르도록 만드는 것이었습니다. 그래야 두 프로젝트 간 코드 스타일을 유지할 수 있고, LLM도 더 안정적으로 동작한다고 판단했습니다.</p>
<p><code>spec.md</code> → <code>plan.md</code>ㅁ → <code>tasks.md</code> 로 이어지는 문서화 과정을 통해 구현 전에 설계를 충분히 정리했습니다.</p>
<p>에이전트에게 내리는 각 단계의 지침(Prompt)은 다음과 같습니다.</p>
<ul>
<li><a href="https://github.com/SangWoo9734/unified-agent/blob/main/specs/pr-automation/spec.md"><strong>spec.md</strong></a>: &quot;코드를 건드리지 말고, 우리가 해결하려는 도메인 문제와 비즈니스 로직의 정의(What &amp; Why)만 서술하라.&quot;</li>
<li><a href="https://github.com/SangWoo9734/unified-agent/blob/main/specs/pr-automation/plan.md"><strong>plan.md</strong></a>: &quot;정의된 스펙을 달성하기 위해 어떤 파일을 어떻게 수정할지 기술적인 설계도(How)를 작성하라. 이때 안전장치를 반드시 포함하라.&quot;</li>
<li><a href="https://github.com/SangWoo9734/unified-agent/blob/main/specs/pr-automation/tasks.md"><strong>tasks.md</strong></a>: &quot;설계도를 바탕으로 에이전트가 한 번에 하나씩 수행할 수 있는 원자 단위 작업 리스트를 체크리스트 형태로 생성하라.&quot;</li>
</ul>
<p>이 과정을 도입하면서 얻은 효과는 명확했습니다.</p>
<ul>
<li><strong>설계 강제:</strong> 무작정 코딩에 들어가는 대신, 도메인 문제와 리스크(Risk &amp; Mitigation)를 먼저 정의할 수 있었습니다.</li>
<li><strong>안전장치 내재화:</strong> &quot;어떻게 안전하게 수정할 것인가&quot;를 설계 단계에서 LibCST 도입 등으로 미리 결정했기 때문에 실제 구현 과정의 시행착오를 크게 줄일 수 있었습니다.</li>
<li><strong>작업의 원자성:</strong> 모든 태스크를 <code>todos/</code> 아래 작은 단위로 나누어 에이전트와의 협업 정확도를 높였습니다.</li>
</ul>
<br/>

<h2 id="코드-에이전트별-강점-살리기">코드 에이전트별 강점 살리기</h2>
<p>이번 프로젝트에서는 <strong>Claude Code</strong>와 <strong>Antigravity</strong> 두 개의 코딩 에이전트를 함께 활용했습니다.</p>
<p>개인적으로는 Antigravity를 전반적인 기능 개발에, Claude Code를 세부 리팩토링과 코드 정리에 활용했습니다.</p>
<p><strong>Antigravity</strong>는 기능 개발이나 디버깅 과정에서 기본적으로 plan 문서를 생성해주기 때문에 작업 흐름을 파악하기 쉬웠고, Chrome을 통해 실제 화면을 분석하면서 기능과 UI 설명을 잘 이해하는 편이었습니다.</p>
<p>반면 <strong>Claude Code</strong>는 기존에 활용하던 Skill을 기반으로 코드 리뷰, 컴포넌트 분리, 커밋 단위 정리에 강점이 있었고, 코드가 더 의도에 맞게 정리되는 느낌을 받았습니다.</p>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/9b47450f-9e11-4fc4-b264-569cf62dab8f/image.png' width='400px' />
</div>

<br/>

<h2 id="마치며">마치며</h2>
<p>현재 unified agent는 다음 역할을 수행할 수 있는 구조까지 구성되어 있습니다.</p>
<ul>
<li>여러 프로젝트의 GSC / GA4 데이터를 통합 분석</li>
<li>서비스별 Health Score 기반 우선순위 제안</li>
<li>실행 가능한 액션 추출</li>
<li>안전 검증 후 repository dispatch 기반 PR 생성</li>
</ul>
<p>즉 단순히 리포트를 생성하는 도구가 아니라, 어느 프로젝트에 먼저 개입해야 하는지 판단하고 실행까지 이어지는 흐름을 목표로 설계했습니다.</p>
<p>다만 실제 운영 단계에서는 트래픽 규모가 아직 충분하지 않아, 생성된 PR이 지속적으로 유효한 개선으로 이어지는 수준까지는 검증하지 못했습니다.</p>
<p>오히려 이 과정에서 데이터가 부족한 환경에서는 AI가 의미 있는 개선 방향을 제안하기 어렵고, 도메인 맥락과 우선순위를 함께 제공해야 한다는 점을 더 분명히 확인할 수 있었습니다.</p>
<p>특히 &quot;기능을 수행하는 AI&quot;와 &quot;실제로 효과가 있는 AI Product&quot; 사이에는 생각보다 많은 설계가 필요하다는 점을 직접 체감할 수 있었던 작업이었습니다. 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js에서 데이터를 어디서 불러올까? Server Component vs TanStack Query
]]></title>
            <link>https://velog.io/@tkddn_dev8430/Next.js%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%96%B4%EB%94%94%EC%84%9C-%EB%B6%88%EB%9F%AC%EC%98%AC%EA%B9%8C-Server-Component-vs-TanStack-Query</link>
            <guid>https://velog.io/@tkddn_dev8430/Next.js%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%96%B4%EB%94%94%EC%84%9C-%EB%B6%88%EB%9F%AC%EC%98%AC%EA%B9%8C-Server-Component-vs-TanStack-Query</guid>
            <pubDate>Wed, 11 Mar 2026 13:52:25 GMT</pubDate>
            <description><![CDATA[<p>Next.js 기반으로 서버 데이터를 활용하는 컴포넌트를 만들때 매번 고민되는 지점이 있다.</p>
<p>바로 데이터를 언제 어떻게 불러올 것 인가에 대해서 고민을 해야한다.</p>
<p>인턴 기간 중 Nuxt.js 로직을 Next.js로 마이그레이션 하는 과정에서도 고민을했던 영역이다.</p>
<p>Next.js에서 데이터 fetching 방식을 분류해 어떤 특징들이 있는지 알아보고, 그때의 기억을 되살려서 적절하게 의사결정을 했는지 한번 돌아보고자 한다.</p>
<h2 id="nextjs-데이터-fetching-옵션">Next.js 데이터 Fetching 옵션</h2>
<ol>
<li><p><strong>서버 컴포넌트에서 fetching하기</strong></p>
<pre><code class="language-tsx"> // app/posts/page.tsx
 export default async function PostsPage() {
   const posts = await fetch(&#39;/api/posts&#39;).then(res =&gt; res.json())
   return &lt;PostList posts={posts} /&gt;
 }</code></pre>
<ul>
<li>서버에서 실행, <strong>클라이언트 번들에 포함되지 않는다.</strong></li>
<li>캐싱은 Next.js fetch 옵션으로 제어 (<code>cache</code>, <code>revalidate</code>) 할 수 있다.</li>
<li><strong>인터랙션 없는 정적 데이터</strong>에 적합하다.</li>
</ul>
</li>
<li><p><strong>Server Component + Tanstack Query ( Hydration 활용 )</strong></p>
<pre><code class="language-jsx"> // lib/getQueryClient.ts
 import { QueryClient } from &#39;@tanstack/react-query&#39;
 import { cache } from &#39;react&#39;

 // React의 cache()로 감싸면 같은 요청 내에서는 동일한 인스턴스 반환
 // 요청이 다르면 새로 생성 → 요청 간 오염 없음
 const getQueryClient = cache(() =&gt; new QueryClient())
 export default getQueryClient</code></pre>
<pre><code class="language-jsx"> // app/posts/page.tsx
 export default async function PostsPage() {
   const queryClient = getQueryClient() // 같은 요청 내에선 동일 인스턴스
   await queryClient.prefetchQuery(...)

   return (
     &lt;HydrationBoundary state={dehydrate(queryClient)}&gt;
       &lt;PostList /&gt;
     &lt;/HydrationBoundary&gt;
   )
 }</code></pre>
<ul>
<li>초기 로딩은 <strong>SSR</strong>, 이후 클라이언트에서 <strong>refetch/캐싱</strong> 가능하다.</li>
<li>구성이 복잡하지만 <strong>두 가지 장점을 다 챙길 수 있다.</strong></li>
</ul>
</li>
<li><p><strong>클라이언트 컴포넌트 내부에서 Tanstack Query활용하기</strong></p>
<pre><code class="language-tsx"> &#39;use client&#39;
 export default function PostList() {
   const { data, isLoading } = useQuery({
     queryKey: [&#39;posts&#39;],
     queryFn: fetchPosts,
   })
 }</code></pre>
<ul>
<li><strong>사용자 인터랙션</strong>에 반응하는 데이터에 적합하다.</li>
<li>Tanstack Query에서 제공하는 Caching, Refetching, 낙관적 업데이트 등 클라이언트 상태 관리에 용이하다.</li>
</ul>
</li>
</ol>
<br />

<h2 id="언제-어떤-방식을-쓸까">언제 어떤 방식을 쓸까?</h2>
<p>레퍼런스를 참고했을 때 대략 아래와 같은 케이스에서 각각의 방식이 적합하다고 볼 수 있다.</p>
<p><strong>Server Component가 적합한 경우</strong></p>
<ul>
<li>SEO가 중요한 페이지 (게시글 목록, 상세)</li>
<li>로그인 여부와 무관한 공개 데이터</li>
<li>실시간 업데이트 불필요</li>
<li>번들 사이즈를 줄이고 싶을 때</li>
</ul>
<p><strong>TanStack Query가 적합한 경우</strong></p>
<ul>
<li>사용자 액션 후 데이터 갱신 (좋아요, 댓글, 필터링)</li>
<li>낙관적 업데이트 필요</li>
<li>무한 스크롤 / 페이지네이션</li>
<li>여러 컴포넌트에서 같은 데이터 공유 (캐싱 활용)</li>
<li>폴링(주기적 refetch) 필요</li>
</ul>
<p><strong>Hydration이 적합한 경우</strong></p>
<ul>
<li>SEO와 클라이언트 인터랙션 둘 다 필요한 경우</li>
<li>서버에서 prefetch 후 클라이언트에서 실시간 갱신이 필요한 경우</li>
</ul>
<p>정리하면 아래와 같이 보여줄 수 있다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Server Component</th>
<th>TanStack Query</th>
<th>Hydration</th>
</tr>
</thead>
<tbody><tr>
<td>SEO</td>
<td>✅</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>인터랙션</td>
<td>❌</td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td>번들 사이즈</td>
<td>작음</td>
<td>증가</td>
<td>중간</td>
</tr>
<tr>
<td>구성 복잡도</td>
<td>낮음</td>
<td>낮음</td>
<td>높음</td>
</tr>
</tbody></table>
<p>Hydration은 <strong>SEO와 인터랙션의 장점을 모두 챙길 수 있지만</strong>, <strong>구성 복잡도가 높고 번들 사이즈가 증가하는 트레이드오프</strong>가 있다. SEO와 클라이언트 인터랙션이 동시에 필요한 경우에만 선택하는 것이 좋다.</p>
<br />

<h2 id="마이그레이션-당시-판단-기준">마이그레이션 당시 판단 기준</h2>
<p>마이그레이션 과정에서 일부 API를 새로 작성해야 하는 경우가 있었고, 일부는 이미 특정 방식으로 구현된 로직을 그대로 활용할 수 있는 상황이었다. 이 두 가지 경우에 따라 데이터 페칭 위치를 어떻게 결정했는지 정리해보았다.</p>
<h3 id="1-api를-새로-작성해야-하는-경우">1. API를 새로 작성해야 하는 경우</h3>
<p>새로 작성이 필요한 경우, 아래 세 가지 기준을 고려하여 호출 위치를 결정했다.</p>
<p><strong>[ 데이터가 재사용되어야 하는가? ]</strong></p>
<p>TanStack Query가 제공하는 <strong>캐싱 기능을 고려한 판단</strong>이었다. <strong>동일한 요청이 반복될 가능성이 있는 데이터</strong>라면 TanStack Query를 활용하는 것이 더 적합하다고 생각했다.</p>
<p><strong>[ 데이터가 props에 따라 달라지는가? ]</strong></p>
<p>props에 의존하는 데이터는 그만큼 <strong>요청 빈도가 높고, 같은 데이터를 다시 조회할 가능성도 높다고 생각했다.</strong> 재사용성을 고려한 맥락과 동일하게 TanStack Query가 더 나은 선택이라고 판단했다. </p>
<pre><code class="language-typescript">&#39;use client&#39;

import { useInfiniteQuery } from &#39;@tanstack/react-query&#39;

type Item = {
  id: number
  title: string
}

async function fetchItems({ pageParam = 1 }: { pageParam?: number }) {
  const res = await fetch(`/api/items?page=${pageParam}`)
  if (!res.ok) {
    throw new Error(&#39;Failed to fetch items&#39;)
  }

  return res.json() as Promise&lt;{
    items: Item[]
    nextPage?: number
  }&gt;
}

export default function ItemList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: [&#39;items&#39;],
      queryFn: ({ pageParam }) =&gt; fetchItems({ pageParam }),
      initialPageParam: 1,
      getNextPageParam: (lastPage) =&gt; lastPage.nextPage,
    })

  const items = data?.pages.flatMap((page) =&gt; page.items) ?? []

  return (
    &lt;div&gt;
      &lt;ul&gt;
        {items.map((item) =&gt; (
          &lt;li key={item.id}&gt;{item.title}&lt;/li&gt;
        ))}
      &lt;/ul&gt;

      {hasNextPage &amp;&amp; (
        &lt;button onClick={() =&gt; fetchNextPage()} disabled={isFetchingNextPage}&gt;
          {isFetchingNextPage ? &#39;불러오는 중...&#39; : &#39;더 보기&#39;}
        &lt;/button&gt;
      )}
    &lt;/div&gt;
  )
}</code></pre>
<p><strong>페이지네이션</strong>이 필요한 데이터도 사용자의 액션에 따라 다음 데이터를 계속 불러와야 했기 때문에 TanStack Query를 활용했다. 특히 무한 스크롤을 적용하려면 “다음 페이지를 불러오는 상태”, “기존 목록 유지”, “추가 요청 제어”가 중요했는데, 이런 점에서 서버 컴포넌트만으로 처리하는 것보다 클라이언트에서 쿼리 상태를 관리하는 편이 더 자연스럽다고 생각했다.</p>
<p><strong>[ 빠른 사용자 피드백이 중요한가? ]</strong></p>
<p>화면 깜빡임 없이 데이터를 최대한 빠르게 보여줘야 하는 경우, 서버 컴포넌트에서 데이터를 페칭해 하위 컴포넌트로 내려주는 방식을 선택했다. <strong>페이지 진입 시점에 데이터가 반드시 존재해야 하고, 이후 클라이언트에서 상태로 관리할 필요가 없는 데이터</strong>라면 서버 컴포넌트를 선택했다.</p>
<h3 id="2-기존-api-로직을-재사용하는-경우">2. 기존 API 로직을 재사용하는 경우</h3>
<p><strong>[ 기존 로직이 TanStack Query 훅으로 구현된 경우 ]</strong></p>
<p>훅으로 만들어진 이유가 재사용성을 고려한 것이라고 판단했다. 따라서 해당 훅이 호출되는 시점을 먼저 확인했다. 마이그레이션 대상 페이지 진입 전에 이미 훅이 호출된 적 있다면, 이후에는 캐싱된 데이터를 활용할 가능성이 높기 때문에 훅을 그대로 사용했다. 그렇지 않은 경우에는 처음부터 다시 판단해 적절한 방식을 결정했다.</p>
<pre><code class="language-ts">export default function UserProfile() {
  // 기존 구현된 유저 정보 조회 커스텀 훅을 그대로 활용
  const { data: user, isLoading } = useUserInfo()

  if (isLoading) return &lt;div&gt;로딩 중...&lt;/div&gt;

  return (
    &lt;div&gt;
      &lt;p&gt;{user.name}&lt;/p&gt;
      &lt;p&gt;{user.email}&lt;/p&gt;
    &lt;/div&gt;
  )
}</code></pre>
<p><strong>[ 기존 로직이 서버 컴포넌트에서 구현된 경우 ]</strong></p>
<p>데이터의 재사용 가능성이 이미 고려된 시점이라고 봤다. 기존에 서버 컴포넌트로 구현된 이유를 먼저 파악하고, 마이그레이션 이후에도 동일한 방식이 적합한지, 혹은 로직 수정이 필요한지를 함께 검토했다.</p>
<br />

<h2 id="마치며">마치며</h2>
<p>마이그레이션 당시에도 헷갈리기도 했고, 이렇게 기준을 잡아도 되나 싶었던 부분이었는데 이번에 확실히 정리를 하게 되어 다행이라 생각한다.</p>
<p>당시에 하이드레이션할 생각까지는 못했던 것 같아 조금 더 알아보고 작업을 했다면 조금 더 확장된 사고로 의사결정을 할 수 있지 않았을까하는 아쉬움도 남았다.</p>
<p>그리고 각 방식에서 디테일하게 놓치고 있던 부분들도 이번에 확실히 알아가는 것 같아서 정리하길 잘한 것 같다.</p>
<br />

<h2 id="reference">Reference</h2>
<p><a href="https://nextjs.org/docs/app/getting-started/fetching-data">https://nextjs.org/docs/app/getting-started/fetching-data</a></p>
<p><a href="https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr">https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr</a></p>
<p><a href="https://ko.react.dev/reference/rsc/server-components">https://ko.react.dev/reference/rsc/server-components</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리스트 가상화 (List Virtualization) 알아보기]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%83%81%ED%99%94-List-Virtualization-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%83%81%ED%99%94-List-Virtualization-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 11 Feb 2026 18:57:25 GMT</pubDate>
            <description><![CDATA[<p>우아콘의 한 세션에서 앱 내 웹 뷰에서 리스트 컴포넌트를 리스트 가상화 (List <strong>Virtualization</strong>)를 통해 최적화 했다라는 이야기를 들었다.</p>
<p>그 발표를 계기로 리스트 가상화에 대해서 궁금해졌고, 이게 실제로 어떤 원리로 최적화가 이루어질 수 있는지 알아보자.</p>
<h2 id="리스트-가상화-list-virtualization">리스트 가상화 (List <strong>Virtualization</strong>)?</h2>
<p>리스트 가상화는 동적 리스트를 랜더링할 때 전체 리스트를 랜더링하지 않고 화면에 보이는 콘텐츠만 랜더링하는 방식을 말한다. </p>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/4f243ae7-cd50-47fb-b9ff-23679ff0da4c/image.png' width='1024' />
</div>

<h2 id="리스트-가상화는-어떻게-동작할까">리스트 가상화는 어떻게 동작할까?</h2>
<h2 id="1-어떤-아이템을-렌더링할지-계산">1. 어떤 아이템을 렌더링할지 계산</h2>
<ol>
<li>각 아이템의 높이를 캐싱 (측정값 또는 예상값)</li>
<li>현재 스크롤 위치(scrollOffset)를 기준으로 이진 탐색</li>
<li>화면에 보이는 범위의 startIndex, endIndex 계산<ul>
<li>이진 탐색으로 시작 인덱스를 빠르게 찾음 (O(log n))</li>
</ul>
</li>
<li>overscan으로 위아래 버퍼 추가 (깜빡임 방지)</li>
</ol>
<p><strong>TanStack Virtual 구현:</strong></p>
<pre><code class="language-tsx">// packages/virtual-core/src/index.ts
private calculateRange() {
  // 이진 탐색으로 startIndex 찾기
  const startIndex = this.findStartIndex(scrollOffset);

  // 뷰포트 끝까지 endIndex 확장
  const endIndex = this.findEndIndex(startIndex, scrollOffset + outerSize);

  // overscan 적용 (스크롤 시 깜빡임 방지)
  return {
    startIndex: Math.max(0, startIndex - overscan),
    endIndex: Math.min(count - 1, endIndex + overscan)
  };
}</code></pre>
<hr>
<h2 id="2-스크롤바-높이-유지">2. 스크롤바 높이 유지</h2>
<ul>
<li>실제로는 10개만 렌더링하지만</li>
<li>스크롤바는 10,000개처럼 보이게 하는 트릭</li>
</ul>
<p><strong>TanStack Virtual 구현:</strong></p>
<pre><code class="language-tsx">// 전체 리스트의 가상 높이 계산
getTotalSize() {
  const measurements = this.getMeasurements();
  return measurements[measurements.length - 1]?.end ?? 0;
}</code></pre>
<hr>
<h2 id="3-동적-높이-처리">3. 동적 높이 처리</h2>
<ul>
<li>ResizeObserver로 실제 렌더링된 요소 크기 측정</li>
<li>측정값을 캐싱해서 다음 계산에 활용</li>
</ul>
<p><strong>TanStack Virtual 구현:</strong></p>
<pre><code class="language-tsx">// 각 아이템의 크기 측정 및 캐싱
measureElement(element: HTMLElement, index: number) {
  const size = element.getBoundingClientRect().height;

  // 캐시에 저장
  this.itemSizeCache.set(index, size);

  // 예상 크기와 다르면 재계산 트리거
  if (this.measurementsCache[index]?.size !== size) {
    this.notify(false); // 3번부터 다시 계산
  }
}

// 측정값 활용
getMeasurements() {
  return this.options.count.map((_, index) =&gt; {
    // 캐시에 있으면 사용, 없으면 추정값 사용
    const size = this.itemSizeCache.get(index)
      ?? this.options.estimateSize(index);

    return { start, size, end: start + size };
  });
}
</code></pre>
<hr>
<h2 id="전체-흐름">전체 흐름</h2>
<div align='center'>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/38144299-d1c9-410a-a8dc-46b774945af6/image.png' width='512' />
</div>

<h2 id="직접-측정해보기">직접 측정해보기</h2>
<p>실제로 리스트 가상화가 어떻게 향상된 사용자 경험을 제공하는지 직접 프로젝트를 통해서 알아봤다.</p>
<blockquote>
<p><strong>리스트 가상화 체험해보기 :</strong> <a href="https://list-virtualization-flax.vercel.app/">list-virtualization</a>
<strong>Repo 주소 :</strong> <a href="https://github.com/SangWoo9734/list-virtualization.git">https://github.com/SangWoo9734/list-virtualization.git</a></p>
</blockquote>
<h3 id="성능-측정-로직-설명">성능 측정 로직 설명</h3>
<p>1. <strong>FPS (Frames Per Second) 측정</strong></p>
<pre><code class="language-tsx">let frameCount = 0;
let lastTime = performance.now();

const measureFPS = () =&gt; {
  frameCount++;
  const currentTime = performance.now();
  const elapsed = currentTime - lastTime;

  if (elapsed &gt;= 1000) {  // 1초마다
    const fps = Math.round((frameCount * 1000) / elapsed);
    // fps 업데이트
  }
  requestAnimationFrame(measureFPS);
};
</code></pre>
<ul>
<li><code>requestAnimationFrame</code>이 호출될 때마다 프레임 카운트 증가</li>
<li>1초(1000ms)가 지나면 &quot;1초 동안 몇 프레임이 그려졌는지&quot; 계산</li>
</ul>
<p>2. <strong>DOM 노드 개수 측정</strong></p>
<pre><code class="language-tsx">const domNodes = document.getElementsByTagName(&#39;*&#39;).length;</code></pre>
<ul>
<li>페이지의 모든 HTML 요소 개수를 센다</li>
<li><strong>Virtualized</strong>: 보이는 부분만 렌더링 → 적은 노드 수 (~100-200개)</li>
<li><strong>Regular List</strong>: 전체 아이템 렌더링 → 많은 노드 수 (10,000개 = 수만 개)</li>
</ul>
<p>3. <strong>메모리 사용량 측정</strong></p>
<pre><code class="language-tsx">if (&#39;memory&#39; in performance) {
  const memory = performance.memory;
  const usedMB = memory.usedJSHeapSize / 1048576;
}</code></pre>
<ul>
<li>Chrome에서만 지원되는 기능</li>
<li>JavaScript 힙 메모리 사용량을 MB 단위로 표시</li>
<li>많은 DOM 노드 = 더 많은 메모리 사용</li>
</ul>
<h3 id="결과">결과</h3>
<p><strong>리스트 요소 100개</strong></p>
<p><img src="https://velog.velcdn.com/images/tkddn_dev8430/post/8565321c-c263-4f1a-9d9d-34fb48379906/image.png" alt=""></p>
<p><strong>리스트 요소 1000개</strong></p>
<p><img src="https://velog.velcdn.com/images/tkddn_dev8430/post/bc93ed6b-7981-4703-a5b9-b282a6612d42/image.png" alt=""></p>
<p><strong>리스트 요소 10000개</strong></p>
<p><img src="https://velog.velcdn.com/images/tkddn_dev8430/post/62cb312d-40f4-4018-8951-6d487cd6e11a/image.png" alt=""></p>
<ul>
<li>가상화를 적용하지 않은 리스트의 경우 리스트 요소에 비례하게 DOM 노드의 개수가 증가하는 것을 볼 수 있다.</li>
<li>가상화가 적용된 리스트의 경우 리스트의 개수가 늘어나더라도 동일한 노드의 개수를 갖는 것을 알 수 있다.</li>
<li>그 외 다른 지표들의 경우 리스트 요소 개수에 의해서 크게 영향을 받는 느낌은 아니었다.</li>
</ul>
<br/>

<p>이제 여기서 리플로우 / 리페인트를 발생시켜보면 조금 더 체감이 될 것 같다.</p>
<p>10000개를 기준으로 2초마다 반복적으로 스타일을 변경하도록 해서 부하를 추가해서 확인해보았다.</p>
<ul>
<li>가상화 적용(전)
<img src="https://velog.velcdn.com/images/tkddn_dev8430/post/418e1e4e-c843-43fa-b2fc-0118b546f543/image.png" alt=""></li>
</ul>
<ul>
<li>가상화 적용(후)
<img src="https://velog.velcdn.com/images/tkddn_dev8430/post/6c192061-8c62-4346-b8a1-b2e41675596e/image.png" alt=""></li>
</ul>
<p>가상화 전에는 스타일 변경이 시작되자마자 스크롤이 되지 않을 정도로 부하가 많이 걸리는 느낌이 확실히 들었다.</p>
<p>반면에 가상화를 적용했을 때는 부하를 주기 전과 거의 동일한 느낌으로 자연스러웠다.</p>
<p>이런 경험 뿐만 아니라 메모리 사용량도 차이가 많이 나는 것을 확인할 수 있었다.</p>
<p>다만 가상화를 적용한 리스트에서 약간 어색했던 부분은 리스트 전체 요소에 스타일이 적용되도록 해 두었는데 가상화 리스트의 경우 리스트 밖의 요소가 리스트에 보일때 랜더링을 하기 때문에 기존에 리스트에 있던 요소와 스타일이 변하는 타이밍이 달라지는 부분이 있었다.</p>
<h2 id="왜-이렇게-성능-차이가-날까">왜 이렇게 성능 차이가 날까?</h2>
<p>DOM 노드가 많을수록:</p>
<ul>
<li>브라우저가 관리해야 할 요소 증가 → 메모리 사용량 증가</li>
<li>스타일 변경 시 리플로우/리페인트 계산 범위 증가 → FPS 저하</li>
<li>이벤트 리스너, 레이아웃 계산 등 모든 작업이 느려짐</li>
</ul>
<p>가상화는 <strong>&quot;DOM 노드 개수를 일정하게 유지&quot;</strong>하여 이 문제를 해결</p>
<h2 id="reference">Reference</h2>
<p><a href="https://patterns-dev-kr.github.io/performance-patterns/list-virtualization/">https://patterns-dev-kr.github.io/performance-patterns/list-virtualization/</a></p>
<p><a href="https://tanstack.com/virtual/latest">https://tanstack.com/virtual/latest</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 접근성 알아보기]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/%EC%9B%B9-%EC%A0%91%EA%B7%BC%EC%84%B1-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 11 Feb 2026 09:18:57 GMT</pubDate>
            <description><![CDATA[<h2 id="웹-접근성">웹 접근성?</h2>
<p>연령, 장애 여부에 관계없이 웹 사이트에서 제공하는 정보에 동등하게 접근하고, 이용할 수 있도록 보장하는 것</p>
<h2 id="웹-접근성-고려사항">웹 접근성 고려사항</h2>
<ul>
<li>시각 - 저시력, 실명, 색각 이상 같은 시각 장애를 고려</li>
<li>이동성 - 근육 속도 저하, 근육 제어 손실로 인해 손을 쓰기 어렵거나 쓸 수 없는 상태를 고려</li>
<li>청각 - 영상, 음성 콘텐츠에 자막, 원고, 수화등의 대체 수단을 고려</li>
<li>인지 - 정신 지체 장애, 학습 장애를 고려</li>
</ul>
<h2 id="웹-접근성에-도움을-주는-브라우저-보조-기술">웹 접근성에 도움을 주는 브라우저 보조 기술</h2>
<ul>
<li>스크린 리더</li>
<li>화면 확대 도구</li>
<li>음성 인식</li>
<li>키보드 오버레이 <div align='left'>
  <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/00f41fa8-9ca5-4dbd-a89f-e08767dda4cf/image.png' width='500' />
</div>


</li>
</ul>
<h2 id="wai-aria">WAI-ARIA?</h2>
<p>동적 컨텐츠와 인터페이스 컨트롤을 위한 웹 접근성 표준으로, 보조 기술를 사용해 웹 애플리케이션을 효과적으로 탐색하는데 설계되 있다.</p>
<p>HTML Tag들에 추가적으로 시멘틱을 부여해서 브라우저 보조 기술들이 각 요소의 역할, 상태, 설명 등을 참고 할 수 있도록 한다.</p>
<br/>

<h2 id="✏️-간단-적용해보기">✏️ 간단 적용해보기</h2>
<h3 id="1-시맨틱-html과-aria-속성-적용">1. 시맨틱 HTML과 ARIA 속성 적용</h3>
<h4 id="1-1-모달-다이얼로그-접근성">1-1. 모달 다이얼로그 접근성</h4>
<p><strong>적용 전:</strong></p>
<pre><code class="language-tsx">&lt;div v-if=&quot;isOpen&quot; class=&quot;form-modal-background&quot; @click.self=&quot;handleClose&quot;&gt;
  &lt;div class=&quot;form-modal&quot;&gt;
    &lt;h3&gt;{{ modalTitle }}&lt;/h3&gt;
    &lt;!-- 폼 내용 --&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<p><strong>적용 후:</strong></p>
<pre><code class="language-tsx">&lt;div
  v-if=&quot;isOpen&quot;
  class=&quot;form-modal-background&quot;
  @click.self=&quot;handleClose&quot;
  role=&quot;dialog&quot;
  :aria-modal=&quot;isOpen&quot;
  aria-labelledby=&quot;modal-title&quot;
&gt;
  &lt;div class=&quot;form-modal&quot;&gt;
    &lt;h3 id=&quot;modal-title&quot;&gt;{{ modalTitle }}&lt;/h3&gt;
    &lt;!-- 폼 내용 --&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li><code>role=&quot;dialog&quot;</code>: 스크린 리더에게 이것이 대화상자임을 알림</li>
<li><code>aria-modal=&quot;true&quot;</code>: 모달이 열려있을 때 배경 컨텐츠를 무시하도록 지시</li>
<li><code>aria-labelledby</code>: 모달의 제목을 명시적으로 연결</li>
</ul>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/f650bb9a-be9a-4050-be7b-ca71476e3c3c/image.png' width='1024' />
</div>

<p>추가한 접근성 속성은 개발자 도구 Elements → Accessibility 에서 접근성 트리와 각 요소에 적용된 접근성 속성을 확인할 수 있다.</p>
<hr>
<h4 id="1-2-폼-입력-필드-접근성">1-2. 폼 입력 필드 접근성</h4>
<p><strong>적용 전:</strong></p>
<pre><code class="language-tsx">&lt;input
  :value=&quot;modelValue&quot;
  :placeholder=&quot;placeholder&quot;
  :required=&quot;required&quot;
  @input=&quot;handleInput&quot;
/&gt;
</code></pre>
<p><strong>적용 후:</strong></p>
<pre><code class="language-tsx">&lt;input
  :id=&quot;props.id&quot;
  :name=&quot;props.name&quot;
  :value=&quot;modelValue&quot;
  :placeholder=&quot;placeholder&quot;
  :required=&quot;required&quot;
  @input=&quot;handleInput&quot;
  :aria-invalid=&quot;props.invalid&quot;
  :aria-describedby=&quot;props.invalid ? `${props.id}-error` : undefined&quot;
/&gt;
&lt;p
  v-if=&quot;props.invalid &amp;&amp; props.message&quot;
  :id=&quot;`${props.id}-error`&quot;
  class=&quot;error-message&quot;
&gt;
  {{ props.message }}
&lt;/p&gt;
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li><code>id</code>와 <code>name</code>: 폼 요소 식별 및 label 연결</li>
<li><code>aria-invalid</code>: 입력값이 유효하지 않음을 스크린 리더에게 알림</li>
<li><code>aria-describedby</code>: 에러 메시지를 입력 필드와 연결</li>
<li>에러 메시지에 고유 <code>id</code> 부여</li>
</ul>
<hr>
<h4 id="1-3-label과-input-연결">1-3. Label과 Input 연결</h4>
<p><strong>적용 전:</strong></p>
<pre><code class="language-tsx">&lt;label class=&quot;form-label&quot;&gt;
  {{ field.label }}
  &lt;span v-if=&quot;field.required&quot;&gt;*&lt;/span&gt;
&lt;/label&gt;
&lt;FormInput :model-value=&quot;getFieldValue(field.name)&quot; /&gt;
</code></pre>
<p><strong>적용 후:</strong></p>
<pre><code class="language-tsx">&lt;label class=&quot;form-label&quot; :for=&quot;field.name&quot;&gt;
  {{ field.label }}
  &lt;span v-if=&quot;field.required&quot; class=&quot;required-mark&quot;&gt;*&lt;/span&gt;
&lt;/label&gt;
&lt;FormInput
  :id=&quot;field.name&quot;
  :name=&quot;field.name&quot;
  :model-value=&quot;getFieldValue(field.name)&quot;
/&gt;
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li><code>&lt;label for=&quot;field-name&quot;&gt;</code>: label과 input을 명시적으로 연결</li>
<li>스크린 리더가 입력 필드에 포커스할 때 레이블을 읽어줌</li>
<li>레이블을 클릭하면 해당 입력 필드에 포커스 이동</li>
</ul>
<hr>
<h3 id="2-키보드-접근성">2. 키보드 접근성</h3>
<h4 id="2-1-버튼-접근성">2-1. 버튼 접근성</h4>
<pre><code class="language-tsx">&lt;button
  :type=&quot;type&quot;
  :disabled=&quot;disabled&quot;
  :aria-label=&quot;ariaLabel&quot;
  class=&quot;icon-button&quot;
&gt;
  &lt;slot /&gt;
&lt;/button&gt;
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li>아이콘만 있는 버튼에 <code>aria-label</code> 추가</li>
<li><code>disabled</code> 상태를 명시적으로 관리</li>
<li>키보드로 포커스 및 Enter/Space로 실행 가능</li>
</ul>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/649b56fb-c3a1-4d0c-9aa8-43831cfeffa7/image.gif' width='1024' />
</div>


<p>탭(Tab) 키 입력시 Todo 버튼에 먼저 포커스가 가는 것을 확인할 수 있다.</p>
<hr>
<h4 id="2-2-포커스-스타일-명시">2-2. 포커스 스타일 명시</h4>
<pre><code class="language-tsx">.form-input:focus,
.form-textarea:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.form-input.form-error:focus {
  border-color: #ef4444;
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li><code>:focus</code> 상태에서 명확한 시각적 피드백 제공</li>
<li><code>outline</code>을 제거하는 대신 <code>box-shadow</code>로 포커스 표시</li>
<li>에러 상태에서도 구분되는 포커스 스타일</li>
</ul>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/a8a9af92-4d47-463b-80b8-d254b0a4ec7c/image.gif' width='1024' />
</div>


<p>폼 내에서도 동일하게 설정한 속성에 따라 포커스가 이동한다.</p>
<hr>
<h3 id="3-반응형-디자인과-접근성">3. 반응형 디자인과 접근성</h3>
<pre><code class="language-tsx">// 모바일에서도 충분한 터치 영역 확보
.icon-button {
  padding: 0.5rem;  // 최소 44x44px 터치 영역
  cursor: pointer;
}

// 가독성을 위한 반응형 폰트 크기
h1 {
  font-size: 1.5rem;

  @media (min-width: 768px) {
    font-size: 2rem;
  }
}
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li>WCAG 권장 최소 터치 영역(44x44px) 준수</li>
<li>작은 화면에서도 가독성 있는 폰트 크기</li>
<li>충분한 색상 대비 (텍스트와 배경)</li>
</ul>
<hr>
<h3 id="4-의미-있는-에러-메시지">4. 의미 있는 에러 메시지</h3>
<p><strong>적용 전:</strong></p>
<pre><code class="language-tsx">if (!value) {
  showToast({ message: &#39;입력해주세요.&#39;, variant: &#39;error&#39; });
}
</code></pre>
<p><strong>적용 후:</strong></p>
<pre><code class="language-tsx">if (!value || String(value).trim() === &#39;&#39;) {
  field.invalid = true;
  field.message = `${field.label}을(를) 입력해주세요.`;
  showToast({ message: &#39;필수 항목을 입력해주세요.&#39;, variant: &#39;error&#39; });
}
</code></pre>
<p><strong>개선 사항:</strong></p>
<ul>
<li>어떤 필드에 문제가 있는지 명확히 표시</li>
<li>Toast와 인라인 에러 메시지를 함께 제공</li>
<li>스크린 리더가 <code>aria-describedby</code>를 통해 에러 메시지 읽기</li>
</ul>
<hr>
<h3 id="5-스크린-리더-테스트">5. 스크린 리더 테스트</h3>
<p>실제로 스크린 리더(macOS VoiceOver, Windows Narrator)로 테스트한 결과:</p>
<p><strong>개선 전:</strong></p>
<ul>
<li>&quot;버튼&quot; (아이콘 버튼의 역할을 알 수 없음)</li>
<li>&quot;입력 필드&quot; (어떤 정보를 입력해야 하는지 모름)</li>
<li>모달이 열려도 배경 컨텐츠를 계속 읽음</li>
</ul>
<p><strong>개선 후:</strong></p>
<ul>
<li>&quot;Todo 추가 버튼&quot;</li>
<li>&quot;제목, 필수 항목, 입력 필드&quot;</li>
<li>&quot;모달 대화상자 - Todo 추가&quot;</li>
<li>유효하지 않은 입력: &quot;제목을 입력해주세요&quot;</li>
</ul>
   <div align='center'>
    <video src='https://velog.velcdn.com/images/tkddn_dev8430/post/ea644814-0dca-417f-8618-cf0937163aa1/image.mp4' width='1024' />
</div>

</br>

<h2 id="정리">정리</h2>
<p>웹 접근성을 적용한 결과:</p>
<ul>
<li>✅ 키보드만으로 모든 기능 사용 가능</li>
<li>✅ 스크린 리더로 의미 있는 정보 전달</li>
<li>✅ 시각적 피드백이 명확함 (포커스, 에러 상태)</li>
<li>✅ 모바일에서도 충분한 터치 영역</li>
<li>✅ WCAG 2.1 Level A 기준 충족</li>
</ul>
<h3 id="그럼-앱에서-접근성을-고려하려면">그럼 앱에서 접근성을 고려하려면?</h3>
<p>모바일 서비스를 이용하는 누구나 불편한 없이 기기를 이용할 수 있게 하는 개념</p>
<ul>
<li>다크모드</li>
<li>큰 글씨 모드</li>
<li>UX 동작 수정 ( 길게 터치 후 끌어다 놓기 → 손이 불편하신 분들은 활용이 어려움)</li>
</ul>
<p>⇒ 웹 접근성은 특별한 기능이나, 정해진 규칙에 맞추는 것이 아닌 <strong>다양한 어려움을 예방하는 UX/UI를 제공하는 것이 핵심</strong></p>
</br>

<h2 id="🤨-오-이거-뭔가-만들-수-있을-것-같은데">🤨 오? 이거 뭔가 만들 수 있을 것 같은데</h2>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/8f0d3c11-0ea5-476e-8edf-633e85f9c1fe/image.png' width='1024' />
</div>
지난 우아한 테크 컴퍼런스에서 AST관련한 세션을 들었었다. AST를 통해서 코드에 대한 다양한 처리가 가능하다는 가능성을 실감했던 세션이라서 이번 기회에 활용해보면 좋지 않을까 하는 생각이 들었다.


<h3 id="🎯-문제-정의">🎯 문제 정의</h3>
<p>개발자들이 ARIA 속성을 사용할 때 겪는 어려움:</p>
<ol>
<li><strong>ARIA 속성과 값에 대한 이해 부족</strong><ul>
<li>어떤 속성에 어떤 값을 넣어야 하는지 헷갈림</li>
<li>예: <code>aria-expanded</code>에 <code>true</code>를 넣어야 할까, <code>&quot;true&quot;</code>를 넣어야 할까?</li>
</ul>
</li>
<li><strong>프로젝트별 맥락 차이</strong><ul>
<li>다른 리소스를 참고해도 내 프로젝트에는 다르게 적용해야 하는 경우</li>
<li>예: 같은 토글 버튼이라도 디자인 시스템에 따라 구현이 다름</li>
</ul>
</li>
<li><strong>동적 상태 관리의 어려움</strong> ⭐ <strong>(핵심)</strong><ul>
<li>상태에 따라 동적으로 변경되어야 하는 ARIA 속성</li>
<li>예: 토글 버튼의 <code>aria-expanded</code>가 항상 <code>&quot;false&quot;</code>로 고정됨</li>
</ul>
</li>
</ol>
<h3 id="🔧-기존-해결-방식의-한계">🔧 기존 해결 방식의 한계</h3>
<p><strong>기존 도구</strong>: <code>eslint-plugin-jsx-a11y</code></p>
<p>✅ <strong>해결 가능</strong></p>
<ul>
<li>ARIA 속성과 값에 대한 이해 부족 → 문법 검증으로 해결</li>
<li>프로젝트별 맥락 차이 → 설정 커스터마이징으로 해결</li>
</ul>
<p>❌ <strong>해결 불가능</strong></p>
<ul>
<li><p><strong>동적 상태 관리</strong> → 정적 분석의 한계</p>
<pre><code class="language-jsx">  // ❌ 이런 오류를 기존 도구는 찾지 못함
  &lt;button onClick={toggle} aria-expanded=&quot;false&quot;&gt;
    {/* 클릭해도 aria-expanded가 항상 false! */}
  &lt;/button&gt;
</code></pre>
</li>
</ul>
<p><strong>왜 불가능한가?</strong></p>
<ul>
<li>동적 값은 <strong>런타임</strong>에 결정됨</li>
<li>기존 ESLint 플러그인은 <strong>빌드 타임</strong> 정적 분석만 수행</li>
<li>상태(State)와 ARIA 속성의 연결 관계를 추론하지 못함</li>
</ul>
<h3 id="💡-해결-방식-eslint-plugin-aria-state-validator">💡 해결 방식: <code>eslint-plugin-aria-state-validator</code></h3>
<p><a href="https://www.npmjs.com/package/eslint-plugin-aria-state-validator"></a></p>
<p><strong>핵심 아이디어</strong>: AST 분석으로 <strong>상태와 ARIA 속성의 연결 관계를 추론</strong></p>
<h3 id="작동-원리">작동 원리</h3>
<pre><code class="language-jsx">// 1. JSXElement 순회: role 및 aria-* 속성 식별
&lt;button onClick={() =&gt; setOpen(!isOpen)} aria-expanded=&quot;false&quot;&gt;

// 2. 스코프 추적: context.getScope()로 변수 추적
//    → onClick 핸들러에서 &#39;isOpen&#39; 변수 발견

// 3. 상태 추론: useState 훅에서 파생된 상태인지 판단
//    → const [isOpen, setOpen] = useState(false)

// 4. 패턴 검증 &amp; 자동 수정
//    ❌ aria-expanded=&quot;false&quot; (정적)
//    ✅ aria-expanded={isOpen ? &#39;true&#39; : &#39;false&#39;} (동적)
</code></pre>
<h3 id="두-가지-검증-규칙">두 가지 검증 규칙</h3>
<ol>
<li><strong><code>state-dependent-aria-validator</code></strong> (동적 상태 검증)<ul>
<li>상태와 ARIA 속성의 바인딩 검증</li>
<li>80% 자동 수정 가능</li>
</ul>
</li>
<li><strong><code>static-aria-validator</code></strong> (정적 ARIA 검증)<ul>
<li>기존 도구 보완 (오타, 잘못된 값, 충돌 감지)</li>
</ul>
</li>
</ol>
<h3 id="⚖️-트레이드오프">⚖️ 트레이드오프</h3>
<p><strong>React 생태계로 범위 한정</strong></p>
<ul>
<li><code>useState</code> 훅 또는 Props 통한 상태 전달 추론</li>
<li>React 외 프레임워크(Vue, Svelte 등)는 미지원</li>
</ul>
<p><strong>이유</strong>:</p>
<ul>
<li><p>프레임워크마다 상태 관리 패턴이 다름</p>
<ul>
<li><p>Vue: <code>ref</code>, <code>reactive</code></p>
</li>
<li><p>Svelte: <code>$:</code> reactive statements</p>
</li>
<li><p>Solid: <code>createSignal</code></p>
<pre><code class="language-jsx">// React
const [isOpen, setOpen] = useState(false)

// Vue (향후 지원 가능)
const isOpen = ref(false)

// Svelte (향후 지원 가능)
let isOpen = $state(false)</code></pre>
</li>
</ul>
</li>
<li><p>React의 <code>useState</code> 패턴이 가장 명확하고 일관적</p>
</li>
</ul>
</br>

<h2 id="reference">Reference</h2>
<p><strong>한국 접근성 인증 평가원 / 웹 접근성이란? -</strong> <a href="https://www.wa.or.kr/m1/sub1.asp">https://www.wa.or.kr/m1/sub1.asp</a></p>
<p><strong>토스의 모바일 접근성 -</strong> <a href="https://toss.im/tossfeed/article/tinyquestions-disability-5">https://toss.im/tossfeed/article/tinyquestions-disability-5</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useDeferredValue 이해하기]]></title>
            <link>https://velog.io/@tkddn_dev8430/useDeferredValue-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/useDeferredValue-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 03 Feb 2026 08:42:37 GMT</pubDate>
            <description><![CDATA[<p>여러가지 간단한 케이스로 라이브 코딩 테스트 준비를 하는 과정에서 useDeferredValue를 새롭게 알게 되었고, 앞으로 활용도가 많은 훅인 것 같아 어떤 역할을 하고, 어떤 경우에 사용하면 좋을지 생각해보았다.</p>
<h3 id="usedeferredvalue">useDeferredValue</h3>
<p>useDeferredValue는 UI 업데이트를 지연시키는 역할을 하는 React Hook이고, 용법은 아래와 같다.</p>
<pre><code class="language-jsx">const deferedValue = useDeferredValue(value, initialValue);</code></pre>
<ul>
<li>value는 지연시키려는 값, 상태를 의미한다.</li>
<li>initialValue는 옵셔널 값이며, 초기 렌더링에 사용할 값을 의미한다.
→ initialValue를 설정하지 않은 경우 초기 렌더링 동안에는 값을 지연시키지 않는다.</li>
</ul>
<h3 id="지연">지연?</h3>
<p>계속 &#39;지연&#39;시킨다는 표현을 사용하고 있는데, 정확히 무엇을 지연시키는 걸까?</p>
<p>useDeferredValue는 <strong>값의 업데이트를 지연</strong>시킨다. 상태가 변경되어도 무거운 컴포넌트에 전달되는 값을 즉시 업데이트하지 않고, React의 우선순위 스케줄링을 통해 나중에 업데이트한다. 이를 통해 사용자 입력과 같은 긴급한 UI 업데이트가 무거운 렌더링에 의해 블로킹되지 않도록 한다.</p>
<h4 id="예시">예시</h4>
<p><a href="https://ko.react.dev/reference/react/useDeferredValue#usage">공식문서의 예시</a>를 가져오면 아래와 같다.</p>
<pre><code class="language-jsx">// App.js
import SearchResults from &#39;./SearchResults.js&#39;;

export default function App() {
  const [query, setQuery] = useState(&#39;&#39;);
  const deferredQuery = useDeferredValue(query);
  return (
    &lt;&gt;
      &lt;label&gt;
        Search albums:
        &lt;input value={query} onChange={e =&gt; setQuery(e.target.value)} /&gt;
      &lt;/label&gt;
      &lt;Suspense fallback={&lt;h2&gt;Loading...&lt;/h2&gt;}&gt;
        &lt;SearchResults query={deferredQuery} /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}

// SearchResult
import {use} from &#39;react&#39;;
import { fetchData } from &#39;./data.js&#39;;

export default function SearchResults({ query }) {
  if (query === &#39;&#39;) {
    return null;
  }
  const albums = use(fetchData(`/search?q=${query}`));
  if (albums.length === 0) {
    return &lt;p&gt;No matches for &lt;i&gt;&quot;{query}&quot;&lt;/i&gt;&lt;/p&gt;;
  }
  return (
    &lt;ul&gt;
      {albums.map(album =&gt; (
        &lt;li key={album.id}&gt;
          {album.title} ({album.year})
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}
</code></pre>
<p>여기서 쿼리가 변함에 따라 검색 결과가 바뀌게 된다. useDeferredValue를 쓰지 않는다면 유저가 타이핑을 치는 모든 순간에 SearchResult가 리렌더링된다.</p>
<p>useDeferredValue로 인해서 query로 인한 SearchResult의 리렌더링을 상태 업데이트 전에 UI를 임시로 보여줌으로서 매번 리렌더링 되는 것을 막는다.</p>
<br/>

<h3 id="헷갈리는데">헷갈리는데…</h3>
<p>useDeferredValue를 보면서 헷갈리는 지점이 꽤 있었다.</p>
<h4 id="1-suspense와-함께-사용하기">1. Suspense와 함께 사용하기</h4>
<p>Suspense와 useDeferredValue를 함께 사용할 때 어떤 UI가 표시되는지가 가장 헷갈렸다.</p>
<p>처음에는 검색어를 타이핑한 후 Loading 컴포넌트 없이 바로 UI가 리렌더링되는 것을 보고, useDeferredValue로 인해 보이는 이전 UI가 Fallback UI 역할을 하는 것처럼 느껴졌다.</p>
<p>하지만 이는 잘못된 이해였다. Suspense는 하위 컴포넌트에서 Promise를 감지했을 때, 해당 범위 내에 이전에 commit된 컴포넌트가 없으면 Fallback UI를 표시한다.</p>
<p>useDeferredValue와 함께 사용하면, Suspense가 새로운 Promise를 감지하더라도 이미 이전에 commit된 UI(null 포함)가 존재하기 때문에 Fallback UI를 표시하지 않고 기존 UI를 유지한다. 이것이 useDeferredValue와 Suspense를 조합했을 때의 핵심 동작 방식이다.</p>
<h4 id="2-디바운스-쓰로틀">2. 디바운스? 쓰로틀?</h4>
<p>공식 문서 마지막에서 다루는 내용이 바로 이 부분이었다.</p>
<p>평소 스크롤이나 키보드 입력에 debounce를 주로 사용해왔기 때문에, 비슷해 보이는 useDeferredValue라는 Hook이 왜 등장했는지, 어떤 차이가 있는지 궁금했다.</p>
<p>결론적으로 두 방식 모두 최적화를 목적으로 하지만, 최적화 대상이 다르다. Debounce와 throttle은 함수 호출 횟수를 제어한다. Debounce는 일정 시간 내 마지막 함수 호출만 처리하고, throttle은 일정 시간 내 중복 호출을 무시하는 방식으로 최적화한다.</p>
<p>반면 useDeferredValue는 렌더링 최적화에 특화되어 있다. 상태 변경으로 인한 렌더링을 지연시키되, 이전에 commit된 UI를 유지함으로써 무거운 렌더링이 다른 UI 업데이트를 블로킹하지 않도록 한다. 따라서 백그라운드에서 useDeferredValue는 함수 호출 자체를 최적화하지는 않는다.</p>
<p>처음에는 &quot;어느 것이 더 좋은가&quot;의 관점으로 접근했지만, 둘의 목적이 다르다는 것을 이해하고 나니 각각의 사용 시점이 명확해졌다.</p>
<br />

<h3 id="마치며">마치며</h3>
<p>이번 훅을 이해해보면서, 기존에 debouce와 throttle로 해결하던 문제를 React의 렌더링 시스템과 통합하여 효과적으로 해결하려는 의도가 있지 않았나 생각해보았다.</p>
<p>새로운 훅을 보면서 어떤 부분들을 지속적으로 개선하려고하는지 상상해보는 점은 늘 흥미로운 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[재사용 가능한 필터 컴포넌트 설계하기]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-%ED%95%84%ED%84%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-%ED%95%84%ED%84%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 28 Jan 2026 09:35:33 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이번 글에서는 필터 컴포넌트 설계하면서 고민했던 과정을 공유하려고 합니다. 단순히 컴포넌트를 나누는 것을 넘어, <strong>어떻게 하면 재사용 가능하면서도 유연한 구조를 만들 수 있을까</strong>에 초점을 맞췄습니다.</p>
<h2 id="문제-정의">문제 정의</h2>
<p>( *****예시는 실제 프로젝트와 다소 차이가 있습니다. )</p>
<p>프로젝트에는 여러 개의 리스트 페이지가 있었습니다:</p>
<ul>
<li>영화 검색 페이지: 장르, 연도, 평점 필터 + 정렬</li>
<li>감독별 영화 페이지: 연도, 평점 필터 + 정렬 (장르 필터 제외)</li>
<li>배우별 영화 페이지: 역할, 연도 필터 + 정렬</li>
</ul>
<p>각 페이지마다 필터 조합이 다르지만, UI 패턴은 유사했습니다:</p>
<ul>
<li>드롭다운 선택 (정렬, 연도)</li>
<li>체크박스 다중 선택 (장르, 역할)</li>
<li>칩 선택 (평점)</li>
<li>필터 초기화 버튼</li>
</ul>
<p>처음에는 각 페이지마다 필터를 구현했지만, 코드 중복이 심각했고 수정 시 여러 곳을 동시에 고쳐야 하는 문제가 있었습니다.</p>
<h2 id="요구사항-정리">요구사항 정리</h2>
<p>리팩토링을 시작하기 전에 요구사항을 정리했습니다:</p>
<ol>
<li><strong>재사용성</strong>: 같은 UI 패턴(드롭다운, 체크박스)을 여러 곳에서 사용</li>
<li><strong>유연성</strong>: 페이지마다 다른 필터 조합을 쉽게 구성</li>
<li><strong>일관성</strong>: 모든 필터가 동일한 방식으로 상태를 관리</li>
<li><strong>조건부 표시</strong>: 필터가 활성화되었을 때만 초기화 버튼 표시</li>
<li><strong>확장성</strong>: 새로운 필터 타입을 쉽게 추가 가능</li>
</ol>
<h2 id="3단계의-계층-구조">3단계의 계층 구조</h2>
<p>고민 끝에 선택한 구조는 <strong>3단계 계층 구조</strong>였습니다:</p>
<pre><code class="language-jsx">ListOptions/
├── core/           # 재사용 가능한 기본 UI 컴포넌트
│   ├── RadioSelector.tsx
│   ├── CheckboxSelector.tsx
│   └── ChipSelector.tsx
├── tools/          # 비즈니스 로직이 포함된 도구 컴포넌트
│   ├── SortDropdown.tsx
│   ├── GenreCheckbox.tsx
│   └── RatingChip.tsx
└── groups/         # 페이지별 필터 조합
    ├── SearchPageTools.tsx
    └── DirectorPageTools.tsx
</code></pre>
<h3 id="core-layer-순수-ui-컴포넌트">Core Layer: 순수 UI 컴포넌트</h3>
<p>가장 하위 계층은 <strong>비즈니스 로직이 전혀 없는 순수 UI 컴포넌트</strong>입니다.</p>
<pre><code class="language-jsx">// core/RadioSelector.tsx
export default function RadioSelector({
  options,
  selected,
  paramKey,
  valueMap,
}: {
  options: string[];
  selected?: string;
  paramKey: string;
  valueMap?: Record&lt;string, string&gt;;
}) {
  const { updateSearchParams } = useRouterWithParams();

  const handleSelect = (option: string) =&gt; {
    const value = valueMap ? valueMap[option] : option;
    updateSearchParams({ [paramKey]: value });
  };

  return (
    &lt;Modal&gt;
      {options.map((option) =&gt; (
        &lt;button onClick={() =&gt; handleSelect(option)}&gt;
          {option}
        &lt;/button&gt;
      ))}
    &lt;/Modal&gt;
  );
}
</code></pre>
<p>핵심은 <strong><code>paramKey</code>와 <code>valueMap</code>을 외부에서 주입</strong>받는다는 점입니다. 이렇게 하면:</p>
<ul>
<li>&quot;추천순&quot; → &quot;recommended&quot; 같은 변환 로직을 외부에서 제어</li>
<li>같은 UI를 다른 URL 파라미터에 재사용 가능</li>
</ul>
<h3 id="tools-layer-도메인-로직을-가진-컴포넌트">Tools Layer: 도메인 로직을 가진 컴포넌트</h3>
<p>중간 계층은 <strong>특정 필터의 데이터와 로직을 담당</strong>합니다.</p>
<pre><code class="language-jsx">// tools/SortDropdown.tsx
const SORT_OPTIONS = [&quot;추천순&quot;, &quot;최신순&quot;, &quot;평점순&quot;];

const SORT_MAP = {
  추천순: &quot;recommended&quot;,
  최신순: &quot;latest&quot;,
  평점순: &quot;rating&quot;,
};

const REVERSE_SORT_MAP = {
  recommended: &quot;추천순&quot;,
  latest: &quot;최신순&quot;,
  rating: &quot;평점순&quot;,
};

export default function SortDropdown() {
  const router = useRouter();
  const currentSort = (router.query.sort as string) || &quot;recommended&quot;;

  return (
    &lt;RadioSelector
      options={SORT_OPTIONS}
      selected={REVERSE_SORT_MAP[currentSort]}
      paramKey=&quot;sort&quot;
      valueMap={SORT_MAP}
    /&gt;
  );
}
</code></pre>
<p>여기서 중요한 점:</p>
<ul>
<li><strong>데이터 변환 로직</strong>: URL의 &quot;recommended&quot;를 화면의 &quot;추천순&quot;으로 변환</li>
<li><strong>기본값 처리</strong>: sort가 없을 때 &quot;recommended&quot;를 기본값으로</li>
<li><strong>Core 컴포넌트 활용</strong>: RadioSelector를 재사용</li>
</ul>
<h3 id="groups-layer-페이지별-조합">Groups Layer: 페이지별 조합</h3>
<p>최상위 계층은 <strong>페이지에 필요한 필터를 조합</strong>합니다.</p>
<pre><code class="language-jsx">// groups/SearchPageTools.tsx
export default function SearchPageTools() {
  const { hasActiveFilters, resetFilterQuery } = useRouterWithParams();
  const excludeKeys = [&quot;query&quot;]; // 검색어는 초기화 대상에서 제외
  const showReset = hasActiveFilters(excludeKeys);

  return (
    &lt;div className=&quot;flex gap-4&quot;&gt;
      &lt;SortDropdown /&gt;
      {showReset &amp;&amp; &lt;FilterResetButton onClick={() =&gt; resetFilterQuery(excludeKeys)} /&gt;}
      &lt;GenreCheckbox /&gt;
      &lt;RatingChip /&gt;
    &lt;/div&gt;
  );
}`

`// groups/DirectorPageTools.tsx
export default function DirectorPageTools() {
  const { hasActiveFilters, resetFilterQuery } = useRouterWithParams();
  const excludeKeys = [&quot;directorId&quot;]; // 감독 ID는 초기화 대상에서 제외
  const showReset = hasActiveFilters(excludeKeys);

  return (
    &lt;div className=&quot;flex gap-4&quot;&gt;
      &lt;SortDropdown /&gt;
      {showReset &amp;&amp; &lt;FilterResetButton onClick={() =&gt; resetFilterQuery(excludeKeys)} /&gt;}
      &lt;RatingChip /&gt;
      {/* GenreCheckbox는 제외 */}
    &lt;/div&gt;
  );
}
</code></pre>
<p>이렇게 하면:</p>
<ul>
<li>검색 페이지는 모든 필터를 표시</li>
<li>감독 페이지는 장르 필터를 제외</li>
<li>각 페이지의 초기화 로직도 독립적으로 관리</li>
</ul>
<p>가 가능해집니다.</p>
<h2 id="의사결정-유연함과-재사용성">의사결정: 유연함과 재사용성</h2>
<p>유연함과 재사용성을 모두 만족하는 컴포넌트를 만들기 위해, 먼저 각 UI가 어떤 성격을 가지고 있는지를 이해하는 것부터 시작했습니다.
그리고 그 성격 안에서 무엇이 변할 수 있는 지점인지, 또 그 변화를 구현부에서 얼마나 편리하게 다룰 수 있도록 만들 수 있을지라는 흐름으로 고민을 이어갔습니다.</p>
<p>정렬 기능을 예로 들어보면, 정렬은 그 자체로 하나의 비즈니스 요구사항입니다. 동시에 이 요구사항을 해결하는 UI는 드롭다운, 버튼 그룹 등 다양한 형태로 확장될 수 있다고 판단했습니다.
이 시점에서 이미 두 가지 관심사가 분리됩니다.</p>
<ul>
<li>정렬이라는 도메인 로직</li>
<li>이를 표현하는 UI</li>
</ul>
<p>여기에 더해, 정렬 기능을 사용하는 각 페이지마다 어떤 정렬 옵션을 제공할지, 혹은 다른 필터들과 어떻게 함께 사용될지가 모두 달랐습니다.
이 요구사항을 하나의 컴포넌트에서 props나 조건문으로 처리하기보다는, 구현하는 쪽에서 선택적으로 조합할 수 있는 구조가 더 적합하다고 판단했습니다.</p>
<p>그래서 단일 컴포넌트에 모든 책임을 몰아넣는 대신, 조합(Composition) 패턴을 활용해 역할을 분리했습니다.
그 결과, UI만 책임지는 컴포넌트, 도메인 로직을 담은 컴포넌트, 그리고 이를 페이지 단위로 조합하는 컴포넌트까지 총 3단계의 컴포넌트 구조로 정렬 기능을 구현할 수 있었습니다.</p>
<p>이러한 의사결정을 통해, 각 컴포넌트는 더 단순해졌고, 새로운 페이지나 요구사항이 추가되더라도 기존 코드를 크게 수정하지 않고 유연하게 대응할 수 있는 구조를 만들 수 있었습니다</p>
<h2 id="의사결정--조건부-필터-초기화">의사결정 : 조건부 필터 초기화</h2>
<p>&quot;초기화 버튼을 언제 보여줄까?&quot;가 의외로 복잡한 문제였습니다.</p>
<p><strong>문제점:</strong></p>
<ul>
<li>검색 페이지: 검색어(<code>query</code>)는 유지하고 필터만 초기화</li>
<li>감독 페이지: 감독 ID(<code>directorId</code>)는 라우트 파라미터이므로 필터가 아님</li>
</ul>
<p>처음에는 초기화 버튼이 필터 종류를 직접 알고 있었지만, 이는 페이지마다 필터 구성이 바뀔 때마다 수정이 필요했습니다.</p>
<p><strong>해결책: excludeKeys 패턴</strong></p>
<pre><code class="language-jsx">const FILTER_KEYS = [&quot;sort&quot;, &quot;genre&quot;, &quot;rating&quot;] as const;

function hasActiveFilters(excludeKeys: string[] = []) {
return Object.keys(router.query).some(
(key) =&gt;
FILTER_KEYS.includes(key as typeof FILTER_KEYS[number]) &amp;&amp;
!excludeKeys.includes(key)
);
}

function resetFilterQuery(excludeKeys: string[] = []) {
const query = { ...router.query };

Object.keys(query).forEach((key) =&gt; {
if (!excludeKeys.includes(key)) {
delete query[key];
}
});

router.push({ pathname: router.pathname, query }, undefined, {
shallow: true,
});
}`

사용 예시:

`// 검색 페이지: query는 제외
const excludeKeys = [&quot;query&quot;];
const showReset = hasActiveFilters(excludeKeys);

// 감독 페이지: directorId는 제외
const excludeKeys = [&quot;directorId&quot;];
const showReset = hasActiveFilters(excludeKeys);</code></pre>
<p>이 패턴의 장점:</p>
<ul>
<li><strong>유연성</strong>: 각 페이지가 제외할 키를 자유롭게 지정</li>
<li><strong>명확성</strong>: 무엇을 제외하는지 코드에 명시적으로 표현</li>
<li><strong>확장성</strong>: 새로운 페이지를 추가해도 로직 수정 불필요</li>
</ul>
<h2 id="컴포넌트-합성의-힘">컴포넌트 합성의 힘</h2>
<p>이 아키텍처의 가장 큰 장점은 <strong>레고 블록처럼 조립</strong>할 수 있다는 점입니다.</p>
<p>새로운 페이지를 추가한다면?</p>
<pre><code class="language-jsx">// groups/ActorPageTools.tsx
export default function ActorPageTools() {
    const { hasActiveFilters, resetFilterQuery } = useRouterWithParams();
    const excludeKeys = [&quot;actorId&quot;];
    const showReset = hasActiveFilters(excludeKeys);

    return (
        &lt;div className=&quot;flex gap-4&quot;&gt;
            &lt;SortDropdown /&gt;
            {showReset &amp;&amp; &lt;FilterResetButton onClick={() =&gt; resetFilterQuery(excludeKeys)} /&gt;}
            &lt;RoleCheckbox /&gt;  {/* 새로운 필터 */}
        &lt;/div&gt;
    );
}</code></pre>
<p>새로운 필터 타입을 추가한다면?</p>
<pre><code class="language-jsx">// tools/RoleCheckbox.tsx
const ROLE_OPTIONS = [
    { value: &quot;lead&quot;, label: &quot;주연&quot; },
    { value: &quot;supporting&quot;, label: &quot;조연&quot; },
];

export default function RoleCheckbox() {
    const router = useRouter();
    const selected = (router.query.role as string)?.split(&quot;,&quot;) || [];

    return (
        &lt;CheckboxSelector
            options={ROLE_OPTIONS}
            selected={selected}
            paramKey=&quot;role&quot;
            label=&quot;역할&quot;
        /&gt;
                );
}</code></pre>
<p>Core 컴포넌트(<code>CheckboxSelector</code>)는 전혀 수정할 필요가 없습니다.</p>
<h2 id="배운-점과-트레이드오프">배운 점과 트레이드오프</h2>
<h3 id="잘된-점">잘된 점</h3>
<ol>
<li><strong>명확한 책임 분리</strong><ul>
<li>Core: UI 렌더링만</li>
<li>Tools: 데이터 변환과 기본값</li>
<li>Groups: 조합과 페이지별 로직</li>
</ul>
</li>
<li><strong>URL 기반 상태 관리</strong><ul>
<li>공유, 새로고침, 히스토리 모두 자연스럽게 해결</li>
<li>디버깅이 쉬움 (URL만 보면 현재 상태 파악)</li>
</ul>
</li>
<li><strong>높은 재사용성</strong><ul>
<li>RadioSelector는 정렬, 연도, 언어 필터 등에 재사용</li>
<li>CheckboxSelector는 장르, 역할, 태그 등에 재사용</li>
</ul>
</li>
</ol>
<h3 id="아쉬운점">아쉬운점</h3>
<ol>
<li><strong>관리 파일의 개수 증가</strong>
재사용성과 유연성을 제공하려고 하다보니 자연스럽게 파일의 개수가 늘어났다. 파일의 개수에 따라서 다른 개발자가 이 컴포넌트를 사용했을 때 각 컴포넌트들이 어떤 역할을 하고, 어떤 식으로 사용해야하는지에 대한 가이드를 곧바로 파악하기에는 어려움이 있을 것 같다고 느껴졌다.</li>
<li><strong>상태 관리 방식에 따른 대응 불가</strong>
현재 컴포넌트는 URL의 query 기반으로 상태를 관리하고 있다. 하지만 params를 사용하지 않는 경우에 대해서는 유연하게 대처하기가 어렵다.</li>
</ol>
<h2 id="마치며">마치며</h2>
<p>AI로 빠른 개발이 가능해지면서 설계에 대한 역량이 더 필요해지는 것 같다. 이번에 설계를 해보면서 단번에 최적의 설계를 찾기에는 어려움이 있는 것 같아서 앞으로 블로그 글을 통해서 정리하는 방식을 주로 활용해려고 한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Deep Dive Study - 2]]></title>
            <link>https://velog.io/@tkddn_dev8430/React-Deep-Dive-Study-2</link>
            <guid>https://velog.io/@tkddn_dev8430/React-Deep-Dive-Study-2</guid>
            <pubDate>Sun, 30 Nov 2025 13:28:45 GMT</pubDate>
            <description><![CDATA[<h2 id="-usestate와-usereducer">!!. useState와 useReducer</h2>
<ol>
<li>update 방식을 제한할 수 있다.</li>
</ol>
<pre><code class="language-jsx">// ❌ useState: 어떤 방식으로든 수정 가능
const [state, setState] = useState({ count: 0, user: {} });
setState({ count: 999 });  // 가능
setState({});  // 이것도 가능... 😱

// ✅ useReducer: 정의된 action으로만 수정 가능
const [state, dispatch] = useReducer(reducer, initialState);

dispatch({ type: &#39;INCREMENT&#39; });  // ✅ 허용된 action
dispatch({ type: &#39;UPDATE_USER&#39;, payload: user });  // ✅ 허용된 action
dispatch({ type: &#39;RANDOM_ACTION&#39; });  // ❌ reducer에서 처리 안 됨</code></pre>
<ol>
<li>비슷한 성격의 state를 묶어서 관리할 수 있다.</li>
</ol>
<p>+) state를 조작하는 Action도 묶어서 관리할 수 있다. reducer 내부에서 관리되기 때문에 응집도가 높다.</p>
<pre><code class="language-jsx">// ❌ useState: state가 분산됨
function Form() {
  const [name, setName] = useState(&#39;&#39;);
  const [email, setEmail] = useState(&#39;&#39;);
  const [age, setAge] = useState(0);
  const [address, setAddress] = useState(&#39;&#39;);
  const [phoneNumber, setPhoneNumber] = useState(&#39;&#39;);
  // state가 5개로 분산되어 관리가 어려움
}

// ✅ useReducer: 관련된 state를 하나로 묶음
function formReducer(state, action) {
  switch (action.type) {
    case &#39;UPDATE_FIELD&#39;:
      return { ...state, [action.field]: action.value };
    case &#39;RESET_FORM&#39;:
      return initialFormState;
    default:
      return state;
  }
}

const initialFormState = {
  name: &#39;&#39;,
  email: &#39;&#39;,
  age: 0,
  address: &#39;&#39;,
  phoneNumber: &#39;&#39;
};

function Form() {
  const [formData, dispatch] = useReducer(formReducer, initialFormState);

  // 하나의 state로 관련 데이터 관리
  // 모든 업데이트 로직이 reducer에 집중
  const updateField = (field, value) =&gt; 
    dispatch({ type: &#39;UPDATE_FIELD&#39;, field, value });
}</code></pre>
<h2 id="-useeffect는-언제-써야할까">??. useEffect는 언제 써야할까?</h2>
<p>useEffect는 &#39;부수효과&#39;를 처리하는 로직이다보니 설명이 사이드 이펙트처럼 느껴지기도 했고, 그만큼 디버깅도 어려운 훅이라고 생각이 들어 잘 사용해야하는 훅이라고 생각이 든다.</p>
<p>아래는 공식 문서에서 제공하는 useEffect 사용을 고민해야하는 사례를 설명하고 있는데 그중 몇가지만 소개해보려고 한다.
<a href="https://react.dev/learn/you-might-not-need-an-effect">You Might Not Need an Effect – React</a></p>
<h3 id="사례-1-props-변경-시-state-초기화하기">사례 1: Props 변경 시 State 초기화하기</h3>
<pre><code class="language-jsx">function ProfilePage({ userId }) {
  const [comment, setComment] = useState(&#39;&#39;);

  // 🔴 userId가 변경될 때마다 comment를 초기화하려고 Effect 사용
  useEffect(() =&gt; {
    setComment(&#39;&#39;);
  }, [userId]);

  return (
    &lt;div&gt;
      &lt;h1&gt;User {userId}&#39;s Profile&lt;/h1&gt;
      &lt;textarea
        value={comment}
        onChange={e =&gt; setComment(e.target.value)}
      /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li>Effect가 실행되면서 불필요한 리렌더링 발생</li>
<li>userId 변경 → 렌더링 → Effect 실행 → 다시 렌더링 (2번 렌더링!)</li>
</ul>
<p>수정안</p>
<pre><code class="language-jsx">function ProfilePage({ userId }) {
  return &lt;Profile userId={userId} key={userId} /&gt;;
}

function Profile({ userId }) {
  // ✅ key가 변경되면 React가 자동으로 컴포넌트를 재생성
  // State가 자동으로 초기화됨
  const [comment, setComment] = useState(&#39;&#39;);

  return (
    &lt;div&gt;
      &lt;h1&gt;User {userId}&#39;s Profile&lt;/h1&gt;
      &lt;textarea
        value={comment}
        onChange={e =&gt; setComment(e.target.value)}
      /&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p><strong>장점:</strong></p>
<ul>
<li>Effect 없이 자연스럽게 state 초기화</li>
<li>1번의 렌더링만 발생</li>
<li>React의 key 메커니즘 활용</li>
</ul>
<hr>
<h3 id="사례-2-렌더링을-위한-데이터-변환">사례 2: 렌더링을 위한 데이터 변환</h3>
<pre><code class="language-jsx">function TodoList({ todos, filter }) {
  const [visibleTodos, setVisibleTodos] = useState([]);

  // 🔴 todos나 filter가 변경될 때마다 Effect로 필터링
  useEffect(() =&gt; {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  return (
    &lt;ul&gt;
      {visibleTodos.map(todo =&gt; (
        &lt;li key={todo.id}&gt;{todo.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li>불필요한 state와 Effect</li>
<li>todos/filter 변경 → 렌더링 → Effect 실행 → 다시 렌더링 (2번 렌더링!)</li>
<li>메모리 낭비 (state를 별도로 저장)</li>
</ul>
<p><strong>수정안</strong></p>
<pre><code class="language-jsx">function TodoList({ todos, filter }) {
  // ✅ 렌더링 중에 직접 계산
  const visibleTodos = getFilteredTodos(todos, filter);

  return (
    &lt;ul&gt;
      {visibleTodos.map(todo =&gt; (
        &lt;li key={todo.id}&gt;{todo.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}

function getFilteredTodos(todos, filter) {
  if (filter === &#39;active&#39;) {
    return todos.filter(todo =&gt; !todo.completed);
  }
  if (filter === &#39;completed&#39;) {
    return todos.filter(todo =&gt; todo.completed);
  }
  return todos;
}</code></pre>
<p><strong>장점:</strong></p>
<ul>
<li>Effect 없이 간단하고 명확</li>
<li>1번의 렌더링만 발생</li>
<li>추가 state 불필요</li>
</ul>
<p>오히려 이때는 <code>useMemo</code> 를 사용하는게 효과적</p>
<pre><code class="language-jsx">function TodoList({ todos, filter }) {
  // ✅ 비용이 큰 계산은 useMemo로 캐싱
  const visibleTodos = useMemo(() =&gt; {
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);

  return (
    &lt;ul&gt;
      {visibleTodos.map(todo =&gt; (
        &lt;li key={todo.id}&gt;{todo.text}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}</code></pre>
<hr>
<h3 id="사례-3-이벤트-핸들러에서-처리해야-할-로직">사례 3: 이벤트 핸들러에서 처리해야 할 로직</h3>
<pre><code class="language-jsx">function ProductPage({ product, addToCart }) {
  // 🔴 product가 변경될 때마다 알림을 보내려고 Effect 사용
  useEffect(() =&gt; {
    if (product.isInCart) {
      showNotification(`${product.name}이(가) 장바구니에 추가되었습니다!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  return (
    &lt;button onClick={handleBuyClick}&gt;
      구매하기
    &lt;/button&gt;
  );
}
</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li>사용자가 구매 버튼을 누르지 않아도 알림이 뜰 수 있음</li>
<li>페이지 새로고침 시에도 알림이 뜸</li>
<li>&quot;특정 상호작용&quot;에 대한 반응이 아니라 &quot;상태 변화&quot;에 반응</li>
</ul>
<pre><code class="language-jsx">function ProductPage({ product, addToCart }) {
  function handleBuyClick() {
    // ✅ 사용자가 구매 버튼을 눌렀을 때만 실행
    addToCart(product);
    showNotification(`${product.name}이(가) 장바구니에 추가되었습니다!`);
  }

  return (
    &lt;button onClick={handleBuyClick}&gt;
      구매하기
    &lt;/button&gt;
  );
}</code></pre>
<p><strong>장점:</strong></p>
<ul>
<li>사용자의 <strong>특정 행동</strong>에 정확히 반응</li>
<li>의도가 명확함</li>
<li>예상치 못한 시점에 실행되지 않음</li>
</ul>
<hr>
<h3 id="그래서-effect는-언제-사용하나">그래서, Effect는 언제 사용하나?</h3>
<pre><code class="language-jsx">// ✅ 외부 시스템과 동기화할 때만 사용
useEffect(() =&gt; {
  // 브라우저 API
  const connection = createConnection(serverUrl, roomId);
  connection.connect();

  return () =&gt; {
    connection.disconnect();
  };
}, [serverUrl, roomId]);
</code></pre>
<p><strong>외부 시스템의 예:</strong></p>
<ul>
<li>네트워크 요청</li>
<li>브라우저 API (localStorage, geolocation 등)</li>
<li>타사 라이브러리 (D3, jQuery 등)</li>
<li>타이머 (setTimeout, setInterval)</li>
</ul>
<p><strong>관련  ESlint Plugin</strong></p>
<p><a href="https://www.npmjs.com/package/eslint-plugin-react-you-might-not-need-an-effect">ESLint - React - You Might Not Need An Effect
</a></p>
<h2 id="useref는-왜-current를-가지고-있을까-무슨-의도를-갖고-있는-걸까">??.useRef는 왜 current를 가지고 있을까? 무슨 의도를 갖고 있는 걸까?</h2>
<div align='center'>
<image src="https://velog.velcdn.com/images/tkddn_dev8430/post/a6b484a3-6192-4918-a988-b54d358f2c37/image.png"/>
</div>

<p>useRef는 기본적으로 렌더링에 필요하지 않을 값을 관리하려는 의도를 가지고 있는 훅이다.</p>
<p>만약 useRef내부가 일반 변수들 처럼 primitive하게 관리되었다면 매번 호출에 있어 초기화될 것이다. (불변성 유지 X)</p>
<pre><code class="language-jsx">// ❌ primitive로 관리했다면?
function Component() {
  let count = 0;  // 매 렌더마다 0으로 초기화

  const increment = () =&gt; {
    count += 1;
    console.log(count);  // 1, 2, 3...
  };

  return &lt;button onClick={increment}&gt;Click&lt;/button&gt;;
  // 리렌더링 시 count는 다시 0이 됨
}</code></pre>
<p>반대로 useState 처럼 관리된다면 useRef의 의도와 멀어지게 된다. ( 랜더링 발생 )</p>
<pre><code class="language-jsx">// ❌ useState로 관리했다면?
function Component() {
  const [count, setCount] = useState(0);

  const increment = () =&gt; {
    setCount(count + 1);  // 값은 유지되지만 리렌더링 발생!
  };

  return &lt;button onClick={increment}&gt;Click&lt;/button&gt;;
}</code></pre>
<p>위 두 문제 상황으로 접근해보면 useRef는 상태 변화에 있어서 리랜더링이 발생하면 안되고, 불변성이 유지되어야한다.</p>
<p>따라서 값은 바뀌어야하는데, 그 대상은 불변해야한다라는 모순적인 요구사항이 생기게 된다.</p>
<p>그래서 객체를 통해 이를 만족시키고자 한 것 같다.</p>
<pre><code class="language-jsx">// ✅ useRef의 실제 구조
const ref = useRef(0);

// ref 객체 자체는 불변 (같은 참조 유지)
console.log(ref);  // { current: 0 }

// current만 변경 가능
ref.current = 5;
ref.current = 10;

// 리렌더링 후에도
console.log(ref === 이전_렌더의_ref);  // true (같은 객체!)</code></pre>
<p>객체를 선언한 대상으로 불변성을 유지하고, 내부의 값(current)를 통해서 값은 지속적으로 바뀔 수 있도록 의도했다.</p>
<p>뿐만 아니라 React 내부에서 선언된 상태는 ReactFiber를 통해 사용되는 컴포넌트로 전달된다. 이때는 JS가 값을 주고받는 원리와 연결되어 있는데</p>
<pre><code class="language-jsx">// JavaScript 값 전달 특성

// Primitive: 값 복사
let a = 5;
let b = a;  // 새로운 메모리에 5를 복사
a = 10;
console.log(b);  // 5 (변경 안됨)

// Object: 참조 전달
let obj1 = { value: 5 };
let obj2 = obj1;  // 같은 객체를 참조
obj1.value = 10;
console.log(obj2.value);  // 10 (같은 객체를 가리킴)</code></pre>
<p>primvitive한 값인 경우에는 동일한 값을 복사하지만, 객체는 원본 객체를 참조하는 값을 전달하기 때문에 React Fiber 작업내에서도 불변성을 유지할 수 있는 점 때문에 객체를 채택한 것 같다.</p>
<p>current라는 필드 명은 네이밍 적인 요인으로 ‘현재 저장된 값’으로 생각하면 납득이 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Deep Dive Study - 1]]></title>
            <link>https://velog.io/@tkddn_dev8430/React-Deep-Dive-Study-2.1-2.2</link>
            <guid>https://velog.io/@tkddn_dev8430/React-Deep-Dive-Study-2.1-2.2</guid>
            <pubDate>Sun, 16 Nov 2025 12:01:16 GMT</pubDate>
            <description><![CDATA[<h1 id="21-jsx">2.1 JSX</h1>
<p>페이스북이 임의로 만든 언어로서 다양한 속성을 가진 트리 구조를 토큰화해 ECMA Script로 변환하는데 초점을 두고 있다.</p>
<p>→ 표현하기 까다로웠던 XML 스타일의 트리 구분을 작성하는 데 많은 도움을 주는 새로운 문법</p>
<h2 id="211-jsx의-정의">2.1.1 JSX의 정의</h2>
<h3 id="jsxelement">JSXElement</h3>
<p>JSX를 구성하는 기본 요소로 HTML의 요소와 비슷한 역할을 한다.</p>
<ul>
<li>JSXOpeningElement - <code>&lt;JSXElement JSXAttributes(optional)&gt;</code></li>
<li>JSXClosingElement - <code>&lt;/JSXElement&gt;</code></li>
<li>JSXSelfClosingElement - <code>&lt;JSXElement JSXAttributes(optional) /&gt;</code></li>
<li>JSXFragment - <code>&lt;&gt;JSXChildren(optional)&lt;/&gt;</code></li>
</ul>
<h3 id="jsxelementname">JSXElementName</h3>
<p><strong>JSXIdentifier</strong></p>
<pre><code class="language-tsx">function Valid1() {
    return &lt;_&gt;&lt;/_&gt;
}</code></pre>
<p>JSX 내부에서 사용할 수 있는 식별자. 함수명, 변수명과 같이 JSXElement 사이에 구분할 수 있는 이름을 말한다. 숫자 또는 <code>$</code> 와 <code>_</code> 이외에 다른 특수문자로 시작할 수 없다.</p>
<p><strong>JSXNameSpacedName</strong></p>
<pre><code class="language-tsx">function Valid() {
    return &lt;foo:bar&gt;&lt;/foo:bar&gt;
}</code></pre>
<p><code>JSXIdentifier:JSXIdentifier</code> 의 조합을 의미하며, <code>:</code> 로 묶을 수 있는 것은 한 번 뿐이다.</p>
<p><strong>JSXMemberExpression</strong></p>
<pre><code class="language-tsx">function Valid() {
    return &lt;foo.bar&gt;&lt;/foo.bar&gt;
}</code></pre>
<p>JSXIdentifier.JSXIdentifier의 조합을 의미하며, <code>.</code> 여러 개를 활용하여 묶을 수 있다. 대신 JSXNameSpacedName와 혼합하여 사용하는 것은 불가능하다.</p>
<h3 id="jsxattributes">JSXAttributes</h3>
<p><strong>JSXSpreadAttributes</strong></p>
<p><code>{…AssignmentExpression}</code> : 객체 뿐만 아니라 AssignmentExpression에서 취급할 수 있는 모든 표현식이 존재할 수 있다.</p>
<p><strong>JSXAttribute</strong></p>
<p>속성을 나타내는 키(JSXAttributeName)와 값(JSXAttributeValue)의 짝으로 표현한다.</p>
<ul>
<li><strong>JSXAttributeName</strong> - 속성의 키 값을 의미, JSXIdentifier(<code>&lt;$&gt;&lt;/$&gt;</code>)와 JSXNamespacedName(<code>foo:bar</code>) 를 통해 키를 나타낼 수 있다.</li>
<li><strong>JSXAttributeValue</strong> - 속성의 키에 할당하는 값<ul>
<li>&quot;&quot; (큰 따옴표로 구성된 문자열)</li>
<li>&#39;&#39; (작은 따옴표로 구성된 문자열)</li>
<li>{ AssignmentExpression }</li>
<li>JSXElement</li>
<li>JSXFragment ( 별도의 속성을 갖지 않는 형태의 JSX, &lt;&gt;&lt;/&gt; )</li>
</ul>
</li>
</ul>
<h3 id="jsxchildren">JSXChildren</h3>
<p>JSXElement의 자식 값을 나타낸다. JSX가 트리 구조를 효과적으로 보여주기 위한 목적이 있기 때문에 JSX로 부모/자식에 대한 표현이 가능하고 이 자식을 JSXChildren이라고 한다.</p>
<p><strong>JSXChild</strong></p>
<p>→ JSXChildren을 구성하는 기본 단위 JSXChildren은 0개 이상의 JSXChild가 필요하다. 0개 이상의 의미처럼 JSXElement는 JSXChildren은 JSXChild가 필요하다.</p>
<ul>
<li>JSXText - <code>{</code> , <code>&lt;</code> , <code>&gt;</code> , <code>}</code> 를 제외한 문자열</li>
<li>JSXElement</li>
<li>JSXFragment</li>
<li>{ JSXChildExpression (optional) } - JSXAttributes의 AssignmentExpression을 의미.</li>
</ul>
<h3 id="jsxstring">JSXString</h3>
<p>HTML에서 사용 가능한 문자열을 JSXStrings에서 사용 가능하다. 여기서의 문자열은 &#39;큰 따옴표&#39;, &#39;작은 따옴표&#39; 또는 JSXText를 의미한다.</p>
<hr>
<h2 id="jsx-string-characters-관련-최근-논의">JSX String Characters 관련 최근 논의</h2>
<p>JSX 스펙에서 문자열 처리와 관련해서 최근에는 논의가 있었는지 궁금해서 타임라인과 함께 간단히 정리해봤습니다.</p>
<h3 id="현재-jsx의-문자-처리-방식">현재 JSX의 문자 처리 방식</h3>
<p>JSX는 HTML4의 252개 문자 엔티티(<code>&amp;nbsp;</code>, <code>&amp;amp;</code> 등)만 지원하고, HTML5의 2,231개 확장 문자는 의도적으로 제외하고 있습니다. HTML과의 복사-붙여넣기 호환성을 위해서죠.</p>
<h3 id="주요-논의-내용">주요 논의 내용</h3>
<p><strong>HTML Entities vs JavaScript Escaping (2014~)</strong></p>
<ul>
<li>HTML entities를 제거하고 JavaScript 이스케이프(<code>\u1234</code>)를 사용하자는 제안</li>
<li>JSX가 HTML이 아닌 ECMAScript 기능이므로 JS 문법이 더 적절하다는 의견</li>
<li>하지만 HTML 복사-붙여넣기 편의성 때문에 채택되지 않음</li>
</ul>
<p><strong>JSX 2.0 제안 무산 (2016)</strong></p>
<ul>
<li>여러 breaking changes를 모아 한번에 처리하려 했으나 실패</li>
<li>203개 찬성에도 불구하고 생태계 안정성을 위해 보류</li>
</ul>
<p><strong>보안 이슈 (2022)</strong></p>
<ul>
<li><code>&amp;#0;</code> 같은 특수 문자 참조의 동작 논의</li>
<li>HTML 스펙상 보안을 위해 특정 문자는 U+FFFD로 대체되는데, JSX 스펙과의 불일치 문제 제기</li>
</ul>
<p><strong>Babel 8 변경사항</strong></p>
<ul>
<li>JSX 텍스트에서 <code>}</code>, <code>&gt;</code> 문자 금지 예정</li>
<li>마이그레이션: <code>{&#39;}&#39;}</code>와 <code>{&#39;&gt;&#39;}</code>로 대체</li>
</ul>
<h3 id="현재-상황">현재 상황</h3>
<p>JSX는 안정성을 최우선으로 하여 의도적으로 변경을 최소화하고 있습니다. 향후 TC39 제안으로 진행될 가능성이 언급되고 있지만, 2022년 이후로는 활발한 논의가 없는 상태입니다.</p>
<p><strong>참고</strong></p>
<ul>
<li><a href="https://facebook.github.io/jsx/#sec-jsx-string-characters">JSX 스펙 - String Characters</a></li>
<li><a href="https://github.com/facebook/jsx/issues/146">GitHub Issue #146 - Unicode Entities</a></li>
<li><a href="https://github.com/facebook/jsx/issues/65">GitHub Issue #65 - JSX 2.0</a></li>
</ul>
<hr>
<h2 id="213-jsx는-어떻게-자바스크립트에서-변환될까">2.1.3 JSX는 어떻게 자바스크립트에서 변환될까?</h2>
<p>아래 JSX 코드는</p>
<pre><code class="language-tsx">const ComponentA = &lt;A required={true}&gt;Hello World&lt;/A&gt;
const ComponentB = &lt;&gt;Hello World&lt;/&gt;

const ComponentC = (
    &lt;div&gt;
        &lt;span&gt;hello world&lt;/span&gt;
    &lt;/div&gt;
)</code></pre>
<p><code>@babel/plugin-transform-react-jsx</code> 로 변환하면 아래와 같이 바뀐다.</p>
<pre><code class="language-tsx">&#39;use strict&#39;
var ComponentA = React.createElement(
    A,
    {
        required: true,
    },
    &#39;Hello World&#39;,
)
var ComponentB = React.createElement(
    React.Fragment,
    null,
    &#39;Hello World&#39;,
)
var ComponentC = React.createElement(
    &#39;div&#39;,
    null,
    React.createElement(&#39;span&#39;, null, &#39;hello world&#39;)
)</code></pre>
<p>JSXElement(<code>A</code>, <code>React.Fragment</code>, <code>&#39;div&#39;</code>)가 먼저 인자로 들어가고 이후 옵셔널 요소인 JSXChildren( <code>React.createElement(&#39;span&#39;, null, &#39;hello world&#39;)</code>, JSXAttributes(<code>{ required: true }</code>), JSXStrings(<code>&#39;Hello World&#39;</code>) 들이 다음 인자들로 들어간다.</p>
<blockquote>
<h3 id="💡-a-는-저대로-두는건가-a는-중첩된-컴포넌트일텐데-변환될-때도-중첩된-구조로-바뀌는-건가">💡 <code>A</code> 는 저대로 두는건가..? A는 중첩된 컴포넌트일텐데 변환될 때도 중첩된 구조로 바뀌는 건가?</h3>
<ol>
<li>Babel은 JSX 문법만 변환합니다<ul>
<li><code>&lt;A&gt;</code> → React.createElement(A, ...)<ul>
<li>A 자체는 건드리지 않음</li>
</ul>
</li>
</ul>
</li>
<li>런타임에 실행됩니다<ul>
<li>React.createElement(A, ...)가 호출되면<ul>
<li>React가 A 함수를 실행하고</li>
<li>그 안의 JSX도 이미 변환된 상태</li>
</ul>
</li>
</ul>
</li>
<li>중첩 구조는 함수 호출로 해결<ul>
<li>A 컴포넌트 내부의 JSX는 A 함수 안에서 변환됨</li>
<li>각 컴포넌트는 독립적으로 변환됨</li>
</ul>
</li>
</ol>
</blockquote>
<p>위 코드를 트랜스파일하면 아래와 같이 결과가 나온다.</p>
<pre><code class="language-tsx">&#39;use strict&#39;

var _jsxRuntime = require(&#39;custom-jsx-library/jsx-runtime&#39;)

var ComponentA = (0, _jsxRuntime.jsx)(A, {
    required: true,
    children: &#39;Hello World&#39;,
})

var ComponentB = (0, _jsxRuntime.jsx)(_jsxElement.Fragment, {
    children: &#39;Hello World&#39;,
});

var ComponentC = (0, _jsxRuntime.jsx)( &#39;div&#39;, {
    children: (0, _jsxRuntime.jsx)( &#39;span&#39;, {
        children: &#39;Hello World&#39;,
    }),
})</code></pre>
<p>그래서 동일한 props를 갖지만 children 요소만 달라지는 경우, 두 개의 컴포넌트를 만들고 삼항연산자를 할 필요 없고, 아래와 같이 createElement의 옵션을 통해서 간결하게 처리할 수 있다.</p>
<pre><code class="language-tsx">function TextOrHeading({ 
    isHeading,
    children
}: PropsWithChildren&lt;{isHeading: boolean}&gt;) {
 return isHeading ? &lt;h1 {...args}&gt;{children}&lt;/h1&gt; : &lt;span {...args}&gt;{children}&lt;/span&gt;
}

function TextOrHeading({ 
    isHeading,
    children
}: PropsWithChildren&lt;{isHeading: boolean}&gt;) {
 return React.createElement(
     isHeading ? &#39;h1&#39; : &#39;span&#39;,
     { className: &#39;text&#39; },
     children,
 )
}</code></pre>
<hr>
<h1 id="22-가상-dom과-리액트-파이버">2.2 가상 DOM과 리액트 파이버</h1>
<h2 id="dom과-렌더링-과정">DOM과 렌더링 과정</h2>
<p><strong>DOM</strong>: 웹 페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.</p>
<p><strong>렌더링 과정</strong></p>
<ol>
<li>요청한 주소에 대한 HTML 파일을 다운로드</li>
<li>브라우저 렌더링 엔진이 HTML을 파싱해서 DOM 트리를 구성</li>
<li>CSS 파일을 만나면 CSS 파일 다운로드</li>
<li>브라우저 렌더링 엔진이 CSS를 파싱해 CSSOM 트리를 구성</li>
<li>DOM을 순회하면서 트리를 분석한다. 이때 display: none 같이 화면에 보이지 않는 요소에 대해서는 작업하지 않는다. (렌더 트리 생성)</li>
</ol>
<blockquote>
<h3 id="❓-displaynone-말고-렌더-트리-형성에-영향을-주는-옵션은-어떤-것이-있을까">❓ display:none 말고 렌더 트리 형성에 영향을 주는 옵션은 어떤 것이 있을까.</h3>
<h4 id="렌더-트리에서-완전히-제외되는-속성">렌더 트리에서 완전히 제외되는 속성</h4>
<ul>
<li><p><code>display: none</code></p>
<ul>
<li>DOM 트리에는 존재하지만 렌더 트리에서 완전히 제외</li>
<li>레이아웃 계산 및 페인팅 과정에서 제외</li>
</ul>
</li>
<li><p><code>&lt;head&gt;</code>, <code>&lt;script&gt;</code>, <code>&lt;meta&gt;</code> 등</p>
<ul>
<li>시각적으로 표시되지 않는 요소들</li>
</ul>
</li>
</ul>
<h4 id="렌더-트리에는-포함되지만-특별하게-처리되는-속성">렌더 트리에는 포함되지만 특별하게 처리되는 속성</h4>
<ul>
<li><p>visibility: hidden</p>
<ul>
<li>렌더 트리에 포함됨 (display: none과 다름)</li>
<li>레이아웃 공간은 차지하지만 화면에 그려지지 않음</li>
<li>자식 요소가 visibility: visible이면 보임</li>
</ul>
</li>
<li><p><code>opacity: 0</code></p>
<ul>
<li>렌더 트리에 포함됨</li>
<li>레이아웃 공간 차지, 투명하게 렌더링<ul>
<li>이벤트는 여전히 받을 수 있음</li>
</ul>
</li>
</ul>
</li>
<li><p><code>position: absolute/fixed</code> + <code>left: -9999px</code></p>
<ul>
<li>렌더 트리에는 포함</li>
<li>화면 밖으로 이동시킴</li>
</ul>
</li>
<li><p><code>clip-path</code>, <code>transform: scale(0)</code></p>
<ul>
<li>렌더 트리에 포함</li>
<li>시각적으로만 숨김</li>
</ul>
</li>
</ul>
</blockquote>
<ol start="6">
<li>눈에 보이는 노드에 대해서는 해당 노드에 대한 CSSOM 정보를 찾고 노드에 적용한다.<ul>
<li><strong>레이아웃(layout, reflow)</strong>: 브라우저 화면 어디에 나타나야하는지 계산하는 과정, 레이아웃 과정을 거치면 페인팅 과정을 반드시 수행</li>
<li><strong>페인팅(painting)</strong>: 레이아웃 단계 이후에 색과 같은 실제 유효한 모습을 그리는 과정</li>
</ul>
</li>
</ol>
<hr>
<h2 id="가상-dom-등장-배경">가상 DOM 등장 배경</h2>
<h3 id="브라우저의-관점">브라우저의 관점</h3>
<p>추가 렌더링 과정은 SPA에서 많이 발생하게 된다. SPA는 페이지 전환시 새롭게 HTML 파일을 다운로드 받는 것이 아니라 하나의 HTML 파일 내부에서 리플로우, 리페인트 과정을 반복하면서 새로운 요소들을 보여주는 방식이다. 요소를 삭제, 삽입, 위치 계산 등의 작업이 반복되니 사용자 경험상으로는 뛰어나지만 DOM을 관리하는 비용이 커지고 있었다.</p>
<h3 id="개발자의-관점">개발자의 관점</h3>
<p>하나의 인터렉션을 통해서 내부 DOM의 수많은 요소가 변경이 일어난다. 개발자는 DOM 요소 하나하나의 변경점을 아는 것보다, 결과적으로 만들어지는 DOM 결과물에 더 포커싱이 되어있다. 그래서 인터렉션의 최종 결과물 DOM을 간편하게 제공하는 것은 개발자에게도 유용한 방법이다.</p>
<hr>
<h2 id="가상-dom과-리액트-파이버">가상 DOM과 리액트 파이버</h2>
<h3 id="가상-dom의-역할">가상 DOM의 역할</h3>
<p>가상 DOM은 실제 브라우저 DOM이 아닌 리액트가 관리하는 가상의 DOM을 의미한다. 상태나 props가 변경될 때마다 새로운 가상 DOM이 생성되고, 그 변화가 기존 가상 DOM과 비교(diff)된다. DOM 계산을 브라우저가 아닌 메모리에서 수행함으로써 실제 DOM의 렌더링 과정을 최소화할 수 있다.</p>
<h3 id="리액트-파이버의-역할">리액트 파이버의 역할</h3>
<p>리액트 파이버는 재조정자(reconciler)가 관리하며, 가상 DOM과 실제 DOM을 비교해서 변경사항을 수집한다. 파이버가 가지고 있는 둘 사이 차이점에 대해서 화면에 렌더링을 요청하는 역할을 한다.</p>
<p>여기서 <strong>재조정(reconciliation)</strong>은 가상 DOM을 활용한 렌더링 과정을 최적화하는데 도와주는 리액트의 아키텍처 또는 알고리즘을 말한다.</p>
<h3 id="리액트-파이버가-하는-일">리액트 파이버가 하는 일</h3>
<ul>
<li>작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매긴다.</li>
<li>이러한 작업을 일시 중지하고 나중에 다시 시작할 수 있다.</li>
<li>이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다.</li>
</ul>
<p>⇒ 이런 과정이 비동기적으로 일어난다.</p>
<h3 id="리액트-파이버가-작업을-수행하는-단계">리액트 파이버가 작업을 수행하는 단계</h3>
<p>파이버의 작업은 크게 두 단계로 나뉜다.</p>
<p><strong>렌더 단계(Render Phase)</strong></p>
<p>사용자에게 노출되지 않는 모든 비동기 작업을 수행한다. 이 단계에서 앞서 언급한 파이버의 작업, 즉 우선순위를 지정하거나 중지시키거나 버리는 등의 작업이 이루어진다. </p>
<p>이 단계의 특징:</p>
<ul>
<li>비동기적으로 실행</li>
<li>작업을 중단하고 재개할 수 있음</li>
<li>우선순위가 높은 작업이 들어오면 현재 작업을 중단하고 새로운 작업을 먼저 처리</li>
<li><code>beginWork()</code>, <code>completeWork()</code> 등이 실행됨</li>
</ul>
<p><strong>커밋 단계(Commit Phase)</strong></p>
<p>DOM에 실제 변경 사항을 반영하기 위한 작업으로, <code>commitWork()</code>가 실행된다. 이 과정은 동기적으로 수행되며 중단될 수도 없다.</p>
<p>이 단계의 특징:</p>
<ul>
<li>동기적으로 실행</li>
<li>중단 불가능</li>
<li>실제 DOM 업데이트, 라이프사이클 메서드 실행 등이 발생</li>
<li>사용자에게 보이는 변경사항이 이 단계에서 반영됨</li>
</ul>
<blockquote>
<h3 id="💡-왜-커밋-단계는-동기적일까">💡 <strong>왜 커밋 단계는 동기적일까?</strong></h3>
<p>렌더 단계에서는 여러 번 작업을 중단하고 재개해도 사용자에게 보이지 않기 때문에 문제가 없다. 하지만 커밋 단계에서 DOM 업데이트를 중단하면 사용자는 절반만 그려진 화면을 보게 되므로, 이 단계는 한 번에 동기적으로 완료되어야 한다.</p>
</blockquote>
<h3 id="리액트-파이버-구현체">리액트 파이버 구현체</h3>
<p>리액트 파이버는 실제로 어떻게 구현되어 있을까?</p>
<p><strong>자바스크립트 객체로 관리</strong></p>
<p>리액트 파이버는 하나의 자바스크립트 객체로 관리된다. 각 파이버는 리액트 엘리먼트에 1:1로 대응되며, 해당 컴포넌트에 대한 정보를 문자열, 숫자, 배열과 같은 값으로 저장한다.</p>
<p><strong>관계 정의를 통한 트리 구조</strong></p>
<p>파이버는 <code>child</code>, <code>sibling</code>, <code>return</code> 과 같은 속성을 통해 다른 파이버와의 관계를 정의하고 있다.</p>
<ul>
<li><code>child</code>: 첫 번째 자식 파이버를 가리킴</li>
<li><code>sibling</code>: 다음 형제 파이버를 가리킴  </li>
<li><code>return</code>: 부모 파이버를 가리킴 (작업 완료 후 돌아갈 곳)</li>
</ul>
<p>이러한 구조를 통해 부모는 하나의 자식만 참조하고, <code>sibling</code>을 통해 형제들을 연결함으로써 1대다 관계를 효율적으로 표현한다.</p>
<pre><code class="language-typescript">// 파이버 객체의 간소화된 예시
const fiber = {
  type: &#39;div&#39;,              // 컴포넌트 타입
  key: null,
  props: { className: &#39;container&#39; },

  // 트리 구조를 위한 포인터
  child: childFiber,        // 첫 번째 자식
  sibling: siblingFiber,    // 다음 형제
  return: parentFiber,      // 부모

  // 작업 관련
  alternate: currentFiber,  // current ↔ workInProgress
  effectTag: &#39;UPDATE&#39;,      // 어떤 작업을 해야 하는지
  // ...
}</code></pre>
<p>이렇게 링크드 리스트 형태로 구현함으로써 재귀 없이 트리를 순회할 수 있고, 작업을 언제든 중단하고 재개할 수 있는 유연성을 확보했다.</p>
<h3 id="가상-dom과-파이버의-협력">가상 DOM과 파이버의 협력</h3>
<ol>
<li>상태나 props가 변경되면 새로운 가상 DOM이 생성되고, 기존 가상 DOM과 비교(diff)된다.</li>
<li>React Fiber는 가상 DOM의 변경 사항을 <strong>렌더 단계</strong>에서 관리하며, 우선순위를 계산하고 중요한 작업부터 처리할 수 있도록 한다.</li>
<li>여러 변경 사항이 모이면, Fiber는 <strong>커밋 단계</strong>에서 그 변경 사항을 최종적으로 실제 DOM에 일괄적으로 반영한다. 이를 통해 불필요한 DOM 조작을 줄이고 성능을 최적화한다.</li>
</ol>
<p>따라서 가상 DOM은 매번 모든 변경 사항을 저장하는 것이 아니라, 변경이 일어날 때마다 새롭게 생성되고, 이 변경 사항들을 React Fiber가 추적하고 관리하여, 최적화된 방식으로 실제 DOM에 업데이트하는 것이다.</p>
<hr>
<h2 id="리액트-파이버-트리">리액트 파이버 트리</h2>
<p>리액트 내에는 두 개의 파이버 트리가 존재한다.</p>
<ul>
<li>현재 모습을 담고 있는 파이버 트리 (current)</li>
<li>작업 중인 상태를 나타내는 workInProgress 트리</li>
</ul>
<p>리액트는 파이버의 작업이 완료되고 나면, 커밋 단계에서 workInProgress 트리의 포인터를 변경해 current 트리로 바꾼다. 이를 더블 버퍼링이라고 한다.</p>
<blockquote>
<p><strong>더블 버퍼링</strong> - 주로 컴퓨터 그래픽에서 사용되는 용어로, 내부 버퍼에서 그림을 지웠다가 그리는 과정을 수행한 다음에 외부 버퍼로 보내 화면이 부드럽게 보이도록 하는 방법이다.</p>
</blockquote>
<hr>
<h2 id="파이버의-작업-순서">파이버의 작업 순서</h2>
<ol>
<li>리액트는 <code>beginWork()</code> 함수를 실행해서 파이버 작업을 수행한다. 더 이상 자식이 없는 파이버를 만날 때까지 트리 형식으로 시작한다.</li>
<li>1번 작업이 끝나면 <code>completeWork()</code>를 실행해 파이버 작업을 완료한다.</li>
<li>형제가 있으면 형제로 넘어간다.</li>
<li>앞선 작업이 모두 완료되면 return을 통해 부모로 돌아가며, 2~4번을 반복한다.</li>
</ol>
<hr>
<h2 id="파이버의-트리-순회-방식">파이버의 트리 순회 방식</h2>
<p>React Fiber는 <strong>재귀 대신 Singly Linked List를 사용한 Parent-first, Depth-first traversal</strong>을 수행한다.</p>
<p>Fiber 구조체 내에서 자식, 형제, 부모에 대한 정보를 구조체로 관리하고 있다.</p>
<pre><code class="language-tsx">// packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {
  return: Fiber | null,    // 부모
  child: Fiber | null,     // 첫 자식
  sibling: Fiber | null,   // 형제
  alternate: Fiber | null, // current ↔ workInProgress
  // ...
};</code></pre>
<p>리액트 내부의 <code>performUnitOfWork</code>에서는 재귀보다 Linked List로 순회하는 방식을 활용한다.</p>
<pre><code class="language-tsx">function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;

  const next = beginWork(current, unitOfWork, renderLanes);

  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // 자식이 없으면 complete 단계로
    completeUnitOfWork(unitOfWork);
  } else {
    // 자식이 있으면 자식을 다음 작업으로
    workInProgress = next;
  }
}</code></pre>
<p>과거 Stack Reconciler에서는 재귀 방식으로 동기적으로 업데이트를 수행했기 때문에 중단할 수 없었다. 현재는 Linked List 기반 DFS로 개선하여 작업을 중단/재개할 수 있고, 작업 단위와 우선순위를 정할 수 있게 되었다.</p>
<hr>
<h2 id="업데이트-처리">업데이트 처리</h2>
<p>새롭게 업데이트가 생기면 workInProgress 트리를 다시 빌드한다. 최초 렌더링 시에는 모든 파이버를 새롭게 만들어야 했지만, 이후에는 파이버가 이미 존재하므로 되도록 새로 생성하지 않고 기존 파이버에 업데이트된 props를 받아 파이버 내부에서 처리한다. 파이버 객체를 계속 생성하는 것은 리소스 낭비이므로, 기존의 파이버 객체를 재사용해서 내부 속성값만 초기화하고 변경하여 트리를 업데이트한다.</p>
<hr>
<h2 id="파이버와-가상-dom">파이버와 가상 DOM</h2>
<ul>
<li>파이버는 리액트 컴포넌트의 정보를 1:1로 가지고 있으며, 파이버의 작업(재조정)은 리액트 아키텍처 내에서 비동기적으로 작동한다. 단, 실제 DOM으로 반영되는 커밋 단계는 동기적으로 수행된다.</li>
<li>가상 DOM은 웹 애플리케이션에 대해서만 통용되는 개념이다. 리액트 네이티브에서도 파이버를 통한 재조정은 일어나지만 렌더링 엔진이 다르기 때문에, 가상 DOM은 브라우저 환경에 특화된 개념이라는 점을 알아둘 필요가 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드프로젝트 LCP 지표 개선하기]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-LCP-%EC%A7%80%ED%91%9C-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-LCP-%EC%A7%80%ED%91%9C-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 06 Nov 2025 17:14:21 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-문제-정의-before">📌 문제 정의 (Before)</h2>
<div align='center'>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/f2674685-99f9-4be0-aec5-0c985a8d0837/image.png' width='700' />
</div>


<p>메인 페이지에서 CPU 성능을 낮춰 테스트를 해봤을 때 LCP 지표가 3.35s로 개선이 필요한 상태로 측정이 돠었다. 그래서 메인 페이지 컴포넌트 로직 수정을 통해 개선을 기획하게 되었다.</p>
<div align='center'>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/36a5aa7c-9dba-472e-b01c-4a3cc8f34207/image.png' width='700' />
</div>

<p>LCP는 페이지에서 가장 큰 영역을 차지하는 컨텐츠가 로드 되었을 때의 시점을 표기하는 Core Web Vital 지표이다.</p>
<div align='center'>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/8b31b61f-1fb2-4b15-b9b7-736b9810457b/image.png' width='700' />
</div>


<p>현재 모멘티아 프로젝트의 메인 화면에서 보이는 케로셀의 이미지들이 효과적으로 랜더링 되지 않기 때문에 나쁘게 지표가 나오고 있는 것 같다.</p>
<br/>

<h2 id="🎯-개선-목표-및-방향"><strong>🎯 개선 목표 및 방향</strong></h2>
<h4 id="ssr-적용">SSR 적용</h4>
<pre><code class="language-tsx">    import LatestArtworkSection from &#39;@/components/MainPage/LatestArtworkSection&#39;;
    import MonthlyBestArtistSection from &#39;@/components/MainPage/MonthlyBestArtistSection&#39;;
    import MonthlyPopularArtworkSection from &#39;@/components/MainPage/MonthlyPopularArtworkSection&#39;;

    const Home = () =&gt; {
      return (
        &lt;div className=&#39;max-w-[1960px] w-full mx-auto my-[126px] flex flex-col gap-[126px] px-[32px] tablet:px-[140px]&#39;&gt;
          &lt;MonthlyPopularArtworkSection /&gt;
          &lt;LatestArtworkSection /&gt;
          &lt;MonthlyBestArtistSection /&gt;
        &lt;/div&gt;
      );
    };

    export default Home;</code></pre>
<p>→ 메인 페이지는 크게 3개의 영역으로 구성되어 있다. 각 영역의 경우 현재 CSR로 랜더링 되어있는 상태이다. 현재 페이지는 메인 페이지로서 유저의 상호작용 보다 컨텐츠를 보여주는 역할이 더 큰 페이지이라고 생각한다.</p>
<p>→ LCP는 페이지 요청된 시점에서 가장 큰 컨텐츠 요소가 랜더링 될 때까지 걸리는 시간을 말한다. 따라서 중간에 데이터 요청을 기다리는 시간이 필요한 CSR에 비해 SSR은 서버에서 랜더링 된 화면을 바로 보여주면 되기 때문에 CSR → SSR로 개선하는 과정은 효과적인 방향이라는 생각이 들었다.</p>
<br/>


<h4 id="nextjs의-image-태그의-priority-옵션-선택적-적용">Next.js의 Image 태그의 priority 옵션 선택적 적용</h4>
<pre><code class="language-tsx">    const ArtworkCard = ({
      mode = &#39;artwork-popular&#39;,
      rank,
      artworkInfo,
    }: ArtworkCardProps) =&gt; {
      ...

      return (
        &lt;CardLayout onClick={clickArtworkCard} classname={modeClasses[mode]}&gt;
          &lt;Image
            src={postImage || &#39;/images/defaultArtworkImage.png&#39;}
            alt={postImage ? `artwork-${postId}` : &#39;default_image&#39;}
            fill={true}
            sizes={modeClasses[mode] || &#39;402px&#39;}
            className={&#39;object-cover&#39;}
            priority
          /&gt;

          ...
        &lt;/CardLayout&gt;
      );
    };

    export default ArtworkCard;
</code></pre>
<p>→ 현재 메인페이지에서 사용되는 ArtworkCard, ArtistProfileCard 컴포넌트에는 <code>&lt;Image/&gt;</code> 태그가 사용되는데 모두 <code>priority</code> 옵션이 적용되고 있다. <code>priority</code>  옵션을 사용하는 경우 이미지 리소스가 우선적으로 로드되는데 많은 이미지에 해당 옵션이 적용되고 있다보니 그만큼 랜더링에 필요한 다른 리소스들의 우선순위가 낮아져 LCP 지표에 영향을 주고 있다는 추측을 했다.</p>
<p>→ 케로셀 특성상 특정 순위 아래에 있는 이미지들은 빠르게 로드할 필요가 없기 때문에 <code>특정 순위 이상인 경우 priority 옵션을 적용</code> 하도록 수정할 계획이다.</p>
<br/>


<h2 id="✅-리팩토링-결과-after"><strong>✅ 리팩토링 결과 (After)</strong></h2>
<h4 id="csr-→-ssr">CSR → SSR</h4>
<pre><code class="language-tsx">const MonthlyBestArtistSection = () =&gt; {
  const { cardsInfo, isLoading } = useGetMonthlyPopularArtists();

  if (isLoading) return &lt;div&gt;로딩중&lt;/div&gt;;

  return (
    &lt;div className=&#39;flex flex-col gap-[90px]&#39;&gt;
      &lt;div className=&#39;flex flex-col gap-[30px]&#39;&gt;
        &lt;p className=&#39;title-l&#39;&gt;이달의 예술가 TOP10&lt;/p&gt;
        &lt;p className=&#39;subtitle1&#39;&gt;
          지난 한 달간 가장 많은 작품 좋아요를 받은 작가들이에요.
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;ControlledCarousel
        slides={cardsInfo}
        renderSlide={(info: ArtistInfoType, index: number) =&gt; (
          &lt;ArtistProfileCard artistInfo={{ ...info }} rank={index + 1} /&gt;
        )}
      /&gt;
    &lt;/div&gt;
  );
};

export default MonthlyBestArtistSection;</code></pre>
<p>→ 기존 코드의 경우 Tanstack Query를 통해 데이터를 요청하고, 데이터를 통해 반복적으로 컴포넌트를 랜더링하는 단순한 로직이었다.Tanstack Query를 사용하는 커스텀 훅을 활용하기 때문에 <code>use client</code> 옵션을 적용하게 되었다.</p>
<pre><code class="language-tsx">const MonthlyBestArtistSection = async () =&gt; {
  const users: ArtistInfoType[] = await getMonthlyPopularArtists();

  const slides = users.map((info: ArtistInfoType, index: number) =&gt; (
    &lt;ArtistProfileCard
      artistInfo={{ ...info }}
      rank={index + 1}
      isPriority={index &lt; 4}
    /&gt;
  ));

  return (
    &lt;div className=&#39;flex flex-col gap-[90px]&#39;&gt;
      &lt;div className=&#39;flex flex-col gap-[30px]&#39;&gt;
        &lt;p className=&#39;title-l&#39;&gt;이달의 예술가 TOP10&lt;/p&gt;
        &lt;p className=&#39;subtitle1&#39;&gt;
          지난 한 달간 가장 많은 작품 좋아요를 받은 작가들이에요.
        &lt;/p&gt;
      &lt;/div&gt;
      &lt;ControlledCarousel slides={slides} /&gt;
    &lt;/div&gt;
  );
};

export default MonthlyBestArtistSection;
</code></pre>
<p>→ 개선 후에는 서버에서 활용하지 않는 Tanstack Query 관련한 로직들을 제거하여 데이터 fetching 하는 로직만 남겨두었다.</p>
<hr>
<br/>


<h4 id="priority-옵션">priority 옵션</h4>
<pre><code class="language-tsx">    const MonthlyBestArtistSection = async () =&gt; {
        ...

      const slides = users.map((info: ArtistInfoType, index: number) =&gt; (
        &lt;ArtistProfileCard
          artistInfo={{ ...info }}
          rank={index + 1}
          isPriority={index &lt; 4}
        /&gt;
      ));

      return (
        &lt;div className=&#39;flex flex-col gap-[90px]&#39;&gt;
          &lt;div className=&#39;flex flex-col gap-[30px]&#39;&gt;
            &lt;p className=&#39;title-l&#39;&gt;이달의 예술가 TOP10&lt;/p&gt;
            &lt;p className=&#39;subtitle1&#39;&gt;
              지난 한 달간 가장 많은 작품 좋아요를 받은 작가들이에요.
            &lt;/p&gt;
          &lt;/div&gt;
          &lt;ControlledCarousel slides={slides} /&gt;
        &lt;/div&gt;
      );
    };

    export default MonthlyBestArtistSection;</code></pre>
<pre><code class="language-tsx">    const ArtworkCard = ({
      mode = &#39;artwork-popular&#39;,
      rank,
      artworkInfo,
      isPriority = false,
    }: ArtworkCardProps) =&gt; {
      ...

      return (
        &lt;CardLayout onClick={clickArtworkCard} classname={modeClasses[mode]}&gt;
          &lt;Image
            src={postImage || &#39;/images/defaultArtworkImage.png&#39;}
            alt={postImage ? `artwork-${postId}` : &#39;default_image&#39;}
            fill={true}
            sizes={modeClasses[mode] || &#39;402px&#39;}
            className={&#39;object-cover&#39;}
            priority={isPriority}
          /&gt;

          ...
        &lt;/CardLayout&gt;
      );
    };

    export default ArtworkCard;
</code></pre>
<p>→     <code>ArtistProfileCard</code> , <code>ArtworkCard</code> 컴포넌트의 Props에 isPriority라는 props를 추가적으로 넘겨주고 받는 컴포넌트에서 해당 값을 활용하여 선택적으로 <code>priority</code> 옵션이 적용되도록 수정했다.</p>
<br/>



<h2 id="📈-리팩토링-효과"><strong>📈 리팩토링 효과</strong></h2>
<ul>
<li>SSR 적용 후</li>
</ul>
<div align='center'>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/4307ca12-9c53-4250-8ed5-07b3ab065711/image.png' width='700' />
</div>


<ul>
<li>priority 옵션 선택적 적용 후</li>
</ul>
<div align='center'>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/dda7e5a9-5356-4846-b169-f860c4eef945/image.png' width='700' />
</div>


<p>→ 전반적으로 SSR을 적용했을 떄, priority 옵션을 적용했을 때 순차적으로 LCP 지표가 개선되는 것을 확인할 수 있었다.</p>
<hr>
<h2 id="💬-회고-및-배운-점"><strong>💬 회고 및 배운 점</strong></h2>
<ul>
<li>이번 개선 작업에서 지표 측정을 lighthouse를 통해서 진행했었는데, 몇 회정도는 너무 지표가 좋게 나오거나 반대로 너무 나쁘게 지표가 나오는 경우가 있었다. 성능 측정 기준을 어떻게 잡아야 해당 지표를 신뢰할 수 있을지 고민이 되었다.</li>
<li>두 개선 방안 모두 어쩌면 구현 단계에서 적용해 볼 수 있는 선택지들이었다는 생각도 들었다. 이런 부분들도 설계 과정에 포함하는 것과 이번처럼 구현 후 성능 개선을 하는 방향 중에 어떤 것이 더 효과적이고 잘한 방법인지도 고민이 되었다.</li>
</ul>
<hr>
<h2 id="🧩-참고-자료"><strong>🧩 참고 자료</strong></h2>
<ul>
<li><a href="https://web.dev/articles/lcp?hl=ko!%5B%5D(https://velog.velcdn.com/images/tkddn_dev8430/post/a2c6f51d-c04b-4621-8a1c-a2a7c03666d0/image.png)">https://web.dev/articles/lcp?hl=ko![](https://velog.velcdn.com/images/tkddn_dev8430/post/a2c6f51d-c04b-4621-8a1c-a2a7c03666d0/image.png)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 16 Beta & React 19.2 알아보기]]></title>
            <link>https://velog.io/@tkddn_dev8430/Next-16-Beta-React-19.2-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/Next-16-Beta-React-19.2-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 26 Oct 2025 08:29:34 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯-nextjs-16-beta-주요-업데이트">🎯 Next.js 16 Beta 주요 업데이트</h2>
<p><strong>그 전에!</strong>
업데이트 내용 옆에 alpha, beta 이런 식으로 키워드가 달려있는데 이것부터 정확하게 하고...</p>
<ul>
<li><strong>Alpha (알파)</strong>: 초기 테스트 단계로, 소프트웨어가 불완전하고 불안정하며 버그를 포함할 수 있지만, 핵심 기능은 포함된 단계</li>
<li><strong>Beta (베타)</strong>: 다음 단계로, 알파보다는 안정적이지만, 여전히 버그가 있을 수 있으며 사용자 피드백에 따라 변경될 수 있는 단계</li>
<li><strong>Stable (안정 버전)</strong>: 최종적으로 공식 릴리스된 버전으로, 완전히 테스트되었으며 신뢰할 수 있고 일반 사용자가 사용할 준비가 된 단계</aside>


</li>
</ul>
<h3 id="1-turbopack-stable---이제-기본-번들러">1. Turbopack (Stable) - 이제 기본 번들러</h3>
<ul>
<li><strong>성능 개선</strong><ul>
<li>프로덕션 빌드: 2-5배 빠름</li>
<li>Fast Refresh: 최대 10배 빠름</li>
</ul>
</li>
<li>Next.js 15.3+ 기준으로 개발 세션의 50%, 프로덕션 빌드의 20%가 이미 Turbopack 사용 중</li>
<li>모든 새 프로젝트에 자동으로 적용</li>
<li><code>next dev --webpack</code> 플래그로 계속 사용 가능</li>
</ul>
<h3 id="파일시스템-캐싱-beta">파일시스템 캐싱 (Beta)</h3>
<p>→ turbopack에서의 Beta 기능 / webpack에는 이미 있음</p>
<p>→ 컴파일 결과물을 디스크에 저장</p>
<pre><code class="language-jsx">const nextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true,
  },
};</code></pre>
<ul>
<li>컴파일 아티팩트를 디스크에 저장하여 재시작 시 훨씬 빠른 컴파일 가능</li>
</ul>
<hr>
<h3 id="2-react-compiler-지원-stable">2. React Compiler 지원 (Stable)</h3>
<ul>
<li>컴포넌트를 자동으로 메모이제이션하여 불필요한 리렌더링 감소</li>
<li>수동 최적화 코드 작성 불필요</li>
<li>Babel 의존성 때문에 빌드 시간이 증가할 수 있음</li>
</ul>
<pre><code class="language-jsx">const nextConfig = {
  reactCompiler: true, // 이제 stable!
};</code></pre>
<ul>
<li><p>예시</p>
<p>  React Compiler는 빌드시 메모이제이션을 추가해준다!</p>
<pre><code class="language-tsx">  function ProductList({ products, onSelect }) {
    return (
      &lt;div&gt;
        {products.map(product =&gt; (
          &lt;ProductCard 
            key={product.id}
            product={product}
            onClick={() =&gt; onSelect(product)}
          /&gt;
        ))}
      &lt;/div&gt;
    );
  }</code></pre>
<pre><code class="language-tsx">  function ProductList({ products, onSelect }) {
    // React Compiler가 자동으로 추가한 메모이제이션
    const memoizedProducts = useMemo(() =&gt; products, [products]);
    const memoizedOnSelect = useCallback(onSelect, [onSelect]);

    const memoizedMap = useMemo(() =&gt; 
      memoizedProducts.map(product =&gt; ({
        key: product.id,
        product,
        onClick: () =&gt; memoizedOnSelect(product)
      }))
    , [memoizedProducts, memoizedOnSelect]);

    return (
      &lt;div&gt;
        {memoizedMap.map(item =&gt; (
          &lt;ProductCard {...item} /&gt;
        ))}
      &lt;/div&gt;
    );
  }</code></pre>
</li>
</ul>
<blockquote>
<h3 id="💡-react-compiler-업데이트가-왜-nextjs에">💡 React Compiler 업데이트가 왜 Next.js에?</h3>
<p>React Compiler가 최신 React 19 버전으로 올라가면서 나오게 되어서, React에서 기본적으로 제공하는 기능을 Next.js에서 제한적으로 제공했었나? 라는 생각을 했었는데,
그건 오해였고,
React Compiler는 기본적으로 플러그인처럼 제공되어서, React에서도 기본적으로 설정이 필요했다. 이번 Next.js에서는 React Compiler를 사용하기 쉽도록 설정값을 추가해준 업데이트인 것이었다. 😥</p>
</blockquote>
<hr>
<h3 id="3-향상된-라우팅--내비게이션">3. 향상된 라우팅 &amp; 내비게이션</h3>
<h3 id="layout-deduplication-레이아웃-중복-제거">Layout Deduplication (레이아웃 중복 제거)</h3>
<ul>
<li><strong>문제</strong>: 50개의 제품 링크가 있는 페이지에서 공유 레이아웃이 50번 다운로드됨</li>
<li><strong>해결</strong>: 공유 레이아웃(layout.tsx)은 한 번만 다운로드하고, 각 링크별로 차이나는 부분만 다운로드</li>
<li><strong>결과</strong>: 네트워크 전송 크기 대폭 감소</li>
</ul>
<h3 id="incremental-prefetching-증분-프리페칭">Incremental Prefetching (증분 프리페칭)</h3>
<ul>
<li><p>prefetching을 청크 단위로 구분</p>
<p>  → 이미 캐시된 부분은 건너뛰고 필요한 부분만 프리페칭</p>
</li>
<li><p>링크가 뷰포트를 벗어나면 요청 취소</p>
</li>
<li><p>호버 시 우선순위 높여서 프리페칭</p>
</li>
<li><p>데이터 무효화 시 자동으로 재프리페칭</p>
</li>
</ul>
<p><strong>트레이드오프</strong>: 개별 요청 수는 증가(청크)하지만, 전체 전송 크기는 크게 감소(캐싱)</p>
<hr>
<h3 id="4-개선된-캐싱-api">4. 개선된 캐싱 API</h3>
<ul>
<li>Caching API → Next.js에서 제공하는 <a href="https://nextjs.org/docs/app/guides/caching#data-cache">캐싱 제어 유틸</a></li>
</ul>
<h3 id="revalidatetag-변경-⚠️"><code>revalidateTag()</code> 변경 ⚠️</h3>
<p>→ 기본동작 : 캐싱된 데이터를 반환하고, 새 데이터를 fetch 한 후에 캐시 값 갱신</p>
<pre><code class="language-jsx">// ✅ 새로운 방식 (필수)
revalidateTag(&#39;blog-posts&#39;, &#39;max&#39;);  // 대부분의 경우 &#39;max&#39; 권장
revalidateTag(&#39;news-feed&#39;, &#39;hours&#39;);
revalidateTag(&#39;analytics&#39;, &#39;days&#39;);
revalidateTag(&#39;products&#39;, { revalidate: 3600 }); // 인라인 설정

// ❌ 기존 방식 (deprecated)
revalidateTag(&#39;blog-posts&#39;);
</code></pre>
<ul>
<li><strong>두 번째 인자 필수</strong>: <code>cacheLife</code> 프로필 지정</li>
<li><strong>SWR 동작</strong>: 사용자는 캐시된 데이터를 즉시 받고, 백그라운드에서 재검증</li>
</ul>
<h3 id="updatetag-신규-🆕"><code>updateTag()</code> (신규) 🆕</h3>
<p>→ 기본 동작 : 기존 캐시를 만료 시키고, 새 값을 fetch해서 반환 후 캐시 갱신</p>
<pre><code class="language-jsx">&#39;use server&#39;;
import { updateTag } from &#39;next/cache&#39;;

export async function updateUserProfile(userId, profile) {
  await db.users.update(userId, profile);
  updateTag(`user-${userId}`); // 즉시 캐시 만료 및 갱신
}
</code></pre>
<ul>
<li><strong>Server Actions 전용</strong></li>
<li><strong>Read-your-writes</strong>: 사용자가 변경한 내용을 즉시 확인 가능</li>
<li><strong>용도</strong>: 폼, 사용자 설정 등 즉각적인 피드백이 필요한 경우</li>
</ul>
<h3 id="refresh-신규-🆕"><code>refresh()</code> (신규) 🆕</h3>
<pre><code class="language-jsx">&#39;use server&#39;;
import { refresh } from &#39;next/cache&#39;;

export async function markNotificationAsRead(notificationId) {
  await db.notifications.markAsRead(notificationId);
  refresh(); // 캐시되지 않은 데이터만 갱신
}
</code></pre>
<ul>
<li><strong>Server Actions 전용</strong></li>
<li><strong>캐시 건드리지 않음</strong>: 알림 카운트, 실시간 메트릭 등 캐시되지 않은 데이터만 갱신</li>
</ul>
<hr>
<h3 id="6-주요-breaking-changes">6. 주요 Breaking Changes</h3>
<h3 id="필수-요구사항">필수 요구사항</h3>
<ul>
<li><strong>Node.js</strong>: 20.9+ (18 지원 종료)</li>
<li><strong>TypeScript</strong>: 5.1.0+</li>
<li><strong>브라우저</strong>: Chrome/Edge/Firefox 111+, Safari 16.4+</li>
</ul>
<h3 id="비동기로-변경-필수-⚠️">비동기로 변경 필수 ⚠️</h3>
<p>→ 내부 동작 자체가 비동기로 마이그레이션됨
→ 성능 최적화</p>
<pre><code class="language-jsx">// ❌ 기존 방식
const params = props.params;
const searchParams = props.searchParams;
const cookieStore = cookies();

// ✅ 새로운 방식
const params = await props.params;
const searchParams = await props.searchParams;
const cookieStore = await cookies();
</code></pre>
<h3 id="nextimage-변경사항">next/image 변경사항</h3>
<ul>
<li><code>images.minimumCacheTTL</code>: 60초 → 4시간 (14400초)</li>
<li><code>images.imageSizes</code>: 16 제거 (사용률 4.2%에 불과)</li>
<li><code>images.qualities</code>: [1..100] → [75]로 단순화</li>
<li><code>images.maximumRedirects</code>: 무제한 → 3으로 제한</li>
<li><strong>보안</strong>: <code>images.dangerouslyAllowLocalIP</code> 기본값 false</li>
</ul>
<h3 id="middleware-파일명">Middleware 파일명</h3>
<ul>
<li><code>middleware.ts</code> → <code>proxy.ts</code>로 변경 권장 (deprecated)</li>
</ul>
<hr>
<h2 id="⚛️-react-192-주요-업데이트">⚛️ React 19.2 주요 업데이트</h2>
<h3 id="1-activity--컴포넌트-🆕">1. <code>&lt;Activity /&gt;</code> 컴포넌트 🆕</h3>
<p>애플리케이션을 &quot;활동&quot; 단위로 나누어 제어 가능</p>
<pre><code class="language-jsx">// 기존 방식
{isVisible &amp;&amp; &lt;Page /&gt;}

// 새로운 방식
&lt;Activity mode={isVisible ? &#39;visible&#39; : &#39;hidden&#39;}&gt;
  &lt;Page /&gt;
&lt;/Activity&gt;
</code></pre>
<h3 id="모드">모드</h3>
<ul>
<li><strong><code>visible</code></strong>: 자식 표시, 이펙트 마운트, 업데이트 정상 처리</li>
<li><strong><code>hidden</code></strong>: 자식 숨김 (<code>display: none</code>), 이펙트 언마운트, 업데이트를 가장 낮은 우선순위로 지연</li>
</ul>
<h3 id="사용-사례">사용 사례</h3>
<ul>
<li>사용자가 다음에 탐색할 가능성이 높은 부분을 미리 렌더링 (백그라운드에서 데이터, CSS, 이미지 로딩)</li>
<li>뒤로 가기 시 입력 필드 등의 상태 유지</li>
</ul>
<blockquote>
<h3 id="💡-기존-방식이랑-코드-작성법만-바뀐거-아니에요-🤬억지">💡 기존 방식이랑 코드 작성법만 바뀐거 아니에요?! 🤬(억지)</h3>
<p>Activity를 사용하면 아래와 같이 동작하게된다.
컴포넌트는 마운트와 백그라운드 랜더링, 데이터 또는 이미지 Fetching, 상태 유지가 가능하다. 하지만 useEffect는 동작하지 않는다. 
컴포넌트는 마운트되어 있고 State도 유지되지만, 모든 Effect(부수효과)가 클린업되어 실행되지 않는 대기 상태로 유지된다.
기존 방식에 비해서 해당 컴포넌트가 화면에 보일 수 있게 사전 작업을 미리 해둘 수 있어 UX적으로도 뛰어난 업데이트라고 볼 수 있다.</p>
</blockquote>
<hr>
<h3 id="2-useeffectevent-🆕">2. <code>useEffectEvent()</code> 🆕</h3>
<p>Effect에서 상태 갱신으로 인한 side effect와 로직을 분리하는 패턴
→ Effect 내부에서 사용되는 값이 의존성 배열에 포함되면서 Effect가 지속적으로 재실행되는 현상을 개선
→ 최신 값을 사용해야 하지만, 그 값의 변경으로 인한 Effect 재실행(부수 효과)을 방지하고 싶을 때 사용</p>
<h3 id="문제-상황">문제 상황</h3>
<pre><code class="language-jsx">function ChatRoom({ roomId, theme }) {
  useEffect(() =&gt; {
    const connection = createConnection(serverUrl, roomId);
    connection.on(&#39;connected&#39;, () =&gt; {
      showNotification(&#39;Connected!&#39;, theme); // theme 사용
    });
    connection.connect();
    return () =&gt; connection.disconnect();
  }, [roomId, theme]); // theme 변경 시 재연결 발생 🤦
}
// -&gt; 채팅 룸 상태가 theme에 의존함</code></pre>
<h3 id="해결-방법">해결 방법</h3>
<pre><code class="language-jsx">function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() =&gt; {
    showNotification(&#39;Connected!&#39;, theme); // 항상 최신 theme 참조
  });

  useEffect(() =&gt; {
    const connection = createConnection(serverUrl, roomId);
    connection.on(&#39;connected&#39;, () =&gt; {
      onConnected();
    });
    connection.connect();
    return () =&gt; connection.disconnect();
  }, [roomId]); // ✅ roomId만 의존성 배열에 남김
}
</code></pre>
<h3 id="특징">특징</h3>
<ul>
<li>Effect Event는 항상 최신 props/state를 &quot;봄&quot;</li>
<li>의존성 배열에 포함하지 않음</li>
<li><code>eslint-plugin-react-hooks@6.1.0</code> 업그레이드 필요</li>
</ul>
<hr>
<h3 id="3-cachesignal-🆕">3. <code>cacheSignal()</code> 🆕</h3>
<blockquote>
<h3 id="💡-react-cache">💡 React cache()</h3>
</blockquote>
<ul>
<li>렌더링 사이클 동안 동일한 데이터 요청의 중복을 제거하는 함수</li>
<li>여러 중첩된 Server Component가 같은 데이터를 요청할 때, 실제 fetch는 1번만 수행</li>
<li>일반 캐싱과의 차이<ul>
<li><strong>일반 캐싱</strong>: 시간 기반 저장 (예: 1시간), 재방문 시 재사용</li>
<li><strong>React cache()</strong>: 렌더링 기반 저장, 렌더링 끝나면 즉시 삭제</li>
</ul>
</li>
</ul>
<p><code>cache()</code> 라이프타임이 끝날 때를 알 수 있는 신호</p>
<pre><code class="language-jsx">import { cache, cacheSignal } from &#39;react&#39;;

const dedupedFetch = cache(fetch);

async function Component() {
  await dedupedFetch(url, { signal: cacheSignal() });
}</code></pre>
<h3 id="정리-시점">정리 시점</h3>
<ul>
<li>렌더링이 성공적으로 완료됨</li>
<li>렌더링이 중단됨</li>
<li>렌더링이 실패함</li>
</ul>
<h3 id="그래서-이걸-어따씀">그래서 이걸 어따씀..?</h3>
<ul>
<li><p>여러 데이터를 조회하는 과정에서 빠르게 페이지를 넘어갈 때 현재 요청을 중단하고 리소스를 정리할 때.</p>
</li>
<li><p>예시</p>
<pre><code class="language-jsx">  async function ProductPage({ id }) {
    const product = await fetchProduct(id);      // 요청 1
    const reviews = await fetchReviews(id);      // 요청 2
    const related = await fetchRelated(id);      // 요청 3

    return &lt;div&gt;...&lt;/div&gt;;
  }

  const fetchProduct = cache(async (id) =&gt; {
    return fetch(`/api/products/${id}`, { signal: cacheSignal() });
  });

  const fetchReviews = cache(async (id) =&gt; {
    return fetch(`/api/reviews/${id}`, { signal: cacheSignal() });
  });

  const fetchRelated = cache(async (id) =&gt; {
    return fetch(`/api/related/${id}`, { signal: cacheSignal() });
  });</code></pre>
<h4 id="케이스-1-렌더링-정상-완료">케이스 1: 렌더링 정상 완료</h4>
<pre><code>  렌더링 시작
    ↓
  fetchProduct 완료 (1초)
    ↓
  fetchReviews 완료 (1초)
    ↓
  fetchRelated 완료 (1초)
    ↓
  렌더링 완료
    ↓
  cacheSignal 발동
    → 모든 fetch 이미 완료됨
    → 아무 일도 안 일어남 ✅</code></pre><hr>
<h4 id="케이스-2-렌더링-중간에-중단-페이지-이동">케이스 2: 렌더링 중간에 중단 (페이지 이동)</h4>
<pre><code>  렌더링 시작
    ↓
  fetchProduct 시작 (3초 소요 예상)
  fetchReviews 시작 (3초 소요 예상)
  fetchRelated 시작 (3초 소요 예상)
    ↓
  1초 후 사용자가 다른 페이지로 이동!
    ↓
  렌더링 중단!
    ↓
  cacheSignal 발동
    → fetchProduct의 fetch 취소 ✅
    → fetchReviews의 fetch 취소 ✅
    → fetchRelated의 fetch 취소 ✅</code></pre><p>  <strong>이 경우 3개 모두 취소되는 이유:</strong></p>
<ul>
<li><p>3개 모두 <strong>같은 렌더링 사이클</strong>에 속함</p>
</li>
<li><p>렌더링 중단 = 모든 cache() 정리</p>
</li>
<li><p>각자의 cacheSignal이 각자의 fetch 취소</p>
<h4 id="케이스-3-일부만-완료된-경우">케이스 3: 일부만 완료된 경우</h4>
<pre><code>렌더링 시작
↓
fetchProduct 완료 (0.5초) ✅
↓
fetchReviews 시작 (3초 소요 예상) 🔄
fetchRelated 시작 (3초 소요 예상) 🔄
↓
1초 후 사용자가 페이지 이동
↓
렌더링 중단!
↓
cacheSignal 발동
→ fetchProduct: 이미 완료, 취소할 게 없음
→ fetchReviews: 진행 중이던 fetch 취소 ✅
→ fetchRelated: 진행 중이던 fetch 취소 ✅</code></pre><blockquote>
<p>&quot;각 fetch의 cacheSignal은 자기 자신의 요청만 취소한다. 단, 같은 렌더링 사이클에 속한 모든 cache()가 정리될 때 각자의 fetch가 각자 취소된다.&quot;</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="4-performance-tracks-🆕">4. Performance Tracks 🆕</h3>
<p>Chrome DevTools에 React 전용 성능 트랙 추가</p>
<h3 id="scheduler-⚛-track">Scheduler ⚛ Track</h3>
<ul>
<li>React가 다양한 우선순위로 작업하는 내용 표시</li>
<li>&quot;blocking&quot; (사용자 인터랙션) vs &quot;transition&quot; (startTransition 내부)</li>
<li>업데이트를 스케줄한 이벤트 타입</li>
<li>어떤 우선순위가 다른 우선순위를 기다리는지</li>
</ul>
<h3 id="components-⚛-track">Components ⚛ Track</h3>
<ul>
<li>React가 작업 중인 컴포넌트 트리 표시</li>
<li>&quot;Mount&quot;, &quot;Blocked&quot; 등의 라벨</li>
<li>컴포넌트가 렌더링되거나 이펙트를 실행하는 시점과 소요 시간</li>
</ul>
<hr>
<h3 id="5-partial-pre-rendering-ppr">5. Partial Pre-rendering (PPR)</h3>
<ul>
<li>앱의 일부를 정적 컨텐츠로 빌드 시 미리 생성(SSG) CDN에서 제공.</li>
<li>동적 영역에 대해서는 SSR로  스트리밍하여 나머지 컨텐츠 제공</li>
</ul>
<p>→ <strong>SSG(Static Site Generation)</strong>는 빠르지만 동적 컨텐츠를 제공하지 못한다는 특징과, <strong>SSR(Server Side Rendering)</strong>은 동적 컨텐츠를 제공할 수 는 있지만 서버를 거쳐 컨텐츠를 생성해야한다는 점을 상호 보완한 하이브리드 랜더링 방식</p>
<p>→ <strong>ISR(Incremental Static Regeneration)</strong>은 정적 데이터를 주기적으로 생성해서 새로운 데이터를 보여주는 방식이라 PPR과의 랜더링 방식에는 차이가 있음</p>
<pre><code class="language-jsx">// 1. 사전 렌더링
const { prelude, postponed } = await prerender(&lt;App /&gt;, {
  signal: controller.signal,
});
await savePostponedState(postponed);
// prelude를 클라이언트나 CDN으로 전송

// 2. 나중에 재개 (SSR 스트림)
const postponed = await getPostponedState(request);
const resumeStream = await resume(&lt;App /&gt;, postponed);

// 또는 SSG를 위한 정적 HTML
const { prelude } = await resumeAndPrerender(&lt;App /&gt;, postponedState);</code></pre>
<hr>
<h3 id="6-그-외-주요-변경사항">6. 그 외 주요 변경사항</h3>
<h3 id="suspense-boundary-batching-ssr">Suspense Boundary Batching (SSR)</h3>
<ul>
<li><strong>이전</strong>: 스트리밍 SSR 중 Suspense 콘텐츠가 준비되는 즉시 fallback 교체</li>
<li><strong>19.2</strong>: 짧은 시간 동안 배치하여 더 많은 콘텐츠를 함께 표시</li>
<li><strong>장점</strong>: <code>&lt;ViewTransition&gt;</code> 지원 준비, 애니메이션을 더 큰 배치로 실행</li>
</ul>
<h3 id="nodejs에서-web-streams-지원">Node.js에서 Web Streams 지원</h3>
<ul>
<li><code>renderToReadableStream</code> Node.js에서 사용 가능</li>
<li><code>prerender</code> Node.js에서 사용 가능</li>
<li><code>resume</code>, <code>resumeAndPrerender</code> API 추가</li>
</ul>
<h3 id="eslint-plugin-react-hooks-v6">eslint-plugin-react-hooks v6</h3>
<ul>
<li>Flat config가 기본값 (ESLint v10 준비)</li>
<li>React Compiler 기반 규칙 opt-in 가능</li>
<li>레거시 설정: <code>recommended-legacy</code> 사용</li>
</ul>
<h3 id="useid-기본-prefix-변경">useId 기본 prefix 변경</h3>
<ul>
<li><code>_r_</code> 로 변경 (이전: <code>:r:</code> 또는 <code>«r»</code>)</li>
<li>이유: View Transitions 지원을 위해 <code>view-transition-name</code>과 XML 1.0 이름에 유효한 ID 필요 </li>
</ul>
<hr>
<h2 id="📚-참고-자료">📚 참고 자료</h2>
<ul>
<li><a href="https://nextjs.org/blog/next-16-beta">Next.js 16 Beta 공식 블로그</a></li>
<li><a href="https://react.dev/blog/2025/10/01/react-19-2">React 19.2 공식 발표</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 통신, 어떻게 할래? 🤔 폴링부터 WebSocket까지]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%A0%EB%9E%98-%ED%8F%B4%EB%A7%81%EB%B6%80%ED%84%B0-WebSocket%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@tkddn_dev8430/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%A0%EB%9E%98-%ED%8F%B4%EB%A7%81%EB%B6%80%ED%84%B0-WebSocket%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Sun, 02 Mar 2025 18:25:26 GMT</pubDate>
            <description><![CDATA[<p>우연한 기회를 통해서 실시간 통신 방식들을 서로 비교해봐야할 상황이 생겼다.</p>
<p>실시간과 관련해서는 거의 경험이 없다시피 했고, 대충 WebSocket 개념 정도..? 만 알고있었다.</p>
<p>언제가되든 실시간과 관련된 기능을 꼭 한 번 경험해볼 날이 올 것 같아 다양한 방식들을 알아보는 시간을 가졌다..!</p>
<h2 id="polling">Polling</h2>
<p>Polling은 특정 장치가 다른 장치의 상태를 주기적으로 검사하고, 일정 조건을 만족할때 송수신 자료처리를 하는 방식을 말한다. 웹에서 실시간 통신을 폴링 방식으로 구현할 경우, 클라이언트는 일정 주기로 서버에 요청을 보내어 상태를 확인한다.</p>
<div align=’center’>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/f8e0d143-2fb6-44f5-9291-9e04bd73bc1e/image.png' width='700'/>
</div>

<p>폴링 방식은 구현 방식이 가장 간단하지만, 서버로 보내는 요청이 길어질 수 록 서버 리소스 낭비가 심해지고, 이벤트가 발생하는 시점과, 서버가 요청에 대한 응답을 보내는 시점이 정확하게 일치하지 않기 때문에 실시간 성에서 단점을 가진다.</p>
<h3 id="long-polling">Long Polling</h3>
<p>Long Polling은 Polling과 반복적으로 서버에 요청을 보내는 측면에서는 같지만 다음과 같은 특징을 가진다.</p>
<ul>
<li>Polling과는 달리 서버에 요청을 보내고 서버에서 곧바로 응답을 보내는 것이 아니라 이벤트가 발생하기 까지 일정시간 대기 했다가 이벤트가 발생하면 응답을 내려주는 방식이다.</li>
<li>이벤트가 발생하면 즉시 응답하고, 일정 시간이 지나면 타임아웃 처리 후 다시 요청한다.</li>
</ul>
<div align=’center’>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/3df3893e-edbe-4d1f-8470-8eae71a2fcf7/image.png' width='700' />
</div>

<p>Long Polling은 이벤트 발생 빈도가 매우 높다면, 일반 Polling과 차이가 줄어들지만, 그렇지 않은 경우에는 서버 부하를 줄이는 데 더 효과적이다.</p>
<h3 id="server-sent-event-sse">Server-Sent Event (SSE)</h3>
<p>SSE(Server-Sent Events)는 <strong>서버 → 클라이언트 단방향 통신을 형성</strong>하여, 클라이언트가 별도의 요청을 보내지 않아도 <strong>서버가 지속적으로 이벤트를 푸시할 수 있는 방식</strong>을 말한다.</p>
<p>초기에 클라이언트가 연결 요청(Request Connect)을 보내고, 서버가 OK 응답을 반환하면 <strong>단방향 통신이 유지</strong>되면서 서버에서 여러 이벤트를 보낸다.</p>
<p>연결을 종료할 때는 <strong>클라이언트가 명시적으로 연결 해제 요청(Request Disconnect)을 보내거나,서버에서 특정 조건(예: 타임아웃, 리소스 제한 등)에 의해 연결을 종료할 수도 있다.</strong></p>
<div align=’center’>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/6252e16b-f4c4-4f63-9875-463db292cd4f/image.png' width='700' />
</div>


<p>SSE는 서버에서 이벤트가 발생했을 때 클라이언트로 메세지를 보내기 때문에 실시간 성이 뛰어나지만 HTTP/HTTPS 프로토콜에 따라 도메인에 연결할 수 있는 요청 개수가 한계가 있다.</p>
<p>HTTP의 경우 6개, HTTPS의 경우 100개 정도이기 때문에 대규모 서비스에서 SSE를 활용하는 것에는 어려움이 있을 수 있다.</p>
<p>또한, 양방향 연결이 아닌 서버에서 클라이언트로 연결되는 단방향 연결이기 때문에 클라이언트와 서버의 양방향 통신이 필요한 경우에는 적절하지 않은 방식이다.</p>
<h3 id="web-socket">Web Socket</h3>
<p>Web Socket은 웹 브라우저에서 소켓 통신을 가능하게 설계된 방식으로 양 끝단의 컴퓨터가 서로 자유롭게 데이터를 주고받을 수 있다는 점이 특징이다.</p>
<div align=’center’>
<img src='https://velog.velcdn.com/images/tkddn_dev8430/post/9ce21607-900a-43fd-934c-261e400f2f76/image.png' width='700' />
</div>

<p>기존 소켓 통신을 브라우저에서 활용할 수 있도록 최적화 된 방식이기 때문에 아래와 같은 소켓 연결 / 해제 과정을 따른다.</p>
<ol>
<li><code>connect()</code><ul>
<li>클라이언트가 서버로 WebSocket 연결 요청을 보냄. ( HTTP / HTTPS )</li>
<li>HTTP 요청의 헤더로 <code>Upgrade : websocket</code> 을 함께 보내 연결된 이후 WebSocket 프로토콜로 전환하여 요청/응답을 처리</li>
</ul>
</li>
<li><code>read()</code> / <code>write()</code><ul>
<li>WebSocket 연결이 완료된 상태에서 서버/클라이언트 순서에 관계없이 데이터를 주고 받을 수 있음</li>
<li>HTTP/HTTPS 프로토콜 대신 WS/WSS 프로토콜을 활용한다.</li>
</ul>
</li>
<li><code>close()</code><ul>
<li>클라이언트의 close()을 보내 소켓 통신을 중단한다.</li>
</ul>
</li>
</ol>
<h4 id="🧐-why-http-→-ws">🧐 Why HTTP → WS?</h4>
<p>그냥 두 프로토콜의 목적이 다르기 때문이다.</p>
<ul>
<li>WebSocket 프로토콜은 <code>전이중 통신을 지원하는 프로토콜</code>이고, 연결된 이후 클라이언트와 서버 간 연결을 지속하며 동시에 데이터를 주고 받을 수 있다.</li>
<li>HTTP의 경우 <strong><code>요청-응답 프로토콜</code></strong>이며 WebSocket과 다르게 요청을 보내는 경우에만 응답을 받을 수있고, 응답을 받으면 연결을 종료한다.</li>
</ul>
<br/>

<h3 id="그-외">그 외</h3>
<p>여러 통신 방식을 찾아보면서 추가로 언급된 방식들이다. 짧게 읽어본 내용들이라 디테일하게 파악하진 못했지만 주로 멀티미디어와 관련된 실시간 통신 방식인 것으로 보였다. 이번 실시간 통신 방식들을 주로 서버로부터 메세지를 받거나 서버의 이벤트를 감지하기 위한 목적의 실시간 통신을 비교해보고 있었기 때문에 해당 방식들은 조금만 알아보았다..ㅎ</p>
<h4 id="web-real-time-communication-webrtc">Web Real Time Communication (WebRTC)</h4>
<p>웹 브라우저간 외부 플러그인의 도움 없이 P2P 통신이 가능하도록 설계된 API. 음성 통화, 화상 통화, 파일 공유 등에 활용한다.</p>
<p><a href="https://webrtc.org/?hl=ko">https://webrtc.org/?hl=ko</a></p>
<h4 id="http-live-streaming-hls">HTTP Live Streaming (HLS)</h4>
<p>HTTP 기반 적응형 비트레이트 스트리밍 통신 프로토콜을 의미하며, 여러 미디어 플레이어에서 HTTP 기반  파일을 작은 단위로 다운로드하여 잠재적으로 무한한 전송 스트림을 적재하며 동작하는 방식.</p>
<p><a href="https://ko.wikipedia.org/wiki/HTTP_%EB%9D%BC%EC%9D%B4%EB%B8%8C_%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D">https://ko.wikipedia.org/wiki/HTTP_라이브_스트리밍</a></p>
<br/>

<h3 id="fin">Fin.</h3>
<p>우연한 기회로 실시간 통신들의 여러가지 방식을 알아보게 되었다.</p>
<p>어떤 곳에 어떤 방식을 적용하는 것이 좋을지 생각도 해밨다. 각 바식을 이해하면서 각 방식의 장단점이 명확하다는 생각이 들었고, 어느 한가지만 사용한다기 보다 서로 단점을 보완할 수 있게 여러가지 방식을 함께 활용하고 있을 수 도 있겠다는 생각도 들었다.</p>
<p>예를 들어 SSE의 경우 도메인에 연결할 수 있는 인스턴스의 수가 적기 때문에 모든 개체를 무한정 연결해두었을 때 나중에 연결조차 하지 못하는 경우가 발생할 수 있다.</p>
<p>이 경우에는 특정 이벤트가 발생하는 시점을 대략 예측할 수 있다는 가정하에 롱 폴링과 SSE를 섞어 사용해볼 수도 있을 것 같다.</p>
<p>특정 이벤트 처리를 위해 9분이 걸린다고 하면 7 ~ 8분 쯤에는 롱 폴링으로 실시간 통신을 시도하고, 이벤트 발생 확률이 높은 9분 대에는 SSE를 적용하여 실시간성을 최대한 높이면서 짧은시간 연결을 유지하도록해서 현재 연결상태에 있는 인스턴스의 개수를 최소한으로 유지할 수 있을 것 같다.</p>
<p>이런 생각들이 옳은 방향으로 생각하고 있는 것인진 모르겠지만, 이런 식으로 여러가지 생각을 많이 해볼 수 있었던 주제였던 것 같다.</p>
<br/>

<h3 id="reference">Reference</h3>
<p><a href="https://sendbird.com/ko/developer/tutorials/websocket-vs-http-communication-protocols">https://sendbird.com/ko/developer/tutorials/websocket-vs-http-communication-protocols</a></p>
<p><a href="https://ko.wikipedia.org/wiki/%ED%8F%B4%EB%A7%81_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)">https://ko.wikipedia.org/wiki/폴링_(컴퓨터_과학)</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events">https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events/Using_server-sent_events</a></p>
<p><a href="https://velog.io/@charmull/%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0">https://velog.io/@charmull/실시간-통신-기술</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[낙관적 업데이트와 onMutate]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EB%82%99%EA%B4%80%EC%A0%81-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EC%99%80-onMutate</link>
            <guid>https://velog.io/@tkddn_dev8430/%EB%82%99%EA%B4%80%EC%A0%81-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EC%99%80-onMutate</guid>
            <pubDate>Thu, 20 Feb 2025 17:30:25 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하면서 팔로우/좋아요 버튼에 낙관적 업데이트를 처음 적용해보았고, 그 과정에서 관련 자료들을 찾아보고, 낙관적 업데이트에 대한 생각들이나 초기 코드를 리팩토링하는 과정을 적어보았다.</p>
<h3 id="이전에는요">이전에는요…</h3>
<pre><code class="language-tsx">const useToggleFollow = ({
  initFollowState,
}: {
  initFollowState: boolean | null;
}) =&gt; {
  ...

  const mutation = useMutation&lt;
    void,
    Error,
    { userId: number; following: boolean }
  &gt;({
    mutationFn: async ({ userId, following }) =&gt; {
      following ? await deleteFollow(userId) : await postFollow(userId);
    },

    onSuccess: (_, { following }) =&gt; {
      const currentUserId = TokenHandler.getUserIdFromToken();

      if (currentUserId) {
        [
          ARTWORK.followedArtists,
          USER.followerList(currentUserId),
          USER.followingList(currentUserId),
        ].forEach((queryKey) =&gt; {
          queryClient.invalidateQueries({ queryKey: [queryKey] });
        });
      }

      setIsFollowing(!following); // 성공 시 데이터 변경
    },

    onError: (error) =&gt; {
      console.error(&#39;팔로우 상태 변경 에러: &#39;, error.message);
    },
  });

    ...

  return { isFollowing, toggleFollow };
};

export default useToggleFollow;</code></pre>
<h4 id="기존-로직의-개선점">기존 로직의 개선점</h4>
<ul>
<li>useMutation의 동작에 따라 onSuccess는 mutateFn이 정상적으로 마쳐야 동작. 따라서 <u>follow 상태를 변경하는 로직은 api 로직이 모두 끝난 다음에야 동작.</u>  </li>
</ul>
<pre><code>→ UI 갱신이 follow/unfollow의 로직 처리 시간에 영향을 받음.</code></pre><ul>
<li>에러가 발생했을 때, 함수 자체가 미실행 된 것인지, 버튼 이벤트가 실패한 것인지에 대한 피드백이 없음 (다른 UI 장치가 없다고 가정.)</li>
</ul>
<br />

<h3 id="낙관적-업데이트">낙관적 업데이트</h3>
<p>사용자 경험(UX)를 증진시키기 위해 적용하는 개념으로, 상태, 데이터 업데이트와 관련해서 우선 데이터 변경이 정상적으로 이루어진 다는 것을 가정하여 요청이 마무리 되기전에 상태를 변경하고, 추후 실패 상태에 대해서는 다시 원래 상태로 복구 시켜 에러 이벤트에 대한 피드백을 전달하는 방식</p>
<table width='70%' align='center'>
  <thead>
    <tr>
      <th width="50%">기존 업데이트 플로우</th>
      <th width="50%">낙관적 업데이트 플로우</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td align="left">1. 사용자 동작 수행<br/>2. 서버에 변경 사항 전달<br/>3. 전달 성/패에 따라 UI 업데이트</td>
      <td align="left">1. 사용자 동작 수행<br/>2. 성공 케이스로 UI 업데이트<br/>3. 서버에 변경 사항 전달<br/><strong>4. 실패 시 기존 상태로 롤백</strong></td>
    </tr>
  </tbody>
</table>

<br/>

<h3 id="낙관적-업데이트는-언제-활용해야할까">낙관적 업데이트는 언제 활용해야할까.</h3>
<p>낙관적 업데이트를 적용하는 기준에 대해서 프로젝트 데모 발표 때에도 받았었다. 개인적으로, 이번에 적용한 팔로우, 좋아요 버튼과 같이 <u><strong>사용자가 api 요청에 대한 결과를 사용하지 않아 사용자 액션에 영향을 주지 않는 UI의 UX를 개선하기 위해</strong></u> 사용하려고 했다.</p>
<p>반면에 갱신된 서버 데이터로 인해서 이후 다른 인터렉션에 영향이 미치는 경우나 중요한 비즈니스 로직의 경우 빠른 반응보다 정확한 처리가 더 중요하기 때문에 이런 경우에는 지양하는 것이 좋겠다라는 생각을 했다.</p>
<br/>


<h3 id="낙관적-업데이트-적용하기">낙관적 업데이트 적용하기</h3>
<h4 id="1-setstate로-적용하기">1. setState로 적용하기</h4>
<pre><code>단순히 ‘상태를 미리 갱신한다.’는 점에서 setState를 통해서 구현이 가능하다.

mutateFn 내에서 api 요청을 실행하기 전에 state를 수정하고, 성공 시에 상태는 그대로 두고, 에러 발생 시 mutateFn의 인자로 전달받은 값을 통해 이전 값으로 갱신한다.

```jsx
const useToggleFollow = ({
  initFollowState,
}: {
  initFollowState: boolean | null;
}) =&gt; {
    ...

  const mutation = useMutation&lt;
    void,
    Error,
    TogglePropsType,
    MutationContextType
  &gt;({
    mutationFn: async ({ userId, following }) =&gt; {
        // 상태 먼저 갱신
      setState(!following);
      following ? await deleteFollow(userId) : await postFollow(userId);
    },

    onSuccess: () =&gt; {
      const currentUserId = TokenHandler.getUserIdFromToken();

      if (currentUserId) {
        [
          ARTWORK.followedArtists,
          USER.followerList(currentUserId),
          USER.followingList(currentUserId),
        ].forEach((queryKey) =&gt; {
          queryClient.invalidateQueries({ queryKey: [queryKey] });
        });
      }

      // 성공시 변경된 상태 그대로 유지
    },

    onError: (error, { following }) =&gt; {
        // 에러 발생시 상태 롤백
        setState(following);
      console.error(&#39;팔로우 상태 변경 에러: &#39;, error.message);
    },
  });

    ...
};
```

이것도 충분히 잘 작동한다.</code></pre><h4 id="2-usemutate로-적용하기">2. useMutate로 적용하기</h4>
<p>   tanstack query에서는 낙관적 업데이트를 위한 useMutation의 onMutate 옵션을 제공하고 있다. 공식 문서에서는 다음과 같이 onMutate에 대해서 설명하고 있다.</p>
<p><img src="attachment:fe1b21d8-bc01-4935-bf7c-7cfeeeaf2d57:image.png" alt="image.png"></p>
<ul>
<li>이함수는 mutation 함수가 실행되기 이전에 실행된다. 그리고 mutation 함수가 받아 하는 변수와 동일한 변수를 전달 받는다.</li>
<li>mutation이 성공했을 때에 대한 낙관적 업데이트에 활용할 수 있다.</li>
<li>mutation이 실패한 경우, 이 함수로 부터 return된 값은 onError와 onSettled로 전달 된다. 그리고 낙관적업데이트를 롤백하는 데 유용하다.  </li>
</ul>
</br>

<p>onMutate를 활용하여 다음과 같이 적용해볼 수 있다.</p>
<pre><code class="language-tsx">const useToggleFollow = ({
    initFollowState,
  }: {
      initFollowState: boolean | null;
  }) =&gt; {
    ...

    const mutation = useMutation&lt;
      void,
      Error,
        TogglePropsType,
        MutationContextType
    &gt;({
        mutationFn: async ({ userId, following }) =&gt; {
          following ? await deleteFollow(userId) : await postFollow(userId);
        },

        onMutate: async ({ following }) =&gt; {
          await queryClient.cancelQueries();

          // 에러 케이스를 대비하여 기존 상태 값 저장
          const prevFollowStatus = isFollowing;

          // 일단 상태 변경
          setIsFollowing(!following);

          return { prevFollowStatus };
        },

        onSuccess: () =&gt; {
          const currentUserId = TokenHandler.getUserIdFromToken();

          if (currentUserId) {
            [
              ARTWORK.followedArtists,
              USER.followerList(currentUserId),
              USER.followingList(currentUserId),
            ].forEach((queryKey) =&gt; {
              queryClient.invalidateQueries({ queryKey: [queryKey] });
            });
          }

          // 성공시 변경된 상태 그대로 유지
        },

        onError: (error, _, context) =&gt; {
          console.error(&#39;팔로우 상태 변경 에러: &#39;, error.message);

          // 에러가 발생한 경우 원래 상태로 롤백
          if (context?.prevFollowStatus !== undefined) {
              setIsFollowing(context.prevFollowStatus);
        }
      },
    });

  ...

};
</code></pre>
<br/>

<h3 id="왜-setstate-방식으로도-할-수-있는데-onmutate라는-옵션을-제공할까">왜 setState 방식으로도 할 수 있는데, onMutate라는 옵션을 제공할까.</h3>
<p><strong>내 생각 )</strong></p>
<p>낙관적 업데이트가 필요한 경우는 대부분 서버 상태 갱신을 위해 로직을 처리하는 과정에서 딜레이가 발생하기 때문이다. setState로 낙관적 업데이트를 구현하게 되면 상태를 업데이트하는 과정이 마치 클라이언트 상태 갱신과 같이 동작하게 된다.</p>
<p>따라서, tanstack query는 서버 상태 관리를 위한 라이브러리로서 낙관적 업데이트 관련한 onMutate 옵션을 제공함으로서 보다 서버 상태와 클라이언트 상태를 명확하게 구분하기 위함이 아닐까..? 🧐</p>
<p><strong>GPT )</strong> </p>
<p>낙관적 업데이트가 필요한 이유는 대부분 <strong>서버 상태 갱신 과정에서의 딜레이</strong> 때문입니다.</p>
<p><code>setState</code>로 낙관적 업데이트를 구현할 수도 있지만, 이는 <strong>클라이언트 상태를 관리하는 방식</strong>과 동일하게 동작하게 됩니다.</p>
<p>하지만 TanStack Query는 <strong>서버 상태 관리 라이브러리</strong>이므로, <strong>클라이언트 상태와 서버 상태를 명확하게 구분</strong>하기 위해 <code>useMutation</code>을 제공합니다.</p>
<p>또한 <code>useMutation</code>은 단순히 상태를 변경하는 것을 넘어서, <strong>낙관적 업데이트, 캐싱, 에러 핸들링, 롤백 기능</strong>까지 포함하여 <strong>서버 상태를 더욱 안정적으로 관리할 수 있도록 도와줍니다.</strong></p>
<p>따라서, 서버 상태를 보다 효과적으로 관리하기 위해 <code>setState</code> 대신 <code>useMutation</code>을 사용하는 것이 더 적절합니다. ✅</p>
<p>→ 내가 적은 생각에 보완한 느낌이라 유사한 점이 많다. 캐싱과 에러 핸들링 부분을 추가로 말해주면서 보다 서버 / 클라이언트 상태 갱신을 분리한다는 점에 더해 편리함에 있어서도 이점을 가진다고 말해주고 있다.</p>
<br/>


<h3 id="reference">Reference</h3>
<p><a href="https://tecoble.techcourse.co.kr/post/2023-08-15-how-to-improve-ux-with-optimistic-update/">https://tecoble.techcourse.co.kr/post/2023-08-15-how-to-improve-ux-with-optimistic-update/</a>
<a href="https://tanstack.com/query/v4/docs/framework/react/guides/optimistic-updates">https://tanstack.com/query/v4/docs/framework/react/guides/optimistic-updates</a>
<a href="https://tanstack.com/query/v4/docs/framework/react/reference/useMutation">https://tanstack.com/query/v4/docs/framework/react/reference/useMutation</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Service Worker API 알아보기]]></title>
            <link>https://velog.io/@tkddn_dev8430/Service-Worker-API-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/Service-Worker-API-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 06 Feb 2025 06:53:08 GMT</pubDate>
            <description><![CDATA[<p>최근 프로젝트에서 PWA 기능을 구현 하거나, MSW를 통해서 백엔드 데이터를 mocking한 경험들이 있었다.</p>
<p>두 가지 경험을 하면서 공통적으로 나왔던 용어가 있었는데 바로 <code>Service Worker</code> 다.</p>
<p>Service Worker라는 개념이 여러번 나오다보니 조금 더 알고 있으면 좋을 것 같아 정리하게 되었다.</p>
<h3 id="먼저-우리-어디서-봤더라">먼저, 우리 어디서 봤더라..</h3>
<p>먼저 <a href="https://mswjs.io/docs/philosophy#using-the-platform">MSW의 공식문서</a>를 살펴 봤다. MSW의 철학에 대한 문서 중간에 아래 이미지에서 볼 수 있듯이, <code>브라우저 네트워크 수준에서 요청을 가로채기 위한</code> 목적 때문에 Service Worker를 채택하여 사용한다.</p>
<div>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/4a7540e3-a4bb-4b63-9c56-4e67b097a786/image.png' witdth='700' />
</div>


<p>다음은 <a href="https://developer.mozilla.org/ko/docs/Web/Progressive_web_apps/Tutorials/js13kGames#%EC%9D%B4_%EB%AA%A8%EB%93%A0_%EA%B2%83%EB%93%A4%EC%9D%84_%ED%95%A0%EB%A7%8C%ED%95%9C_%EA%B0%80%EC%B9%98%EA%B0%80_%EC%9E%88%EB%82%98%EC%9A%94">PWA에 관한 MDN 문서</a>인데, PWA에서는 캐싱을 활용해서 로딩 시간을 줄이기 위해 사용한다고 한다.</p>
<div>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/085d962a-27de-4f4c-ac5e-ab2a40759257/image.png' witdth='700' />
</div>



<p>같은 도구를 활용했는데 서로 다른 목적을 가지고 있는 것을 알 수 있었고, 당연한 소리지만 Service Woker가 다양한 기능을 제공하는 것을 짐작할 수 있었다. ( 예리한 척. 🧐 )</p>
<br />

<h3 id="service-worker">Service Worker</h3>
<p>Service Worker는 출처와 경로에 페이지를 제어하는 스크립트이며, 이벤트 기반의 워커로 JS 파일을 말한다.</p>
<p>연관된 웹 페이지/사이트를 통제해서 리소스 요청을 가로채 수신하여 수정할 수 있고, 다시 페이지로 돌려보낼 수 있습니다. 또한 서비스의 리소스를 세부적으로 캐싱할 수 있다.</p>
<p>그리고 브라우저와 별도의 Worker Context를 가지기 때문에 DOM에 접근할 수 없고, 웹/앱을 구동하는 JS 스레드와 다른 스레드에서 동작하기 때문에 웹/앱을 구동하는 JS 스레드 입장에서 연산을 가로막지 않는 (Non-blocking) 특징이 있다.</p>
<p>또한, 보안 상의 이유로 HTTPS에서만 동작한다. 네트워크를 가로채 수정한다는 점에서 중간자 공격에 취약할 수 있기 때문이다.</p>
<h3 id="service-worker-기본-흐름">Service Worker 기본 흐름</h3>
<ol>
<li><p>서비스 워커 로드 ( Download )</p>
<p> 브라우저가 웹 페이지를 열면 리소스(HTML, JS, CSS)를 로드되는데, 이때 JS를 통해서 ServiceWorker 파일가 로드 됩니다. 브라우저가 새로고침 되었을때, 변경 여부를 다시 확인하는데, 파일이 변경되었다면 다시 Service Worker를 다운로드 한다.</p>
</li>
<li><p>설치(Install) / 캐싱 (Caching)</p>
<p> 다운로드 이후에 설치(Install) 이벤트가 발생한다. 이때  필요한 리소스를 캐싱한다. 해당 리소스들을 추후에 오프라인에서도 활용할 수 있다.</p>
</li>
<li><p>활성화 (Activate)</p>
<p> 설치가 완료된 이후 활성화(Activate) 이벤트가 발생하고, Service Worker가 활성화된다. 이때 오래된 캐시를 삭제하거나 앱 초기화 작업을 수행할 수 있다.</p>
<p> 이 시점 부터 Service Woker는 네트워크 요청을 가로챌 수 있다.</p>
</li>
<li><p>가로채기 (Fetch)</p>
<p> 페이지 요청을 가로채서 Service Worker의 fetch 이벤트를 통해서 요청을 핸들링할 수 있다(Proxy). 캐싱된 값을 전달하거나, 네트워크로 보내거나, 응답을 임의로 설정할 수 있다.</p>
</li>
</ol>
<p>위 과정들은 아래 MDN에서 제공하는 이미지를 통해 더 쉽게 이해할 수 있다..!</p>
<div>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/9a894d21-8390-4e2b-bde0-31f3b7670bdd/image.png' witdth='700' />
</div>


<h3 id="궁금한-점">궁금한 점</h3>
<p><strong>1. 캐싱되는 장소는 어디인가?</strong></p>
<p>Service Worker가 설치되는 시점에 초기 리소스를 캐싱하게 된다. 이 캐싱 데이터가 실제 저장된 위치는 어디일까?</p>
<p>추측컨데 Service Worker는 앞서 웹/앱과 다른 스레드에서 실행된다고 했다. 그렇다면 스레드 메모리 구조에서 Stack에 저장되는 게 아닐까하는 생각이 들었다.</p>
<p>→ 땡 ❌. 바보 같게도 오프라인에서도 유지되어야 하는데 스레드의 스택에 저장되면 브라우저 내 지속성을 유지하기 어렵다…</p>
<p>캐싱된 리소스들은 <strong>브라우저 내에 cache storage</strong>에 저장된다.</p>
<p>이전 프로젝트에서 PWA를 찍먹 했을 때 코드를 활용해서 실제 리소스가 캐싱된 위치를 찾아가보았다.</p>
<pre><code class="language-jsx">    self.addEventListener(&#39;install&#39;, function () {
      self.waitUntil(
        caches.open(&#39;v1&#39;).then((cache) =&gt; {
          return cache.addAll([&#39;/index.html&#39;]);
        }),
      );
    });</code></pre>
<div>
    <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/a0cb10b2-b1bd-454b-9e93-8e9bdc21e402/image.png' witdth='700' />
</div>

<p>service worker가 install 될때 <code>/index.html</code>을 캐싱하도록 지정해주었고,
브라우저 개발자 도구에서 Application &gt; Cache Strage 내에 지정한대로 캐싱이 이루어지고 있는 것을 확인할 수 있었다.</p>
<p><strong>추가) Service Worker 확인하기</strong></p>
<ul>
<li><a href="chrome://serviceworker-internals/">chrome://serviceworker-internals/</a> 접속  </li>
</ul>
<p>  로컬에 연결된 모든 서비스 워커를 확인할 수 있다. </p>
  <div>
        <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/bc9c869e-136b-4f73-a0a1-747939bc9c64/image.png' witdth='700' />
    </div>

<ul>
<li><p>개발자 도구 활용
브라우저의 개발자 도구에서 <code>Application &gt; Service workers</code> 탭에서 현재 도메인에 설치된 Service Worker에 대한 정보를 확인할 수 있다.</p>
<div>
      <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/fe0ffb84-0f31-4522-86e7-c716cfe13a7e/image.png' witdth='700' />
      </div>    

</li>
</ul>
<br />


<p><strong>2. JS Proxy API vs Service Worker</strong></p>
<p>지금까지 알아봤던 Service Worker는 Proxy의 역할을 가지고 있다.
단순히 요청을 가로채서 원하는 값만 주는 것을 원하면 JS Proxy API와 같이 다른 방식도 있을 텐데 굳이 Service Worker를 사용한 이유에 대해서도 생각해 보았다.</p>
<p>우선 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">JS Proxy API</a>는 객체 대한 작업을 가로채서 재정의 할 수 있게 해준다.
만약 JS Proxy API로 Mocking하는 API를 만든다면 아래와 같이 만들 수도 있다.
<em>( Thanks to GPT… )</em></p>
<pre><code class="language-jsx">import axios from &#39;axios&#39;;

// Axios 인스턴스 생성
const apiClient = axios.create({
    baseURL: &#39;https://api.example.com&#39;,
    timeout: 5000,
});

// Proxy를 활용해 요청을 가로채기
const proxiedApiClient = new Proxy(apiClient, {
    get(target, propKey) {
    // propKey: &#39;get&#39;, &#39;post&#39; 등 Axios의 HTTP 메서드

    // 원래 메서드 가져오기 (예: axios.get)
    const originalMethod = target[propKey];

    // 요청 가로채기
    return async (...args) =&gt; {
         const [url] = args;

        // 특정 엔드포인트 모킹
        if (url.includes(&#39;/mock-endpoint&#39;)) {
          console.log(&#39;Mocking response for:&#39;, url);

          return Promise.resolve({
            data: { mock: &#39;This is mock data!&#39; },
            status: 200,
          });
        }

        // 나머지는 실제 네트워크 요청
        return originalMethod.apply(target, args);
      };
    },
  });</code></pre>
<p>단순히 코드와 API 모킹에 대한 봤을 때는 가능한 이야기지만 JS Proxy의 경우 단순히 코드 레벨에서 <code>기존 메서드의 동작과 다르게 동작</code> 시키는 것이었고, Service Worker를 사용한다면 <code>실제 브라우저에서 보낸 네트워크 요청을 가로채서 API 모킹을 하게</code> 된다.</p>
<p>Service Worer를 사용하는 것이 보다 백엔드 서버에 요청 / 응답을 보내는 실제 동작에 더 유사한 가로채기? 방식이기 때문에 내가 비교했던 Proxy보다 Service Worker를 활용하는 쪽이 더 적합한 방식이다.</p>
<br />

<h3 id="끝내며">끝내며…</h3>
<p>PWA의 오프라인 환경에서의 앱 실행과 MSW의 Mocking 전략에 대해서 이해할 수 있었다. 결국 두 방식 모두 요청을 주고 받을 대상이 없는 상태에서 Service Worker를 통해서 마치 실제 앱이 네트워크를 통해 데이터를 요청/응답하는 것 처럼 보이게 하기 위해서 Service Worker를 활용하는 것이다.</p>
<p>이번에 Service Worker가 무엇인지, 어떤 흐름을 통해서 동작하는지를 보다 잘 알게 된 것 같다. 동시에 내 프로젝트에 만들어뒀던 service worker 파일이 얼마나 엉망인지도 알게 되었다… ㅎ… </p>
<p>해당 글과는 살짝 관계 없는 내용이지만,</p>
<p>매번 새로운 개념을 공부하면서 수동적으로 인터넷에 있는 글들을 받아들이지 않기 위해서, 내가 기존에 알고있는 개념을 비교해보거나, 왜?라는 질문을 달기 위해서 노력하려고 한다.</p>
<p>하지만 고민해서 낸 질문에 비해서 생각보다 어렵지 않게 질문에 대한 답을 찾는 것 같아서 뭔가 생각하는 방향이 잘못된건지, 억지로 질문을 짜내려고해서 그런건지, 문득 잘못 공부하고 있나? 하는 생각이 들었다.</p>
<p>다른 개발자 분들은 블로그나 GPT를 통한 정보를 어떻게 비판적으로 수용하는지 궁금해졌다.</p>
<h3 id="reference">Reference</h3>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Service_Worker_API">https://developer.mozilla.org/ko/docs/Web/API/Service_Worker_API</a></p>
<p><a href="https://fe-developers.kakaoent.com/2022/221208-service-worker/">https://fe-developers.kakaoent.com/2022/221208-service-worker/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클릭 이벤트로 특정 위치로 이동하기]]></title>
            <link>https://velog.io/@tkddn_dev8430/%ED%81%B4%EB%A6%AD-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A1%9C-%ED%8A%B9%EC%A0%95-%EC%9C%84%EC%B9%98%EB%A1%9C-%EC%9D%B4%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tkddn_dev8430/%ED%81%B4%EB%A6%AD-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A1%9C-%ED%8A%B9%EC%A0%95-%EC%9C%84%EC%B9%98%EB%A1%9C-%EC%9D%B4%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 03 Feb 2025 06:49:29 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 서비스 소개 페이지를 구현하면서 소개 리스트와 목차 컴포넌트를 구현하고, 목차 컴포넌트를 클릭했을때 특정 소개 영역으로 이동하도록 하는 요구사항이 있었다.</p>
<p>간단한 구현이라고 생각했는데 그 과정에서 알게된 부분들이 몇가지 있어 구현 과정을 정리해보았다.</p>
<h3 id="ref-여러개-관리하기">ref 여러개 관리하기</h3>
<div align='center'>
  <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/ea7c03b4-9c6b-4617-af88-40c3f0d977ed/image.gif' width='900px' />

</div>

<p>위 이미지 처럼 소개 페이지 내부 영역이 여러 개이고 각 컴포넌트의 DOM 객체를 모두 관리해야하는데, 여러 DOM 객체를 배열로 관리해도 되는지에 대해서 헷갈리는 부분이 있었다. useRef내에서 배열로 관리할지, 아니면 useRef를 N 개 만큼 생성해서 관리할지 고민을 했었다.</p>
<p>우선 소개 영역이 거의 변하진 않겠지만 가능하다면 수정 가능성을 열어두고 개발한다면 임의로 N개의 useRef를 직접 만드는 것은 적절하지 않다고 판단했고, useRef 내부에서 배열로 관리하는 방향으로 방법을 찾아보았다.</p>
<p>그래서 ref 타입을 </p>
<pre><code class="language-jsx">const refs = useRef&lt;(HTMLDivElement | null)[]&gt;([]);</code></pre>
<p>로 선언해서 활용했다. 이렇게 활용하면 index를 통해 각 DOM 객체에 직접 접근할 수 있다.</p>
<p>이제 ref 내 요소를 넣는 방식이 떠오르지 않았는데 기존에 ref에 DOM 객체를 할당하는 방식은</p>
<pre><code class="language-jsx">...
&lt;div ref={ref}&gt; ... &lt;/div&gt;
...</code></pre>
<p>위 코드처럼 ref 객체를 props로 넘겨주는 방식이었는데 기본적인 사용 방식을 기반으로</p>
<pre><code class="language-jsx">...
{
    someArray.map((_, index) =&gt; {
        return &lt;div key={index} ref={refs[index]}&gt; ... &lt;/div&gt;;
    })
}
...</code></pre>
<p>작성해보았는데 refs가 가지고 있는 값이 배열이지 refs 자체는 배열이 아니기 때문에 index를 활용할 수 없었다.</p>
<pre><code class="language-jsx">...
{
    someArray.map((_, index) =&gt; {
        return &lt;div key={index} ref={refs.current[index]}&gt; ... &lt;/div&gt;;
    })
}
...</code></pre>
<p>이런 식으로도 해볼까 했지만 props 타입이 RefObject가 넘어가야하는데, HTMLElement 객체 값이 직접 넘어가기 때문에 이 방식도 적절하지 않다.</p>
<p>그래서 ref를 활용할때 ref 객체 대신 콜백 함수를 넘겨줄 수 있다는 점을 활용해서</p>
<pre><code class="language-jsx">...
{
    someArray.map((_, index) =&gt; {
        return &lt;div 
            key={index}
            ref={ref={(element: HTMLDivElement) =&gt; {
          refs.current[index] = element;  // 각 요소를 배열의 해당 인덱스에 저장
        }}
            &gt; ... &lt;/div&gt;;
    })
}
...</code></pre>
<p>이렇게 작성하면 된다고 한다.</p>
<p>일반적으로 ref를 props로 넘겨주는 방식은 내부적으로 React가 랜더링이 완료되고, DOM 요소를 생성한 직후에 해당 DOM 요소를 ref.current에 저장하는데, 콜백함수를 활용하게 되면 이 동작 자체를 직접 정의할 수 있다. 콜백 함수의 인자로 DOM 요소를 받게 되고, 마찬가지로 React가 랜더링이 완료되고, DOM 요소를 생성한 시점에 콜백 함수를 실행한다.</p>
<p>위 코드에서는 refs.current의 특정 위치(Index)에 DOM 정보를 저장하도록 하고 있다.</p>
<br />

<h3 id="특정-dom-위치로-스크롤-이동-시키기">특정 DOM 위치로 스크롤 이동 시키기</h3>
<p>이 부분에 대한 요구사항을 봤을 때 DOM 객체에 현재 페이지에 위치에 대한 정보나 스크롤에 대한 정보가 있어서 그걸 활용해야하나 추측했었다.</p>
<p>이부분은 생각보다 수월했던 것이 이런 요구사항을 위해 <code>.scrollIntoView()</code> 라는 내장 API를 활용하면 되는 것을 알았다. </p>
<pre><code class="language-jsx"> const moveContent = (index: number) =&gt; {
    if (contentRefs.current[index]) {
      contentRefs.current[index]?.scrollIntoView({
        behavior: &#39;smooth&#39;,
        block: &#39;center&#39;,
      });
    }
  };</code></pre>
<p>그래서 아까 저장한 refs 내 DOM 정보에 <code>.scrollIntoView()</code> 를 적용시키고,  <code>.scrollIntoView()</code> 의 인자로 스크롤 이동시 부드럽게 이동시키기 위해 behavior 속성과, DOM이 뷰포트 내 위치를 지정하기 위해 block 속성을 각각 지정해주었다.</p>
<br />

<h3 id="react-19에서-ref를-props로-받기">React 19에서 ref를 props로 받기</h3>
<p>프로젝트내 React 버전이 19 버전으로 되어있어 최근 정식으로 업그레이드 된 19버전에 따라 Ref를 props로 받는 방법이 달라졌다.</p>
<p>기존 18버전 까지는 <code>React.fowardRef</code> 를 활용해서 ref를 다른 props들과 구별하여 전달받아 활용했는데, 19버전으로 올라가면서 forwardRef를 사용하지 않고 다른 <a href="https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop">props와 동일하게 ref를 넘겨주도록 수정</a>되었다.</p>
<p>그래서 컴포넌트 내에 props 타입을 정의할 때로 Ref를 포함하여 정의했다.</p>
<pre><code class="language-jsx">interface IntroBlockProps {
  title: string;
  describe: ReactNode;
  content: ReactNode;
  ref: Ref&lt;HTMLDivElement | null&gt;;
}</code></pre>
<br />

<h3 id="끝내며">끝내며…</h3>
<p>ref를 element 속성에 직접 넘겨주고 ref.current를 활용하는 단순하고 반복적으로 활용하다보니 더 디테일하게 사용하는 방식이나 ref에 언제 DOM 정보가 저장되는지에 대한 내부적인 구현까지 파악하지 못하고 있었던 것 같다. ref를 더 잘 사용할 수 있게 된 것 같다.</p>
<br />

<h3 id="reference">Reference</h3>
<p><a href="https://ko.react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback">https://ko.react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback</a></p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView">https://developer.mozilla.org/ko/docs/Web/API/Element/scrollIntoView</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Icon 활용 방식 개선하기 ( w. svg-sprite )]]></title>
            <link>https://velog.io/@tkddn_dev8430/Icon-%ED%99%9C%EC%9A%A9-%EB%B0%A9%EC%8B%9D-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-w.-svg-sprite</link>
            <guid>https://velog.io/@tkddn_dev8430/Icon-%ED%99%9C%EC%9A%A9-%EB%B0%A9%EC%8B%9D-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-w.-svg-sprite</guid>
            <pubDate>Fri, 31 Jan 2025 10:51:31 GMT</pubDate>
            <description><![CDATA[<p>최근 두 개의 프로젝트를 병행하고 있다. 항상 프로젝트를 하면 icon을 많이 활용하게 된다.</p>
<p>앞서 시작한 프로젝트에서 icon을 관리하고 호출하는 방식을 이후 시작한 프로젝트에 적용해보면서 더 나은 방식이 있을 것이라는 생각이 들어 개선 과정을 정리하게 되었다.</p>
<h3 id="이전-프로젝트에서는-어떻게-사용하고-있나">이전 프로젝트에서는 어떻게 사용하고 있나.</h3>
<p>우선 모든 svg 아이콘을 개별 컴포넌트( 또는 svg 파일 )로 관리하고 있다.</p>
<div align='center'>
      <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/b080c18b-76d5-496a-a9ac-d5bbbee4e0d9/image.gif' width='500px'/>
  </div>


<p>그리고 icon 컴포넌트 리스트를 통해 어떤 활용할 수 있는 이미지들을 관리하고, 활용하는 컴포넌트 또는 페이지에서 Icon 컴포넌트를 호출하고 컴포넌트 이름을 props로 넘겨주어, Icon 컴포넌트 내부에서 해당 Icon을 특정하여 활용하고 있다.</p>
<p>이 방식은 다른 프로젝트에서도 비슷하게 활용하고 있어 크게 어색한 방식이라는 생각이 들진 않았지만, 초기 세팅과정에 번거로움 있고, 아이콘을 등록하는 과정에서 2 ~ 3개의 파일을 거쳐야한다는 점이 불편하게 느껴졌다.</p>
<p>그래서 개선할 수 있는 방법을 찾다가 svg sprite이라는 기법을 발견하게 되었다.</p>
<br />

<h3 id="svg-sprite">svg sprite?</h3>
<p>svg sprite은 여러 개의 svg 파일을 하나의 svg 파일로 합쳐서 관리하고 <code>&lt;use&gt;</code> 태그를 통해서 특정 아이콘만 활용하는 방식을 말한다.</p>
<p>svg sprite를 활용하면 <strong>우선 파일 크기를 줄일 수 있고</strong>, 기존의 각각 icon 파일을 요청하는 방식에 비해 <strong>1개의 파일만 요청하여 활용하기 때문에 네트워크 부하가 줄어들며</strong>, 뿐만 아니라 다운로드 받은 1개의 파일에 대해서 <strong>브라우저 캐싱이 가능하기 때문에 더 빠르게 로딩</strong>이 가능하다.</p>
<br />


<h3 id="사용해보자">사용해보자</h3>
<p>방법은 간단하다. 모든 svg 파일에 있는 <svg> 태그 정보를 우선 하나의 svg 파일로 모은다. 그리고 각 svg 파일에 해당하는 부분을 <symbol> 태그로 구분한다.</p>
<p>그리고 이후 컴포넌트에서 사용할 아이콘을 지정하기 위해 각 symbol 태그의 id 속성에 icon의 이름을 지정해둔다.</p>
<div align='center'>
      <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/de06380c-1698-4d3c-87a0-97f72453279f/image.png' width='1200px'/>
  </div>

<h4 id="svg-sprite-코드-설명">svg-sprite 코드 설명</h4>
<ul>
<li><strong><code>&lt;defs&gt;</code> 태그</strong> : svg 내부에서 재사용할 요소들을 그룹화 하는 컨테이너 역할</li>
<li><strong><code>&lt;symbol&gt;</code> 태그</strong> : 재사용할 그래픽 요소 (svg Icon)를 구분/정의하는 역할, 이때 <code>&lt;symbol&gt;</code> 태그 내부에 있는 요소들은 랜더링 되지 않고, 이후 <code>&lt;use&gt;</code> 태그를 통해 사용해야 한다.</li>
</ul>
<p>그 다음 svg-sprite 기법으로 만든 하나의 파일을 통해 icon을 랜더링할 Icon 컴포넌트를 만든다 ( React / Next.js 기준 )</p>
<pre><code class="language-jsx">interface IconProps {
  id: string;
  size?: &#39;md&#39; | &#39;lg&#39;;
  className?: string;
}

const sizeValue = {
  md: 20,
  lg: 24,
};

const Icon = ({ id, size = &#39;lg&#39;, className }: IconProps) =&gt; {
  return (
    &lt;svg
      width={sizeValue[size]}
      height={sizeValue[size]}
      className={className}
      fill=&#39;none&#39;
    &gt;
      &lt;use href={`/images/svg-sprite.svg#${id}`} /&gt;
    &lt;/svg&gt;
  );
};

export default Icon;</code></pre>
<p>컴포넌트 내부에서 <use> 태그를 통해서 원하는 Icon 컴포넌트를 지정하여 랜더링 할 수 있다.</p>
<p><code>&lt;use&gt;</code> 태그 내 <code>href</code> 속성에 <code>{svg sprite 파일 경로 }#{ svg icon id }</code> 값을 넘겨주어 Icon을 특정할 수 있다.</p>
  <br />

<h3 id="사용해보니">사용해보니…</h3>
<p>일단 눈에 띄게 개선되었다라고 느껴진 부분은 프로젝트 폴더 내 icon 파일들이 사라졌다는 점과 새로운 아이콘을 등록할 때 <code>svg-sprite.svg</code> 파일만 수정하면 된다는 점이었다. </p>
<p>그리고 모든 icon을 프로젝트 디렉토리에서 관리하는 방식과 <code>svg-sprite.svg</code> 단일로 관리하는 방식의 물리적인 용량 차이를 비교해봤을 때도 생각보다 큰 차이가 느껴졌다.</p>
  <div align='center'>
      <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/3d811dc8-e353-48ff-a2ff-547718169a9a/image.png' width='700px'/>
  </div>


<p>초기 단계라 11개의 아이콘만 관리하고 있음에도, 8KB → 2KB로 6KB( 약 75% ) 정도 용량 차이가 있었다. 더 많은 파일을 관리하게 되면 그만큼 더 큰 개선이 이루어질 것이라고 생각되어 개선 방향이 긍정적이지 않았나 하는 생각이 들었다.</p>
  <br />

<h3 id="svg-sprite는-무적">svg-sprite는 무적?</h3>
<p>모든 기술에는 장단점이 있듯 svg-sprite를 활용하는 것에도 한계가 있을 수 있다.</p>
<p><strong>먼저, svg-sprite는 모든 아이콘을 불러온다.</strong></p>
<p>앞서 모든 Icon을 개별 파일로 저장하는 경우, 총 용량이 더 클 수는 있지만 Icon을 활용할 떄 마다 불러오기 때문에 Icon 활용을 적게 하는 경우에서는 svg-sprite를 활용하는 것이 더 많은 리소스를 사용하게 될 수 도 있다.</p>
<p><strong>오히려 복잡한 관리 방식</strong></p>
<p>svg-sprite 방식을 활용하면 단일 파일로 모든 이미지 정보를 관리하고, svg 파일 내부 코드들은 벡터 기반 그래픽들의 정보들이라 읽기가 힘들어 많은 아이콘들을 관리하게 되면 그만큼 파일 내부가 복잡해지기 때문에 오히려 여러 개의 파일로 관리하는 것보다 관리가 어려울 수 있다.</p>
  <br />

<p>이외에도 여러 단점이 있을 수 있지만, 현재 진행하고 있는 우리 프로젝트에서는 <strong>svg-sprite를 활용하는 것이 더 나은 선택</strong>이라고 생각했다.</p>
<p><strong>사이드 프로젝트 규모에서 활용하는 Icon 개수가 그렇게 많지 않고, MVP 기준으로 최소 기능을 구현하기 때문에 대부분의 Icon을 활용하게 되어 적은 용량으로 관리하는 것이 더 나은 방식인 것 같다.</strong></p>
<p>아마 규모가 더 커지거나, 이후 실무에서 이런 방식을 적용하는 경우에는 부분적으로 주요한 비즈니스 로직에 있는 icon 들만 최적화 한다거나, 트래픽을 통해 자주 방문하는 페이지의 icon들에 대해서만 svg-sprite를 활용한다면 svg-sprite의 장점을 더욱 극대화 시킬 수 있지 않을까 생각한다.</p>
  <br />

<h3 id="reference">Reference</h3>
<p><a href="https://velog.io/@sasha1107/SVG-%EC%8A%A4%ED%94%84%EB%9D%BC%EC%9D%B4%ED%8A%B8-%EA%B8%B0%EB%B2%95%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EA%B8%B0%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-%EC%8A%A4%ED%94%84%EB%9D%BC%EC%9D%B4%ED%8A%B8-SVG-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://velog.io/@sasha1107/SVG-스프라이트-기법으로-사이트-성능-향상시키기리액트에서-스프라이트-SVG-사용하기</a></p>
<p><a href="https://velog.io/@sumi-0011/react-icon-manage">https://velog.io/@sumi-0011/react-icon-manage</a></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use">https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use</a></p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol">https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인/회원가입 폼 리팩토링하기 (React hook form + zod)]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%ED%8F%BC-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0-React-hook-form-zod</link>
            <guid>https://velog.io/@tkddn_dev8430/%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%ED%8F%BC-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0-React-hook-form-zod</guid>
            <pubDate>Mon, 06 Jan 2025 11:11:59 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 로그인 회원가입 관련 로직을 전담하게 되었는데, 초기 구현을 끝내고 그냥 넘어가기에는 개선해볼 수 있는 사항이 너무 많아보여서 중간에 다른 구현은 제껴두고 리팩토링 시간을 가지게 되었다.</p>
<pre><code class="language-jsx">// EmailInput.tsx

const EmailInput = ({ mode }: EmailInputProps) =&gt; {
  const [validationMessage, setValidationMessage] = useState(&#39;&#39;);
  const [validationMessageColor, setValidationMessageColor] = useState(&#39;&#39;);
  const {
    register,
    resetField,
    watch,
    formState: { errors },
  } = useFormContext();

  const email = watch(&#39;email&#39;);

  const { isValid, message, isLoading } = useGetValidateEmail(email, mode);

  const isEmailInvalid = () =&gt; !EMAIL_REGEX.test(email);

  const clearEmailField = () =&gt; resetField(&#39;email&#39;);

  const validateEmail = () =&gt; {
    if (isLoading) {
      setValidationMessage(&#39;이메일 검증 중...&#39;);
      setValidationMessageColor(&#39;text-gray-400&#39;);
    } else if (!isValid) {
      setValidationMessage(message || &#39;이미 가입된 이메일입니다.&#39;);
      setValidationMessageColor(&#39;text-system-error&#39;);
    } else {
      setValidationMessage(&#39;가입되어 있지 않은 이메일입니다.&#39;);
      setValidationMessageColor(&#39;text-system-error&#39;);
    }
  };

  useEffect(() =&gt; {
    if (mode !== &#39;sign-up&#39;) return;

    if (email === &#39;&#39;) {
      setValidationMessage(&#39;&#39;);
      setValidationMessageColor(&#39;&#39;);
    } else if (isEmailInvalid()) {
      setValidationMessage(&#39;올바른 이메일 형식으로 입력해주세요.&#39;);
      setValidationMessageColor(&#39;text-system-error&#39;);
    } else if (email) {
      validateEmail();
    } else {
      setValidationMessage(&#39;&#39;);
    }
  }, [email]);

  useEffect(() =&gt; {
    if (errors.email) {
      setValidationMessage(errors.email?.message as string);
      setValidationMessageColor(&#39;text-system-error&#39;);
    }
  }, [errors]);

  return (
    &lt;div&gt;
      &lt;Input
        {...register(&#39;email&#39;, { required: true, pattern: EMAIL_REGEX })}
        type=&#39;email&#39;
        label=&#39;이메일&#39;
        labelPlacement=&#39;outside&#39;
        placeholder=&#39;이메일을 입력해주세요.&#39;
        isInvalid={false}
        classNames={{
          label: &#39;custom-label text-gray-400&#39;,
          input: &#39;placeholder:text-gray-700&#39;,
          inputWrapper: [&#39;bg-gray-900&#39;, &#39;rounded-md&#39;],
        }}
        onClear={clearEmailField}
      /&gt;
        ...
    &lt;/div&gt;
  );
};

export default EmailInput;</code></pre>
<p>처음 작성했던 이메일 Input 컴포넌트인데 보기에도 상당히 복잡해보인다.
폼 자체에 대해서는 폼을 관리하는 페이지에서 <code>React Hook Form</code>을 통해서 폼 데이터를 효율적으로 관리하고 있지만, 각 필드에서 유효성 검사, 에러 메세지 같이 생각보다 <strong>많은 상태들을 관리</strong>하고 있다.</p>
<p>그리고 이메일 입력값에 대한 유효성 검증 로직이 복잡하게 얽혀있는데, 필드 내부에 입력 컴포넌트가 사용되는 폼을 직접 관리하기 때문에 상당히 복잡한 것을 볼 수 있다.</p>
<p>그래서 이번 리팩토링의 목적을</p>
<ol>
<li><strong>각 필드 내 사용되는 상태 줄이기</strong></li>
<li><strong>입력 값에 대한 유효성 검증 로직 개선하기</strong></li>
</ol>
<p>로 잡고 리팩토링을 수행했다.</p>
<h2 id="상태-줄이기">상태 줄이기</h2>
<p>우선 기존 컴포넌트 내부에서는 <strong>에러 메세지와 검증 결과를 표시할 문자의 색상 값을 상태로 관리</strong>하고 있었다. 이 상태들은 상태 갱신이 되는 시점이 동일하기 때문에 굳이 별개의 useState로 정의할 필요가 없다고 생각해서 객체 상태로 정의하여 한번에 관리하였다.</p>
<p>그리고 기존에 두 개의 유효성 검증 조건 문은 로그인/ 회원가입에 활용될 수 있게 조건의 순서를 조절하여 하나의 조건문으로 검증 가능하도록 수정하였다.</p>
<pre><code class="language-tsx">const NicknameInput = () =&gt; {
  const [validateStatus, setValidateStatus] = useState({
    type: &#39;&#39;,
    message: &#39;&#39;,
    messageColor: &#39;&#39;,
  });

  const { register, watch } = useFormContext();

  const nickname = watch(&#39;nickname&#39;);

  const { isValid, message, isLoading } = useGetValidateNickName(nickname);

  const isNicknameInvalid =
    (nickname !== &#39;&#39; &amp;&amp; !NICKNAME_REGEX.test(nickname)) ||
    nickname.length &gt; MAX_NICKNAME_LENGTH;

  const validateNickname = () =&gt; {
    if (nickname === &#39;&#39;) {
      return { type: &#39;&#39;, message: &#39;&#39;, messageColor: &#39;&#39; };
    }

    if (isNicknameInvalid) {
      return {
        type: &#39;error&#39;,
        message: &#39;사용할 수 없는 문자 또는 길이를 초과했습니다.&#39;,
        messageColor: &#39;text-system-error&#39;,
      };
    }

    if (isLoading) {
      return {
        type: &#39;loading&#39;,
        message: &#39;닉네임 검증 중...&#39;,
        messageColor: &#39;text-gray-400&#39;,
      };
    }

    if (!isValid) {
      return {
        type: &#39;error&#39;,
        message: message || &#39;중복된 닉네임이 존재합니다.&#39;,
        messageColor: &#39;text-system-error&#39;,
      };
    }

    return {
      type: &#39;success&#39;,
      message: &#39;사용 가능한 닉네임입니다.&#39;,
      messageColor: &#39;text-system-success&#39;,
    };
  };

  useEffect(() =&gt; {
    const nicknameValidateResult = validateNickname();

    setValidateStatus(nicknameValidateResult);
  }, [nickname, isLoading, isValid, message]);

  const currentNicknameLength = nickname.length;
  const nicknameLengthColor =
    nickname.length === 0
      ? &#39;text-gray-700&#39;
      : currentNicknameLength &gt; MAX_NICKNAME_LENGTH
        ? &#39;text-system-error&#39;
        : &#39;text-white&#39;;

  return (
    &lt;div&gt;
      ...
    &lt;/div&gt;
  );
};

export default NicknameInput;
</code></pre>
<p>하지만 여전히 효과적으로 리팩토링을 했다는 생각이 들지 않았다.</p>
<p>우선 먼저 react hook form을 사용하면서 제출 버튼이 폼의 유효성 검증이 통과하는 경우에 활성화되도록 되어있는데, 이때 <strong>각 필드의 유효성 검증은 Input 태그 내에서 포함된 register의 속성에 정의</strong>되어있어야 한다. 그래서 어색하게 동일한 유효성 검증이 에러 메세지를 위해서, 폼 제출 여부 판단을 위해서 두 번 쓰이고 있다.</p>
<p>이를 개선 하기 위해 아래와 같은 두번의 시행 착오를 거쳤다.</p>
<ol>
<li><p><strong>모든 유효성 검증을 register 속성 내부로 이전</strong></p>
<pre><code class="language-tsx"> &lt;Input
   {...register(&#39;nickname&#39;, {
     required: &#39;닉네임이 입력되지 않았습니다.&#39;,
     max: {
       value: 10,
       message: &#39;최대 닉네임 길이를 초과했습니다.&#39;,
     },
     pattern: {
       value: NICKNAME_REGEX,
       message: &#39;닉네임에 허용되지 않는 문자가 포함되어 있습니다.&#39;,
     },
     validate: {
       duplicatedNickname: async (nickname) =&gt; {
         const isDuplicated = await getValidateNickname(nickname);
         return isDuplicated &amp;&amp; &#39;이미 사용 중인 닉네임입니다.&#39;;
       },
     },
   })}
   type=&#39;text&#39;
   label=&#39;닉네임&#39;
   labelPlacement=&#39;outside&#39;
   placeholder=&#39;닉네임을 입력해주세요.&#39;
   maxLength={MAX_NICKNAME_LENGTH}
   classNames={{
     label: &#39;custom-label&#39;,
     input: &#39;placeholder:text-gray-700&#39;,
     inputWrapper: [&#39;bg-gray-900&#39;, &#39;rounded-md&#39;],
   }}
   endContent={
     &lt;div className=&#39;flex items-center&#39;&gt;
       &lt;span className={`placeholder ${nicknameLengthColor}`}&gt;
         {currentNicknameLength}
       &lt;/span&gt;
       &lt;span className=&#39;placeholder text-gray-700&#39;&gt;
         /{MAX_NICKNAME_LENGTH}
       &lt;/span&gt;
     &lt;/div&gt;
   }
 /&gt;</code></pre>
<p> → 하지만 이렇게 작성하는 경우 로그인 폼 / 회원 가입 폼에 필요한 검증 방식이 다른데 <strong>폼 종류에 관계없이 모두 같은 로직을 사용해야하기 때문에 해당 방식을 활용할 수 없었다.</strong></p>
</li>
</ol>
<br />


<ol start="2">
<li><p><strong>모든 유효성 검증을 외부 로직으로 분리</strong></p>
<p> 반대로 외부로직으로 모든 유효성 검증 로직을 분리하고, 대신 폼과 유효성 결과를 동기화 시키기 위해 <strong>React Hook Form에서 제공하는 setError, formState를 활용하여 기존 setState를 대신하는 방식</strong>을 사용해보았다.</p>
<p> → 수정이 필요했던 부분들을 모두 반영할 수 있었지만, <strong>React Hook Form에서 다시 유효성 검증 로직을 실행하기 이전에 Error 객체를 초기화하기 때문에</strong> 입력 필드의 <code>onChange</code> 이벤트가 발생할 때 마다 <strong>메세지가 사라졌다가, 다시 새로운 메세지가 나와 메세지가 깜빡깜빡 거리는 현상</strong>이 생겼다. </p>
</li>
</ol>
<p>그래서 현재 단계에서 보완이 필요한 점은</p>
<ol>
<li>React Hook Form의 폼에서 관리하는 유효성 로직을 현재 각 필드의 에러 메세지 UI와 동기화 되어야함</li>
<li>현재 필드를 사용하려고하는 폼에 따라 서로다른 유효성 검증 로직이 적용되어야함.</li>
</ol>
<p>이 두가지를 목표로 다른 방식을 찾아보면서 <strong>zod라는 라이브러리를 React Hook Form에서 활용할 수 있다는 것</strong>을 알게 되었다.</p>
<br />

<h2 id="zod">Zod</h2>
<p>Zod는 ‘스키마’라는 개념을 통해서 객체 정의가 가능하고, 동시에 객체에 대한 유효성 검사도 가능하게 하는 라이브러리이다.</p>
<pre><code class="language-tsx">import { z } from &quot;zod&quot;;

// 스키마 선언
const userSchema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email(),
});

// 타입 추출
type UserType = z.infer&lt;typeof userSchema&gt;;
</code></pre>
<p>React Hook Form에서는 useForm을 선언할 때, <strong><a href="https://react-hook-form.com/ts#Resolver">resolver</a>라는 옵션을 사용할 수 있는데, 이 옵션을 사용하면 각 필드에서 작성했던 유효성 검사를 중앙에서 관리할 수 있게 해준다.</strong> resolver의 옵션으로 zodResolver라는 함수와 함께 zod로 선언한 객체의 스키마를 넘겨주면 객체 정의에 따라 유효성 검사가 가능해진다.</p>
<br />

<h2 id="zod로-유효성-검사-로직-분리하기">Zod로 유효성 검사 로직 분리하기</h2>
<p>회원 가입 폼에 대한 스키마를 다음과 같이 정의하여 활용하였다.</p>
<pre><code class="language-tsx">const signUpValidationSchema = object({
  email: string()
    .min(1, &#39;이메일은 필수입니다.&#39;)
    .email(&#39;유효하지 않은 이메일 형식입니다.&#39;)
    .refine(async (email) =&gt; {
      const isDuplicated = await getValidateEmail(email);
      return isDuplicated;
    }, &#39;이미 가입된 이메일입니다.&#39;),
  password: string()
    .min(1, &#39;비밀번호는 필수입니다.&#39;)
    .regex(
      PASSWORD_REGEX,
      &#39;영문, 숫자, 특수문자를 포함해 9자 이상 입력해주세요.&#39;,
    ),
  nickname: string()
    .min(1, &#39;닉네임은 필수입니다.&#39;)
    .max(MAX_NICKNAME_LENGTH, &#39;최대 닉네임 길이를 초과했습니다.&#39;)
    .regex(NICKNAME_REGEX, &#39;한글, 영어, 숫자로 구성된 닉네임을 입력해주세요.&#39;)
    .refine(async (nickname) =&gt; {
      const isDuplicated = await getValidateNickname(nickname);
      return isDuplicated;
    }, &#39;이미 사용중인 닉네임입니다.&#39;),
});</code></pre>
<p>그리고 스키마로 인해서 자연스럽게 각 필드에 있던 유효성 로직이 필요없어졌기 때문에 관련 코드를 이전보다 깔끔하게 정리할 수 있었다.</p>
<pre><code class="language-tsx">const EmailInput = ({ mode }: EmailInputProps) =&gt; {
  const [isEmailValidating, setIsEmailValidating] = useState(false);

  const {
    register,
    resetField,
    watch,
    setValue,
    trigger,
    formState: { errors },
  } = useFormContext();

  const email = watch(&#39;email&#39;);

  const clearEmailField = () =&gt; resetField(&#39;email&#39;);

  return (
    &lt;div&gt;
      &lt;Input
        {...register(&#39;email&#39;)}
        type=&#39;email&#39;
        label=&#39;이메일&#39;
        labelPlacement=&#39;outside&#39;
        placeholder=&#39;이메일을 입력해주세요.&#39;
        isInvalid={false}
        classNames={{
          label: &#39;custom-label&#39;,
          input: &#39;placeholder:text-gray-700&#39;,
          inputWrapper: [&#39;bg-gray-900&#39;, &#39;rounded-md&#39;],
        }}
        onClear={clearEmailField}
      /&gt;

      ...
        &lt;/div&gt;
  );
};

export default EmailInput;
</code></pre>
<br />

<h2 id="추가-개선-사항">추가 개선 사항</h2>
<ol>
<li><p><strong>.superRefine()으로 유효성 검사 커스텀 하기</strong></p>
<p> 비동기 검증 로직을 활용하면서 true/false를 반환하여 현재 이메일을 사용할 수 있는지, 사용할 수 없는지만 검증하였지만, 내부적으로 정확히 어떤 이유에서 이메일을 사용할 수 없는지 에러 메세지를 통해 전달할 필요가 있었다.</p>
<p> 기존의 <a href="https://zod.dev/?id=refine">.refine()</a>은 내부 콜백함수가 true/false를 반환해서 검증 여부만 판단하기 때문에 각 에러 케이스에 따른 에러 메세지를 설정하기 어렵다고 판단했다.</p>
<p> 그래서 .refine의 고급 커스텀이 가능한 <a href="https://zod.dev/?id=superrefine">.superRefine()</a>을 활용하여 아래와 같이 경우에 따른 적절한 에러 메세지를 전달할 수 있도록 개선했다.</p>
<pre><code class="language-jsx"> const signUpValidationSchema = object({
   email: string()
     ...
     .superRefine(async (email, ctx) =&gt; {
       const status = await getValidateEmail(email);

       if (status === 400) {
         ctx.addIssue({
           code: ZodIssueCode.custom,
           message: SIGNIN_ERROR_MESSAGE.INVALID_EMAIL,
         });
       }

       if (status === 409) {
         ctx.addIssue({
           code: ZodIssueCode.custom,
           message: SIGNIN_ERROR_MESSAGE.DUPLICATE_EMAIL,
         });
       }
     }),
   ...
   nickname: string()
       ...
     .superRefine(async (nickname, ctx) =&gt; {
       const status = await getValidateNickname(nickname);

       if (status === 400) {
         ctx.addIssue({
           code: ZodIssueCode.custom,
           message: NICKNAME_VALIDATE_ERROR_MESSAGE.INVALID_NICKNAME,
         });
       }

       if (status === 409) {
         ctx.addIssue({
           code: ZodIssueCode.custom,
           message: NICKNAME_VALIDATE_ERROR_MESSAGE.DUPLICATE_NICKNAME,
         });
       }
     }),
 });</code></pre>
</li>
<li><p><strong>debounce 적용하기</strong></p>
<p> 이전에 onChange마다 유효성 검사가 실행되기 때문에 에러 메세지가 깜빡거리는 현상이 있었다. </p>
<p> 이 부분이 상당히 사용자 입장에서 좋아보이는 UI가 아니기도 했고, 유효성 검증 로직 중에 현재 입력 값이 <code>이미 가입된 이메일인지</code> 확인하는 요청을 보내야하는데 이것이 입력될 때 마다 요청을 주고 받는 부분에 대해서 최적화의 필요성을 느꼈다.</p>
<p>  <strong>lodash의 debounce</strong>를 통해서 onChange 이벤트가 끝난 시점에서 특정 시간동안 입력이 없으면 검증 로직이 실행될 수 있도록 최적화 하였다.</p>
<pre><code class="language-jsx"> import { debounce } from &#39;lodash&#39;;

 ...

 const handleEmailInputOnChange = debounce(async (e) =&gt; {
     setValue(&#39;email&#39;, e.target.value);

     setIsEmailValidating(true);
     await trigger(&#39;email&#39;);
     setIsEmailValidating(false);
   }, 300);</code></pre>
</li>
</ol>
<br />


<h2 id="리팩토링-후기">리팩토링 후기</h2>
<p>누군가 <code>zod 꼭 쓰세요</code> 라고 했던 걸 본적이 있었는데 이번 기회에 zod를 사용해보게 되어서 상당히 의미있는 시간이었던 것 같다. 그리고 React Hook Form을 사용해서 필요한 상태를 줄일 수 있다는 점에 대해서는 상당히 공감하며 사용하고 있었지만 그 외 제공해주는 다양한 기능들에 대해서는 잘 몰랐었다. </p>
<p>이번 기회에 공식 문서도 조금 더 파볼 수 있었고, form 데이터 관리를 좀 더 체계적으로 해볼 수 있는 경험이었던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 19 RC ( 번역 )]]></title>
            <link>https://velog.io/@tkddn_dev8430/%EB%A6%AC%EC%95%A1%ED%8A%B8-19-RC-%EB%B2%88%EC%97%AD</link>
            <guid>https://velog.io/@tkddn_dev8430/%EB%A6%AC%EC%95%A1%ED%8A%B8-19-RC-%EB%B2%88%EC%97%AD</guid>
            <pubDate>Sun, 24 Nov 2024 18:07:01 GMT</pubDate>
            <description><![CDATA[<p>리액트 19버전 출시를 앞두고 있는 지금, 어떤 기능들이 추가되는지 알아보기 위해 개인적으로 번역하며 읽어보았습니다.</p>
<blockquote>
<p><strong>RC?</strong>
Release Candidate의 약자로, 소프트웨어 개발 주기에서 최종 출시 전에 공개되는 버전을 말한다. 안정성과 기능이 최종 버전에 가깝지만 사용자로부터 피드백을 받고 버그를 수정할 가능성이 있는 상태를 의미한다.</p>
</blockquote>
<h2 id="react-19의-새로운-기능">React 19의 새로운 기능</h2>
<h3 id="actions">Actions</h3>
<p>React App들의 일반적인 사용 케이스는 데이터를 변형하고 그 응답에 따라 상태를 업데이트하는 것입니다. 예를 들어, 사용자가 이름을 변경하여 폼을 제출할 때, API 요청을 생성한 다음, 응답을 처리합니다. 과거에는 Pending 상태, 에러, 낙관적 업데이트, 그리고 순차적인 요청을 수동적으로 처리해야 했습니다.</p>
<p>예를 useState를 통해 Pending과 Error 상태를 다룰 수 있었습니다.</p>
<pre><code class="language-jsx">// Before Actions
function UpdateName({}) {
  const [name, setName] = useState(&quot;&quot;);
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () =&gt; {
    setIsPending(true);
    const error = await updateName(name);
    setIsPending(false);
    if (error) {
      setError(error);
      return;
    } 
    redirect(&quot;/path&quot;);
  };

  return (
    &lt;div&gt;
      &lt;input value={name} onChange={(event) =&gt; setName(event.target.value)} /&gt;
      &lt;button onClick={handleSubmit} disabled={isPending}&gt;
        Update
      &lt;/button&gt;
      {error &amp;&amp; &lt;p&gt;{error}&lt;/p&gt;}
    &lt;/div&gt;
  );
}</code></pre>
<p>React 19에서는 Pending 상태, Error, 폼, 낙관적 업데이트를 자동으로 처리하기 위해 transition에서 비동기 함수를 통해 지원하는 방식을 추가하였습니다.</p>
<p>예를 들어, pending state를 다루기 위해 useTransition 훅을 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-jsx">// Using pending state from Actions
function UpdateName({}) {
  const [name, setName] = useState(&quot;&quot;);
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () =&gt; {
    startTransition(async () =&gt; {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      } 
      redirect(&quot;/path&quot;);
    })
  };

  return (
    &lt;div&gt;
      &lt;input value={name} onChange={(event) =&gt; setName(event.target.value)} /&gt;
      &lt;button onClick={handleSubmit} disabled={isPending}&gt;
        Update
      &lt;/button&gt;
      {error &amp;&amp; &lt;p&gt;{error}&lt;/p&gt;}
    &lt;/div&gt;
  );
}</code></pre>
<p>비동기 트랜지선은 <code>isPending</code>상태를 즉시 true로 바꾸고, 트랜지션이 끝난 후에 비동기 요청를 만들고, isPending을 False로 바꿉니다. 이것을 통해서 데이터가 바뀌는 동안 현재 UI를 반응성있고, 상호작용적으로 유지할 수 있습니다.</p>
<p>Actions를 기반으로, React 19에서는 낙관적 업데이트를 관리하는 <code>useOptimistic</code> 과 Actions의 일반적인 케이스를 다루는 <code>React.useActionState</code> 라는 새로운 훅을 소개합니다.</p>
<p><code>react-dom</code> 에서는 form을 자동으로 관리하는 <code>&lt;form&gt;</code>Actions와 form에서의 Actions의 일반적인 케이스를 지원하는 useFormStatus를 추가했습니다.</p>
<p>React 19에서는 위 예시를 다음과 같이 단순화 할 수 있습니다.</p>
<pre><code class="language-jsx"> // Using &lt;form&gt; Actions and useActionState
  function ChangeName({ name, setName }) {
    const [error, submitAction, isPending] = useActionState(
      async (previousState, formData) =&gt; {
        const error = await updateName(formData.get(&quot;name&quot;));
        if (error) {
          return error;
        }
        redirect(&quot;/path&quot;);
        return null;
      },
      null,
    );

    return (
      &lt;form action={submitAction}&gt;
        &lt;input type=&quot;text&quot; name=&quot;name&quot; /&gt;
        &lt;button type=&quot;submit&quot; disabled={isPending}&gt;Update&lt;/button&gt;
        {error &amp;&amp; &lt;p&gt;{error}&lt;/p&gt;}
      &lt;/form&gt;
    );
  }</code></pre>
<p>다음 섹션에서는 React 19의 새로운 Action의 기능들을 하나씩 알아봅니다.</p>
<h3 id="새-훅--useactionstate">새 훅 : <code>useActionState</code></h3>
<p> Actions의 일반적인 케이스를 쉽게 하기 위해 <code>useActionState</code>라고 불리는 새로운 훅을 추가하였습니다.</p>
<pre><code class="language-jsx">const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) =&gt; {
    const error = await updateName(newName);
    if (error) {
      // You can return any result of the action.
      // Here, we return only the error.
      return error;
    }

    // handle success
    return null;
  },
  null,
);</code></pre>
<p>useActionState는 함수를 받아서, 호출할 wrapping된 Action을 반환합니다. Actions가 구성되기 때문에 이것은 동작합니다. 래핑된 Action이 호출될 때, useActionState는 Action의 마지막 결과를 <code>data</code> 로 반환하고 Actions의 pending 상태를 <code>pending</code>으로 반환합니다.</p>
<p>더 많은 정보를 알고 싶으면 <a href="https://react.dev/reference/react/useActionState">useActionState</a>의 문서를 확인하세요.</p>
<h3 id="react-dom--form-actions">React DOM : <code>&lt;form&gt;</code> Actions</h3>
<p>Actions는 또한 React 19에서 <code>&lt;form&gt;</code>의 새로운 기능과도 통합되었습니다. Actions을 통해 자동으로 폼을 제출하기 위해 <code>&lt;form&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;button&gt;</code> 엘리먼트의 formAction props로 함수를 전달하는 기능을 추가했습니다.</p>
<pre><code class="language-jsx">&lt;form action={actionFunction} /&gt;</code></pre>
<p><code>&lt;form&gt;</code> 액션이 성공되었을 때, 리액트는 제어되지 않은 컴포넌트에 대해서 폼을 초기화 하게 됩니다. 만약 <code>&lt;form&gt;</code>의 초기화를 수동으로 해야한다면 새로운 React DOM API의 requestFormReset을호출할 수 있습니다.</p>
<p>더 자세한 내용은 react-dom의 <code>&lt;form&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;button&gt;</code> 문서를 참조하세요.</p>
<h3 id="react-dom--새-훅-useformstatus">React Dom : 새 훅: <code>useFormStatus</code></h3>
<p>디자인 시스템에서, 컴포넌트까지 props를 drilling하지 않고, 컴포넌트에 대한 정보에 접근하는 디자인 컴포넌트를 만드는 것이 일반적입니다. 이것을 Context를 통해서 가능하지만 더 쉽게 만들기 위해서 useFormStatus라는 새로운 훅을 추가하였습니다.</p>
<pre><code class="language-jsx">import {useFormStatus} from &#39;react-dom&#39;;

function DesignButton() {
  const {pending} = useFormStatus();
  return &lt;button type=&quot;submit&quot; disabled={pending} /&gt;
}</code></pre>
<p><code>useFormStatus</code>는 <form>이 Context Provider 인 것 처럼, 부모의 상태를 읽을 수 있게 해줍니다.</p>
<p>자세한 내용은 <code>react-dom</code> 의 <a href="https://react.dev/reference/react-dom/hooks/useFormStatus"><code>useFormStatus</code></a>문서를 참조하세요 .</p>
<h3 id="새-훅--useoptimistic">새 훅 : <code>useOptimistic</code></h3>
<p>데이터 변형(Mutation)을 수행할때 또 다른 일반적인 패턴은 비동기 요청이 수행되는 동안에 낙관적으로 최종 상태를 보여주는 것입니다. React 19에서는 useOptimistic이라고 불리는 새로운 훅을 호출하여 더욱 쉽게 했습니다.</p>
<pre><code class="language-jsx">function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async formData =&gt; {
    const newName = formData.get(&quot;name&quot;);
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    &lt;form action={submitAction}&gt;
      &lt;p&gt;Your name is: {optimisticName}&lt;/p&gt;
      &lt;p&gt;
        &lt;label&gt;Change Name:&lt;/label&gt;
        &lt;input
          type=&quot;text&quot;
          name=&quot;name&quot;
          disabled={currentName !== optimisticName}
        /&gt;
      &lt;/p&gt;
    &lt;/form&gt;
  );
}</code></pre>
<p>updateName 요청이 진행되는 동안에, <code>useOptimistic</code> 훅은 즉시 optimisticName을 랜더할 것입니다. 업데이트가 완료 되거나, 에러가 발생하면 리액트는 자동으로 <code>currentName</code> 값으로 바꾸게 됩니다.</p>
<p>자세한 내용은 <a href="https://react.dev/reference/react/useOptimistic"><code>useOptimistic</code></a> 문서를 참조하세요.</p>
<h3 id="새-api--use">새 API : <code>use</code></h3>
<p>React 19에서 <code>render</code> 에서 리소스를 읽는 새로운 API인 use를 소개합니다.</p>
<p>예를 들어 use를 통해 promise를 읽을 수 있고, 그 동안 React는 promise가 완료될 때 까지 중단합니다.</p>
<pre><code class="language-jsx">import {use} from &#39;react&#39;;

function Comments({commentsPromise}) {
  // `use` will suspend until the promise resolves.
  const comments = use(commentsPromise);
  return comments.map(comment =&gt; &lt;p key={comment.id}&gt;{comment}&lt;/p&gt;);
}

function Page({commentsPromise}) {
  // When `use` suspends in Comments,
  // this Suspense boundary will be shown.
  return (
    &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
      &lt;Comments commentsPromise={commentsPromise} /&gt;
    &lt;/Suspense&gt;
  )
}</code></pre>
<p><code>use</code> 를 활용하여 Context를 읽을 수 있으며, early return 후와 같이 context를 조건부로 읽을 수 있습니다.</p>
<pre><code class="language-jsx">import {use} from &#39;react&#39;;
import ThemeContext from &#39;./ThemeContext&#39;

function Heading({children}) {
  if (children == null) {
    return null;
  }

  // This would not work with useContext
  // because of the early return.
  const theme = use(ThemeContext);
  return (
    &lt;h1 style={{color: theme.color}}&gt;
      {children}
    &lt;/h1&gt;
  );
}</code></pre>
<p><code>use</code> API는 훅과 비슷하게 <code>render</code> 에서 부를 수 있습니다. 훅과는 다르게 <code>use</code> API는 조건부로 호출이 가능합니다. 미래에는 <code>use</code> 를 활용하여 리소스를 활용하는 더 많은 방식을 지원할 계획에 있습니다.</p>
<p>자세한 내용은 <a href="https://react.dev/reference/react/use"><code>use</code></a> 문서를 참조하세요.</p>
<h3 id="react-server-component">React Server Component</h3>
<p><strong>Server Component</strong></p>
<p>서버 컴포넌트는 번들링하기 이전에 클라이언트 애플리케이션이나 SSR 서버와 별도의 환경에서 구성 요소를 미리 랜더링할 수 있는 방식이다. 이 분리된 환경은 “React Server Component”의 ‘서버’입니다. 서버 컴포넌트는 CI 서버에서 빌드 시간에 한 번 실행되거나 웹 서버를 사용해서 각 요청에 대해 실행될 수 있습니다.</p>
<p>React 19에는 Canary 채널에서 포함된 모든 React Server Component의 기능이 포함되어 있습니다. 즉, Server Component와 함께 제공되는 라이브러리는 이제 Full-Stack React Architecture를 지원하기위한 프레임워크를 사용할 때 React 19를 react-server export 조건과 함께 피어 종속성으로 타겟으로 할 수 있습니다.</p>
<p>자세한 내용은 <a href="https://react.dev/reference/rsc/server-components"><code>React Server Components</code></a> 문서를 참조하세요.</p>
<p><strong>Server Actions</strong></p>
<p>Server Actions는 클라이언트 컴ㅍ노너트가 서버에서 실행되는 비동기 함수를 호출할 수 있게 해줍니다.</p>
<p>Server Actions가 <code>use server</code> 로 직접 정의되어 있을 때, 당신의 프레임워키에서는 자동으로 서버 함수의 참조를 생성하고 클라이언트 컴포넌트에게 참조를 전달합니다. 함수가 클라이언트에서 호출되었을 때, React는 함수를 실행하고 결과를 반환하기 위해서 서버에 요청을 보냅니다.</p>
<p>서버 액션은 서버 컴포넌트에서 생성되어 클라이언트 컴포넌트에 Props로 전달될 수 도 있고, 클라이언트 컴포넌트에서 import해서 사용할 수 도 있습니다.</p>
<p>자세한 내용은 <a href="https://react.dev/reference/rsc/server-actions"><code>React Server Actions</code></a> 문서를 참조하세요.</p>
<h2 id="react-19의-개선-사항">React 19의 개선 사항</h2>
<h3 id="prop으로서-ref">prop으로서 <code>ref</code></h3>
<p>React 19를 시작하면서 함수형 컴포넌트에서 prop으로 ref에 접근할 수 있습니다.</p>
<pre><code class="language-jsx">function MyInput({placeholder, ref}) {
  return &lt;input placeholder={placeholder} ref={ref} /&gt;
}

//...
&lt;MyInput ref={ref} /&gt;</code></pre>
<p>새로운 함수형 컴포넌트는 더이상 forwardRef가 필요하지 않습니다. 그리고 새로운 ref prop을 사용하여 자동으로 당신의 컴포넌트를 업데이트하는 codemod도 배포할 예정힙니다. 미래의 버전에서는 forwardRef를 deprecate하고 삭제할 예정입니다.</p>
<h3 id="diff-for-hydration-errors">Diff for hydration errors</h3>
<p>우리는 또한 react-dom에서 hrdration error를 보고하는 에러 보고를 개선하였습니다. 예를 들어 DEV에서 불일치에 대한 어떠한 정보도 없이 기록하는 대신</p>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/75430efa-3606-49b1-b951-ab1128d5ecc5/image.png' width='500'/>
</div>


<p>이제 불일치의 diff에 대한 메세지를 포함하여 기록합니다.</p>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/2110a752-44dc-425e-902c-a67f7710a5a7/image.png' width='500'/>
</div>


<h3 id="context-as-provider"><Context> as Provider</h3>
<p>React 19에서 Context.Provider 대신 <Context>를 Provider로 활용할 수 있습니다.</p>
<pre><code class="language-jsx">const ThemeContext = createContext(&#39;&#39;);

function App({children}) {
  return (
    &lt;ThemeContext value=&quot;dark&quot;&gt;
      {children}
    &lt;/ThemeContext&gt;
  );  
}</code></pre>
<p>새로운 Context Provider는 <code>&lt;Context&gt;</code> 를 활용할 수 있으며, 기존 Provider를 변환하기 위한 codemod를 게시할 예정입니다. 향후 버전에서는  <code>&lt;Context.Provider&gt;</code>을 사용하지 않을 예정입니다.</p>
<h3 id="cleanup-functions-for-refs">Cleanup functions for refs</h3>
<p>이제 ref의 콜백에서 정리 함수를 반환하는 기능을 지원합니다.</p>
<pre><code class="language-jsx">&lt;input
  ref={(ref) =&gt; {
    // ref created

    // NEW: return a cleanup function to reset
    // the ref when element is removed from DOM.
    return () =&gt; {
      // ref cleanup
    };
  }}
/&gt;
</code></pre>
<p>컴포넌트가 언마운트 되었을 때, 리액트는 ref 콜백함수로부터 반환되는 cleanup 함수를 호출합니다. 이것은 DOM refs, 클래스 컴포넌트의 refs, useImperativeHandle에 유효합니다.</p>
<p>ref 정리함수가 도입되었기 때문에 ref 콜백함수에서 다른 것을 반환하는 것은 이제 TS에서 거부됩니다. 아래와 같이 return을 사용하지 않음으로서 문제를 해결할 수 있습니다.</p>
<pre><code class="language-jsx">- &lt;div ref={current =&gt; (instance = current)} /&gt;
+ &lt;div ref={current =&gt; {instance = current}} /&gt;</code></pre>
<p>원래 코드는 HTMLDivElement의 인스턴스를 반환했고, TypeScript는 이것이 cleanup 함수인지 cleanup 함수를 반환하고 싶지 않아하는지 알 수 없었습니다.</p>
<p>당신은 <code>no-implicit-ref-callback-return</code> 을 통해서 해당 패턴을 codemod 할 수 있습니다.</p>
<p><code>useDeferredValue</code> inital Value</p>
<p>우리는 <code>useDeferredValue</code> 에 initial Option을 추가하였습니다.</p>
<pre><code class="language-jsx">function Search({deferredValue}) {
  // On initial render the value is &#39;&#39;.
  // Then a re-render is scheduled with the deferredValue.
  const value = useDeferredValue(deferredValue, &#39;&#39;);

  return (
    &lt;Results query={value} /&gt;
  );
}</code></pre>
<p>initialValue가 제공되면, useDeferedValue는 이것을 컴포넌트의 초기 랜더 값으로서 반환합니다. 그리고 반환되는 defferedValue와 함께 백그라운드에서 리랜더를 예약합니다.</p>
<p>더 많은 내용은 <code>useDefferedValue</code> 를 참조하세요.</p>
<h3 id="support-for-document-metadata">Support for Document Metadata</h3>
<p>HTML에서 <title>, <link>, <meta>와 같은 document metadata tag들은 document의 <head> 부분에 배치되기 위해 예약되어 있습니다. React에서 앱에 적합한 메타데이터를 결정하는 컴포넌트는 <head>와 멀리 떨어져 있거나, React에서 <head>를 랜더링하지 않을 수 있습니다. 과거에는 이런 엘리먼트를 수동으로 삽입해야하거나 <code>react-helmet</code> 과 같은 외부 라이브러릴 활용해야했습니다. 그리고 리액트 앱을 서버에서 랜더링할 때 신중하게 처리해야 했습니다.</p>
<p>React 19에서는 컴포넌트에서 기본적으로 메타데이터 태그를 랜더링하는 기능을 추가합니다.</p>
<pre><code class="language-jsx">function BlogPost({post}) {
  return (
    &lt;article&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;title&gt;{post.title}&lt;/title&gt;
      &lt;meta name=&quot;author&quot; content=&quot;Josh&quot; /&gt;
      &lt;link rel=&quot;author&quot; href=&quot;https://twitter.com/joshcstory/&quot; /&gt;
      &lt;meta name=&quot;keywords&quot; content={post.keywords} /&gt;
      &lt;p&gt;
        Eee equals em-see-squared...
      &lt;/p&gt;
    &lt;/article&gt;
  );
}</code></pre>
<p>리액트가 이 컴포넌트를 랜더링 할 때. <title>, <link>, <meta> 데이터 태그를 보고 자동으로 document의<head> 영역으로 끌어올립니다. 메타데이터 태크를 기본적으로 지원함으로써, 클라이언트 전용 앱, 스트리밍 SSR, 서버 컴포넌트와 함께 작동할 수 있도록 합니다.</p>
<h3 id="support-for-stylesheets">Support for stylesheets</h3>
<p>외부 링크( <code>&lt;link rel=&quot;stylesheet&quot; href=&quot;...&quot;&gt;</code>)와 인라인( <code>&lt;style&gt;...&lt;/style&gt;</code>) 스타일시트는 스타일 우선순위 규칙으로 인해 DOM에서 신중하게 배치해야 합니다. 컴포넌트 내에서 구성 가능성을 허용하는 스타일시트 기능을 구축하는 것은 어렵기 때문에 사용자들은 컴포넌트 자신에게 종속될 수 있는 구성 요소에서 멀리 떨어진 모든 스타일을 로드하거나 복잡성을 캡슐화하는 스타일 라이브러릴 사용합니다.</p>
<p>React 19에서 우리는 이런 복자섭ㅇ을 해결하고 클라이언트에서 도이성 랜더링과 서버에서 스트리밍 랜더링에 대해 깊은 통합을 제공하고, 스타일 시트에 대한 기본 지원을 제공합니다. 만약 React에 스타일시트의 <code>precedence</code> 를 알려주면, DOM에서 스타일시트의 삽입 순서를 관리하고, 스타일시트가 로드된 이후 스타일 규칙에 따라 달라지는 컨텐츠를 표시합니다.</p>
<pre><code class="language-jsx">function ComponentOne() {
  return (
    &lt;Suspense fallback=&quot;loading...&quot;&gt;
      &lt;link rel=&quot;stylesheet&quot; href=&quot;foo&quot; precedence=&quot;default&quot; /&gt;
      &lt;link rel=&quot;stylesheet&quot; href=&quot;bar&quot; precedence=&quot;high&quot; /&gt;
      &lt;article class=&quot;foo-class bar-class&quot;&gt;
        {...}
      &lt;/article&gt;
    &lt;/Suspense&gt;
  )
}

function ComponentTwo() {
  return (
    &lt;div&gt;
      &lt;p&gt;{...}&lt;/p&gt;
      &lt;link rel=&quot;stylesheet&quot; href=&quot;baz&quot; precedence=&quot;default&quot; /&gt;  &lt;-- will be inserted between foo &amp; bar
    &lt;/div&gt;
  )
}</code></pre>
<p>서버 사이드 랜더링 동안 React는 <head>에 스타일 시트를 포함시키고, 로드될 때까지 그리지 않도록합니다. 만약 스타일시트가 스트리밍이 시작되고 늦게 발견되면 Reactsms <head>에 해당 스타일시트에 의존하는 Suspense 바운더리의 내용을 공개하기 전에 클라이언트에서 스타일시트가 삽입되도록 합니다.</p>
<p>클라이언트 사이드 랜더링 동안 React는 랜더링을 커밋하기 전에 새롭게 랜더링된 스타일시트가 로드될 때까지 기다립니다. 애플리케이션 내 여러 위치에서 이 구성 요소를 랜더링하는 경우 React는 문서에 스타일시트를 한 번만 포함합니다.</p>
<pre><code class="language-jsx">function App() {
  return &lt;&gt;
    &lt;ComponentOne /&gt;
    ...
    &lt;ComponentOne /&gt; // won&#39;t lead to a duplicate stylesheet link in the DOM
  &lt;/&gt;
}</code></pre>
<p>스타일시트를 수동적으로 로드하는데 익숙한 사용자에게 이것은 해당 스타일시트와 그 종속성을 찾을 수 있는 기회이고, 더 나은 지역적 추론과 실제 종속된 스타일시트만 로드하도록 보장하는 데 도움이 됩니다.</p>
<p>스타일 라이브라리와 번들러를 통한 스타일 통합도 이런 새로운 사용성을 채택할 수 있기 때문에, 스타일 시트를 직접 랜더링하지 않더라도 도구가 이 기능을 사용하도록 업르게이 됨에 따라 해당 장점을 활용할 수 있습니다.</p>
<p>더 많은 정보가 필요하면 <a href="https://react.dev/reference/react-dom/components/link"><code>&lt;link&gt;</code></a> 와 <a href="https://react.dev/reference/react-dom/components/style"><code>&lt;style&gt;</code></a>문서를 확인하세요.</p>
<h3 id="support-for-async-scripts">Support for async scripts</h3>
<p>HTML에서 일반 스크립트( <code>&lt;script src=&quot;...&quot;&gt;</code>)와 지연 스크립트( <code>&lt;script defer=&quot;&quot; src=&quot;...&quot;&gt;</code>)는 문서 순서대로 로드되므로 구성 요소 트리에서 이러한 종류의 스크립트를 깊숙이 렌더링하는 것이 어렵습니다. </p>
<p>하지만 비동기 스크립트(<code>&lt;script async=&quot;&quot; src=&quot;...&quot;&gt;</code>)는 임의의 순서로 로드됩니다.</p>
<p>React 19에서는 스크립트에 실제적으로 의존하는 구성요소 내에서 구성 요소 트리의 어느 곳에서나 스크립트 인스턴스를 다시 배치하거나 중복을 제거할 필요없이 비동기 스크립트를 랜더링할 수 있도록 하여 비동기 스크립트를 위한 지원을 포함하고 있습니다.</p>
<pre><code class="language-jsx">function MyComponent() {
  return (
    &lt;div&gt;
      &lt;script async={true} src=&quot;...&quot; /&gt;
      Hello World
    &lt;/div&gt;
  )
}

function App() {
  &lt;html&gt;
    &lt;body&gt;
      &lt;MyComponent&gt;
      ...
      &lt;MyComponent&gt; // won&#39;t lead to duplicate script in the DOM
    &lt;/body&gt;
  &lt;/html&gt;
}</code></pre>
<p>모든 랜더링 환경 속에서 비동기 스크립트는 중복이 제거되므로 React는 만약 여러 개의 서로 다른 구성 요소에 의해 랜더링되더라도 스크립트를 한번만 수행합니다.</p>
<p>서버 사이드 랜더링에서는 비동기 스크립트가 <head> 태그에 포함되고, 스타일시트, 글꼴, 이미지 프리로드와 같은 페인트 차단보다 낮은 우선순위를 가집니다.</p>
<p>더 많은 정보가 필요하면 <a href="https://react.dev/reference/react-dom/components/script"><code>&lt;script&gt;</code></a> 문서를 확인하세요.</p>
<h3 id="support-for-preloading-resources">Support for preloading resources</h3>
<p>초기 document를 로드하고 클라이언트 사이트에서 업데이트가 일어나는 동안, 브라우저에 가능한 한 빨리 필요한 리소스에 대해서 말해주면 페이지 성능에 극적인 영향을 줄 수 잇다.</p>
<p>React 19에서는 브라우저 리소스를 로드하고, 프리로딩을 위한 여러가지 API가 포함되어 비효율적인 리소스 로딩에 의한 방해없이 아주 좋은 경험을 쉽게 빌드할 수 있습니다.</p>
<pre><code class="language-jsx">import { prefetchDNS, preconnect, preload, preinit } from &#39;react-dom&#39;
function MyComponent() {
  preinit(&#39;https://.../path/to/some/script.js&#39;, {as: &#39;script&#39; }) // loads and executes this script eagerly
  preload(&#39;https://.../path/to/font.woff&#39;, { as: &#39;font&#39; }) // preloads this font
  preload(&#39;https://.../path/to/stylesheet.css&#39;, { as: &#39;style&#39; }) // preloads this stylesheet
  prefetchDNS(&#39;https://...&#39;) // when you may not actually request anything from this host
  preconnect(&#39;https://...&#39;) // when you will request something but aren&#39;t sure what
}</code></pre>
<pre><code class="language-jsx">&lt;!-- the above would result in the following DOM/HTML --&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;!-- links/scripts are prioritized by their utility to early loading, not call order --&gt;
    &lt;link rel=&quot;prefetch-dns&quot; href=&quot;https://...&quot;&gt;
    &lt;link rel=&quot;preconnect&quot; href=&quot;https://...&quot;&gt;
    &lt;link rel=&quot;preload&quot; as=&quot;font&quot; href=&quot;https://.../path/to/font.woff&quot;&gt;
    &lt;link rel=&quot;preload&quot; as=&quot;style&quot; href=&quot;https://.../path/to/stylesheet.css&quot;&gt;
    &lt;script async=&quot;&quot; src=&quot;https://.../path/to/some/script.js&quot;&gt;&lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    ...
  &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>이러한 API는 글꼴과 같은 추가 리소스의 발견을 스타일시트 로딩을 제외해서 초기 페이지 로드를 최적화하는 데활용할 수 있습니다. 또한 예상 탐색에서 사용되는 리소스 목록을 미리 페치한 다음 클릭 또는 호버 시 해당 리소스를 적극적으로 미리 로드하여 클라이언트 업데이트를 더 빠르게 만들 수도 있습니다.</p>
<p>자세한 내용은 <a href="https://react.dev/reference/react-dom#resource-preloading-apis">Resource Preloading API</a>를 확인하세요.</p>
<h3 id="compatibility-with-third-party-scripts-and-extensions">Compatibility with third-party scripts and extensions</h3>
<p>우리는 third-party 스크립트와 브라우저 익스텐션을 고려해서 hydration을 개선하였습니다.</p>
<p>Hydrating 될 때, 클라이언트에서 랜더링되는 엘리먼트가 서버에서 온 HTML 내에서 찾은 요소와 일치하지 않을 때, ㄷ클라이언트가 다시 수정하여 리랜더링 되도록 강제합니다. 이전에는 만약 엘리먼트가 third-party 스크립트나 브라우저 익스텐션에 의해 요소가 삽입되면 불일치 오류가 발생하고 클라이언트가 랜더링 되었습니다.</p>
<p>React 19에서는 <head>와 <body> 내에 있는 불분명한 태그를 건너뛰어 불일치 오류를 방지합니다. 만약 리액트와 관계없는 hydration 불일치로 인해서 전체 document를 다시 랜더링해야하는 경우 third-party 스크립트나 브라우저 익스텐션으로 인해 삽입된 스타일 시트는 내버려둡니다.</p>
<h3 id="better-error-reporting">Better error reporting</h3>
<p>우리는 React 19에서 중복을제거하고, 잡힌 오류와 잡히지 않은 에러를 처리하기 위한 옵션을 제공하기 위해 오류 처리를 개선하였습니다. 예를 들어, Error Boundary에 의해 잡은 랜더링 오류가 있는 경우에 이전 React는 오류를 두 번 발생시킨 다음(원래 오류에 대해 한 번, 자동 복구에 실패한 후 다시 한 번) 에러가 발생한 위치에 대한 정보에 대해서 console.error를 호출합니다.</p>
<p>이로 인해 1개의 오류에 대해서 3개의 오류가 검출되었습니다.</p>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/684e0841-be52-4021-bb75-34502255e1e5/image.png' width='500'/>
</div>

<p>React 19에서는 모든 정보를 포함한 1개의 에러만 기록합니다.</p>
<div align='center'>
    <image src='https://velog.velcdn.com/images/tkddn_dev8430/post/a2e08a07-d4eb-4201-82a0-8e2f31c7916e/image.png' width='500'/>
</div>


<p>추가적으로, onRecoverableError를 보완하기 위한 새로운 루트 옵션들을 추가했습니다.</p>
<ul>
<li>onCaughtError : Error Boundary에서 오류를 잡았을 때 호출됩니다.</li>
<li>onUnCaughtError: 오류가 발생했지만 Error Boundary에 의해 포착되지 않았을 때 호출됩니다.</li>
<li>onRecoverableError: 오류가 발생하고 자동으로 복구될 때 호출됩니다.</li>
</ul>
<p>더 많은 정보와 예시를 보고 싶으면, <a href="https://react.dev/reference/react-dom/client/createRoot"><code>creatRoot</code></a>와 <a href="https://react.dev/reference/react-dom/client/hydrateRoot"><code>hydrateRoot</code></a>를 참조하세요.</p>
<h3 id="support-for-custom-elements">Support for Custom Elements</h3>
<p>React 19는 <strong>Custom Elements</strong>(사용자 정의 요소)에 대한 완전한 지원을 추가했으며, 이제 모든 <strong>Custom Elements Everywhere</strong> 테스트를 통과합니다.</p>
<p>이전 버전에서는 React에서 Custom Elements를 사용하는 것이 어려웠습니다. 왜냐하면 React가 인식하지 못하는 props(속성)를 <strong>속성(attributes)</strong>으로 처리했기 때문입니다. React 19에서는 클라이언트(Client)와 <strong>서버 사이드 렌더링(SSR)</strong> 모두에서 동작하는 방식으로 <strong>properties</strong>(프로퍼티)를 지원하도록 개선했습니다. 다음과 같은 전략이 적용됩니다:</p>
<ol>
<li><strong>서버 사이드 렌더링(SSR):</strong></li>
</ol>
<ul>
<li>Custom Element에 전달된 <code>props</code>는 아래와 같은 조건에 따라 속성(attributes)으로 렌더링됩니다:<ul>
<li>값이 <strong>문자열(string)</strong>, <strong>숫자(number)</strong>, 또는 <code>true</code>와 같은 원시 값(primitive value)인 경우.</li>
<li>반면, 값이 <strong>객체(object)</strong>, <strong>심볼(symbol)</strong>, <strong>함수(function)</strong> 또는 <code>false</code>인 경우, 해당 props는 생략됩니다.</li>
</ul>
</li>
</ul>
<ol start="2">
<li><strong>클라이언트 사이드 렌더링(CSR):</strong></li>
</ol>
<p>Custom Element 인스턴스의 프로퍼티(property)와 일치하는 props는 <strong>프로퍼티</strong>로 설정됩니다. 그 외의 props는 속성(attribute)으로 설정됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Warning: Received ‘false’ for a non-boolean attribute “mode”]]></title>
            <link>https://velog.io/@tkddn_dev8430/Warning-Received-false-for-a-non-boolean-attribute-mode</link>
            <guid>https://velog.io/@tkddn_dev8430/Warning-Received-false-for-a-non-boolean-attribute-mode</guid>
            <pubDate>Thu, 31 Oct 2024 10:00:14 GMT</pubDate>
            <description><![CDATA[<div>
  <img src='https://velog.velcdn.com/images/tkddn_dev8430/post/5ff19c73-9523-4d2c-bf9b-85509db9a31a/image.png'>
</div>

<p>오래된 사이드 프로젝트 에러들을 잡는 과정에서 다음과 같은 경고 문구가 나왔다.</p>
<p>찾아보니 styled-component를 위해 넘겨준 props가 걸러지지 않고 <u><strong>DOM Element의 attribute에 넘겨졌고, DOM Element에서는 props에 해당하는 attribute가 존재하지 않기 때문에 발생하는 문제</strong></u>였다.</p>
<p>공식문서를 참조하여 문제를 해결할 수 있었다. 현재 프로젝트에서 사용하는 styled-component 버전이 5.1 이상이라 props의 이름 앞에 $ 표기를 추가하는 transient props를 통해 에러를 해결해주었다.</p>
<p><strong>transient props</strong>를 활용하면 styled-component에서만 활용될 props들이 Reat 노드와 DOM Element에 전달되는 것을 방지해줄 수 있고, 추가로 시각적으로도 일반 props들과 styled-component props들을 구분할 수 있다.</p>
<pre><code class="language-tsx">function Toggle() {
    ...

  return (
        ...
      &lt;S.ToggleCircle $mode={mode}&gt;&lt;/S.ToggleCircle&gt;
    ...
  );
}

export default Toggle;
</code></pre>
<h3 id="reference">Reference</h3>
<p><a href="https://styled-components.com/docs/api#transient-props">https://styled-components.com/docs/api#transient-props</a></p>
]]></description>
        </item>
    </channel>
</rss>