{"componentChunkName":"component---src-templates-blog-post-js","path":"/translation/2026/use-sync-external-store/","result":{"data":{"site":{"siteMetadata":{"title":"Ykss","author":"[Ykss]","siteUrl":"https://ykss.netlify.app","comment":{"disqusShortName":"","utterances":"ykss/ykss.netlify.com"},"sponsor":{"buyMeACoffeeId":""}}},"markdownRemark":{"id":"dd6a4bf5-cabc-50fe-bc85-b78484b42c6f","excerpt":"원문: useSyncExternalStore: Demystified for Practical React Development 리액트의 는 매일 쓰게 되는 훅은 아니지만, 필요할 때는 정말로 필요한 훅입니다. 리액트가 제어하지 않는 외부 상태 관리 시스템이나 브라우저 API…","html":"<blockquote>\n<p>원문: <a href=\"https://www.epicreact.dev/use-sync-external-store-demystified-for-practical-react-development-w5ac0\">useSyncExternalStore: Demystified for Practical React Development</a></p>\n</blockquote>\n<p>리액트의 <code class=\"language-text\">useSyncExternalStore</code>는 매일 쓰게 되는 훅은 아니지만, 필요할 때는 <em>정말로</em> 필요한 훅입니다. 리액트가 제어하지 않는 외부 상태 관리 시스템이나 브라우저 API와 리액트 컴포넌트를 연결할 때 핵심적인 역할을 합니다. 안타깝게도 이 훅은 자주 오해를 받기도 합니다. 이번 글에서는 이러한 혼란을 해소하고, 실제 예시를 살펴보며, 가장 흔한 실수들을 짚어보겠습니다.</p>\n<h2 id=\"usesyncexternalstore는-왜-존재하나요-테어링tearing-문제\" style=\"position:relative;\"><a href=\"#usesyncexternalstore%EB%8A%94-%EC%99%9C-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%82%98%EC%9A%94-%ED%85%8C%EC%96%B4%EB%A7%81tearing-%EB%AC%B8%EC%A0%9C\" aria-label=\"usesyncexternalstore는 왜 존재하나요 테어링tearing 문제 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>useSyncExternalStore는 왜 존재하나요? “테어링(tearing)” 문제</h2>\n<p>리액트의 내장 상태(<code class=\"language-text\">useState</code>, <code class=\"language-text\">useReducer</code>)와 Context API는 리액트 <em>내부</em>에서 관리되는 데이터에 매우 적합합니다. 하지만 컴포넌트가 리액트의 <em>외부</em>에 있는 소스의 데이터를 표시해야 할 때는 어떻게 해야 할까요?</p>\n<ul>\n<li><strong>브라우저 API:</strong> <code class=\"language-text\">navigator.onLine</code>(온라인 상태), <code class=\"language-text\">document.visibilityState</code>(페이지 가시성), <code class=\"language-text\">window.matchMedia</code>(미디어 쿼리)</li>\n<li><strong>서드파티 상태 관리 라이브러리:</strong> 리액트의 동시성 기능을 고려하지 않고 설계된 구버전 Redux나 MobX, 또는 커스텀 스토어. (참고: Redux, Zustand, Jotai 같은 라이브러리의 최신 버전은 리액트 바인딩에 내부적으로 <code class=\"language-text\">useSyncExternalStore</code>를 사용합니다.)</li>\n<li><strong>전역 자바스크립트 변수나 커스텀 이벤트 시스템:</strong> 리액트가 관리하지 않는 모든 가변 데이터 소스</li>\n</ul>\n<p><code class=\"language-text\">useSyncExternalStore</code> 이전에는 개발자들이 보통 <code class=\"language-text\">useEffect</code>와 <code class=\"language-text\">useState</code>를 사용해 이러한 외부 소스를 구독하고 로컬 컴포넌트 상태를 업데이트했습니다. 단순한 경우에는 잘 작동했지만, 리액트의 동시성 렌더링 기능과 충돌하는 문제가 발생할 수 있었습니다.</p>\n<p><strong>동시성 렌더링</strong>을 사용하면 리액트가 여러 UI 업데이트를 동시에 처리할 수 있습니다. 필요에 따라 렌더링 작업을 일시 중지하고, 재개하거나, 취소할 수 있습니다. 이를 통해 체감 성능이 크게 향상됩니다. 하지만 리액트가 컴포넌트 트리를 렌더링하는 도중에 외부 스토어가 변경되면, 서로 다른 컴포넌트가 외부 데이터의 서로 다른 버전을 읽을 수 있습니다. 이런 불일치를 <strong>“테어링”</strong>이라 부르는데, UI가 충돌하는 정보를 표시하면서 말 그대로 “찢어지는” 현상입니다.</p>\n<p><code class=\"language-text\">useSyncExternalStore</code>는 리액트가 직접 관리하는 동기적 방식으로 외부 스토어를 구독할 수 있게 해줌으로써 이 문제를 해결합니다. 동시 업데이트 중에도 렌더링 패스의 모든 컴포넌트가 동일하고 일관된 데이터 스냅샷을 보도록 보장하여 테어링을 방지합니다.</p>\n<h2 id=\"api-살펴보기\" style=\"position:relative;\"><a href=\"#api-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0\" aria-label=\"api 살펴보기 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>API 살펴보기</h2>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token keyword\">const</span> synchronizedState <span class=\"token operator\">=</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>\n  subscribe<span class=\"token punctuation\">,</span>\n  getSnapshot<span class=\"token punctuation\">,</span>\n  getServerSnapshot<span class=\"token operator\">?</span> <span class=\"token comment\">// 선택</span>\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>각 인자를 자세히 살펴보겠습니다.</p>\n<ul>\n<li>\n<p><strong><code class=\"language-text\">subscribe(callback)</code>:</strong></p>\n<ul>\n<li>외부 데이터 스토어에 대한 구독을 설정하는 함수입니다.</li>\n<li>리액트가 제공하는 <code class=\"language-text\">callback</code> 함수를 인자로 받습니다.</li>\n<li>외부 스토어의 데이터가 변경될 때마다 <code class=\"language-text\">subscribe</code> 함수는 반드시 이 <code class=\"language-text\">callback</code>을 호출해야 합니다. 이를 통해 리액트에 스토어가 변경되었고 리렌더링이 필요할 수 있다는 것을 알립니다.</li>\n<li>반드시 <code class=\"language-text\">unsubscribe</code> 함수를 반환해야 합니다. 리액트는 컴포넌트가 언마운트되거나 <code class=\"language-text\">subscribe</code> 함수 자체가 렌더링 사이에 변경될 때 이 정리 함수를 호출합니다.</li>\n</ul>\n</li>\n<li>\n<p><strong><code class=\"language-text\">getSnapshot()</code>:</strong></p>\n<ul>\n<li>컴포넌트가 필요로 하는 스토어의 현재 데이터 스냅샷을 반환하는 함수입니다.</li>\n<li><strong>순수(pure)</strong>(사이드 이펙트 없음)하고 <strong>동기적(synchronous)</strong>(즉시 값을 반환)이어야 합니다. 스토어가 변경되지 않았더라도 리액트가 여러 번 호출할 수 있으므로, 빠르게 실행되어야 합니다.</li>\n<li>중요한 점은, 기저 데이터가 변경되지 않았다면 <code class=\"language-text\">getSnapshot</code>은 마지막 호출 때와 <strong>동일한 참조 값</strong>(객체나 배열인 경우) 또는 동일한 원시 값을 반환해야 한다는 것입니다. 이를 통해 리액트는 <code class=\"language-text\">Object.is</code> 비교를 사용해 리렌더링을 최적화할 수 있습니다. 자세한 내용은 아래 “자주 하는 실수” 섹션을 참조하세요.</li>\n</ul>\n</li>\n<li>\n<p><strong><code class=\"language-text\">getServerSnapshot?()</code> (선택 사항):</strong></p>\n<ul>\n<li>이 함수는 서버 사이드 렌더링(SSR)과 클라이언트 사이드 하이드레이션(hydration)에만 필요합니다.</li>\n<li>서버에서 표시되어야 하는 데이터의 초기 스냅샷을 반환해야 합니다.</li>\n<li>외부 스토어가 클라이언트 전용인 경우(예: 서버에 없는 브라우저 API에 의존), 기본값이나 플레이스홀더를 제공할 수 있습니다.</li>\n<li>이 인자를 생략하고 SSR 환경에서 사용하면, 컴포넌트는 보통 하이드레이션 때까지 클라이언트에서 서스펜드 상태가 되거나, 서버에서 렌더링된 HTML이 초기 클라이언트 렌더링과 일치하지 않으면 리액트가 오류를 던질 수 있습니다.</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"예시-온라인-상태-추적-브라우저-api\" style=\"position:relative;\"><a href=\"#%EC%98%88%EC%8B%9C-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%83%81%ED%83%9C-%EC%B6%94%EC%A0%81-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-api\" aria-label=\"예시 온라인 상태 추적 브라우저 api permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>예시: 온라인 상태 추적 (브라우저 API)</h2>\n<p>사용자의 브라우저가 온라인인지 추적하는 커스텀 훅 <code class=\"language-text\">useOnlineStatus</code>를 만들어보겠습니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> useSyncExternalStore <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'react'</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token comment\">// 1. 컴포넌트 밖에서 getSnapshot 정의</span>\n<span class=\"token comment\">// 외부 소스에서 현재 상태를 읽습니다.</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">getOnlineStatusSnapshot</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> navigator<span class=\"token punctuation\">.</span>onLine<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// 2. 컴포넌트 밖에서 subscribe 정의</span>\n<span class=\"token comment\">// 리스너를 등록하고, 변경 시 리액트가 넘긴 callback을 호출합니다.</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">subscribeToOnlineStatus</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">callback</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  window<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'online'</span><span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  window<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'offline'</span><span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// 정리(cleanup) 함수 반환</span>\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n    window<span class=\"token punctuation\">.</span><span class=\"token function\">removeEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'online'</span><span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    window<span class=\"token punctuation\">.</span><span class=\"token function\">removeEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'offline'</span><span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// 3. 커스텀 훅 만들기</span>\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">useOnlineStatus</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// useSyncExternalStore로 동기 읽기와 테어링 방지</span>\n  <span class=\"token keyword\">const</span> isOnline <span class=\"token operator\">=</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>\n    subscribeToOnlineStatus<span class=\"token punctuation\">,</span>\n    getOnlineStatusSnapshot<span class=\"token punctuation\">,</span>\n    <span class=\"token comment\">// SSR을 견고하게 하려면 getServerSnapshot을 넘기세요.</span>\n    <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token boolean\">true</span> <span class=\"token comment\">// 클라이언트가 온라인이라고 가정하거나, 합리적인 기본값</span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">return</span> isOnline<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// 컴포넌트에서 사용</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">StatusBar</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> isOnline <span class=\"token operator\">=</span> <span class=\"token function\">useOnlineStatus</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">return</span> <span class=\"token operator\">&lt;</span>h1<span class=\"token operator\">></span><span class=\"token punctuation\">{</span>isOnline <span class=\"token operator\">?</span> <span class=\"token string\">'✅ 온라인'</span> <span class=\"token operator\">:</span> <span class=\"token string\">'❌ 연결 끊김'</span><span class=\"token punctuation\">}</span><span class=\"token operator\">&lt;</span><span class=\"token operator\">/</span>h1<span class=\"token operator\">></span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>이 예시는 핵심 패턴을 보여줍니다. 안정적인 <code class=\"language-text\">subscribe</code>와 <code class=\"language-text\">getSnapshot</code> 함수가 외부 소스(<code class=\"language-text\">navigator.onLine</code>과 그 이벤트)와 상호작용합니다.</p>\n<h2 id=\"자주-하는-실수-주의-사항-그리고-올바른-사용법\" style=\"position:relative;\"><a href=\"#%EC%9E%90%EC%A3%BC-%ED%95%98%EB%8A%94-%EC%8B%A4%EC%88%98-%EC%A3%BC%EC%9D%98-%EC%82%AC%ED%95%AD-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%82%AC%EC%9A%A9%EB%B2%95\" aria-label=\"자주 하는 실수 주의 사항 그리고 올바른 사용법 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>자주 하는 실수, 주의 사항, 그리고 올바른 사용법</h2>\n<p><code class=\"language-text\">useSyncExternalStore</code>에 관한 많은 질문을 받았습니다. 가장 자주 받는 질문들을 정리했습니다.</p>\n<h3 id=\"1-code-classlanguage-textuseeffectcode--code-classlanguage-textusestatecode를-쓰면-안-되나요\" style=\"position:relative;\"><a href=\"#1-code-classlanguage-textuseeffectcode--code-classlanguage-textusestatecode%EB%A5%BC-%EC%93%B0%EB%A9%B4-%EC%95%88-%EB%90%98%EB%82%98%EC%9A%94\" aria-label=\"1 code classlanguage textuseeffectcode  code classlanguage textusestatecode를 쓰면 안 되나요 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>1. ”<code class=\"language-text\">useEffect</code> + <code class=\"language-text\">useState</code>를 쓰면 안 되나요?”</h3>\n<p><code class=\"language-text\">useEffect</code>와 <code class=\"language-text\">useState</code>로 외부 스토어를 구독할 수 있지만, 이 패턴은 동시성 렌더링에서 테어링이 발생하기 쉽습니다. 리액트가 컴포넌트 렌더링을 일시 중지하는 사이 외부 스토어가 업데이트되면, 리액트는 오래된 데이터로 렌더링을 재개하거나 UI 전반에 걸쳐 불일치가 발생할 수 있습니다. <code class=\"language-text\">useSyncExternalStore</code>는 리액트의 렌더링 생명주기와 통합되도록 특별히 설계되어, 렌더링 패스 중 읽기가 동기적이고 일관되게 이루어집니다. <strong>리액트 외부의 상태와 동기화해야 한다면, <code class=\"language-text\">useSyncExternalStore</code>가 올바르고 견고한 해결책입니다.</strong></p>\n<h3 id=\"2-매-렌더링마다-code-classlanguage-textsubscribecode나-code-classlanguage-textgetsnapshotcode-함수가-새로-생성됩니다\" style=\"position:relative;\"><a href=\"#2-%EB%A7%A4-%EB%A0%8C%EB%8D%94%EB%A7%81%EB%A7%88%EB%8B%A4-code-classlanguage-textsubscribecode%EB%82%98-code-classlanguage-textgetsnapshotcode-%ED%95%A8%EC%88%98%EA%B0%80-%EC%83%88%EB%A1%9C-%EC%83%9D%EC%84%B1%EB%90%A9%EB%8B%88%EB%8B%A4\" aria-label=\"2 매 렌더링마다 code classlanguage textsubscribecode나 code classlanguage textgetsnapshotcode 함수가 새로 생성됩니다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>2. “매 렌더링마다 <code class=\"language-text\">subscribe</code>나 <code class=\"language-text\">getSnapshot</code> 함수가 새로 생성됩니다!”</h3>\n<p>메모이제이션(memoization) 없이 컴포넌트나 커스텀 훅 내부에서 <code class=\"language-text\">subscribe</code>나 <code class=\"language-text\">getSnapshot</code>을 인라인으로 정의하면, 매 렌더링마다 새로운 함수가 생성됩니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token comment\">// ❌ 나쁨: 매 렌더마다 subscribe·getSnapshot이 새로 만들어짐</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">MyComponentUsesStore</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// MyComponentUsesStore가 렌더될 때마다 이 함수들의 참조가 바뀜</span>\n  <span class=\"token keyword\">function</span> <span class=\"token function\">subscribe</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">callback</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">/* ... */</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">function</span> <span class=\"token function\">getSnapshot</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">/* ... */</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">const</span> value <span class=\"token operator\">=</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>subscribe<span class=\"token punctuation\">,</span> getSnapshot<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// ...</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p><code class=\"language-text\">useSyncExternalStore</code>가 <em>새로운</em> <code class=\"language-text\">subscribe</code> 함수 인스턴스를 받으면, 스토어를 재구독합니다(먼저 이전 구독 해제 함수를 호출하고 새 subscribe를 호출합니다). 이는 비효율적이며, 완벽하게 처리하지 않으면 버그나 메모리 누수로 이어질 수 있습니다.</p>\n<p><strong>해결책</strong></p>\n<ul>\n<li><strong>컴포넌트 외부에서 정의하기:</strong> <code class=\"language-text\">useOnlineStatus</code> 예시처럼 정의합니다. 가장 간단하고 대체로 최선의 방법입니다.</li>\n<li><strong><code class=\"language-text\">useCallback</code>으로 메모이제이션하기:</strong> <code class=\"language-text\">subscribe</code>나 <code class=\"language-text\">getSnapshot</code> 함수가 props나 state에 의존하는 경우(예: 특정 문서를 구독하기 위한 ID), <code class=\"language-text\">useCallback</code>으로 감싸줍니다.</li>\n</ul>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token comment\">// ✅ 좋음: subscribe와 getSnapshot이 안정적</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">subscribeToStore</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">callback</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">/* ... */</span>\n<span class=\"token punctuation\">}</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">getStoreSnapshot</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">/* ... */</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">MyComponentUsesStore</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> value <span class=\"token operator\">=</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>subscribeToStore<span class=\"token punctuation\">,</span> getStoreSnapshot<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// ...</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// ✅ 이것도 좋음 (props에 의존할 때, 예: storeId)</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> useCallback<span class=\"token punctuation\">,</span> useSyncExternalStore <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'react'</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">MyComponentUsesStore</span><span class=\"token punctuation\">(</span><span class=\"token parameter\"><span class=\"token punctuation\">{</span> storeId <span class=\"token punctuation\">}</span></span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> subscribe <span class=\"token operator\">=</span> <span class=\"token function\">useCallback</span><span class=\"token punctuation\">(</span>\n    <span class=\"token parameter\">callback</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n      <span class=\"token keyword\">return</span> externalStoreAPI<span class=\"token punctuation\">.</span><span class=\"token function\">subscribe</span><span class=\"token punctuation\">(</span>storeId<span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n    <span class=\"token punctuation\">[</span>storeId<span class=\"token punctuation\">]</span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> getSnapshot <span class=\"token operator\">=</span> <span class=\"token function\">useCallback</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> externalStoreAPI<span class=\"token punctuation\">.</span><span class=\"token function\">getSnapshot</span><span class=\"token punctuation\">(</span>storeId<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">[</span>storeId<span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">const</span> value <span class=\"token operator\">=</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>subscribe<span class=\"token punctuation\">,</span> getSnapshot<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// ...</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<h3 id=\"3-code-classlanguage-textgetsnapshotcode이-너무-많이-호출됩니다\" style=\"position:relative;\"><a href=\"#3-code-classlanguage-textgetsnapshotcode%EC%9D%B4-%EB%84%88%EB%AC%B4-%EB%A7%8E%EC%9D%B4-%ED%98%B8%EC%B6%9C%EB%90%A9%EB%8B%88%EB%8B%A4\" aria-label=\"3 code classlanguage textgetsnapshotcode이 너무 많이 호출됩니다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>3. ”<code class=\"language-text\">getSnapshot</code>이 너무 많이 호출됩니다!”</h3>\n<p>리액트는 렌더링 패스 중에 <code class=\"language-text\">getSnapshot</code>을 여러 번 호출할 수 있고, 리렌더링이 발생하지 않더라도 일관성을 검증하기 위해 호출할 수 있습니다. 이는 정상적인 동작입니다. 따라서 <strong><code class=\"language-text\">getSnapshot</code>은 반드시 다음 조건을 충족해야 합니다.</strong></p>\n<ul>\n<li><strong>빠르게:</strong> 비용이 큰 연산을 피해야 합니다.</li>\n<li><strong>순수하게:</strong> 사이드 이펙트가 없어야 합니다. 스코프 외부의 것을 수정하지 마세요.</li>\n<li><strong>일관되게:</strong> 스냅샷과 관련된 기저 스토어 데이터가 변경되지 않았다면, <code class=\"language-text\">getSnapshot</code>은 정확히 동일한 값을 반환해야 합니다(<code class=\"language-text\">Object.is</code>를 사용한 객체/배열의 참조 동등성).</li>\n</ul>\n<p>리액트는 <code class=\"language-text\">getSnapshot</code>의 반환값을 사용해 리렌더링이 필요한지 판단합니다. 데이터가 변경되지 않았는데도 매번 새로운 객체/배열 인스턴스를 반환하면 불필요한 리렌더링이 발생합니다(아래 실수 #8 참조).</p>\n<h3 id=\"4-code-classlanguage-textusesyncexternalstore-is-not-a-functioncode-오류가-발생합니다\" style=\"position:relative;\"><a href=\"#4-code-classlanguage-textusesyncexternalstore-is-not-a-functioncode-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%A9%EB%8B%88%EB%8B%A4\" aria-label=\"4 code classlanguage textusesyncexternalstore is not a functioncode 오류가 발생합니다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>4. ”<code class=\"language-text\">useSyncExternalStore is not a function</code> 오류가 발생합니다.”</h3>\n<p>이 오류는 거의 항상 리액트 18 미만 버전을 사용하고 있다는 의미입니다. <code class=\"language-text\">useSyncExternalStore</code>는 리액트 18에서 도입되었습니다. <strong>해결책은</strong> <code class=\"language-text\">react</code>와 <code class=\"language-text\">react-dom</code> 패키지를 <code class=\"language-text\">v18.0.0</code> 이상으로 업그레이드하는 것입니다.</p>\n<h3 id=\"5-서버-사이드-렌더링ssr에서는-어떻게-사용하나요\" style=\"position:relative;\"><a href=\"#5-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81ssr%EC%97%90%EC%84%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%82%98%EC%9A%94\" aria-label=\"5 서버 사이드 렌더링ssr에서는 어떻게 사용하나요 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>5. “서버 사이드 렌더링(SSR)에서는 어떻게 사용하나요?”</h3>\n<p>외부 스토어가 서버에서 의미 있는 값을 제공할 수 있다면, 세 번째 인자인 <code class=\"language-text\">getServerSnapshot</code>을 <strong>반드시</strong> 제공해야 합니다. 제공하지 않으면 콘솔에 다음과 같은 오류가 표시됩니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">Missing getServerSnapshot, which is required for server-rendered content. Will\nrevert to client rendering.</code></pre></div>\n<p><code class=\"language-text\">getServerSnapshot</code> 사용 예시입니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token keyword\">const</span> value <span class=\"token operator\">=</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>subscribe<span class=\"token punctuation\">,</span> getSnapshot<span class=\"token punctuation\">,</span> getServerSnapshot<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p><code class=\"language-text\">getServerSnapshot</code>은 서버에서 렌더링되어야 하는 스토어의 초기 상태를 반환해야 합니다. 예를 들어 <code class=\"language-text\">localStorage</code>와 동기화하는 경우(서버에는 존재하지 않음), <code class=\"language-text\">getServerSnapshot</code>은 <code class=\"language-text\">null</code>이나 기본값을 반환할 수 있습니다. 다만 이 경우 잘못된 콘텐츠가 잠깐 깜빡이는 현상이 발생할 수 있으므로, 이를 처리할 창의적인 디자인 방법을 고민해야 합니다(또는 더 좋은 방법으로, 쿠키나 서버 사이드 상태 관리 솔루션을 사용하세요).</p>\n<p><code class=\"language-text\">getServerSnapshot</code>을 생략하고 컴포넌트가 서버에서 렌더링되면, 리액트는 초기 클라이언트 사이드 렌더링(하이드레이션)이 서버에서 렌더링된 HTML과 일치하기를 기대합니다. 클라이언트에서 <code class=\"language-text\">getSnapshot()</code>이 서버에서 암묵적으로 렌더링된 것과 다른 값을 반환하면 하이드레이션 불일치 오류가 발생합니다. 훅을 사용하는 컴포넌트가 서버에서 서스펜드 상태가 되면(값을 가져올 수 없어서), 클라이언트에서도 <code class=\"language-text\">subscribe</code> 함수가 호출되고 클라이언트 사이드 스냅샷을 사용할 수 있을 때까지 하이드레이션 중에 서스펜드 상태가 됩니다.</p>\n<h3 id=\"6-nextjs-remix-같은-ssr-프레임워크에서도-사용할-수-있나요\" style=\"position:relative;\"><a href=\"#6-nextjs-remix-%EA%B0%99%EC%9D%80-ssr-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EC%97%90%EC%84%9C%EB%8F%84-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94\" aria-label=\"6 nextjs remix 같은 ssr 프레임워크에서도 사용할 수 있나요 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>6. “Next.js, Remix 같은 SSR 프레임워크에서도 사용할 수 있나요?”</h3>\n<p>네, 물론입니다! 이 훅은 이런 환경에서 외부 스토어를 안전하게 사용하는 데 필수적입니다.</p>\n<ul>\n<li><strong>스토어가 서버 호환 가능한 경우:</strong> <code class=\"language-text\">getServerSnapshot</code>을 제공하세요.</li>\n<li>\n<p><strong>스토어가 브라우저 전용인 경우</strong>(예: <code class=\"language-text\">window.matchMedia</code>):</p>\n<ul>\n<li><code class=\"language-text\">getServerSnapshot</code>은 적절한 기본값을 반환해야 합니다(예: 미디어 쿼리는 <code class=\"language-text\">false</code>, <code class=\"language-text\">navigator.onLine</code>은 <code class=\"language-text\">true</code>).</li>\n<li>클라이언트 사이드 <code class=\"language-text\">getSnapshot</code>은 하이드레이션 시 실제 브라우저 값을 제공합니다. 리액트가 매끄러운 전환을 보장합니다.</li>\n<li>\n<p><code class=\"language-text\">window.matchMedia</code> 예시입니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token comment\">// 훅 안에서</span>\n<span class=\"token keyword\">const</span> <span class=\"token function-variable function\">getServerSnapshot</span> <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token boolean\">false</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// 상황에 맞는 기본값</span>\n<span class=\"token comment\">// ...</span>\n<span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>subscribe<span class=\"token punctuation\">,</span> getSnapshot<span class=\"token punctuation\">,</span> getServerSnapshot<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n</li>\n<li>또는, 적절한 서버 기본값이 없다면 컴포넌트를 클라이언트에서만 조건부로 렌더링하거나, <code class=\"language-text\">getServerSnapshot</code>을 제공하지 않고 하이드레이션 중 서스펜드 상태가 될 것으로 예상하는 경우 <code class=\"language-text\">&lt;Suspense></code>로 감싸는 방법도 있습니다.</li>\n</ul>\n</li>\n</ul>\n<h3 id=\"7-redux-zustand-jotai-react-context-대신-언제-사용해야-하나요\" style=\"position:relative;\"><a href=\"#7-redux-zustand-jotai-react-context-%EB%8C%80%EC%8B%A0-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%82%98%EC%9A%94\" aria-label=\"7 redux zustand jotai react context 대신 언제 사용해야 하나요 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>7. “Redux, Zustand, Jotai, React Context 대신 언제 사용해야 하나요?”</h3>\n<p>흔히 혼동되는 부분입니다.</p>\n<ul>\n<li><strong>React Context:</strong> prop drilling 없이 컴포넌트 트리 전반에 걸쳐 공유해야 하는 리액트 상태에 Context를 사용합니다. <em>리액트가 관리하는</em> 상태를 위한 것입니다.</li>\n<li><strong>Redux, Zustand, Jotai 등:</strong> 이 라이브러리들의 최신 버전은 동시성 렌더링과 외부 스토어 로직을 연결하기 위해 내부적으로 <code class=\"language-text\">useSyncExternalStore</code>를 사용합니다. 이 라이브러리를 사용하는 <em>개발자 입장</em>에서는 보통 제공되는 훅(<code class=\"language-text\">useSelector</code>, <code class=\"language-text\">useStore</code>)을 사용하고 <code class=\"language-text\">useSyncExternalStore</code>를 직접 호출하지 않습니다.</li>\n</ul>\n<p>애플리케이션 개발자로서 <code class=\"language-text\">useSyncExternalStore</code>를 직접 사용하는 경우는 다음과 같습니다.</p>\n<ul>\n<li><strong>리액트를 인식하지 못하는 외부 스토어와 통합하는 경우:</strong> 서드파티 바닐라 JS 라이브러리, 전역 변수, Web Worker, 또는 리액트 외부에서 상태를 관리하고 자체 리액트 바인딩이 없는 시스템.</li>\n<li><strong>브라우저 API를 직접 구독하는 경우:</strong> <code class=\"language-text\">navigator.onLine</code>, <code class=\"language-text\">document.visibilityState</code>, <code class=\"language-text\">window.matchMedia</code> 등.</li>\n<li><strong>자체 상태 관리 라이브러리를 만드는 경우:</strong> 새로운 상태 관리 솔루션을 개발한다면, <code class=\"language-text\">useSyncExternalStore</code>가 리액트의 동시성 기능과 호환되게 만드는 기본 빌딩 블록입니다.</li>\n</ul>\n<p>즉, <code class=\"language-text\">useSyncExternalStore</code>는 Zustand 같은 라이브러리와 양자택일 관계가 아닙니다. 오히려 이 라이브러리들의 <em>구현 세부 사항</em>이거나, 특정 외부 데이터 소스와 인터페이스하는 데 해당 라이브러리가 적합하지 않을 때 직접 사용하는 도구입니다.</p>\n<h3 id=\"8-code-classlanguage-textgetsnapshotcode-때문에-무한-루프나-불필요한-리렌더링이-발생합니다\" style=\"position:relative;\"><a href=\"#8-code-classlanguage-textgetsnapshotcode-%EB%95%8C%EB%AC%B8%EC%97%90-%EB%AC%B4%ED%95%9C-%EB%A3%A8%ED%94%84%EB%82%98-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A9%EB%8B%88%EB%8B%A4\" aria-label=\"8 code classlanguage textgetsnapshotcode 때문에 무한 루프나 불필요한 리렌더링이 발생합니다 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>8. ”<code class=\"language-text\">getSnapshot</code> 때문에 무한 루프나 불필요한 리렌더링이 발생합니다!”</h3>\n<p>리액트는 <code class=\"language-text\">Object.is()</code>를 사용해 이전 스냅샷과 <code class=\"language-text\">getSnapshot</code>이 반환하는 현재 스냅샷을 비교합니다. <code class=\"language-text\">getSnapshot</code>이 매번 새로운 객체나 배열 참조를 반환하면, 기저 데이터가 동일해도 리액트는 상태가 변경되었다고 판단해 리렌더링을 유발합니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"js\"><pre class=\"language-js\"><code class=\"language-js\"><span class=\"token comment\">// 외부 스토어 (예시)</span>\n<span class=\"token keyword\">const</span> myExternalStore <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token literal-property property\">_data</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">user</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">name</span><span class=\"token operator\">:</span> <span class=\"token string\">'Alex'</span><span class=\"token punctuation\">,</span> <span class=\"token literal-property property\">preferences</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span> <span class=\"token literal-property property\">theme</span><span class=\"token operator\">:</span> <span class=\"token string\">'dark'</span> <span class=\"token punctuation\">}</span> <span class=\"token punctuation\">}</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token literal-property property\">listeners</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span>\n  <span class=\"token function\">getData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token keyword\">this</span><span class=\"token punctuation\">.</span>_data<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token function\">subscribe</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">listener</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token comment\">/* ... */</span> <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n      <span class=\"token comment\">/* ... */</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token comment\">// ... _data를 갱신하고 리스너에 알리는 메서드들</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token comment\">// ❌ 나쁨: getSnapshot이 항상 새 객체를 반환</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">getPreferencesSnapshot_Bad</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// 설정이 바뀌지 않아도 매번 새 객체 인스턴스</span>\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>myExternalStore<span class=\"token punctuation\">.</span><span class=\"token function\">getData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>user<span class=\"token punctuation\">.</span>preferences <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// ✅ 좋음: 데이터가 같으면 같은 객체 참조를 반환</span>\n<span class=\"token comment\">// 스토어나 스냅샷 로직을 조금 더 똑똑하게 짜야 합니다.</span>\n\n<span class=\"token comment\">// 옵션 1: 스토어가 선택용 불변 데이터를 직접 관리하는 경우</span>\n<span class=\"token keyword\">function</span> <span class=\"token function\">getPreferencesSnapshot_Good_Immutable</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// myExternalStore.getData().user.preferences가 불변 객체이고,</span>\n  <span class=\"token comment\">// 실제로 바뀔 때만 교체된다고 가정</span>\n  <span class=\"token keyword\">return</span> myExternalStore<span class=\"token punctuation\">.</span><span class=\"token function\">getData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>user<span class=\"token punctuation\">.</span>preferences<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// 옵션 2: 파생 스냅샷을 수동으로 캐시</span>\n<span class=\"token keyword\">let</span> lastKnownPreferences <span class=\"token operator\">=</span> myExternalStore<span class=\"token punctuation\">.</span><span class=\"token function\">getData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>user<span class=\"token punctuation\">.</span>preferences<span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">let</span> cachedPreferencesSnapshot <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>lastKnownPreferences <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">getPreferencesSnapshot_Good_Cached</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> currentPreferences <span class=\"token operator\">=</span> myExternalStore<span class=\"token punctuation\">.</span><span class=\"token function\">getData</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>user<span class=\"token punctuation\">.</span>preferences<span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// 얕은 비교. 깊은 객체는 깊은 비교가 필요할 수 있음</span>\n  <span class=\"token comment\">// 또는 중첩 변경 시 스토어가 currentPreferences 참조를 교체하도록 보장</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>currentPreferences <span class=\"token operator\">!==</span> lastKnownPreferences<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    cachedPreferencesSnapshot <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span> <span class=\"token operator\">...</span>currentPreferences <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n    lastKnownPreferences <span class=\"token operator\">=</span> currentPreferences<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n  <span class=\"token keyword\">return</span> cachedPreferencesSnapshot<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p><strong>핵심은 참조 안정성입니다.</strong> 데이터가 변경되지 않았다면, <code class=\"language-text\">getSnapshot</code>은 이전과 <em>정확히 동일한 객체 인스턴스</em>를 반환해야 합니다. 원시 값(문자열, 숫자, 불리언)이라면 불필요하게 재계산하지 않는 한 덜 문제가 됩니다.</p>\n<h2 id=\"보너스-재사용-가능한-code-classlanguage-textusemediaquerycode-훅\" style=\"position:relative;\"><a href=\"#%EB%B3%B4%EB%84%88%EC%8A%A4-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5%ED%95%9C-code-classlanguage-textusemediaquerycode-%ED%9B%85\" aria-label=\"보너스 재사용 가능한 code classlanguage textusemediaquerycode 훅 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>보너스: 재사용 가능한 <code class=\"language-text\">useMediaQuery</code> 훅</h2>\n<p>미디어 쿼리를 위한 재사용 가능한 훅을 만드는 방법입니다. 쿼리가 변경될 수 있는 경우 <code class=\"language-text\">useCallback</code>을 사용해 <code class=\"language-text\">subscribe</code>와 <code class=\"language-text\">getSnapshot</code>을 안정적으로 유지합니다. 이 코드는 <a href=\"https://www.epicreact.dev/workshops/advanced-react-apis\">Advanced React APIs</a> Epic React 워크숍에서 가져왔습니다.</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> Suspense<span class=\"token punctuation\">,</span> useSyncExternalStore <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'react'</span><span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">import</span> <span class=\"token operator\">*</span> <span class=\"token keyword\">as</span> ReactDOM <span class=\"token keyword\">from</span> <span class=\"token string\">'react-dom/client'</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">function</span> <span class=\"token function\">makeMediaQueryStore</span><span class=\"token punctuation\">(</span>mediaQuery<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">function</span> <span class=\"token function\">getSnapshot</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> window<span class=\"token punctuation\">.</span><span class=\"token function\">matchMedia</span><span class=\"token punctuation\">(</span>mediaQuery<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>matches<span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">function</span> <span class=\"token function\">subscribe</span><span class=\"token punctuation\">(</span><span class=\"token function-variable function\">callback</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token keyword\">void</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">const</span> mediaQueryList <span class=\"token operator\">=</span> window<span class=\"token punctuation\">.</span><span class=\"token function\">matchMedia</span><span class=\"token punctuation\">(</span>mediaQuery<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    mediaQueryList<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'change'</span><span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n      mediaQueryList<span class=\"token punctuation\">.</span><span class=\"token function\">removeEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'change'</span><span class=\"token punctuation\">,</span> callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span>\n\n  <span class=\"token keyword\">return</span> <span class=\"token keyword\">function</span> <span class=\"token function\">useMediaQuery</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">return</span> <span class=\"token function\">useSyncExternalStore</span><span class=\"token punctuation\">(</span>subscribe<span class=\"token punctuation\">,</span> getSnapshot<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">const</span> useNarrowMediaQuery <span class=\"token operator\">=</span> <span class=\"token function\">makeMediaQueryStore</span><span class=\"token punctuation\">(</span><span class=\"token string\">'(max-width: 600px)'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">NarrowScreenNotifier</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> isNarrow <span class=\"token operator\">=</span> <span class=\"token function\">useNarrowMediaQuery</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token keyword\">return</span> isNarrow <span class=\"token operator\">?</span> <span class=\"token string\">'좁은 화면입니다'</span> <span class=\"token operator\">:</span> <span class=\"token string\">'넓은 화면입니다'</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">App</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token punctuation\">(</span>\n    <span class=\"token operator\">&lt;</span>div<span class=\"token operator\">></span>\n      <span class=\"token operator\">&lt;</span>div<span class=\"token operator\">></span>좁은 화면 여부<span class=\"token operator\">:</span><span class=\"token operator\">&lt;</span><span class=\"token operator\">/</span>div<span class=\"token operator\">></span>\n      <span class=\"token operator\">&lt;</span>Suspense fallback<span class=\"token operator\">=</span><span class=\"token string\">\"...로딩 중...\"</span><span class=\"token operator\">></span>\n        <span class=\"token operator\">&lt;</span>NarrowScreenNotifier <span class=\"token operator\">/</span><span class=\"token operator\">></span>\n      <span class=\"token operator\">&lt;</span><span class=\"token operator\">/</span>Suspense<span class=\"token operator\">></span>\n    <span class=\"token operator\">&lt;</span><span class=\"token operator\">/</span>div<span class=\"token operator\">></span>\n  <span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">const</span> root <span class=\"token operator\">=</span> ReactDOM<span class=\"token punctuation\">.</span><span class=\"token function\">hydrateRoot</span><span class=\"token punctuation\">(</span>rootEl<span class=\"token punctuation\">,</span> <span class=\"token operator\">&lt;</span>App <span class=\"token operator\">/</span><span class=\"token operator\">></span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token function\">onRecoverableError</span><span class=\"token punctuation\">(</span>error<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span><span class=\"token function\">String</span><span class=\"token punctuation\">(</span>error<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">includes</span><span class=\"token punctuation\">(</span><span class=\"token string\">'Missing getServerSnapshot'</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">return</span><span class=\"token punctuation\">;</span>\n    <span class=\"token builtin\">console</span><span class=\"token punctuation\">.</span><span class=\"token function\">error</span><span class=\"token punctuation\">(</span>error<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>참고로, <code class=\"language-text\">useMediaQuery</code> 예시에서 동시성 모드를 사용하지 않거나 이 특정 기능에서 테어링을 크게 신경 쓰지 않는 클라이언트 전용 시나리오라면 <code class=\"language-text\">useEffect</code>와 <code class=\"language-text\">useState</code>가 더 간단해 보일 수 있습니다. 하지만 <code class=\"language-text\">useSyncExternalStore</code>는 특히 SSR과 동시성 기능을 함께 사용할 때 가장 견고한 방법입니다. <code class=\"language-text\">hydrateRoot</code>를 사용하는 서버 렌더링을 가정하고 있음에도 <code class=\"language-text\">getServerSnapshot</code>을 제공하지 않은 것은, 서버에서 미디어 쿼리를 확인할 방법이 없기 때문입니다. 따라서 불필요한 오류 로깅을 방지하기 위해 <code class=\"language-text\">onRecoverableError</code> 핸들러를 추가합니다.</p>\n<h2 id=\"트러블슈팅-체크리스트\" style=\"position:relative;\"><a href=\"#%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8\" aria-label=\"트러블슈팅 체크리스트 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>트러블슈팅 체크리스트</h2>\n<p><code class=\"language-text\">useSyncExternalStore</code>를 디버깅할 때 확인할 사항입니다.</p>\n<ul>\n<li><strong>리액트 버전:</strong> 리액트 18 이상을 사용하고 있나요?</li>\n<li><strong>안정적인 함수:</strong> <code class=\"language-text\">subscribe</code>와 <code class=\"language-text\">getSnapshot</code> 함수가 안정적인가요(컴포넌트 외부에서 정의하거나 <code class=\"language-text\">useCallback</code>으로 메모이제이션)?</li>\n<li><strong><code class=\"language-text\">getSnapshot</code> 순수성 &#x26; 성능:</strong> <code class=\"language-text\">getSnapshot</code>이 빠르고 순수하며, 기저 데이터가 변경되지 않았다면 동일한 값 참조(<code class=\"language-text\">Object.is</code> true)를 반환하나요?</li>\n<li><strong><code class=\"language-text\">subscribe</code> 정확성:</strong> <code class=\"language-text\">subscribe</code>가 <em>스토어가 실제로 변경될 때만</em> 리액트가 제공한 <code class=\"language-text\">callback</code>을 올바르게 호출하나요? 적절한 <code class=\"language-text\">unsubscribe</code> 함수를 반환하나요?</li>\n<li><strong>SSR:</strong> SSR을 사용하는 경우 <code class=\"language-text\">getServerSnapshot</code> 함수를 제공했나요? 클라이언트에서 초기에 볼 수 있는 것과 일관된 값이나 안전한 기본값을 반환하나요?</li>\n<li><strong>외부 상태만:</strong> 이 훅을 정말로 리액트 <strong>외부</strong>에 있는 상태에만 사용하고 있나요? 리액트 상태와 Context에는 각각의 메커니즘이 있습니다.</li>\n</ul>\n<h2 id=\"마무리\" style=\"position:relative;\"><a href=\"#%EB%A7%88%EB%AC%B4%EB%A6%AC\" aria-label=\"마무리 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>마무리</h2>\n<p><code class=\"language-text\">useSyncExternalStore</code>는 동시성 시대에 리액트 컴포넌트를 외부 데이터 소스에 안전하게 연결하는 특화된 강력한 훅입니다. 이 훅의 목적(테어링 방지 및 일관된 읽기 보장)을 이해하고 이러한 모범 사례를 따른다면, 어떤 외부 상태 관리 시스템이나 브라우저 API와도 리액트를 자신 있게 통합할 수 있습니다.</p>\n<ul>\n<li><a href=\"https://react.dev/reference/react/useSyncExternalStore\"><code class=\"language-text\">useSyncExternalStore</code> 공식 리액트 문서</a></li>\n<li>더 고급 패턴과 사용 사례를 알고 싶다면, Zustand나 Redux(v8 이상) 같은 라이브러리가 이 훅을 내부적으로 어떻게 활용하는지 살펴보는 것이 매우 유익합니다.</li>\n</ul>\n<p>즐겁고 안전하게 동기화하세요! 더 고급 패턴과 사용 사례를 원한다면 <a href=\"https://www.epicreact.dev/workshops/advanced-react-apis\">Advanced React APIs Epic React 워크숍</a>을 확인해보세요!</p>","frontmatter":{"title":"(번역) useSyncExternalStore : 실전 리액트 개발을 위한 심층 해설","date":"April 13, 2026","thumbnail":null,"canonicalUrl":null}}},"pageContext":{"slug":"/translation/2026/use-sync-external-store/","previous":{"fields":{"slug":"/translation/2026/outsourcing-thinking/"},"frontmatter":{"title":"(번역) 사고의 외주화"}},"next":null}},"staticQueryHashes":["1873090088","3128451518"]}