<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>React · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/react/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Mon, 26 Oct 2026 00:00:00 +1200</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/react/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Zustand vs React Query (TanStack Query) vs AsyncStorage: 어디에 뭘 넣어야 할까</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2026/2026-05-05-zustand-vs-react-query/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2026/2026-05-05-zustand-vs-react-query/</guid>
      <pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>React</category>
      <category>React Native</category>
      <category>Zustand</category>
      <category>React Query</category>
      <category>TanStack Query</category>
      <category>AsyncStorage</category>
      <category>State Management</category>
      <category>Mobile Development</category>
      <category>Web Development</category>
      <description>Zustand와 React Query는 양자택일이 아닙니다. React Query(TanStack Query)는 서버 상태를 캐싱하고, Zustand는 클라이언트 상태를 들고, AsyncStorage는 재시작 후에도 남아야 하는 것을 보관합니다. React와 React Native에서 상태를 셋에 어떻게 나누는지, Zustand와 React Query를 함께 쓰는 법, 그리고 셋을 조합해 오프라인 모드를 동작시키는 방법을 정리했습니다.</description>
      <content:encoded><![CDATA[<p>Zustand와 React Query는 경쟁 관계가 아닙니다. 그래서 &ldquo;Zustand vs React Query&quot;에 대한 진짜 답은 대개 &ldquo;둘 다&quot;입니다. React Query는 지금 TanStack Query라는 이름으로 배포되는데, 서버 상태를 위한 도구입니다. 서버가 소유한 데이터를 가져오고, 캐싱하고, 중복 요청을 하나로 합치고, 낡으면 다시 가져옵니다. Zustand는 클라이언트 상태를 위한 도구입니다. 어떤 탭이 열려 있는지, 반쯤 쓰다 만 폼에 뭐가 들어 있는지처럼 내 앱만 아는 것들이죠. 대부분의 앱은 둘을 함께 쓰고, React Native에서는 재시작 후에도 남아야 하는 것을 맡는 세 번째 조각으로 AsyncStorage가 붙습니다. 이 글의 나머지는 그 경계선을 어디에 긋는지 코드와 함께 보여 주고, 셋이 합쳐져 오프라인에서도 동작하는 앱이 되는 과정을 다룹니다.</p>
<p>지금 만들고 있는 견적 앱에는 절대 양보할 수 없는 요구사항이 하나 있습니다. 신호가 없어도 동작해야 한다는 것. 견적은 현장에 나가서 그 자리에서 작성하는데, 현장이라는 게 신축 건물이거나 지하라서 전파가 잡힌다는 보장이 없거든요. 이 제약 덕분에 많은 React 프로젝트가 얼버무리고 넘어가는 질문, &ldquo;어떤 상태를 어디에 둘 것인가&quot;를 정면으로 마주하게 됐습니다.</p>
<p>흔한 실패 패턴은 스토어 하나에 전부 밀어 넣는 겁니다. API 캐싱, 사용자 설정, 폼 상태, 인증 토큰까지 같은 Redux 덩어리 안에 뒤엉켜 있죠. 하지만 이것들은 성격도 수명도 다른 상태이고, 각자에게 맞는 도구가 따로 있습니다. Zustand, React Query, AsyncStorage는 각각 딱 하나의 문제를 제대로 풀어줍니다. 경계선만 잘 그으면 아키텍처는 거의 알아서 정리됩니다.</p>

<h2 class="relative group">상태는 세 종류다
    <div id="상태는-세-종류다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%83%81%ed%83%9c%eb%8a%94-%ec%84%b8-%ec%a2%85%eb%a5%98%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>라이브러리 얘기를 하기 전에, 지금 관리하려는 게 뭔지부터 분명히 해두는 게 좋습니다.</p>
<p>클라이언트 상태는 앱 런타임에만 존재하는 데이터입니다. UI 토글, 선택된 탭, 폼 입력, 모달 열림 여부. 상위에 소유자가 없고, 프로세스가 끝나면 같이 사라집니다.</p>
<p>서버 상태는 사정이 다릅니다. 소유자는 서버이고, 앱이 들고 있는 건 로컬 복사본일 뿐이죠. 사용자 프로필, 상품 목록, 알림, 피드. 내 복사본은 낡아가고, 다시 가져와야 하고, 지금 이 순간 다른 클라이언트는 다른 버전을 보고 있을지도 모릅니다.</p>
<p>영속 상태는 재시작을 버텨야 하는 데이터입니다. 인증 토큰, 온보딩 완료 플래그, 캐시된 설정, 오프라인 데이터. 기기 자체에 저장됩니다.</p>
<p>대부분의 앱에는 셋 다 있습니다. 그리고 대부분의 사고는 셋을 도구 하나로 처리하려다 생깁니다.</p>

<h2 class="relative group">Zustand: 클라이언트 상태 담당
    <div id="zustand-클라이언트-상태-담당" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zustand-%ed%81%b4%eb%9d%bc%ec%9d%b4%ec%96%b8%ed%8a%b8-%ec%83%81%ed%83%9c-%eb%8b%b4%eb%8b%b9" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Zustand는 격식 차릴 게 없는 작은 상태 라이브러리입니다. Provider도, 보일러플레이트도, 몇 겹씩 쌓인 Context 래퍼도 없습니다. 스토어 만들고 컴포넌트에서 쓰면 끝. 배울 게 사실상 그게 전부입니다.</p>
<p>여러 컴포넌트가 같은 UI 상태를 공유할 때(사이드바 열림/닫힘, 활성 필터, 선택된 항목), API에서 오지 않는 앱 레벨 상태가 있을 때(테마, 언어, 시작 시 읽는 기능 플래그), 장바구니 계산이나 멀티 스텝 위저드처럼 진짜 클라이언트 로직이 있을 때가 Zustand의 자리입니다. 동기적이고 예측 가능하게 업데이트돼야 하는 것들이죠.</p>
<p>기본 스토어는 이렇게 생겼습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">AppState</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span> <span class="o">|</span> <span class="s1">&#39;dark&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">sidebarOpen</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl">  <span class="nx">selectedFilters</span>: <span class="kt">string</span><span class="p">[]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setTheme</span><span class="o">:</span> <span class="p">(</span><span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span> <span class="o">|</span> <span class="s1">&#39;dark&#39;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl">  <span class="nx">toggleSidebar</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setFilters</span><span class="o">:</span> <span class="p">(</span><span class="nx">filters</span>: <span class="kt">string</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useAppStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">AppState</span><span class="p">&gt;((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">sidebarOpen</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">selectedFilters</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setTheme</span><span class="o">:</span> <span class="p">(</span><span class="nx">theme</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">theme</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">toggleSidebar</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">sidebarOpen</span><span class="o">:</span> <span class="o">!</span><span class="nx">state</span><span class="p">.</span><span class="nx">sidebarOpen</span> <span class="p">})),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setFilters</span><span class="o">:</span> <span class="p">(</span><span class="nx">filters</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">selectedFilters</span>: <span class="kt">filters</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span></span></span></code></pre></div></div>
<p>컴포넌트에서는 이렇게 씁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">Sidebar() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">sidebarOpen</span><span class="p">,</span> <span class="nx">toggleSidebar</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useAppStore</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">sidebarOpen</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="o">=</span><span class="s">&#34;sidebar&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">toggleSidebar</span><span class="p">}&gt;</span><span class="nx">Close</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="cm">/* sidebar content */</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">Zustand에 넣지 않는 것
    <div id="zustand에-넣지-않는-것" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zustand%ec%97%90-%eb%84%a3%ec%a7%80-%ec%95%8a%eb%8a%94-%ea%b2%83" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>우선 API 응답입니다. API를 호출할 때마다 Zustand 스토어에 쓰고, 컴포넌트는 쿼리 대신 스토어를 읽는 프로젝트를 본 적이 있습니다. 그 끝은 캐시 무효화, 로딩 상태, 에러 처리, 리페치, 페이지네이션을 전부 손으로 다시 구현하는 것이었죠. 전부 React Query가 공짜로 해주는 일들입니다.</p>
<p>또 하나는 재시작 후에도 남아야 하는 데이터입니다. Zustand 상태는 메모리에 있어서 앱을 닫으면 사라집니다. persist 미들웨어로 AsyncStorage에 다리를 놓을 수는 있지만(뒤에서 다룹니다), 그건 의식적으로 내리는 결정이어야지 습관이어서는 안 됩니다.</p>

<h2 class="relative group">React Query: 서버 상태 담당
    <div id="react-query-서버-상태-담당" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#react-query-%ec%84%9c%eb%b2%84-%ec%83%81%ed%83%9c-%eb%8b%b4%eb%8b%b9" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>React Query(요즘은 TanStack Query죠)는 원격 데이터의 라이프사이클 전체를 관리합니다. 페칭, 캐싱, 동기화, 업데이트, 가비지 컬렉션까지. 발상의 전환 포인트는 서버 데이터를 &ldquo;내가 소유한 상태&quot;가 아니라 &ldquo;신선하게 유지해야 할 캐시&quot;로 취급한다는 점입니다. 소유자는 서버고, 나는 복사본을 맡아둔 것뿐입니다.</p>
<p>API에서 오는 데이터라면 저는 전부 여기에 둡니다. 같은 엔드포인트를 여러 컴포넌트가 쓰는 데이터(요청은 자동으로 중복 제거됩니다), 페이지네이션과 무한 스크롤, 앱에 돌아왔을 때 백그라운드에서 다시 가져와야 하는 데이터, UI를 먼저 바꾸고 서버가 거부하면 되돌리는 낙관적 업데이트까지 전부요.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useQuery</span><span class="p">,</span> <span class="nx">useMutation</span><span class="p">,</span> <span class="nx">useQueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useUser</span><span class="p">(</span><span class="nx">userId</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useQuery</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="nx">userId</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryFn</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/users/</span><span class="si">${</span><span class="nx">userId</span><span class="si">}</span><span class="sb">`</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">staleTime</span>: <span class="kt">5</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 5분 동안 fresh로 간주
</span></span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useUpdateUser() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="nx">useQueryClient</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useMutation</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mutationFn</span><span class="o">:</span> <span class="p">(</span><span class="nx">data</span><span class="o">:</span> <span class="p">{</span> <span class="nx">id</span>: <span class="kt">string</span><span class="p">;</span> <span class="nx">name</span>: <span class="kt">string</span> <span class="p">})</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/users/</span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;PATCH&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="p">}),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">onSuccess</span><span class="o">:</span> <span class="p">(</span><span class="nx">_</span><span class="p">,</span> <span class="nx">variables</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">queryClient</span><span class="p">.</span><span class="nx">invalidateQueries</span><span class="p">({</span> <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="nx">variables</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>컴포넌트에서는:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">UserProfile</span><span class="p">({</span> <span class="nx">userId</span> <span class="p">}</span><span class="o">:</span> <span class="p">{</span> <span class="nx">userId</span>: <span class="kt">string</span> <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">data</span>: <span class="kt">user</span><span class="p">,</span> <span class="nx">isLoading</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useUser</span><span class="p">(</span><span class="nx">userId</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">updateUser</span> <span class="o">=</span> <span class="nx">useUpdateUser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isLoading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">Spinner</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">ErrorMessage</span> <span class="na">error</span><span class="o">=</span><span class="p">{</span><span class="nx">error</span><span class="p">}</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;{</span><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">}&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">button</span>
</span></span><span class="line"><span class="cl">        <span class="na">onClick</span><span class="o">=</span><span class="p">{()</span> <span class="o">=&gt;</span> <span class="nx">updateUser</span><span class="p">.</span><span class="nx">mutate</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">userId</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;New Name&#39;</span> <span class="p">})}</span>
</span></span><span class="line"><span class="cl">        <span class="na">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">updateUser</span><span class="p">.</span><span class="nx">isPending</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Update</span> <span class="nx">Name</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">안 짜도 되는 코드들
    <div id="안-짜도-되는-코드들" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%95%88-%ec%a7%9c%eb%8f%84-%eb%90%98%eb%8a%94-%ec%bd%94%eb%93%9c%eb%93%a4" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>서버 데이터를 여기 두라고 제가 고집하는 이유는, 공짜로 딸려오는 장치가 그만큼 많아서입니다. 컴포넌트 다섯 개가 같은 사용자를 요청해도 네트워크 요청은 한 번입니다. 창이 포커스를 되찾거나 네트워크가 재연결되면 알아서 리페치합니다. 오래된 캐시는 알아서 가비지 컬렉션되고요. 모든 쿼리가 <code>isLoading</code>, <code>isError</code>, <code>data</code> 같은 값을 넘겨주니 로딩·에러 UI를 매번 새로 만들 필요가 없습니다. 실패한 요청은 지수 백오프로 재시도되고, 롤백까지 포함된 낙관적 업데이트도 내장 패턴으로 제공됩니다. 직접 만들면 하루가 통째로 날아가는 것들이죠.</p>

<h3 class="relative group">어울리지 않는 곳
    <div id="어울리지-않는-곳" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%96%b4%ec%9a%b8%eb%a6%ac%ec%a7%80-%ec%95%8a%eb%8a%94-%ea%b3%b3" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>클라이언트 전용 상태입니다. 서버를 거치지 않는 데이터에 React Query를 쓰면 절차만 늘어납니다. 모달 열림 상태에 캐시 무효화나 리페치 간격이 필요할 리 없으니까요.</p>
<p>장기 영속 레이어도 아닙니다. 캐시는 메모리에 있으니 재시작하면 텅 비고, 전부 다시 가져옵니다. 캐시를 영속화할 수는 있고 React Native에서는 그래야 하지만(아래 오프라인 섹션에서 다룹니다), 원본은 언제나 서버입니다.</p>

<h2 class="relative group">AsyncStorage: 재시작을 버티는 것들 담당
    <div id="asyncstorage-재시작을-버티는-것들-담당" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#asyncstorage-%ec%9e%ac%ec%8b%9c%ec%9e%91%ec%9d%84-%eb%b2%84%ed%8b%b0%eb%8a%94-%ea%b2%83%eb%93%a4-%eb%8b%b4%eb%8b%b9" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>AsyncStorage는 React Native의 키-값 저장소입니다. 웹에서의 대응물은 <code>localStorage</code>(동기)나 <code>IndexedDB</code>(비동기, 더 강력함)이고요. 어느 쪽이든 개념은 같습니다. 기기에 기록해서 프로세스보다 오래 사는 데이터라는 것.</p>

<h3 class="relative group">플랫폼마다 속은 다르다
    <div id="플랫폼마다-속은-다르다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%94%8c%eb%9e%ab%ed%8f%bc%eb%a7%88%eb%8b%a4-%ec%86%8d%ec%9d%80-%eb%8b%a4%eb%a5%b4%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h3>
<p><code>@react-native-async-storage/async-storage</code>를 쓰면 API는 하나지만, 그 아래 백엔드는 플랫폼마다 꽤 다릅니다.</p>
<p>Android에서는 <code>RKStorage</code>를 통한 SQLite입니다. 앱 내부 저장소 디렉터리의 데이터베이스에 저장되고, 빠르고 안정적이며 샌드박스 처리돼 있어 다른 앱이 건드릴 수 없습니다. 다만 아래 표에 있는 기본 용량 제한은 염두에 두세요.</p>
<p>iOS에서는 작은 값은 <code>NSUserDefaults</code>, 큰 값은 직렬화된 파일에 들어갑니다. 역시 앱 컨테이너 안에 샌드박스 처리됩니다. Apple이 <code>NSUserDefaults</code>에 하드 리미트를 두진 않지만, 값 하나를 몇백 KB 이하로 유지하는 게 상식선입니다. 그보다 크다면 애초에 WatermelonDB나 Realm 같은 제대로 된 데이터베이스를 검토하는 게 맞습니다.</p>
<p>웹(React Native Web / Expo Web)에서는 <code>localStorage</code>로 폴백되는데, 브라우저에 따라 5~10MB 정도가 한계입니다. 웹 전용 React 앱이라면 <code>localStorage</code>를 직접 쓰거나, 데이터가 커지면 <code>idb-keyval</code> 같은 래퍼로 <code>IndexedDB</code>를 쓰면 됩니다.</p>
<table>
	<thead>
			<tr>
					<th>플랫폼</th>
					<th>백엔드</th>
					<th>용량 제한</th>
					<th>저장 위치</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Android</td>
					<td>SQLite (RKStorage)</td>
					<td>기본 약 6MB (변경 가능)</td>
					<td>앱 내부 저장소</td>
			</tr>
			<tr>
					<td>iOS</td>
					<td>NSUserDefaults / 파일</td>
					<td>하드 리미트 없음 (값은 작게 유지)</td>
					<td>앱 샌드박스 컨테이너</td>
			</tr>
			<tr>
					<td>Web</td>
					<td>localStorage</td>
					<td>약 5~10MB (브라우저마다 다름)</td>
					<td>브라우저 오리진 저장소</td>
			</tr>
	</tbody>
</table>

<h3 class="relative group">내가 넣어두는 것들
    <div id="내가-넣어두는-것들" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%82%b4%ea%b0%80-%eb%84%a3%ec%96%b4%eb%91%90%eb%8a%94-%ea%b2%83%eb%93%a4" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>인증 토큰과 세션 데이터, 계속 유지돼야 할 사용자 설정(언어, 테마, 알림 설정), 온보딩 완료 플래그, 오프라인용 캐시 데이터. 요컨대 &ldquo;재시작을 버텨야 하는 작은 키-값&quot;이 이 카테고리의 전부입니다.</p>
<p>API는 저장소가 가질 수 있는 가장 단순한 모양입니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 값 저장
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">,</span> <span class="nx">token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 값 읽기
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 객체 저장 (직렬화 필요)
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;user_preferences&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;dark&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">language</span><span class="o">:</span> <span class="s1">&#39;en&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">notifications</span>: <span class="kt">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 객체 읽기
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">prefs</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;user_preferences&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="s1">&#39;{}&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 값 삭제
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 전부 삭제 (주의해서 사용)
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">clear</span><span class="p">()</span></span></span></code></pre></div></div>

<h3 class="relative group">웹에서는
    <div id="웹에서는" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9b%b9%ec%97%90%ec%84%9c%eb%8a%94" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>웹 전용 React 앱에는 AsyncStorage가 아예 필요 없습니다. 단순한 키-값이면 <code>localStorage</code>로 충분합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// 동기 - 메인 스레드를 블로킹하지만 작은 데이터에는 괜찮다
</span></span></span><span class="line"><span class="cl"><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;theme&#39;</span><span class="p">,</span> <span class="s1">&#39;dark&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">theme</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;theme&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 구조화된 데이터용
</span></span></span><span class="line"><span class="cl"><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;Jared&#39;</span><span class="p">,</span> <span class="nx">role</span><span class="o">:</span> <span class="s1">&#39;admin&#39;</span> <span class="p">}))</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;user&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="s1">&#39;{}&#39;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>데이터셋이 커지면 <code>IndexedDB</code>를 씁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="kr">get</span><span class="p">,</span> <span class="kr">set</span><span class="p">,</span> <span class="nx">del</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;idb-keyval&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="kr">set</span><span class="p">(</span><span class="s1">&#39;large-dataset&#39;</span><span class="p">,</span> <span class="nx">hugeArray</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="kr">get</span><span class="p">(</span><span class="s1">&#39;large-dataset&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">del</span><span class="p">(</span><span class="s1">&#39;large-dataset&#39;</span><span class="p">)</span></span></span></code></pre></div></div>

<h3 class="relative group">어울리지 않는 곳
    <div id="어울리지-않는-곳-1" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%96%b4%ec%9a%b8%eb%a6%ac%ec%a7%80-%ec%95%8a%eb%8a%94-%ea%b3%b3-1" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>키-값 저장소지 데이터베이스가 아닙니다. 관계형 데이터, 수천 건짜리 배열, 인덱싱이나 쿼리가 필요한 것들은 SQLite(<code>expo-sqlite</code>), WatermelonDB, Realm의 영역입니다.</p>
<p>보안 저장소도 아닙니다. 루팅되거나 탈옥된 기기에서는 AsyncStorage 내용이 그대로 읽힙니다. 민감한 토큰은 <code>expo-secure-store</code>나 <code>react-native-keychain</code>에 두세요.</p>

<h2 class="relative group">셋을 엮는 방법
    <div id="셋을-엮는-방법" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%85%8b%ec%9d%84-%ec%97%ae%eb%8a%94-%eb%b0%a9%eb%b2%95" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>실제 앱에서는 셋을 동시에 씁니다. 제가 자주 쓰는 패턴 몇 가지입니다.</p>

<h3 class="relative group">인증 흐름
    <div id="인증-흐름" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9d%b8%ec%a6%9d-%ed%9d%90%eb%a6%84" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// 1. AsyncStorage: 인증 토큰 영속화
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">saveToken</span><span class="p">(</span><span class="nx">token</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">,</span> <span class="nx">token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">getToken</span><span class="p">()</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">string</span> <span class="err">|</span> <span class="na">null</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 2. Zustand: 메모리에서 인증 상태 추적
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">AuthState</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isAuthenticated</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl">  <span class="nx">token</span>: <span class="kt">string</span> <span class="o">|</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setAuth</span><span class="o">:</span> <span class="p">(</span><span class="nx">token</span>: <span class="kt">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl">  <span class="nx">clearAuth</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useAuthStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">AuthState</span><span class="p">&gt;((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isAuthenticated</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">token</span>: <span class="kt">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setAuth</span><span class="o">:</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">isAuthenticated</span>: <span class="kt">true</span><span class="p">,</span> <span class="nx">token</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">clearAuth</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">isAuthenticated</span>: <span class="kt">false</span><span class="p">,</span> <span class="nx">token</span>: <span class="kt">null</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 3. React Query: 토큰을 사용해 사용자 프로필 가져오기
</span></span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useCurrentUser() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">useAuthStore</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useQuery</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;currentUser&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryFn</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/api/me&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span> <span class="nx">Authorization</span><span class="o">:</span> <span class="sb">`Bearer </span><span class="si">${</span><span class="nx">token</span><span class="si">}</span><span class="sb">`</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">enabled</span><span class="o">:</span> <span class="o">!!</span><span class="nx">token</span><span class="p">,</span> <span class="c1">// 토큰이 있을 때만 패치
</span></span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>시작할 때는 이렇게요.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// 앱 초기화
</span></span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">initializeApp() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getToken</span><span class="p">()</span> <span class="c1">// AsyncStorage에서 읽기
</span></span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">useAuthStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">setAuth</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="c1">// 빠른 접근을 위해 Zustand에 넣기
</span></span></span><span class="line"><span class="cl">    <span class="c1">// React Query가 자동으로 사용자 프로필을 가져온다
</span></span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>AsyncStorage가 토큰을 재시작 너머까지 지켜주고, Zustand가 어디서든 싸게 읽을 수 있게 해주고, React Query는 토큰이 생기는 순간 프로필을 가져옵니다. 각자 잘하는 일 하나씩만 하는 거죠.</p>

<h3 class="relative group">재시작해도 남는 테마 설정
    <div id="재시작해도-남는-테마-설정" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9e%ac%ec%8b%9c%ec%9e%91%ed%95%b4%eb%8f%84-%eb%82%a8%eb%8a%94-%ed%85%8c%eb%a7%88-%ec%84%a4%ec%a0%95" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">persist</span><span class="p">,</span> <span class="nx">createJSONStorage</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand/middleware&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Zustand의 persist 미들웨어가 간극을 메워준다
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useThemeStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nx">persist</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span> <span class="kr">as</span> <span class="s1">&#39;light&#39;</span> <span class="o">|</span> <span class="s1">&#39;dark&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">toggleTheme</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="kr">set</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">          <span class="nx">theme</span>: <span class="kt">state.theme</span> <span class="o">===</span> <span class="s1">&#39;light&#39;</span> <span class="o">?</span> <span class="s1">&#39;dark&#39;</span> <span class="o">:</span> <span class="s1">&#39;light&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">})),</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;theme-storage&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">storage</span>: <span class="kt">createJSONStorage</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">AsyncStorage</span><span class="p">),</span> <span class="c1">// React Native
</span></span></span><span class="line"><span class="cl">      <span class="c1">// storage: createJSONStorage(() =&gt; localStorage), // Web
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>런타임 상태는 Zustand가 관리하고, persist 미들웨어가 뒤에서 AsyncStorage(웹이라면 localStorage)와 동기화합니다. 컴포넌트는 영속 레이어가 있는지도 모르고, 알 필요도 없습니다.</p>

<h3 class="relative group">오프라인 우선 읽기
    <div id="오프라인-우선-읽기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%98%a4%ed%94%84%eb%9d%bc%ec%9d%b8-%ec%9a%b0%ec%84%a0-%ec%9d%bd%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useQuery</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useProducts() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useQuery</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;products&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryFn</span>: <span class="kt">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/api/products&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// 오프라인 사용을 위해 AsyncStorage에 캐시
</span></span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;cached_products&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">data</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 네트워크 실패 - 캐시 데이터 시도
</span></span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">cached</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;cached_products&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">cached</span><span class="p">)</span> <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">cached</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span> <span class="nx">error</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">staleTime</span>: <span class="kt">10</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>페칭과 인메모리 캐싱은 React Query, 네트워크가 없을 때의 폴백은 AsyncStorage. 그런 분담입니다.</p>

<h2 class="relative group">오프라인 모드, 단계별로
    <div id="오프라인-모드-단계별로" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%98%a4%ed%94%84%eb%9d%bc%ec%9d%b8-%eb%aa%a8%eb%93%9c-%eb%8b%a8%ea%b3%84%eb%b3%84%eb%a1%9c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>여기가 제 진짜 관심사입니다. 사용자가 지하철, 엘리베이터, 비행기에서 앱을 연다는 일반론도 맞지만, 제 경우는 더 절박합니다. 견적은 현장에서 쓰는 건데, 현장에 신호가 있으리란 보장이 없거든요. 지하에서 스피너만 돌고 있다는 건 견적 한 건을 놓친다는 뜻입니다.</p>
<p>다행히 이 세 라이브러리만으로도, 무거운 프레임워크 없이 꽤 견고한 오프라인 구성을 만들 수 있습니다.</p>

<h3 class="relative group">1단계: 오프라인인지 알기
    <div id="1단계-오프라인인지-알기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1%eb%8b%a8%ea%b3%84-%ec%98%a4%ed%94%84%eb%9d%bc%ec%9d%b8%ec%9d%b8%ec%a7%80-%ec%95%8c%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>먼저 기기가 네트워크 상태를 앱에 알려주고, 앱은 그 답을 어딘가에 담아둬야 합니다. 이벤트는 <code>@react-native-community/netinfo</code>가 쏴주니, 작은 Zustand 스토어에 담아 어디서든 읽을 수 있게 합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">NetInfo</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-community/netinfo&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">NetworkState</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isOnline</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setOnline</span><span class="o">:</span> <span class="p">(</span><span class="nx">online</span>: <span class="kt">boolean</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useNetworkStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">NetworkState</span><span class="p">&gt;((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isOnline</span>: <span class="kt">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setOnline</span><span class="o">:</span> <span class="p">(</span><span class="nx">online</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">isOnline</span>: <span class="kt">online</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 앱 시작 시 한 번만 구독
</span></span></span><span class="line"><span class="cl"><span class="nx">NetInfo</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">useNetworkStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">setOnline</span><span class="p">(</span><span class="nx">state</span><span class="p">.</span><span class="nx">isConnected</span> <span class="o">??</span> <span class="kc">false</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
<p>이제 어떤 컴포넌트든 <code>useNetworkStore((s) =&gt; s.isOnline)</code>을 확인해서 오프라인 배너를 띄우거나, 전송 버튼을 비활성화하거나, &ldquo;다시 연결되면 동기화됩니다&quot;라고 안내할 수 있습니다.</p>

<h3 class="relative group">2단계: React Query에 오프라인 알려주기
    <div id="2단계-react-query에-오프라인-알려주기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2%eb%8b%a8%ea%b3%84-react-query%ec%97%90-%ec%98%a4%ed%94%84%eb%9d%bc%ec%9d%b8-%ec%95%8c%eb%a0%a4%ec%a3%bc%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>React Query에는 오프라인일 때 쿼리와 뮤테이션이 어떻게 움직일지 정하는 <code>networkMode</code>가 내장돼 있습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">QueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QueryClient</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">defaultOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queries</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">networkMode</span><span class="o">:</span> <span class="s1">&#39;offlineFirst&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// 캐시 데이터를 즉시 반환하고, 온라인일 때 백그라운드에서 리패치
</span></span></span><span class="line"><span class="cl">      <span class="nx">staleTime</span>: <span class="kt">5</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">gcTime</span>: <span class="kt">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 캐시를 24시간 유지
</span></span></span><span class="line"><span class="cl">      <span class="nx">retry</span><span class="o">:</span> <span class="p">(</span><span class="nx">failureCount</span><span class="p">,</span> <span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 오프라인이면 재시도하지 않음 - 어차피 또 실패할 뿐
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">useNetworkStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">isOnline</span><span class="p">)</span> <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">failureCount</span> <span class="o">&lt;</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mutations</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">networkMode</span><span class="o">:</span> <span class="s1">&#39;offlineFirst&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
<p>세 가지 모드는 이렇습니다.</p>
<table>
	<thead>
			<tr>
					<th>모드</th>
					<th>동작</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>online</code> (기본값)</td>
					<td>온라인일 때만 쿼리를 실행하고, 오프라인이면 일시 정지합니다.</td>
			</tr>
			<tr>
					<td><code>always</code></td>
					<td>네트워크 상태와 상관없이 쿼리를 실행합니다. 실패 처리는 <code>queryFn</code>이 맡습니다.</td>
			</tr>
			<tr>
					<td><code>offlineFirst</code></td>
					<td>캐시 데이터를 위해 쿼리를 한 번 실행한 뒤, 리페치는 온라인이 될 때까지 멈춥니다.</td>
			</tr>
	</tbody>
</table>
<p>대부분의 모바일 앱에는 <code>offlineFirst</code>가 정답입니다. 캐시 데이터는 즉시 보이고, 새 데이터는 네트워크가 허락할 때 도착합니다.</p>

<h3 class="relative group">3단계: 쿼리 캐시 영속화
    <div id="3단계-쿼리-캐시-영속화" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3%eb%8b%a8%ea%b3%84-%ec%bf%bc%eb%a6%ac-%ec%ba%90%ec%8b%9c-%ec%98%81%ec%86%8d%ed%99%94" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>기본 상태에서는 캐시가 메모리에만 있어서, 재시작하면 온 화면이 스피너가 됩니다. 오프라인 모드라면 캐시를 AsyncStorage에 영속화해야 합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">QueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">PersistQueryClientProvider</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query-persist-client&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">createAsyncStoragePersister</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/query-async-storage-persister&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QueryClient</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">defaultOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queries</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">gcTime</span>: <span class="kt">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 24시간 - maxAge 이상이어야 함
</span></span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">asyncStoragePersister</span> <span class="o">=</span> <span class="nx">createAsyncStoragePersister</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">storage</span>: <span class="kt">AsyncStorage</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">key</span><span class="o">:</span> <span class="s1">&#39;react-query-cache&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// App 컴포넌트에서
</span></span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">App() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">PersistQueryClientProvider</span>
</span></span><span class="line"><span class="cl">      <span class="na">client</span><span class="o">=</span><span class="p">{</span><span class="nx">queryClient</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="na">persistOptions</span><span class="o">=</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">persister</span>: <span class="kt">asyncStoragePersister</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">maxAge</span>: <span class="kt">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 24시간보다 오래된 데이터는 복원하지 않음
</span></span></span><span class="line"><span class="cl">        <span class="nx">dehydrateOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">shouldDehydrateQuery</span><span class="o">:</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 성공한 쿼리만 영속화
</span></span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nx">query</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">&#39;success&#39;</span>
</span></span><span class="line"><span class="cl">          <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">}}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">YourApp</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">PersistQueryClientProvider</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>이제 신호가 없는 곳에서 앱을 열어도 빈 화면 대신 마지막으로 받아온 데이터가 바로 보이고, 네트워크가 돌아오면 React Query가 조용히 뒤에서 리페치합니다.</p>

<h3 class="relative group">4단계: 오프라인 중의 쓰기를 큐에 쌓기
    <div id="4단계-오프라인-중의-쓰기를-큐에-쌓기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4%eb%8b%a8%ea%b3%84-%ec%98%a4%ed%94%84%eb%9d%bc%ec%9d%b8-%ec%a4%91%ec%9d%98-%ec%93%b0%ea%b8%b0%eb%a5%bc-%ed%81%90%ec%97%90-%ec%8c%93%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>읽기는 쉬운 절반입니다. 재미있어지는 건 쓰기 쪽인데, 신호가 없는 곳에서 댓글을 달거나 주문을 넣거나 프로필을 고치면, 그 변경을 큐에 쌓아뒀다가 나중에 재실행해야 하죠.</p>
<p>React Query의 답은 <code>useMutation</code>과 <code>onMutate</code>의 낙관적 업데이트입니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useMutation</span><span class="p">,</span> <span class="nx">useQueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">Comment</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">id</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">text</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">postId</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">createdAt</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">pending?</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useAddComment</span><span class="p">(</span><span class="nx">postId</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="nx">useQueryClient</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useMutation</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mutationFn</span>: <span class="kt">async</span> <span class="p">(</span><span class="nx">text</span>: <span class="kt">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/posts/</span><span class="si">${</span><span class="nx">postId</span><span class="si">}</span><span class="sb">/comments`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;POST&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span> <span class="s1">&#39;Content-Type&#39;</span><span class="o">:</span> <span class="s1">&#39;application/json&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">({</span> <span class="nx">text</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 낙관적 업데이트 - 댓글을 즉시 표시
</span></span></span><span class="line"><span class="cl">    <span class="nx">onMutate</span>: <span class="kt">async</span> <span class="p">(</span><span class="nx">text</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">await</span> <span class="nx">queryClient</span><span class="p">.</span><span class="nx">cancelQueries</span><span class="p">({</span> <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">previous</span> <span class="o">=</span> <span class="nx">queryClient</span><span class="p">.</span><span class="nx">getQueryData</span><span class="p">&lt;</span><span class="nt">Comment</span><span class="err">[]</span><span class="p">&gt;([</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">optimisticComment</span>: <span class="kt">Comment</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">id</span><span class="o">:</span> <span class="sb">`temp-</span><span class="si">${</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">text</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">postId</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">createdAt</span>: <span class="kt">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="nx">pending</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// UI에서 &#34;전송 중...&#34; 표시
</span></span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="nx">queryClient</span><span class="p">.</span><span class="nx">setQueryData</span><span class="p">&lt;</span><span class="nt">Comment</span><span class="err">[]</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="nx">old</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">[...(</span><span class="nx">old</span> <span class="o">??</span> <span class="p">[]),</span> <span class="nx">optimisticComment</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="p">{</span> <span class="nx">previous</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 실패 시 롤백
</span></span></span><span class="line"><span class="cl">    <span class="nx">onError</span><span class="o">:</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">text</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">context</span><span class="o">?</span><span class="p">.</span><span class="nx">previous</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">queryClient</span><span class="p">.</span><span class="nx">setQueryData</span><span class="p">([</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">],</span> <span class="nx">context</span><span class="p">.</span><span class="nx">previous</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 서버에서 실제 데이터를 가져오기 위해 리패치
</span></span></span><span class="line"><span class="cl">    <span class="nx">onSettled</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">queryClient</span><span class="p">.</span><span class="nx">invalidateQueries</span><span class="p">({</span> <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>뮤테이션에 <code>networkMode: 'offlineFirst'</code>가 설정돼 있으면, 네트워크가 돌아올 때까지 <code>mutationFn</code>이 일시 정지됩니다. 낙관적 업데이트 덕에 댓글은 화면에 바로 뜨고, 실제 API 호출은 재연결 시점에 나갑니다.</p>
<p>순서대로 재실행해야 하는 보류 변경이 수십 건씩 쌓이는, 더 본격적인 큐가 필요하다면 뮤테이션 큐 자체도 영속화할 수 있습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">MutationCache</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 보류 중인 뮤테이션을 AsyncStorage에 저장
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">mutationCache</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationCache</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">onError</span>: <span class="kt">async</span> <span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">variables</span><span class="p">,</span> <span class="nx">context</span><span class="p">,</span> <span class="nx">mutation</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 디버깅을 위해 실패한 뮤테이션 기록
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">pending</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;pending_mutations&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="s1">&#39;[]&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">pending</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">key</span>: <span class="kt">mutation.options.mutationKey</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">variables</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">timestamp</span>: <span class="kt">Date.now</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;pending_mutations&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">pending</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>

<h3 class="relative group">5단계: 네트워크가 돌아오면 맞춰주기
    <div id="5단계-네트워크가-돌아오면-맞춰주기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5%eb%8b%a8%ea%b3%84-%eb%84%a4%ed%8a%b8%ec%9b%8c%ed%81%ac%ea%b0%80-%eb%8f%8c%ec%95%84%ec%98%a4%eb%a9%b4-%eb%a7%9e%ec%b6%b0%ec%a3%bc%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>재연결 처리의 대부분은 React Query가 알아서 합니다. 멈춰 있던 뮤테이션은 재개되고, 낡은 쿼리는 리페치됩니다. 우리가 할 일은 React Query의 매니저들을 React Native 이벤트에 연결해주는 것뿐입니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">NetInfo</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-community/netinfo&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">onlineManager</span><span class="p">,</span> <span class="nx">focusManager</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">AppState</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;react-native&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// React Query에 네트워크 상태 변경을 알려주기
</span></span></span><span class="line"><span class="cl"><span class="nx">onlineManager</span><span class="p">.</span><span class="nx">setEventListener</span><span class="p">((</span><span class="nx">setOnline</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">NetInfo</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">setOnline</span><span class="p">(</span><span class="o">!!</span><span class="nx">state</span><span class="p">.</span><span class="nx">isConnected</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 앱이 포그라운드로 돌아올 때 리패치
</span></span></span><span class="line"><span class="cl"><span class="nx">focusManager</span><span class="p">.</span><span class="nx">setEventListener</span><span class="p">((</span><span class="nx">setFocused</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">subscription</span> <span class="o">=</span> <span class="nx">AppState</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">status</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">setFocused</span><span class="p">(</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">&#39;active&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">subscription</span><span class="p">.</span><span class="nx">remove</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
<p>앱을 한 시간쯤 백그라운드에 뒀다가 돌아오면 <code>focusManager</code>가 리페치를 돌려 최신 데이터를 보여주고, 터널을 빠져나와 신호가 잡히면 <code>onlineManager</code>가 멈춰 있던 것들을 다시 굴립니다.</p>

<h3 class="relative group">6단계: UI로 알려주기
    <div id="6단계-ui로-알려주기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#6%eb%8b%a8%ea%b3%84-ui%eb%a1%9c-%ec%95%8c%eb%a0%a4%ec%a3%bc%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>뭘 하든, 네트워크 에러를 조용히 삼키는 것만은 하지 마세요. 지금 오프라인이라는 것, 어떤 변경이 보류 중인지 사용자에게 보여줘야 합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useNetworkStore</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./stores/network&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">OfflineBanner() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">isOnline</span> <span class="o">=</span> <span class="nx">useNetworkStore</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">isOnline</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isOnline</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">View</span> <span class="na">style</span><span class="o">=</span><span class="p">{</span><span class="nx">styles</span><span class="p">.</span><span class="nx">banner</span><span class="p">}&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Text</span><span class="p">&gt;</span><span class="err">오프라인</span> <span class="err">상태입니다</span><span class="p">.</span> <span class="err">다시</span> <span class="err">연결되면</span> <span class="err">변경사항이</span> <span class="err">동기화됩니다</span><span class="p">.&lt;/</span><span class="nt">Text</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">View</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">CommentItem</span><span class="p">({</span> <span class="nx">comment</span> <span class="p">}</span><span class="o">:</span> <span class="p">{</span> <span class="nx">comment</span>: <span class="kt">Comment</span> <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">View</span> <span class="na">style</span><span class="o">=</span><span class="p">{[</span><span class="nx">styles</span><span class="p">.</span><span class="nx">comment</span><span class="p">,</span> <span class="nx">comment</span><span class="p">.</span><span class="nx">pending</span> <span class="o">&amp;&amp;</span> <span class="nx">styles</span><span class="p">.</span><span class="nx">pending</span><span class="p">]}&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Text</span><span class="p">&gt;{</span><span class="nx">comment</span><span class="p">.</span><span class="nx">text</span><span class="p">}&lt;/</span><span class="nt">Text</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nx">comment</span><span class="p">.</span><span class="nx">pending</span> <span class="o">&amp;&amp;</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">Text</span> <span class="na">style</span><span class="o">=</span><span class="p">{</span><span class="nx">styles</span><span class="p">.</span><span class="nx">pendingLabel</span><span class="p">}&gt;</span><span class="err">전송</span> <span class="err">중</span><span class="p">...&lt;/</span><span class="nt">Text</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">View</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">전체 그림
    <div id="전체-그림" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a0%84%ec%b2%b4-%ea%b7%b8%eb%a6%bc" aria-label="앵커">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌─────────────────────────────────────────────────┐
</span></span><span class="line"><span class="cl">│                   Components                     │
</span></span><span class="line"><span class="cl">│  useQuery() for reads    useMutation() for writes│
</span></span><span class="line"><span class="cl">└──────────┬──────────────────────┬────────────────┘
</span></span><span class="line"><span class="cl">           │                      │
</span></span><span class="line"><span class="cl">     ┌─────▼──────┐        ┌─────▼──────┐
</span></span><span class="line"><span class="cl">     │ React Query │        │ React Query │
</span></span><span class="line"><span class="cl">     │   Cache     │        │  Mutation   │
</span></span><span class="line"><span class="cl">     │ (in-memory) │        │   Queue     │
</span></span><span class="line"><span class="cl">     └─────┬──────┘        └─────┬──────┘
</span></span><span class="line"><span class="cl">           │                      │
</span></span><span class="line"><span class="cl">     ┌─────▼──────────────────────▼──────┐
</span></span><span class="line"><span class="cl">     │     AsyncStorage Persister         │
</span></span><span class="line"><span class="cl">     │  (survives app restart)            │
</span></span><span class="line"><span class="cl">     └─────┬──────────────────────┬──────┘
</span></span><span class="line"><span class="cl">           │                      │
</span></span><span class="line"><span class="cl">     ┌─────▼──────┐        ┌─────▼──────┐
</span></span><span class="line"><span class="cl">     │   Zustand   │        │   Network  │
</span></span><span class="line"><span class="cl">     │ (isOnline,  │◄───────│   NetInfo  │
</span></span><span class="line"><span class="cl">     │  UI state)  │        │            │
</span></span><span class="line"><span class="cl">     └────────────┘        └────────────┘</span></span></code></pre></div></div>
<table>
	<thead>
			<tr>
					<th>레이어</th>
					<th>도구</th>
					<th>역할</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>네트워크 감지</td>
					<td>Zustand + NetInfo</td>
					<td>온라인/오프라인 추적, UI 배너 표시</td>
			</tr>
			<tr>
					<td>데이터 페칭</td>
					<td>React Query</td>
					<td>온라인이면 가져오고, 오프라인이면 캐시를 제공</td>
			</tr>
			<tr>
					<td>캐시 영속화</td>
					<td>React Query + AsyncStorage</td>
					<td>앱 재시작 시 캐시 복원</td>
			</tr>
			<tr>
					<td>오프라인 쓰기</td>
					<td>React Query 뮤테이션</td>
					<td>뮤테이션을 큐에 쌓았다가 재연결 시 재실행</td>
			</tr>
			<tr>
					<td>낙관적 UI</td>
					<td>React Query <code>onMutate</code></td>
					<td>변경을 즉시 보여주고, 실패하면 롤백</td>
			</tr>
			<tr>
					<td>앱 포커스 동기화</td>
					<td>React Query <code>focusManager</code></td>
					<td>앱이 포그라운드로 돌아오면 낡은 데이터를 리페치</td>
			</tr>
	</tbody>
</table>

<h3 class="relative group">진짜 동기화 엔진이 필요해지는 순간
    <div id="진짜-동기화-엔진이-필요해지는-순간" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a7%84%ec%a7%9c-%eb%8f%99%ea%b8%b0%ed%99%94-%ec%97%94%ec%a7%84%ec%9d%b4-%ed%95%84%ec%9a%94%ed%95%b4%ec%a7%80%eb%8a%94-%ec%88%9c%ea%b0%84" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>지금까지의 구성은 서버가 원본이고 오프라인은 일시적이라는 전제 위에 서 있습니다. 충돌 해결까지 포함된 진짜 오프라인 우선(두 기기가 같은 문서를 오프라인에서 고치는 메모 앱 같은 세계)이 필요하다면, 이 스택으로는 부족하고 전용 동기화 엔진이 필요합니다. WatermelonDB는 React Native용으로 만들어져 SQLite 위에서 돌고, 충돌 해결용 동기화 프로토콜을 갖췄습니다. Realm은 Atlas Device Sync와 묶으면 MongoDB Atlas를 통한 자동 충돌 해결이 되는 완전한 오프라인 우선 데이터베이스가 되고요. PowerSync는 기존 Postgres 백엔드와 붙는 SQLite 기반 동기화 레이어입니다. 완전한 제어가 필요하면 Expo SQLite에 직접 만든 동기화 로직이라는 길도 있습니다.</p>
<p>Zustand + React Query + AsyncStorage로 모바일 앱의 80% 정도는 커버됩니다. 나머지 20%, 그러니까 협업 편집이나 멀티 디바이스 동기화 같은 오프라인 중심 워크플로우에는 전용 동기화 데이터베이스가 필요합니다.</p>

<h2 class="relative group">어디에 둘지 헷갈릴 때
    <div id="어디에-둘지-헷갈릴-때" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%96%b4%eb%94%94%ec%97%90-%eb%91%98%ec%a7%80-%ed%97%b7%ea%b0%88%eb%a6%b4-%eb%95%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>저는 애매하면 이 질문들을 순서대로 던져봅니다.</p>
<table>
	<thead>
			<tr>
					<th>질문</th>
					<th>&lsquo;예&rsquo;라면 →</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>서버/API에서 오는 데이터인가?</td>
					<td>React Query</td>
			</tr>
			<tr>
					<td>여러 컴포넌트가 공유하는 클라이언트 전용 UI 상태인가?</td>
					<td>Zustand</td>
			</tr>
			<tr>
					<td>앱을 재시작해도 남아 있어야 하나?</td>
					<td>AsyncStorage (+ 필요하면 Zustand persist)</td>
			</tr>
			<tr>
					<td>민감한 데이터인가(토큰, 비밀번호)?</td>
					<td>expo-secure-store / react-native-keychain</td>
			</tr>
			<tr>
					<td>쿼리가 필요한 대용량 구조화 데이터인가?</td>
					<td>SQLite / WatermelonDB / Realm</td>
			</tr>
			<tr>
					<td>컴포넌트 하나에서만 쓰는 단순한 폼 입력인가?</td>
					<td>useState</td>
			</tr>
	</tbody>
</table>

<h3 class="relative group">자주 나오는 패턴
    <div id="자주-나오는-패턴" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9e%90%ec%a3%bc-%eb%82%98%ec%98%a4%eb%8a%94-%ed%8c%a8%ed%84%b4" aria-label="앵커">#</a>
    </span>
    
</h3>
<table>
	<thead>
			<tr>
					<th>상태</th>
					<th>도구</th>
					<th>이유</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>API 응답 데이터</td>
					<td>React Query</td>
					<td>캐싱, 중복 제거, 리페치, 로딩 상태</td>
			</tr>
			<tr>
					<td>선택된 탭 / 활성 필터</td>
					<td>Zustand</td>
					<td>클라이언트 전용이고, 여러 컴포넌트가 관심을 가짐</td>
			</tr>
			<tr>
					<td>인증 토큰</td>
					<td>AsyncStorage + Zustand</td>
					<td>재시작 후에도 유지되고, 메모리에서 빠르게 접근</td>
			</tr>
			<tr>
					<td>테마 설정</td>
					<td>Zustand + persist 미들웨어</td>
					<td>재시작 후에도 남아야 하는 클라이언트 상태</td>
			</tr>
			<tr>
					<td>장바구니</td>
					<td>Zustand + persist 미들웨어</td>
					<td>복잡한 클라이언트 로직, 재시작 후에도 유지</td>
			</tr>
			<tr>
					<td>폼 입력</td>
					<td>useState</td>
					<td>컴포넌트 하나에서만 쓰고 공유할 필요 없음</td>
			</tr>
			<tr>
					<td>API에서 받은 사용자 프로필</td>
					<td>React Query</td>
					<td>서버 상태이고, 낡았을 수 있음</td>
			</tr>
			<tr>
					<td>온보딩 완료 플래그</td>
					<td>AsyncStorage</td>
					<td>유지되기만 하면 되는 불리언 하나</td>
			</tr>
			<tr>
					<td>오프라인 캐시 피드</td>
					<td>React Query + AsyncStorage</td>
					<td>서버에서 가져오고, 안 되면 캐시로 폴백</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">플랫폼별 설치
    <div id="플랫폼별-설치" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%94%8c%eb%9e%ab%ed%8f%bc%eb%b3%84-%ec%84%a4%ec%b9%98" aria-label="앵커">#</a>
    </span>
    
</h2>

<h3 class="relative group">React Native (Android + iOS)
    <div id="react-native-android--ios" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#react-native-android--ios" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>셋 다 한 번에 설치합니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install zustand @tanstack/react-query @react-native-async-storage/async-storage</span></span></code></pre></div></div>
<p>민감한 데이터용 보안 저장소도 추가하고요.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx expo install expo-secure-store
</span></span><span class="line"><span class="cl"><span class="c1"># or</span>
</span></span><span class="line"><span class="cl">npm install react-native-keychain</span></span></code></pre></div></div>

<h3 class="relative group">Expo
    <div id="expo" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#expo" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>AsyncStorage는 Expo에서 그대로 동작합니다. 네이티브 링킹도 필요 없습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx expo install @react-native-async-storage/async-storage</span></span></code></pre></div></div>

<h3 class="relative group">웹 전용 React
    <div id="웹-전용-react" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9b%b9-%ec%a0%84%ec%9a%a9-react" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>AsyncStorage는 건너뛰고 <code>localStorage</code>나 <code>IndexedDB</code>를 직접 쓰면 됩니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install zustand @tanstack/react-query
</span></span><span class="line"><span class="cl"><span class="c1"># Optional for IndexedDB</span>
</span></span><span class="line"><span class="cl">npm install idb-keyval</span></span></code></pre></div></div>
<p>Zustand의 persist 미들웨어는 웹에서 기본으로 <code>localStorage</code>를 씁니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">persist</span><span class="p">(</span><span class="nx">storeConfig</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;my-store&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 웹에서 localStorage가 기본값 - 추가 설정 필요 없음
</span></span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>

<h2 class="relative group">자꾸 보게 되는 실수들
    <div id="자꾸-보게-되는-실수들" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%9e%90%ea%be%b8-%eb%b3%b4%ea%b2%8c-%eb%90%98%eb%8a%94-%ec%8b%a4%ec%88%98%eb%93%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>API 데이터를 Zustand에 넣는 것. 액션 안에 <code>setUsers(apiResponse.users)</code>가 있다면 일단 손을 멈추세요. 그건 React Query의 일이고, 그 길 끝에는 어설픈 캐시 무효화 재발명이 기다리고 있습니다.</p>
<p>클라이언트 상태에 React Query를 쓰는 것. <code>queryFn</code>이 네트워크 요청을 안 한다면 도구를 잘못 골랐습니다.</p>
<p>AsyncStorage를 데이터베이스처럼 쓰는 것. 10,000건짜리 배열을 키-값 저장소에 직렬화하고 있다면, 그냥 데이터베이스를 쓰는 게 맞습니다.</p>
<p>토큰을 평문으로 두는 것. AsyncStorage는 보안 저장소가 아닙니다. 인증 토큰, API 키, 자격 증명은 <code>expo-secure-store</code>나 플랫폼 키체인으로 보내세요.</p>
<p>영속화를 손으로 만드는 것. 마운트 때 AsyncStorage를 읽고 상태가 바뀔 때마다 쓰는 코드를 짜고 있다면, Zustand의 persist 미들웨어가 하이드레이션과 직렬화까지 이미 다 해줍니다. 코드는 줄고 버그도 줄어들죠.</p>
<hr>
<p>서버 데이터는 React Query로. 클라이언트 상태는 Zustand에. 재시작을 버텨야 하는 건 AsyncStorage에, 민감한 건 보안 저장소에.</p>
<p>제가 겪어본 최악의 아키텍처들은 하나같이 거대한 스토어 하나로 모든 게 흘러들었습니다. 좋은 아키텍처에는 서버 상태, 클라이언트 상태, 영속화할 것 사이에 또렷한 선이 있었고요. 그 선을 일찍 그어두면 그다음 일이 전부 쉬워집니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-05-05-zustand-vs-react-query/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
