<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://itismilob.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://itismilob.github.io/" rel="alternate" type="text/html" /><updated>2025-09-25T06:03:57+00:00</updated><id>https://itismilob.github.io/feed.xml</id><title type="html">itisMilob</title><subtitle>Web Developer from Somewhere</subtitle><author><name>itismilob</name></author><entry><title type="html">Zustand + MMKV 연동하기</title><link href="https://itismilob.github.io/Zustand+MMKV/" rel="alternate" type="text/html" title="Zustand + MMKV 연동하기" /><published>2025-09-25T00:00:00+00:00</published><updated>2025-09-25T00:00:00+00:00</updated><id>https://itismilob.github.io/Zustand+MMKV</id><content type="html" xml:base="https://itismilob.github.io/Zustand+MMKV/"><![CDATA[<p>GooApp을 제작하면서 사용한 로컬 스토리지인 <a href="https://github.com/mrousavy/react-native-mmkv">MMKV storage (react-native-mmkv)</a>와 상태관리 모듈인 Zustand를 연동해 화면 랜더링시 로컬 저장 데이터를 자동으로 Zustand 상태와 동기화 해주는 Persist 미들웨어의 사용법을 정리했다.</p>

<h3 id="문제-상황">문제 상황</h3>

<p>Zustand와 MMKV를 따로 사용하면서 Zustand 상태와 MMKV의 로컬 데이터를 일일이 연동시켜줘야 하는 불편함이 있었다.<br />
첫 랜더링시 로컬 데이터를 불러와 Zustand에 저장하고, 앱 실행 과정에서 데이터가 바뀌면 그 때마다 Zustand 상태 변경과 직접 로컬에 반영하는 과정을 반복해야 했다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">createUser</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">newUser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userDataAPI</span><span class="p">.</span><span class="nx">createUser</span><span class="p">();</span> <span class="c1">// 유저 생성</span>

  <span class="c1">// 데이터를 두 번씩 저장하는 번거로움 발생</span>
  <span class="nx">setLocalUserData</span><span class="p">(</span><span class="nx">newUser</span><span class="p">);</span> <span class="c1">// MMKV 데이터 저장</span>
  <span class="nx">setUser</span><span class="p">(</span><span class="nx">newUser</span><span class="p">);</span> <span class="c1">// Zustand 상태 저장</span>
<span class="p">};</span>

<span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// 직접 데이터를 불러와야 함</span>
  <span class="kd">const</span> <span class="nx">currentUser</span> <span class="o">=</span> <span class="nx">getLocalUserData</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">currentUser</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">setUser</span><span class="p">(</span><span class="nx">currentUser</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nx">createUser</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">},</span> <span class="p">[]);</span>
</code></pre></div></div>

<h3 id="시도한-방법">시도한 방법</h3>

<p>MMKV에 데이터를 저장할 때 JSON 파싱이 필요했기 때문에 간단한 사용을 위해서 데이터 저장하고 불러오는 함수를 따로 만들어 사용해왔다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// User 데이터를 저장하는 함수</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">setLocalUserData</span><span class="p">(</span><span class="nx">userData</span><span class="p">:</span> <span class="nx">UserDataType</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">dataString</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">userData</span><span class="p">);</span>
  <span class="nx">mmkvStorage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">userData</span><span class="dl">'</span><span class="p">,</span> <span class="nx">dataString</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// User 데이터를 불러오는 함수</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">getLocalUserData</span><span class="p">():</span> <span class="nx">UserDataType</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">dataString</span> <span class="o">=</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="dl">'</span><span class="s1">userData</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">dataString</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">dataString</span><span class="p">)</span> <span class="k">as</span> <span class="nx">UserDataType</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>상태가 변경되는 것을 감지해 자동 저장을 하는 커스텀 훅을 만들어 사용하는 생각도 해봤지만 근본적으로 달라지는건 없으니 마음에 들지 않았다.<br />
리서칭을 하다가 Zustand Persist 미들웨어를 발견했고 MMKV에서도 지원한다는것을 알게되었다.</p>

<h3 id="해결-방법">해결 방법</h3>

<p>MMKV 문서에 나와있는 Zustand persist 연결 방법을 찾아서 연결했다.</p>

<h4 id="연결을-위한-custom-storage-생성">연결을 위한 Custom Storage 생성</h4>

<ul>
  <li>기존의 <code class="language-plaintext highlighter-rouge">mmkvStorage</code>를 <code class="language-plaintext highlighter-rouge">createJSONStorage</code>로 감싸 JSON 데이터로 사용할 수 있도록 만들어줬다.</li>
  <li><code class="language-plaintext highlighter-rouge">createJSONStorage</code> : 로컬 스토리지 데이터를 JSON 형태로 직렬화/역직렬화해서 Zustand Persist 미들웨어가 사용할 수 있게 변환하는 함수</li>
  <li>인자로 데이터를 관리하는 객체 <code class="language-plaintext highlighter-rouge">StateStorage ({ setItem, getItem, removeItem })</code>를 반환하는 함수를 받는다.</li>
</ul>

<blockquote>
  <p><a href="https://github.com/pmndrs/zustand?tab=readme-ov-file#persist-middleware">Zustand #persist-middleware</a> 문서에서는 <code class="language-plaintext highlighter-rouge">createJSONStorage</code>를 persist 미들웨어 안에서 사용하지만 <code class="language-plaintext highlighter-rouge">localStorage</code>는 persist로 연결해서만 사용하기 때문에 미리 감싸주었다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 기존에 사용하던 기본 로컬 저장소</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">mmkvStorage</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MMKV</span><span class="p">({</span>
  <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">STORAGE ID</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">encryptionKey</span><span class="p">:</span> <span class="nx">MMKV_KEY</span><span class="p">,</span>
<span class="p">});</span>

<span class="c1">// MMKV를 Zustand persist로 연결하기 위한 Custom Storage 객체</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">localStorage</span> <span class="o">=</span> <span class="nx">createJSONStorage</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">setItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
  <span class="p">},</span>
  <span class="na">getItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">value</span> <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>
  <span class="p">},</span>
  <span class="na">removeItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
  <span class="p">},</span>
<span class="p">}));</span>
</code></pre></div></div>

<h4 id="persist-미들웨어로-연결한-스토어-생성">Persist 미들웨어로 연결한 스토어 생성</h4>

<p>persist 미들웨어를 사용해 각각 데이터 별로 상태 스토어를 만든다.</p>

<ul>
  <li>인자로 <code class="language-plaintext highlighter-rouge">스토어 생성 함수</code>, <code class="language-plaintext highlighter-rouge">저장소 설정 객체</code>를 넣어준다.</li>
  <li>스토어 생성 함수 : 기본적인 Zustand 스토어를 만드는 생성 함수</li>
  <li>저장소 설정 객체 : 로컬 저장소 연결을 위한 정보 객체
    <ul>
      <li><code class="language-plaintext highlighter-rouge">name</code> : MMKV에 저장되는 키를 말함 → 해당 키를 통해서 JSON 데이터가 저장됨</li>
      <li><code class="language-plaintext highlighter-rouge">storage</code> : 위에서 만든 Custom Storage</li>
    </ul>
  </li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">zustand</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">persist</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">zustand/middleware</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">localStorage</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./mmkvStorage</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">UserDataType</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./dataTypes</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">type</span> <span class="nx">UserStoreType</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">user</span><span class="p">:</span> <span class="nx">UserDataType</span><span class="p">;</span>
  <span class="nl">setUser</span><span class="p">:</span> <span class="p">(</span><span class="na">state</span><span class="p">:</span> <span class="nx">UserDataType</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">userLocalStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">UserStoreType</span><span class="o">&gt;</span><span class="p">()(</span>
  <span class="nx">persist</span><span class="p">(</span>
    <span class="p">(</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="na">user</span><span class="p">:</span> <span class="p">{</span> <span class="na">_id</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span> <span class="na">nickname</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span> <span class="na">tag</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="na">rank</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="na">topScore</span><span class="p">:</span> <span class="mi">0</span> <span class="p">},</span>
      <span class="na">setUser</span><span class="p">:</span> <span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kd">set</span><span class="p">({</span> <span class="na">user</span><span class="p">:</span> <span class="nx">state</span> <span class="p">}),</span>
    <span class="p">}),</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userStore</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">storage</span><span class="p">:</span> <span class="nx">localStorage</span><span class="p">,</span>
    <span class="p">}</span>
  <span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<h4 id="스토어를-불러와-사용--useshallow">스토어를 불러와 사용 + useShallow</h4>

<p>기존 Zustand 스토어를 사용하듯이 생성한 스토어를 사용하면 자동으로 데이터를 불러오고 동기화시켜준다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">userLocalStore</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@/stores/userStore</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">userLocalStore</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">user</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">useShallow</code>를 사용하면 여러개의 상태를 한번에 불러올 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useShallow</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">zustand/react/shallow</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="p">[</span><span class="nx">user</span><span class="p">,</span> <span class="nx">setUser</span><span class="p">]</span> <span class="o">=</span> <span class="nx">userLocalStore</span><span class="p">(</span>
  <span class="nx">useShallow</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">state</span><span class="p">.</span><span class="nx">user</span><span class="p">,</span> <span class="nx">state</span><span class="p">.</span><span class="nx">setUser</span><span class="p">])</span>
<span class="p">);</span>
</code></pre></div></div>

<h2 id="배운점">배운점</h2>

<p>이미 있는 기능을 다시 만드는 것은 시간낭비고 제작자가 만든 기능이 훨씬 사용성이 좋을 수 있기 때문에 내 생각대로 작업하기 전에 라이브러리 제작자가 의도한 기능이 있는지 먼저 확인하는게 좋을 것 같다.</p>

<h2 id="참고">참고</h2>

<ul>
  <li><a href="https://github.com/mrousavy/react-native-mmkv">MMKV storage (react-native-mmkv)</a></li>
  <li><a href="https://zustand.docs.pmnd.rs/integrations/persisting-store-data">Zustand - Persisting store data</a></li>
  <li><a href="https://github.com/mrousavy/react-native-mmkv/blob/v3/docs/WRAPPER_ZUSTAND_PERSIST_MIDDLEWARE.md">MMKV persist-middleware wrapper</a></li>
  <li><a href="https://velog.io/@ony_/zustand-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94-shallow-%EB%8C%80%EC%8B%A0-useShallow-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">Zustand - useShallow [velog@ony_]</a></li>
</ul>

<hr />]]></content><author><name>itismilob</name></author><category term="GooApp" /><category term="Zustand" /><category term="MMKV" /><summary type="html"><![CDATA[Zustand Persist 미들웨어를 사용해 MMKV와 연동하기]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/gooapp_logo.png" /><media:content medium="image" url="https://itismilob.github.io/images/logos/gooapp_logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GooApp 백앤드 배포</title><link href="https://itismilob.github.io/%EB%B0%B1%EC%95%A4%EB%93%9C_%EB%B0%B0%ED%8F%AC/" rel="alternate" type="text/html" title="GooApp 백앤드 배포" /><published>2025-07-29T00:00:00+00:00</published><updated>2025-07-29T00:00:00+00:00</updated><id>https://itismilob.github.io/%EB%B0%B1%EC%95%A4%EB%93%9C_%EB%B0%B0%ED%8F%AC</id><content type="html" xml:base="https://itismilob.github.io/%EB%B0%B1%EC%95%A4%EB%93%9C_%EB%B0%B0%ED%8F%AC/"><![CDATA[<p>GooApp 프로젝트에서는 사용자의 점수를 기록하고 다른 사용자들과 점수 랭킹을 경쟁하는게 가장 중요한 기능이다.</p>

<p>랭킹 기능을 위해서는 백앤드 서버가 필요했고 가장 많이 사용해 익숙했던 Express.js를 선택해 만들었다.</p>

<p>AWS를 사용해본적이 단 한번이 없었기 때문에 이번 기회에 사용해보자 하고 어려운길을 선택하였다.</p>

<p>이번 글에서는 익숙하지 않은 배포과정을 더 기억하고 배우자는 마음으로, Express.js 백앤드 서버를 AWS EC2를 통해 배포하는 과정을 정리해보았다.</p>

<h2 id="프로젝트-구성">프로젝트 구성</h2>

<h3 id="사용한-기술-스택">사용한 기술 스택</h3>

<ul>
  <li><strong>Express.js</strong> : 배포 대상 서버 프레임워크, 가장 익숙한 프레임워크라서 선택했다.</li>
  <li><strong>Node.js</strong> : AWS 리눅스와의 버전 호환 문제로 16 버전을 사용했다.</li>
  <li><strong>AWS EC2</strong> : 클라우드 서버 환경, 가장 유명하지만 사용해본 적이 없어서 이번에 사용하기로 했다. (+ 프리티어 무료!)</li>
  <li><strong>Nginx</strong> : 리버스 프록시 설정 (포트 노출 X, 로드 밸런싱, 트래픽 관리)</li>
  <li><strong>PM2</strong> : Node.js 프로세스 관리</li>
</ul>

<hr />

<ul>
  <li><strong>MongoDB Atlas</strong> : 클라우드 DB (서버 배포 불필요)</li>
  <li><strong>SSL(HTTPS)</strong> : 도메인 필요, 이번 프로젝트는 보안 중요도가 낮아 미사용</li>
</ul>

<h3 id="구조-설명">구조 설명</h3>

<p>기본적으로 EC2 클라우드 컴퓨터에 Express.js 서버를 실행하고 요청/응답을 처리한다.</p>

<p>Nginx를 사용하여 포트를 숨겼고, 사용자가 EC2의 Public IP로 요청을 보내면 Nginx가 자동으로 Express.js서버의 포트로 요청을 넘겨준다.</p>

<p>아직 로그인 기능 같은게 없어 보안이 중요하지 않기 때문에 Nginx의 보안적 기능들은 나중에 필요하게 되면 추가할 예정이다.</p>

<p>Node.js는 프로세스를 하나만 돌릴 수 있기 때문에 필요시 프로세스를 추가하기 위해 PM2를 추가해 관리하게 했다.</p>

<h2 id="배포">배포</h2>

<h3 id="배포-과정">배포 과정</h3>

<ol>
  <li>EC2 인스턴스 생성 (AWS 계정 생성은 스킵!)</li>
  <li>Node.js 설치(NVM 사용) 및 코드 가져오기 (git pull → npm install → .env 세팅)</li>
  <li>PM2설치 및 앱 실행 (pm2 start app.js)</li>
  <li>Nginx 설치 → 리버스 프록시 설정 → Nginx 시작</li>
  <li>배포 작동 테스트</li>
</ol>

<h3 id="ec2-인스턴스-생성">EC2 인스턴스 생성</h3>

<h4 id="ami">AMI</h4>
<p>AMI는 프리티어를 사용하기 위해서 무료 요금제의 모델을 찾아 선택했다.<br />
AWS Marketplace AMI → 요금 모델 : 무료 선택 → Amazon Linux 무료 모델 선택 (요금에서 프리티어 확인)<br />
(<del>모든 AMI가 프리티어로 사용 가능한지도 모르겠다. 초보자 이슈</del>)</p>

<p><img src="/images/posts/gooapp/ec2os.png" alt="" /></p>

<p>(25년 7월부터는 프리티어 방식이 변경되었다고 한다. 이제 $200 크래딧으로 주나보다. <a href="https://aws.amazon.com/ko/free/">AWS Free Tier</a>)</p>

<h4 id="인스턴스-유형">인스턴스 유형</h4>
<p>클라우드 컴퓨터의 성능을 선택해주어야 하는데 이번 프로젝트에서 크게 중요하지는 않으니 기본값 <code class="language-plaintext highlighter-rouge">t3a.medium</code>을 선택했다.</p>

<h4 id="키-페어">키 페어</h4>
<p>로그인 보안을 위해 키 페어 파일을 생성해주어야 한다.<br />
SSH로 연결해 사용하기 위해 <code class="language-plaintext highlighter-rouge">.pem</code>으로 새 키페어를 생성했다.</p>

<p><img src="/images/posts/gooapp/pem.png" alt="" /></p>

<h4 id="네트워크-설정">네트워크 설정</h4>

<p>모든 사용자들이 EC2에 요청을 보낼 수 있게 만들기 위해서 HTTP/HTTPS 트래픽을 허용해줘야 한다. 추가로 SSH로도 접속하기 위해 SSH 허용도 해주었다.</p>

<p>보안 그룹 생성 → SSH, HTTP, HTTPS 트래픽 허용 <br />
<img src="/images/posts/gooapp/firewall.png" alt="" /></p>

<h4 id="탄력적-ip-고정-ip">탄력적 IP (고정 IP)</h4>

<p>EC2 인스턴스는 기본적으로 시작할 때마다 IP가 변경된다.<br />
인스턴스 재부팅과 상관 없이 IP를 고정해서 사용하기 위해서는 탄력적 IP 설정이 필요하다.<br />
왼쪽 탭에서 <code class="language-plaintext highlighter-rouge">탄력적 IP 주소</code>로 들어가 탄력적 IP를 할당해준다.<br />
생성한 IP 주소를 선택해 작업에서 <code class="language-plaintext highlighter-rouge">탄력적 IP 주소 연결</code>로 인스턴스와 연결해준다.</p>

<p><img src="/images/posts/gooapp/ec2ip.png" alt="" /></p>

<h4 id="인스턴스-연결-테스트">인스턴스 연결 테스트</h4>

<p>이제 인스턴스를 시작하고 연결 테스트 해보자. 인스턴스 상태를 시작으로 바꾸고 연결에 들어간다.<br />
EC2 인스턴스 연결 기본 상태로 연결을 누르면 터미널 페이지로 이동할 것이다. 👏👏<br />
<img src="/images/posts/gooapp/ec2terminal.png" alt="" /></p>

<hr />

<h3 id="ssh-연결-및-설정-옵션">SSH 연결 및 설정 (옵션)</h3>

<p>iTerm을 통해서 SSH의 별칭을 설정해 간편하게 EC2에 접속할 수 있다.</p>

<h4 id="1-권한-설정">1. 권한 설정</h4>
<p>생성한 키페어 파일을 소유자만 읽을 수 있도록 권한 변경한다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod </span>400 my-key.pem
</code></pre></div></div>

<h4 id="2-일반-접속">2. 일반 접속</h4>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-i</span> &lt;<span class="s1">'.pem'</span> FilePath&gt; &lt;EC2_UserName&gt;@&lt;EC2_PUBLIC_IP&gt;
</code></pre></div></div>

<ul>
  <li>인스턴스를 생성하며 만들었던 키페어 파일 경로 (.pem)</li>
  <li>EC2_UserName : 인스턴스 연결을 선택하면 설정할 수 있는 유저명 (기본 <code class="language-plaintext highlighter-rouge">ec2-user</code>)</li>
  <li>EC2_Public_IP : 탄력적 IP에서 설정한 IP 주소 혹은 퍼블릭 DNS</li>
</ul>

<h4 id="3-ssh-별칭-설정">3. SSH 별칭 설정</h4>

<p><code class="language-plaintext highlighter-rouge">.ssh/config</code> 파일에 별칭 설정을 한다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vim ~/.ssh/config
</code></pre></div></div>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host &lt;별칭&gt;
HostName &lt;EC2_PUBLIC_IP&gt;
User &lt;EC2_UserName&gt;
IdentityFile &lt;<span class="s1">'.pem'</span> FilePath&gt;
</code></pre></div></div>

<ul>
  <li>Host : 별칭으로 사용할 이름</li>
  <li>HostName : EC2에서 설정한 퍼블릭 IP or DNS</li>
  <li>User : EC2에서 설정한 유저명 (기본 <code class="language-plaintext highlighter-rouge">ec2-user</code>)</li>
  <li>IdentityFile : 키페어 파일 경로 (.pem)</li>
</ul>

<h4 id="ssh-접속">SSH 접속</h4>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh &lt;별칭&gt;
</code></pre></div></div>

<p><img src="/images/posts/gooapp/ec2ssh.png" alt="" /></p>

<h3 id="nodejs-설치-nvm">Node.js 설치 (NVM)</h3>

<p>Node.js를 설치해야 하는데 버전 관리의 편의성을 위해서 NVM(Node Version Manager)를 사용했다.<br />
(설치 후 터미널 재시작이 필요하다.)</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-o-</span> https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash

nvm <span class="nt">--version</span>
</code></pre></div></div>

<p>프리티어 AWS Linux 2버전에서는 상위 버전의 Node를 설치하지 못하는 문제가 있어서 16버전으로 설치했다.<br />
(상위 버전이 필요하다면 AL2023 버전이나 도커를 사용해야 할 것 같다.)</p>

<p>항상 동일한 Node 버전으로 사용하기 위해서 설치한 Node 버전을 기본값으로 고정해줘야 한다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvm <span class="nb">install </span>16

nvm use 16 <span class="c"># 현재 터미널 세션의 node 버전 설정</span>
nvm <span class="nb">alias </span>default 16 <span class="c"># 터미널이 열릴 때 기본값 설정</span>
</code></pre></div></div>

<h3 id="코드-가져오기-환경변수-설정">코드 가져오기, 환경변수 설정</h3>

<p>깃을 설치하고 깃허브에 올려놓은 프로젝트 파일을 불러오자.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># GIT 설치</span>
<span class="nb">sudo </span>yum update <span class="nt">-y</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> git
git <span class="nt">--version</span>

<span class="c"># 프로젝트 클론</span>
git clone https://github.com/itismilob/GooApp.git
</code></pre></div></div>

<p>.env 파일을 생성하고 환경변수를 추가해주었다.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vim .env
</code></pre></div></div>

<p>이제 <code class="language-plaintext highlighter-rouge">npm install</code> 한 번 때려주고 테스트로 실행해보자.<br />
<code class="language-plaintext highlighter-rouge">퍼블릭 DNS:서버 포트</code>로 접속하면 잘 작동 할 것이다.</p>

<h3 id="pm2-설치--실행">PM2 설치 &amp; 실행</h3>

<p>Node를 멀티프로세스로 돌려주고 로그도 편리하게 확인 가능한 기능들을 가진 PM2를 설치해 사용했다.<br />
지금은 사용자가 매우 적으니 프로세스는 당연히 하나만 돌리고있다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">-g</span> pm2 <span class="c"># pm2 글로벌 설치</span>
<span class="nv">$ </span>pm2 <span class="nt">-version</span> <span class="c"># pm2 버전 확인</span>
<span class="nv">$ </span>pm2 update <span class="c"># pm2 업데이트</span>

pm2 start build/app.js <span class="c"># 프로젝트 시작</span>
</code></pre></div></div>

<p>이제 우리는 프로세스 CPU, 메모리 사용 상태 등을 볼 수 있고 로그도 에러와 정상 로그로 분리해서 확인할 수 있게 되었다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pm2 monit <span class="c"># 프로세스 상태 보기</span>
pm2 log <span class="c"># 로그 확인</span>
</code></pre></div></div>

<h3 id="nginx-설치--리버스-프록시-설정">Nginx 설치 &amp; 리버스 프록시 설정</h3>

<p>마지막으로 Nginx를 추가해 리버스 프록시로 포트를 감추고 그냥 <code class="language-plaintext highlighter-rouge">DNS 주소</code>로 접속할 수 있도록 만들었다.</p>

<p>(설치 후 nginx를 한 번 실행해 정상적으로 연결되는지 확인하자.)</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>amazon-linux-extras <span class="nb">install</span> <span class="nt">-y</span> nginx1 <span class="c"># nginx 설치</span>

nginx <span class="nt">-v</span> <span class="c"># nginx 버전 확인</span>
</code></pre></div></div>

<p>Nginx 설정 파일을 생성해서 포트가 없는 기본 요청에 리버스 프록시 설정을 해주었다.</p>

<p><code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/default.conf</code> 커스텀 설정 파일 생성</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
	<span class="kn">listen</span>  <span class="mi">80</span><span class="p">;</span>
	<span class="kn">listen</span>  <span class="s">[::]:80</span><span class="p">;</span>
	
	<span class="kn">access_log</span> <span class="n">/var/log/nginx/reverse-access.log</span><span class="p">;</span>
	<span class="kn">error_log</span> <span class="n">/var/log/nginx/reverse-error.log</span><span class="p">;</span>
	
	<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
		<span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:&lt;PORT&gt;</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>기본 설정 파일 <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>에서 커스텀 설정 파일 include</p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">http</span> <span class="p">{</span>
	<span class="kn">include</span> <span class="n">/etc/nginx/conf.d/*.conf</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이렇게 해주면 EC2로 들어가는 기본 요청이 Nginx를 통해 <code class="language-plaintext highlighter-rouge">localhost:서버 포트</code>로 전달된다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl start nginx
</code></pre></div></div>

<h2 id="배포-후-점검">배포 후 점검</h2>

<p>Postman을 사용해서 EC2 퍼블릭 DNS로 요청을 보내서 제대로 처리되는지 확인했다.</p>

<p><img src="/images/posts/gooapp/postman.png" alt="" /></p>

<p>배포 성공 👏👏</p>

<p><img src="https://media1.tenor.com/m/dbezf0hp_HoAAAAC/kim-jong-un-clapping.gif" style="max-width: 400px" /></p>]]></content><author><name>itismilob</name></author><category term="GooApp" /><category term="DevOps" /><category term="Express" /><category term="AWS" /><category term="Nginx" /><category term="PM2" /><summary type="html"><![CDATA[GooApp 백앤드 배포 과정을 정리해보았다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/gooapp_logo.png" /><media:content medium="image" url="https://itismilob.github.io/images/logos/gooapp_logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GooApp API 에러처리</title><link href="https://itismilob.github.io/GooApp_API_%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC/" rel="alternate" type="text/html" title="GooApp API 에러처리" /><published>2025-07-07T00:00:00+00:00</published><updated>2025-07-07T00:00:00+00:00</updated><id>https://itismilob.github.io/GooApp_API_%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC</id><content type="html" xml:base="https://itismilob.github.io/GooApp_API_%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC/"><![CDATA[<h2 id="api-요청-에러">API 요청 에러</h2>

<p>API 요청과 응답 과정에서 원하는 데이터가 전달되지 않거나 예상치 못한 에러가 발생할 경우 앱이 강제종료되지 않도록 핸들링을 해줘야 한다.<br />
에러의 유형은 각 API의 목적에 따라 달라지는데 <code class="language-plaintext highlighter-rouge">유저 생성 API</code>에서 생성이나 저장에 실패하거나 <code class="language-plaintext highlighter-rouge">점수 갱신 API</code>에서 갱신할 유저 ID를 받지 못해 갱신에 실패하는 등 다양한 상황이 발생한다.<br />
대표적으로 발생할 수 있는 에러들을 정리해서 문서화했고, 요청을 재시도 하거나 예상 못한 사소한 에러들은 에러 메시지만 띄우는 방식으로 처리해줬다.</p>

<h2 id="에러-및-에러-코드-정의">에러 및 에러 코드 정의</h2>

<p>각 API 기능마다 발생할 경우 기능에 문제가 생기는 에러들을 정의했다.<br />
API 명세서에도 해당 기능에서 어떤 에러가 발생하는지를 명시해 바로 확인할 수 있도록 했다.<br />
명세서에 없는 예외적인 에러는 시스템에 치명적인 영향을 주지 않도록 기본적인 로깅 및 알림을 통해 대응하며, 필요 시 명세에 반영한다.</p>

<table>
  <thead>
    <tr>
      <th>HTTP 코드</th>
      <th>에러 코드</th>
      <th>에러 메시지</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>400</td>
      <td>INVALID_VALUE</td>
      <td>요청의 형식이 잘못되었습니다.</td>
    </tr>
    <tr>
      <td>404</td>
      <td>INVALID_API</td>
      <td>잘못된 API 요청입니다.</td>
    </tr>
    <tr>
      <td>404</td>
      <td>USER_NOT_FOUND</td>
      <td>유저 검색에 실패했습니다.</td>
    </tr>
    <tr>
      <td>500</td>
      <td>INTERNAL_SERVER_ERROR</td>
      <td>서버 내부 오류가 발생했습니다.</td>
    </tr>
    <tr>
      <td>500</td>
      <td>USER_CREATE_FAIL</td>
      <td>유저 생성에 실패했습니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="커스텀-에러-정의">커스텀 에러 정의</h3>

<p>기본 에러에 <strong>HTTP 상태코드</strong>와 <strong>에러코드</strong>를 넣어 사용하기 위해서 커스텀 에러를 선언했다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">CustomError</span> <span class="kd">extends</span> <span class="nb">Error</span> <span class="p">{</span>
  <span class="nl">statusCode</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">errorCode</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>

  <span class="kd">constructor</span><span class="p">(</span><span class="nx">statusCode</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">errorCode</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">statusCode</span> <span class="o">=</span> <span class="nx">statusCode</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">errorCode</span> <span class="o">=</span> <span class="nx">errorCode</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>선언한 커스텀 에러를 바탕으로 에러 상황마다 반복적으로 사용하는 에러를 추가로 만들어 사용했다.</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">UserCreateFail</span> <span class="kd">extends</span> <span class="nx">CustomError</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="dl">'</span><span class="s1">USER_CREATE_FAIL</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">유저 생성에 실패했습니다.</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">UserNotFound</span> <span class="kd">extends</span> <span class="nx">CustomError</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">(</span><span class="mi">404</span><span class="p">,</span> <span class="dl">'</span><span class="s1">USER_NOT_FOUND</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">유저 검색에 실패했습니다.</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="백엔드expressjs-에러-처리">백엔드(Express.js) 에러 처리</h2>

<p>서버에서 요청을 처리하며 에러가 발생하면 에러 코드 등의 정보와 함게 응답을 보낸다.</p>

<p>Express.js에서는 미들웨어를 사용하여 요청 처리 과정에 함수를 끼워 넣어 데이터를 변환하는 등의 기능이 있다.<br />
미들웨어에 에러 핸들러와 유효성 검사를 넣어서 요청 데이터를 가로체 확인하도록 만들었다.</p>

<p>에러 처리를 하기 위한 작업 과정은 다음과 같다.</p>

<ol>
  <li>에러 핸들러 생성</li>
  <li>요청 유효성 검사 : express-validator</li>
  <li>정상적인 요청과 에러 상황을 테스트</li>
</ol>

<h3 id="에러-핸들러-생성">에러 핸들러 생성</h3>

<p>공통적인 에러 처리와 에러 로깅을 위해 <code class="language-plaintext highlighter-rouge">errorHandler</code> 함수를 만들어 미들웨어로 사용했다.<br />
<code class="language-plaintext highlighter-rouge">errorHandler</code>는 요청 처리중에 에러가 발생하면 자동으로 실행된다.</p>

<p>정의되지 않은 에러라면 기본 <code class="language-plaintext highlighter-rouge">500 - Internal Server Error</code> 를 응답하고, 정의된 <code class="language-plaintext highlighter-rouge">CustomError</code>라면 그 정보를 응답한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 에러 처리 미들웨어</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">errorHandler</span><span class="p">(</span><span class="nx">err</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">,</span> <span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">,</span> <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">,</span> <span class="nx">_next</span><span class="p">:</span> <span class="nx">NextFunction</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 기본값</span>
  <span class="kd">let</span> <span class="nx">errorStatus</span> <span class="o">=</span> <span class="mi">500</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">errorCode</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">INTERNAL_SERVER_ERROR</span><span class="dl">'</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">message</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">서버 내부 오류가 발생했습니다.</span><span class="dl">'</span><span class="p">;</span>

  <span class="c1">// 커스텀 에러 처리</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">err</span> <span class="k">instanceof</span> <span class="nx">CustomError</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">errorStatus</span> <span class="o">=</span> <span class="nx">err</span><span class="p">.</span><span class="nx">statusCode</span><span class="p">;</span>
    <span class="nx">errorCode</span> <span class="o">=</span> <span class="nx">err</span><span class="p">.</span><span class="nx">errorCode</span><span class="p">;</span>
    <span class="nx">message</span> <span class="o">=</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// 에러 로깅 - 에러 내용과 요청 정보를 띄운다.</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">`[</span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">method</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">url</span><span class="p">}</span><span class="s2"> - </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">]`</span><span class="p">);</span>

  <span class="c1">// 에러 정보 응답</span>
  <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="nx">errorStatus</span><span class="p">).</span><span class="nx">json</span><span class="p">({</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">errorCode</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<p>app의 마지막 미들웨어로 <code class="language-plaintext highlighter-rouge">errorHandler</code>를 넣어서 모든 API에서 발생하는 에러를 공통으로 처리하게 만든다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app.js</span>
<span class="k">import</span> <span class="nx">express</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
  
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users</span><span class="dl">'</span><span class="p">,</span> <span class="nx">userRouter</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">errorHandler</span><span class="p">);</span>

<span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
	<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`http://localhost:</span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="express-validator">express-validator</h3>

<p>express-validator 라이브러리를 통한 유효성 검사로 요청속에 잘못된 데이터가 있는지 확인한다.<br />
API 라우터에 <code class="language-plaintext highlighter-rouge">validator</code>를 넣고 <code class="language-plaintext highlighter-rouge">validateRequest</code>미들웨어를 통해서 에러가 있으면 커스텀 에러를 생성한다.</p>

<p>express-validator의 <code class="language-plaintext highlighter-rouge">param</code> 함수로 요청의 path-parameter속 데이터가 MongoDB 아이디 값이 맞는지 유효성 검사를 진행한다.<br />
검사 후 <code class="language-plaintext highlighter-rouge">validateRequest</code> 함수에서 에러가 발생했는지 확인하고 에러 핸들러로 넘긴다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span>
	<span class="dl">'</span><span class="s1">/:userID</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// 요청 Path</span>
	<span class="nx">param</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">).</span><span class="nx">isMongoId</span><span class="p">(),</span> <span class="c1">// 유효성 검사 - userID 데이터가 MongoId 인지 확인</span>
	<span class="nx">validateRequest</span><span class="p">,</span> <span class="c1">// 유효성 검사에서 에러가 있는지 확인해 에러 핸들러로 넘긴다.</span>
	<span class="nx">getUser</span> <span class="c1">// 정상적인 요청일 때 넘어가는 Controller 미들웨어</span>
<span class="p">);</span>
</code></pre></div></div>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">validateRequest</span><span class="p">(</span>
  <span class="nx">req</span><span class="p">:</span> <span class="nx">Request</span><span class="p">,</span>
  <span class="nx">res</span><span class="p">:</span> <span class="nx">Response</span><span class="p">,</span>
  <span class="nx">next</span><span class="p">:</span> <span class="nx">NextFunction</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 유효성 검사의 결과를 가져온다.</span>
  <span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="nx">validationResult</span><span class="p">(</span><span class="nx">req</span><span class="p">);</span>

  <span class="c1">// 에러가 있는지 확인하고 커스텀 에러를 발생시킨다.</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">errors</span><span class="p">.</span><span class="nx">isEmpty</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nx">CustomError</span><span class="p">(</span>
      <span class="mi">400</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">INVALID_VALUE</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">요청의 형식이 잘못되었습니다.</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// 에러가 없다면 다음 미들웨어 함수를 실행하도록 넘긴다.</span>
  <span class="nx">next</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="프론트엔드react-native-에러처리">프론트엔드(React-native) 에러처리</h2>

<p>React Native 에서는 Axios 라이브러리를 통해 API 요청을 보내고 요청 과정에서의 에러를 처리할 수 있게 만들었다.<br />
Axios의 Interceptor를 통해서 요청/응답을 가로체 데이터 변환, 에러 처리 등을 할 수 있다.</p>

<ol>
  <li>Axios Interceptor를 사용한 커스텀 에러 생성과 에러 Alert</li>
  <li>각 상황에 맞는 개별 에러 처리</li>
</ol>

<h3 id="axios-에러-처리">Axios 에러 처리</h3>

<p>Axios의 Interceptor를 사용해 응답 데이터를 확인하고 에러가 발생한 경우 에러처리를 수행한다.<br />
발생한 에러를 백엔드와 동일한 CustomError로 변환하여 같은 방식으로 처리한다.<br />
응답 에러가 발생하면 사용자에게 알려주기 위해서 react native의 <code class="language-plaintext highlighter-rouge">Alert</code>를 사용해서 알려준다.<br />
마지막으로 <code class="language-plaintext highlighter-rouge">Promise.reject</code>를 반환하여 axios 요청을 실행했던 코드에서 개별 처리를 하도록 넘겨준다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 응답 에러를 처리하는 헨들러 정의</span>
<span class="kd">function</span> <span class="nx">resErrorHandler</span><span class="p">(</span><span class="nx">error</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 기본 에러 데이터</span>
  <span class="kd">let</span> <span class="nx">status</span> <span class="o">=</span> <span class="mi">500</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">code</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">UNKNOWN</span><span class="dl">'</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">message</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">알 수 없는 에러입니다.</span><span class="dl">'</span><span class="p">;</span>

  <span class="c1">// axios 에러일 때</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">axios</span><span class="p">.</span><span class="nx">isAxiosError</span><span class="p">(</span><span class="nx">error</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// 에러 상황에 맞는 데이터 입력</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">error</span><span class="p">.</span><span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">status</span> <span class="o">=</span> <span class="mi">503</span><span class="p">;</span>
      <span class="nx">code</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">NETWORK_ERROR</span><span class="dl">'</span><span class="p">;</span>
      <span class="nx">message</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">네트워크 오류가 발생했습니다.</span><span class="dl">'</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">code</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">ECONNABORTED</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">status</span> <span class="o">=</span> <span class="mi">408</span><span class="p">;</span>
      <span class="nx">code</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">TIMEOUT</span><span class="dl">'</span><span class="p">;</span>
      <span class="nx">message</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">요청 시간이 초과되었습니다.</span><span class="dl">'</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">status</span> <span class="o">=</span> <span class="nx">error</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">;</span>
      <span class="nx">code</span> <span class="o">=</span> <span class="nx">error</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">errorCode</span><span class="p">;</span>
      <span class="nx">message</span> <span class="o">=</span> <span class="nx">error</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c1">// 커스텀 에러로 만들어 모든 에러를 같은 방식으로 처리할 수 있도록 제작</span>
  <span class="kd">const</span> <span class="nx">customError</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CustomError</span><span class="p">(</span><span class="nx">status</span><span class="p">,</span> <span class="nx">code</span><span class="p">,</span> <span class="nx">message</span><span class="p">);</span>

  <span class="c1">// react native - Alert 알림</span>
  <span class="nx">showErrorAlert</span><span class="p">(</span><span class="nx">customError</span><span class="p">);</span>
  <span class="c1">// Promise 에러를 반환하여 axios 요청을 실행한 코드에서 처리하도록 넘김</span>
  <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">reject</span><span class="p">(</span><span class="nx">customError</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">axios</span><span class="p">.</span><span class="nx">interceptors</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">use</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">resErrorHandler</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="개별-에러-처리">개별 에러 처리</h3>

<p>API 요청을 실행하던 함수에서 요청 에러가 발생하면 실행 과정을 되돌리거나 페이지를 이동시키는 등의 개별 에러 처리 기능들을 선언해서 처리할 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">{</span>
	<span class="kd">const</span> <span class="nx">newUser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="sr">/.../</span><span class="p">);</span>
	<span class="k">if</span><span class="p">(</span><span class="nx">newUser</span><span class="p">){</span>
		<span class="nx">saveUserData</span><span class="p">(</span><span class="nx">newUser</span><span class="p">);</span>
	<span class="p">}</span><span class="k">else</span><span class="p">{</span>
		<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">유저 생성에 실패했습니다.</span><span class="dl">'</span><span class="p">)</span>
	<span class="p">}</span>
<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 class="c1">// 요청 에러가 발생했거나 유저가 생성되지 않으면 화면을 이동시킨다.</span>
	<span class="nx">navigation</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">UserNotCreated</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="마무리">마무리</h2>

<p>에러 처리 부분에서는 정형화된 작업 방식을 찾기 어려웠기 때문에 가장 고민도 많았고 시간도 많이 들어간 작업이었다. 이번 작업을 통해서 에러가 발생하는 다양한 상황을 생각하고 에러 처리를 하는 흐름을 배웠다.</p>

<p>백엔드에서는 요청을 검증하고(validation) 처리하며, 에러 발생 시 에러 핸들러를 통해 에러 정보를 담은 응답을 반환하고 로그를 남긴다.<br />
프론트엔드에서는 요청과 응답 전반에 대해 에러를 감지하고 처리하며, 사용자에게 알림을 통해 문제를 전달하고, 개발자가 이를 확인할 수 있도록 돕는다.</p>

<p>express-validation과 axios interceptor 같은 기능들은 단순하지만 사용 방법이 자유로워서 더 어렵게 느껴졌다. API를 사용하는 백앤드, 프론트의 에러를 전체적으로 통일하기 위해서 커스텀 에러를 사용해보니 가독성이 향상되고 디버깅을 하며 알아보기 편해졌다.<br />
다음번에는 Jest 같은 테스트 라이브러리를 통해 발생하는 에러 상황들을 전부 테스트해보는 것도 좋을 것 같다.</p>]]></content><author><name>itismilob</name></author><category term="GooApp" /><category term="Errors" /><summary type="html"><![CDATA[API 요청 과정에서 발생하는 에러를 처리해보자]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/gooapp_logo.png" /><media:content medium="image" url="https://itismilob.github.io/images/logos/gooapp_logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Mongoose with Typescript</title><link href="https://itismilob.github.io/Mongoose_typescript/" rel="alternate" type="text/html" title="Mongoose with Typescript" /><published>2025-06-22T00:00:00+00:00</published><updated>2025-06-22T00:00:00+00:00</updated><id>https://itismilob.github.io/Mongoose_typescript</id><content type="html" xml:base="https://itismilob.github.io/Mongoose_typescript/"><![CDATA[<h2 id="mongoose">Mongoose</h2>

<blockquote>
  <p>MongoDB를 Node.js에서 사용할 수 있도록 하는 모듈이다.</p>
</blockquote>

<p>스키마와 모델로 데이터를 불러와 객체화 시킨 후 빠르게 수정, 데이터 접근을 가능하게 만들어준다.</p>

<h2 id="mongodb와-연결하기">MongoDB와 연결하기</h2>

<blockquote>
  <p>서버와 MongoDB(혹은 MongoDB Atlas)를 연결해야 사용 가능하다.</p>
</blockquote>

<p>서버 실행 전에 한 번 연결하고 계속 사용한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">mongoose</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">mongoose</span><span class="dl">'</span><span class="p">;</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">connectDB</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="k">await</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span><span class="dl">'</span><span class="s1">MongoDB_URL</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">MongoDB 연결 성공</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">MongoDB 연결 실패:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="스키마">스키마</h2>

<blockquote>
  <p>데이터의 구조(필드)를 정의하는 객체</p>
</blockquote>

<p>필드와 타입, 기본값, 제한 조건 등을 설정한다.<br />
MongoDB에는 스키마가 없지만, Mongoose에서는 명확한 구조를 위해 사용한다.</p>

<h3 id="스키마-필드의-데이터-타입">스키마 필드의 데이터 타입</h3>

<blockquote>
  <p>필드의 데이터로 사용할 수 있는 타입 목록.<br />
Javascript의 타입을 확장한 Mongoose의 자체적인 타입이다.</p>
</blockquote>

<ul>
  <li>String</li>
  <li>Number</li>
  <li>Boolean</li>
  <li>Date</li>
  <li>Array</li>
  <li>Buffer : 이진 데이터 (이미지, 파일 등)</li>
  <li>ObjectId : 다른 문서의 ID (ref)</li>
  <li>Map : Key-Value 쌍 객체</li>
  <li>Mixed : 타입 검사를 하지 않음, any 타입</li>
  <li>Decimal128 : 고정 소수점 숫자</li>
  <li>UUID : 고유한 값을 만들기 위한 128비트 숫자</li>
</ul>

<h3 id="스키마-제네릭-타입">스키마 제네릭 타입</h3>

<blockquote>
  <p>스키마를 생성할 때 명시적으로 타입을 선언하며 사용되는 제네릭 타입들이다.<br />
<a href="https://mongoosejs.com/docs/typescript/schemas.html#generic-parameters">공식 문서 →</a></p>
</blockquote>

<ul>
  <li><code class="language-plaintext highlighter-rouge">RawDocType</code> : DB에 저장되는 <strong>원시 데이터 타입</strong> (<code class="language-plaintext highlighter-rouge">userSchemaType</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">TModelType</code> : <strong>모델 타입</strong>, 보통 Model 타입 상속<br />
<strong>query helpers, static methods</strong> 함께 지정 ([[Mongoose#Query Helpers]], [[Mongoose#Statics]])<br />
<code class="language-plaintext highlighter-rouge">Model&lt;DocType, QueryHelpers, InstanceMethods&gt;</code></li>
  <li><code class="language-plaintext highlighter-rouge">TInstanceMethods</code> : 도큐먼트 인스턴스에 붙이는 <strong>커스텀 메서드</strong> 타입 ([[Mongoose#Methods]])</li>
  <li><code class="language-plaintext highlighter-rouge">TQueryHelpers</code> : <strong>query helper</strong> 정의용 인터페이스 ([[Mongoose#Query Helpers]])</li>
  <li><code class="language-plaintext highlighter-rouge">TVirtuals</code> : <strong>virtual</strong>로 정의한 필드들의 타입 ([[Mongoose#Virtuals]])</li>
  <li><code class="language-plaintext highlighter-rouge">TStaticMethods</code> : 모델에 붙는 <strong>static</strong> 메서드의 정의 타입 ([[Mongoose#Statics]])</li>
  <li><code class="language-plaintext highlighter-rouge">TSchemaOptions</code> : 스키마 옵션 타입</li>
  <li><code class="language-plaintext highlighter-rouge">DocType</code> : 스키마에서 추론되는 문서 타입 (<code class="language-plaintext highlighter-rouge">RowDocType</code>, <code class="language-plaintext highlighter-rouge">InstanceMethods</code>, <code class="language-plaintext highlighter-rouge">Virtuals</code>로 자동 생성됨)</li>
  <li><code class="language-plaintext highlighter-rouge">THydratedDocumentType</code> : hydrated doc 타입, <code class="language-plaintext highlighter-rouge">Model.findOne()</code>, <code class="language-plaintext highlighter-rouge">Model.hydrate()</code> 등의 기본 반환 타입</li>
</ul>

<h3 id="inferschematype">InferSchemaType</h3>

<blockquote>
  <p>스키마에 정의된 필드를 기반으로 실제 DB에 저장될 데이터 타입을 추론해준다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="p">({</span>
  <span class="na">userID</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
  <span class="na">nickname</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
  <span class="na">topScore</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
<span class="p">});</span>

<span class="kd">type</span> <span class="nx">User</span> <span class="o">=</span> <span class="nx">InferSchemaType</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">userSchema</span><span class="o">&gt;</span><span class="p">;</span>
<span class="cm">/* User 타입은 다음과 같이 정의된다.
User = {
	userID?: String,
	nickname?: String,
	topScore?: Number
}
*/</span>
</code></pre></div></div>

<h2 id="모델">모델</h2>

<blockquote>
  <p>실제 데이터베이스와 연결되는 객체</p>
</blockquote>

<p>스키마를 바탕으로 MongoDB의 컬렉션과 연결한다.<br />
모델이 데이터 쿼리(CRUD) 기능을 제공한다.</p>

<h2 id="스키마-모델-만들기">스키마, 모델 만들기</h2>

<p>스키마 정의</p>

<ul>
  <li>첫 인자 : 스키마 필드 정의</li>
  <li>두번째 인자 : 스키마 옵션 설정<br />
timestamp, _id 자동 생성 등 - <a href="https://mongoosejs.com/docs/guide.html#options">옵션 더보기 →</a></li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Schema</span><span class="p">,</span> <span class="nx">Model</span><span class="p">,</span> <span class="nx">model</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">mongoose</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 필드의 타입 (따로 정의하지 않아도 자동 추론된다.)</span>
<span class="kr">interface</span> <span class="nx">User</span> <span class="p">{</span>
  <span class="nl">userID</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">nickname</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">topScore</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// User 모텔 타입 - Model을 상속받는다.</span>
<span class="c1">// Static 함수가 없다면 불필요함</span>
<span class="kr">interface</span> <span class="nx">UserModel</span> <span class="kd">extends</span> <span class="nx">Model</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="c1">// Static 함수들이 정의된다.</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">UserSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">,</span> <span class="nx">UserModel</span><span class="o">&gt;</span><span class="p">({</span>
  <span class="na">userID</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">required</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">unique</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">type</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">nickname</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">required</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">unique</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">type</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">topScore</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">type</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
    <span class="na">default</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
  <span class="p">},</span>
<span class="p">});</span>

<span class="c1">// 스키마를 바탕으로 모델 생성</span>
<span class="kd">const</span> <span class="nx">userModel</span> <span class="o">=</span> <span class="nx">model</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">,</span> <span class="nx">UserModel</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">'</span><span class="s1">userModel</span><span class="dl">'</span><span class="p">,</span> <span class="nx">UserSchema</span><span class="p">);</span>
<span class="k">export</span> <span class="p">{</span> <span class="nx">userModel</span> <span class="p">};</span>
</code></pre></div></div>

<hr />

<p>모델의 인스턴스(새로운 문서)를 만들어 값을 담아 저장할 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">newUser</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">userModel</span><span class="p">();</span>
<span class="nx">newUser</span><span class="p">.</span><span class="nx">nickname</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">testUser</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">newUser</span><span class="p">.</span><span class="nx">userID</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">;</span>

<span class="k">await</span> <span class="nx">newUser</span><span class="p">.</span><span class="nx">save</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="crud">CRUD</h2>

<blockquote>
  <p>Mongoose를 통해 MongoDB 문서에 대한 CRUD 작업을 할 수 있다.</p>
</blockquote>

<h3 id="create">Create</h3>

<blockquote>
  <p>문서 생성</p>
</blockquote>

<p>인스턴스를 생성해 데이터 create</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">newUser</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">userModel</span><span class="p">({</span>
  <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">nickname</span><span class="p">:</span> <span class="dl">'</span><span class="s1">testUser</span><span class="dl">'</span><span class="p">,</span>
<span class="p">});</span>
<span class="k">await</span> <span class="nx">newUser</span><span class="p">.</span><span class="nx">save</span><span class="p">();</span>
</code></pre></div></div>

<p>인스턴스 없이 데이터 create</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
  <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">nickname</span><span class="p">:</span> <span class="dl">'</span><span class="s1">testUser</span><span class="dl">'</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="read">Read</h3>

<blockquote>
  <p>문서 데이터 불러오기</p>
</blockquote>

<p>전체 데이터 불러오기<br />
(MongoDB의 비교 연산자를 사용했다. <a href="https://www.mongodb.com/ko-kr/docs/manual/reference/operator/query-comparison/">공식 문서 →</a>)</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">();</span>

<span class="c1">// 조건으로 검색</span>
<span class="kd">const</span> <span class="nx">scores</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">scoreModel</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span>
  <span class="na">correct</span><span class="p">:</span> <span class="p">{</span> <span class="na">$gte</span><span class="p">:</span> <span class="mi">5</span> <span class="p">},</span>
  <span class="na">wrong</span><span class="p">:</span> <span class="p">{</span> <span class="na">$lte</span><span class="p">:</span> <span class="mi">2</span> <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<p>하나의 데이터 불러오기</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<p>아이디로 데이터 불러오기</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findById</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="update">Update</h3>

<blockquote>
  <p>문서 데이터 업데이트 (두 번째 인자로 변경할 값을 입력)</p>
</blockquote>

<p>하나의 데이터만 업데이트</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">updateOne</span><span class="p">({</span> <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span> <span class="p">},</span> <span class="p">{</span> <span class="na">nickname</span><span class="p">:</span> <span class="dl">'</span><span class="s1">changed</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<p>아이디로 찾아서 업데이트</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findByIdAndUpdate</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">nickname</span><span class="p">:</span> <span class="dl">'</span><span class="s1">changed</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<h3 id="delete">Delete</h3>

<blockquote>
  <p>문서 삭제</p>
</blockquote>

<p>문서 하나 검색해 삭제</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">deleteOne</span><span class="p">({</span> <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<p>문서를 아이디로 검색해 삭제</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findByIdAndDelete</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="쿼리-메소드">쿼리 메소드</h2>

<blockquote>
  <p>CRUD 이외에 데이터를 검색하거나 수정하기 위해 사용되는 메소드</p>
</blockquote>

<h3 id="sort">Sort</h3>

<blockquote>
  <p>데이터 정렬</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">first</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findOne</span><span class="p">().</span><span class="nx">sort</span><span class="p">({</span> <span class="na">_id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">});</span> <span class="c1">// 생성 순서 오름차순</span>
<span class="kd">const</span> <span class="nx">last</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findOne</span><span class="p">().</span><span class="nx">sort</span><span class="p">({</span> <span class="na">_id</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span> <span class="p">});</span> <span class="c1">// 생성 순서 내림차순</span>
</code></pre></div></div>

<h3 id="where">Where</h3>

<blockquote>
  <p>쿼리에 조건을 체이닝 형식으로 추가할 수 있다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">scores</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">scoreModel</span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="dl">'</span><span class="s1">correct</span><span class="dl">'</span><span class="p">).</span><span class="nx">gt</span><span class="p">(</span><span class="mi">3</span><span class="p">).</span><span class="nx">lt</span><span class="p">(</span><span class="mi">30</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="limit">Limit</h3>

<blockquote>
  <p>쿼리 결과 개수 제한</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">limit</span><span class="p">(</span><span class="mi">5</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="skip">Skip</h3>

<blockquote>
  <p>앞에서 몇 개를 건너뛰고 검색 (페이징에서 주로 사용)</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">skip</span><span class="p">(</span><span class="mi">10</span><span class="p">).</span><span class="nx">limit</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="select">Select</h3>

<blockquote>
  <p>스키마의 필드를 선택해 특정 값들만 검색한다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userNicknames</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">select</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID nickname</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// userID, nickname만 가져옴</span>
<span class="kd">const</span> <span class="nx">userWithoutNicknames</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">select</span><span class="p">(</span><span class="dl">'</span><span class="s1">-nickname</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// nickname 제외</span>
</code></pre></div></div>

<h3 id="countdocuments">CountDocuments</h3>

<blockquote>
  <p>문서 개수 세기</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">count</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">scoreModel</span><span class="p">.</span><span class="nx">countDocuments</span><span class="p">({</span>
  <span class="na">correct</span><span class="p">:</span> <span class="p">{</span> <span class="na">$gte</span><span class="p">:</span> <span class="mi">5</span> <span class="p">},</span>
  <span class="na">wrong</span><span class="p">:</span> <span class="p">{</span> <span class="na">$lte</span><span class="p">:</span> <span class="mi">2</span> <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="distinct">Distinct</h3>

<blockquote>
  <p>특정 필드에서 중복 제거된 값만 불러온다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">nicknames</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">distinct</span><span class="p">(</span><span class="dl">'</span><span class="s1">nickname</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="lean">Lean</h3>

<blockquote>
  <p>결과 문서를 Mongoose Doc이 아닌 Plain Object로 변환해 검색 속도를 빠르게 한다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">lean</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="transform">Transform</h3>

<blockquote>
  <p>문서의 출력 결과를 가공한다.</p>
</blockquote>

<p>lean 메소드를 앞에 먼저 사용해야 타입스크립트에서 타입을 추론할 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span>
  <span class="p">.</span><span class="nx">find</span><span class="p">()</span>
  <span class="p">.</span><span class="nx">lean</span><span class="p">()</span>
  <span class="p">.</span><span class="nx">transform</span><span class="p">((</span><span class="nx">docs</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">docs</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">doc</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="na">id</span><span class="p">:</span> <span class="nx">doc</span><span class="p">.</span><span class="nx">_id</span><span class="p">,</span>
      <span class="na">name</span><span class="p">:</span> <span class="nx">doc</span><span class="p">.</span><span class="nx">nickname</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">(),</span>
    <span class="p">}));</span>
  <span class="p">});</span>
</code></pre></div></div>

<h3 id="populate">Populate</h3>

<blockquote>
  <p>한 문서에서 참조한 ObjectId로 다른 문서의 데이터를 찾아 불러온다.</p>
</blockquote>

<p>스키마에 참조할 필드 정의 (참조할 모델을 정의할 때 사용한 명칭으로 연결한다.)</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">scoreSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="p">({</span>
  <span class="na">user</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">Schema</span><span class="p">.</span><span class="nx">Types</span><span class="p">.</span><span class="nx">ObjectId</span><span class="p">,</span> <span class="na">ref</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userModel</span><span class="dl">'</span> <span class="p">},</span>
  <span class="na">correct</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
  <span class="na">wrong</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
  <span class="na">accuracy</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
<span class="p">});</span>

<span class="kd">const</span> <span class="nx">scoreModel</span> <span class="o">=</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">model</span><span class="p">(</span><span class="dl">'</span><span class="s1">scoreModel</span><span class="dl">'</span><span class="p">,</span> <span class="nx">scoreSchema</span><span class="p">);</span>
</code></pre></div></div>

<p>populate로 user정보도 같이 불러온다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">score</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">scoreModel</span><span class="p">.</span><span class="nx">findById</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">).</span><span class="nx">populate</span><span class="p">(</span><span class="dl">'</span><span class="s1">userModel</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">score</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">nickname</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="에러-처리-방식">에러 처리 방식</h2>

<h3 id="1-asyncawait--trycatch">1. async/await + try/catch</h3>

<p>가독성이 좋아 주로 사용한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">();</span>
  <span class="c1">// 정상 작동 처리</span>
<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 class="c1">// 에러 핸들링</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="2-thencatch-프로미스-체이닝">2. .then().catch() 프로미스 체이닝</h3>

<p>채이닝이 길어질 수 있어 가독성이 떨어진다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userModel</span>
  <span class="p">.</span><span class="nx">find</span><span class="p">()</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">users</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// 정상 작동 처리</span>
  <span class="p">})</span>
  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// 에러 핸들링</span>
  <span class="p">});</span>
</code></pre></div></div>

<h3 id="3-콜백-방식">3. 콜백 방식</h3>

<p>요청 인자의 가장 마지막에 에러 처리 콜백을 넣는다.<br />
레거시 스타일로 거의 사용되지 않는다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userModel</span><span class="p">.</span><span class="nx">findById</span><span class="p">(</span><span class="nx">userID</span><span class="p">,</span> <span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 에러 핸들링</span>
  <span class="p">}</span>
  <span class="c1">// 정상 작동 처리</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="api">API</h2>

<h3 id="methods">Methods</h3>

<blockquote>
  <p><strong>문서(검색 결과)</strong> 인스턴스에서 호출하는 <strong>메소드</strong>를 정의할 수 있다.</p>
</blockquote>

<ul>
  <li>메소드 타입을 정의해서 스키마에 추가한다.</li>
  <li>스키마에 메소드를 정의한다.</li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 메소드 타입을 따로 정의</span>
<span class="kr">interface</span> <span class="nx">userMethods</span> <span class="p">{</span>
	<span class="nx">sayHi</span><span class="p">():</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// 스키마에 메소드 타입 추가</span>
<span class="kd">const</span> <span class="nx">userSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">,</span> <span class="nx">userModel</span><span class="p">,</span> <span class="nx">userMethods</span><span class="o">&gt;</span><span class="p">({...});</span>

<span class="c1">// 스키마에 메소드 정의</span>
<span class="nx">userSchema</span><span class="p">.</span><span class="nx">methods</span><span class="p">.</span><span class="nx">sayHi</span> <span class="o">=</span> <span class="p">()</span><span class="o">=&gt;</span><span class="p">{</span>
	<span class="k">return</span> <span class="s2">`Hello I'm </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">nickname</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">userModel</span> <span class="o">=</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">model</span><span class="p">(</span><span class="dl">'</span><span class="s1">userModel</span><span class="dl">'</span><span class="p">,</span> <span class="nx">userSchema</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findById</span><span class="p">(</span><span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// 메소드 사용</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">sayHi</span><span class="p">());</span>
</code></pre></div></div>

<h3 id="statics">Statics</h3>

<blockquote>
  <p><strong>모델</strong>에서 호출하는 <strong>메소드</strong>를 정의할 수 있다.</p>
</blockquote>

<ul>
  <li>모델의 타입에 메소드를 정의한다.</li>
  <li>스키마에 스테틱 메소드를 추가한다.</li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 모델 타입에 스테틱 메소드 정의</span>
<span class="kr">interface</span> <span class="nx">UserModel</span> <span class="kd">extends</span> <span class="nx">Model</span><span class="o">&lt;</span><span class="nx">UserSchemaType</span><span class="o">&gt;</span> <span class="p">{</span>
	<span class="nx">findByNickname</span><span class="p">():</span> <span class="nx">UserSchemaType</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">userSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">,</span> <span class="nx">userModel</span><span class="o">&gt;</span><span class="p">({...});</span>

<span class="c1">// 스키마에 스테틱 메소드 정의</span>
<span class="nx">userSchema</span><span class="p">.</span><span class="nx">statics</span><span class="p">.</span><span class="nx">findByNickname</span> <span class="o">=</span> <span class="p">(</span><span class="nx">nickname</span><span class="p">)</span><span class="o">=&gt;</span><span class="p">{</span>
	<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span><span class="nx">nickname</span><span class="p">});</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">userModel</span> <span class="o">=</span> <span class="nx">mongoose</span><span class="p">.</span><span class="nx">model</span><span class="p">(</span><span class="dl">'</span><span class="s1">userModel</span><span class="dl">'</span><span class="p">,</span> <span class="nx">userSchema</span><span class="p">);</span>
<span class="c1">// 스테틱 메소드 사용</span>
<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findByNickname</span><span class="p">(</span><span class="dl">'</span><span class="s1">testUser</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="query-helpers">Query Helpers</h3>

<blockquote>
  <p>모델에 커스텀 쿼리 함수를 추가해 사용한다.<br />
복잡한 쿼리 로직을 캡슐화해 만들어 사용할 수 있다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="p">(</span>
  <span class="p">{</span>
    <span class="na">userID</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="na">nickname</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="na">topScore</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="c1">// query helpers 정의</span>
    <span class="na">query</span><span class="p">:</span> <span class="p">{</span>
      <span class="nx">byNickname</span><span class="p">(</span><span class="na">nickname</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">find</span><span class="p">({</span> <span class="nx">nickname</span> <span class="p">});</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">}</span>
<span class="p">);</span>
<span class="c1">//... 모델 정의</span>

<span class="kd">const</span> <span class="nx">testUser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">find</span><span class="p">().</span><span class="nx">byNickname</span><span class="p">(</span><span class="dl">'</span><span class="s1">testUser</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="virtuals">Virtuals</h3>

<blockquote>
  <p>가상 필드, DB에 저장하지 않지만 문서에 포함되는 계산된 필드를 추가한다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="p">(</span>
  <span class="p">{</span>
    <span class="na">userID</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="na">nickname</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
    <span class="na">topScore</span><span class="p">:</span> <span class="nb">Number</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="c1">// Virtual 정의</span>
    <span class="na">virtuals</span><span class="p">:</span> <span class="p">{</span>
      <span class="c1">// 아이디와 닉네임을 연결한 가상 필드 추가</span>
      <span class="na">id_nickname</span><span class="p">:</span> <span class="p">{</span>
        <span class="kd">get</span><span class="p">()</span> <span class="p">{</span>
          <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">userID</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">_</span><span class="dl">'</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">nickname</span><span class="p">;</span>
        <span class="p">},</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">}</span>
<span class="p">);</span>
<span class="c1">//... 모델 정의</span>

<span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">findOne</span><span class="p">({</span> <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span> <span class="p">});</span>
<span class="c1">// 아이디와 닉네임이 연결된 virtual 필드가 출력된다.</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">id_nickname</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<p>스키마 <code class="language-plaintext highlighter-rouge">InferSchemaType</code>는 실제 데이터 구조만 나타내고 <code class="language-plaintext highlighter-rouge">virtual</code>는 포함되지 않는다.</p>

<p>필요하다면 hydrate를 이용해 타입을 추론해야 한다.<br />
<code class="language-plaintext highlighter-rouge">type userDocument = ReturnType&lt;(typeof userModel)['hydrate']&gt;;</code></p>

<h3 id="subdocuments">Subdocuments</h3>

<blockquote>
  <p>문서 안에 다른 문서를 넣는 구조 (populate와 다르게 문서를 직접 넣음)</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 스키마 필드 타입 정의</span>
<span class="kr">interface</span> <span class="nx">Child</span> <span class="p">{</span>
  <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>
<span class="kr">interface</span> <span class="nx">Parent</span> <span class="p">{</span>
  <span class="nl">children</span><span class="p">:</span> <span class="nx">Child</span><span class="p">[];</span>
<span class="p">}</span>

<span class="c1">// 모델에 사용할 타입 정의</span>
<span class="kd">type</span> <span class="nx">ParentModel</span> <span class="o">=</span> <span class="nx">Model</span><span class="o">&lt;</span><span class="nx">Parent</span><span class="o">&gt;</span><span class="p">;</span>

<span class="c1">// 스키마 생성</span>
<span class="kd">const</span> <span class="nx">childSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="o">&lt;</span><span class="nx">Child</span><span class="o">&gt;</span><span class="p">({</span>
	<span class="na">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">;</span>
<span class="p">})</span>
<span class="kd">const</span> <span class="nx">parentSchema</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Schema</span><span class="o">&lt;</span><span class="nx">Parent</span><span class="p">,</span> <span class="nx">ParentModel</span><span class="o">&gt;</span><span class="p">({</span>
  <span class="na">children</span><span class="p">:</span> <span class="p">[</span><span class="nx">childSchema</span><span class="p">]</span>
<span class="p">});</span>

<span class="c1">// 모델 생성</span>
<span class="kd">const</span> <span class="nx">parentModel</span> <span class="o">=</span> <span class="nx">model</span><span class="o">&lt;</span><span class="nx">Parent</span><span class="p">,</span> <span class="nx">ParentModel</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">'</span><span class="s1">Parent</span><span class="dl">'</span><span class="p">,</span> <span class="nx">parentSchema</span><span class="p">);</span>

<span class="c1">// parent 문서 생성, child 값 생성</span>
<span class="kd">const</span> <span class="nx">parent</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">parentModel</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">child</span> <span class="o">=</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">child1</span><span class="dl">'</span><span class="p">};</span>
<span class="c1">// child 추가</span>
<span class="nx">parent</span><span class="p">.</span><span class="nx">children</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">child</span><span class="p">);</span>
<span class="k">await</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">save</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="hydrate">hydrate</h3>

<blockquote>
  <p>Plain object를 mongoose document로 바꿔주는 함수다.</p>
</blockquote>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="na">userID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">userID</span><span class="dl">'</span><span class="p">,</span> <span class="na">nickname</span><span class="p">:</span> <span class="dl">'</span><span class="s1">testUser</span><span class="dl">'</span> <span class="p">};</span>
<span class="kd">const</span> <span class="nx">doc</span> <span class="o">=</span> <span class="nx">userModel</span><span class="p">.</span><span class="nx">hydrate</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
</code></pre></div></div>]]></content><author><name>itismilob</name></author><category term="MongoDB" /><category term="NodeJS" /><category term="Typescript" /><summary type="html"><![CDATA[Mongoose를 Typescript로 사용해보자]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/typescript_logo.webp" /><media:content medium="image" url="https://itismilob.github.io/images/logos/typescript_logo.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GooApp - 디자인 넣는 작업에 발생한 문제들</title><link href="https://itismilob.github.io/GooApp_NativeWind/" rel="alternate" type="text/html" title="GooApp - 디자인 넣는 작업에 발생한 문제들" /><published>2025-06-13T00:00:00+00:00</published><updated>2025-06-13T00:00:00+00:00</updated><id>https://itismilob.github.io/GooApp_NativeWind</id><content type="html" xml:base="https://itismilob.github.io/GooApp_NativeWind/"><![CDATA[<blockquote>
  <p>이 글은 디자인을 적용하면서 실제로 앱에서 UI를 구현할 때 마주한 3가지 문제를 정리했다.  <br />
React Native + TailWind(NativeWind) 환경에서 발생한 스타일링 이슈와 실용적인 해결 방법에 대해 공유한다.</p>
</blockquote>

<p>크게 3가지 문제가 있었다.</p>

<ol>
  <li>동적으로 클래스 변경이 안 되는 문제</li>
  <li>폰트 크기에 따른 레이아웃 깨짐</li>
  <li>스크롤 디자인 문제</li>
</ol>

<p>각각 문제 상황 → 시도했던 방법 → 최종 해결책 순서로 정리한다.</p>

<h2 id="1-tailwind의-동적-스타일링-적용">1. Tailwind의 동적 스타일링 적용</h2>

<blockquote>
  <p>컴포넌트마다 배경색 등 스타일이 바뀌는 상황에서 Tailwind에 동적으로 적용한 클래스가 적용되지 않았다.</p>
</blockquote>

<p>버튼의 색상 코드만 클래스에 <code class="language-plaintext highlighter-rouge">bg-${color}</code>로 넣었지만 Tailwind가 인식 하지 못해서 적용되지 않았다.<br />
해결 방법은 <code class="language-plaintext highlighter-rouge">tailwind.config.js</code>에서 필요한 색상을 정의하거나, 변수 조각이 아닌 완성된 변수로 관리하는 방식이었다.</p>

<h3 id="-안-되는-상황">❌ 안 되는 상황</h3>

<ul>
  <li>색상 코드만 동적으로 넣어줬다.</li>
  <li>Tailwind는 빌드 타임에 정적으로 탐지되는 클래스명만 인식해서 적용한다.</li>
</ul>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="nx">button</span><span class="p">(){</span>
	<span class="kd">const</span> <span class="nx">green</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#00FF00</span><span class="dl">'</span><span class="p">;</span>
	<span class="p">&lt;</span><span class="nc">View</span> <span class="na">className</span><span class="p">=</span><span class="si">{</span><span class="s2">`bg-[</span><span class="p">${</span><span class="nx">green</span><span class="p">}</span><span class="s2">] flex-1 items-center justify-center`</span><span class="si">}</span><span class="p">/&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h3 id="-해결한-코드">✅ 해결한 코드</h3>

<ul>
  <li>클래스 전체를 상수로 선언하고 넣어줬다.</li>
  <li>반복되는 스타일 전체를 선언해 사용이 가능하다.</li>
</ul>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="nx">button</span><span class="p">(){</span>
	<span class="kd">const</span> <span class="nx">greenBg</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">bg-[#00FF00] flex-1 items-center justify-center</span><span class="dl">'</span><span class="p">;</span>
	<span class="k">return</span> <span class="p">&lt;</span><span class="nc">View</span> <span class="na">className</span><span class="p">=</span><span class="si">{</span><span class="nx">greenBg</span><span class="si">}</span><span class="p">/&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h3 id="-tailwindconfigjs에-색상을-선언하는-방법">✅ <code class="language-plaintext highlighter-rouge">tailwind.config.js</code>에 색상을 선언하는 방법</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">tailwind.config.js</code>에 <code class="language-plaintext highlighter-rouge">colors: { default-green: '#00FF00' }</code>처럼 선언해두면 <code class="language-plaintext highlighter-rouge">bg-default-green</code>처럼 바로 사용 가능하다.</li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">theme</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">extend</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">colors</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">'</span><span class="s1">default-green</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#00FF00</span><span class="dl">'</span><span class="p">,</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">};</span>
</code></pre></div></div>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="nx">button</span><span class="p">(){</span>
	<span class="k">return</span> <span class="p">&lt;</span><span class="nc">View</span> <span class="na">className</span><span class="p">=</span><span class="s">'bg-default-green flex-1 items-center justify-center'</span><span class="p">/&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="2-폰트-문제">2. 폰트 문제</h2>

<h3 id="폰트-하단-짤림">폰트 하단 짤림</h3>

<blockquote>
  <p>폰트 크기 때문에 글자가 짤리는 문제가 생겼다.</p>
</blockquote>

<p><img src="/images/posts/gooapp/nickname.png" style="max-width:300px" /></p>

<p><code class="language-plaintext highlighter-rouge">Pretendard</code> 폰트를 사용하면서 폰트가 커졌을 때 <code class="language-plaintext highlighter-rouge">g</code>, <code class="language-plaintext highlighter-rouge">y</code>, <code class="language-plaintext highlighter-rouge">j</code> 같은 문자 하단에서 잘렸고, 여백 설정이 필요했다.<br />
해결을 위해 텍스트 크기에 맞춰 <code class="language-plaintext highlighter-rouge">leading(line-height)</code>을 조정했다.</p>

<hr />

<h3 id="글자-비율-문제">글자 비율 문제</h3>

<blockquote>
  <p>랭킹 리스트에서 닉네임이 차지하는 비율이 너무 넓어서 줄바꿈이 일어나는 문제가 있었다.</p>
</blockquote>

<p>리스트의 배치와 글자 크기를 변경해서 해결했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">랭킹</code>은 최대한 왼쪽으로 밀기</li>
  <li><code class="language-plaintext highlighter-rouge">닉네임</code>은 왼쪽 정렬로 <code class="language-plaintext highlighter-rouge">랭킹</code>에 붙임</li>
  <li><code class="language-plaintext highlighter-rouge">점수</code>는 오른쪽 정렬</li>
</ul>

<h2 id="스크롤-디자인-구현">스크롤 디자인 구현</h2>

<blockquote>
  <p>기록과 랭킹 화면에서 리스트 스크롤이 사용자 정보를 덮고 위로 올라와야 했다.</p>
</blockquote>

<p><img src="/images/posts/gooapp/rank1.png" style="max-width:300px" /><br />
<img src="/images/posts/gooapp/rank2.png" style="max-width:300px" /></p>

<h3 id="구현">구현</h3>

<ul>
  <li>상단 헤더 컴포넌트에 <code class="language-plaintext highlighter-rouge">absolute</code>를 적용해서 <strong>위치 점유를 없앴다.</strong></li>
  <li><code class="language-plaintext highlighter-rouge">ScrollView</code>에는 <code class="language-plaintext highlighter-rouge">flex-1</code>을 적용해 <strong>스크롤이 화면 전체를 뒤덮게</strong> 했다.</li>
  <li>스크롤 내부 <code class="language-plaintext highlighter-rouge">View</code> 에 헤더 컴포넌트의 높이 <code class="language-plaintext highlighter-rouge">margin-top</code>을 적용해 <strong>리스트가 아래에서 시작</strong>하도록 했다.</li>
  <li>마지막으로 해당<code class="language-plaintext highlighter-rouge">View</code> 에 <code class="language-plaintext highlighter-rouge">min-h-screen</code>을 적용해서, <strong>리스트 내용이 비어있어도 스크롤이 화면 끝까지 닿도록</strong> 처리했다.</li>
</ul>

<h3 id="구현한-코드">구현한 코드</h3>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Rank</span><span class="p">(){</span>
	<span class="k">return</span> <span class="p">(</span>
		<span class="p">{</span><span class="cm">/* 유저 랭킹, 최고점수 표시 */</span><span class="p">}</span>
		<span class="p">&lt;</span><span class="nc">View</span> <span class="na">className</span><span class="p">=</span><span class="s">"absolute h-header"</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
		<span class="p">{</span><span class="cm">/* 랭킹 상위 100명 표시 */</span><span class="p">}</span>
		<span class="p">&lt;</span><span class="nc">ScrollView</span> <span class="na">className</span><span class="p">=</span><span class="s">"flex-1"</span><span class="p">&gt;</span>
			<span class="p">&lt;</span><span class="nc">View</span> <span class="na">className</span><span class="p">=</span><span class="s">"mt-header min-h-screen"</span><span class="p">&gt;</span>
				<span class="si">{</span><span class="cm">/* 랭킹 리스트 컴포넌트들 */</span><span class="si">}</span>
			<span class="p">&lt;/</span><span class="nc">View</span><span class="p">&gt;</span>
		<span class="p">&lt;/</span><span class="nc">ScrollView</span><span class="p">&gt;</span>
	<span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="마무리">마무리</h2>

<p>Tailwind는 빠르고 직관적인 스타일링 도구지만, React Native에서 사용할 땐 몇 가지 제약을 이해하고 대응하는 것이 중요했다.  <br />
이번 경험을 통해 Tailwind의 동작 방식, 폰트와 레이아웃의 관계, 스크롤 UI 구성법을 더 깊이 이해하게 되었다.</p>]]></content><author><name>itismilob</name></author><category term="GooApp" /><category term="Tailwindcss" /><category term="디자인" /><summary type="html"><![CDATA[디자인을 넣으며 어떤 문제가 있었는가?]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/gooapp_logo.png" /><media:content medium="image" url="https://itismilob.github.io/images/logos/gooapp_logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Zustand</title><link href="https://itismilob.github.io/Zustand/" rel="alternate" type="text/html" title="Zustand" /><published>2025-06-10T00:00:00+00:00</published><updated>2025-06-10T00:00:00+00:00</updated><id>https://itismilob.github.io/Zustand</id><content type="html" xml:base="https://itismilob.github.io/Zustand/"><![CDATA[<h2 id="zustand">Zustand</h2>

<p><a href="https://zustand-demo.pmnd.rs/">Zustand</a>는 작고 빠르며 확장 가능한 React 등에서 사용하는 상태 관리 라이브러리이다.</p>

<p>설치 : <code class="language-plaintext highlighter-rouge">npm install zustand</code></p>

<h3 id="기본-사용방법">기본 사용방법</h3>

<p><code class="language-plaintext highlighter-rouge">create</code>를 사용해 만들어진 Store를 통해서 상태를 관리한다.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">zustand</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// Store에 저장할 상태의 타입 선언</span>
<span class="kd">type</span> <span class="nx">CountState</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">count</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
  <span class="nl">decrease</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// Store 생성 - 타입을 지정하지 않으면 state는 any타입이 된다.</span>
<span class="kd">const</span> <span class="nx">useStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">CountState</span><span class="o">&gt;</span><span class="p">()((</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">count</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
  <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">})),</span>
  <span class="na">decrease</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">})),</span>
<span class="p">}));</span>

<span class="c1">// Store 사용</span>
<span class="kd">function</span> <span class="nx">Screen</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">count</span> <span class="o">=</span> <span class="nx">useStore</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">increase</span> <span class="o">=</span> <span class="nx">useStore</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">increase</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">decrease</span> <span class="o">=</span> <span class="nx">useStore</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">decrease</span><span class="p">);</span>

  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;&gt;</span>
      <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">count</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">increase</span><span class="si">}</span><span class="p">&gt;</span>INCREASE<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">decrease</span><span class="si">}</span><span class="p">&gt;</span>DECREASE<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
    <span class="p">&lt;/&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="usestore">useStore</h3>

<p><code class="language-plaintext highlighter-rouge">create</code>를 통해 만들어진 useStore는 커스텀 훅으로 <code class="language-plaintext highlighter-rouge">useState</code>처럼 작동한다.<br />
상태를 컴포넌트에 넣어 사용하면 자동으로 리랜더링이 적용된다.</p>

<h3 id="create">create()</h3>

<p><code class="language-plaintext highlighter-rouge">create</code>는 Store를 만드는 핵심 함수로 커스텀 훅을 만들어 반환한다.<br />
반환된 훅을 사용하여 <code class="language-plaintext highlighter-rouge">useState</code>와 비슷하게 리렌더링을 할 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">storeType</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">count</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">useStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">storeType</span><span class="o">&gt;</span><span class="p">((</span><span class="kd">set</span><span class="p">,</span> <span class="kd">get</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">count</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
  <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">})),</span>
<span class="p">}));</span>
</code></pre></div></div>

<hr />

<p>create 함수의 <strong>타입</strong><br />
<code class="language-plaintext highlighter-rouge">function create&lt;T&gt;(stateCreatorFn: StateCreator&lt;T&gt;): UseBoundStore&lt;StoreApi&lt;T&gt;&gt;</code></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">T</code> : 정의하는 스토어 전체의 상태 타입</li>
  <li><code class="language-plaintext highlighter-rouge">StateCreator&lt;T&gt;</code> : 스토어를 생성하는 함수</li>
  <li><code class="language-plaintext highlighter-rouge">UseBoundStore&lt;StoreApi&lt;T&gt;&gt;</code> : 반환 타입, 우리가 사용하는 useStore 훅</li>
</ul>

<hr />

<p>더 정확한 타입 ([[커링 패턴]])<br />
<code class="language-plaintext highlighter-rouge">create&lt;T&gt;()(stateCreatorFn: StateCreator&lt;T, [], []&gt;): UseBoundStore&lt;StoreApi&lt;T&gt;&gt;</code></p>

<p><strong>제네릭 타입</strong>으로 타입을 명시적으로 전달하고, 상태 생성 함수를 나중에 만들어 전달하게 되어있다.<br />
<code class="language-plaintext highlighter-rouge">create</code> 함수가 반환한 상태 생성 함수 (<code class="language-plaintext highlighter-rouge">storeCreator</code>)를 곧바로 실행시킨다.</p>

<p>풀어서 설명한다면 흐름이 이런식이다.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">create&lt;T&gt;()</code> 호출 → <code class="language-plaintext highlighter-rouge">storeCreator</code> 함수 생성</li>
  <li><code class="language-plaintext highlighter-rouge">storeCreator(stateCreatorFn)</code> 호출 → <code class="language-plaintext highlighter-rouge">userStore</code> 생성</li>
</ol>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">storeCreator</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">userStore</span> <span class="o">=</span> <span class="nx">storeCreator</span><span class="p">(</span><span class="nx">stateCreatorFn</span><span class="p">);</span>
</code></pre></div></div>

<p>즉, 아래 한 줄은 위 두 줄과 같다.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">userStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">()(</span><span class="nx">stateCreatorFn</span><span class="p">);</span>
</code></pre></div></div>

<p>create의 사용 방식들</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">create&lt;T&gt;(...)</code> : 미들웨어를 사용하지 않을 때</li>
  <li><code class="language-plaintext highlighter-rouge">create&lt;T&gt;()(...)</code> : 미들웨어를 사용하며, 타입을 명시적으로 지정할 때</li>
  <li><code class="language-plaintext highlighter-rouge">create(...)</code> : 미들웨어를 사용하지만 타입 추론에 의존할 때 (combine)</li>
</ul>

<hr />

<ul>
  <li><strong>set</strong> : 상태를 변경하는 함수 (<code class="language-plaintext highlighter-rouge">setState</code>)</li>
  <li><strong>get</strong> : 현재 상태를 가져오는 함수 (<code class="language-plaintext highlighter-rouge">getState</code>)</li>
</ul>

<p>아래 두 set 함수는 동일하게 작동한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">}));</span>
<span class="kd">set</span><span class="p">({</span> <span class="na">count</span><span class="p">:</span> <span class="kd">get</span><span class="p">().</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">});</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">state =&gt; ({})</code> : <code class="language-plaintext highlighter-rouge">set()</code> 내부에서만 사용하며 상태가 이전과 달라질 때 사용한다.</li>
  <li><code class="language-plaintext highlighter-rouge">get()</code> : <code class="language-plaintext highlighter-rouge">set()</code> 외부에서도 사용 가능하며 여러 상태를 조합할 때 사용한다.</li>
</ul>

<h3 id="set">set()</h3>

<p><code class="language-plaintext highlighter-rouge">set</code> 함수는 상태를 업데이트 하는 핵심 함수다.<br />
현재 상태를 받아서, 새 상태로 갱신하거나 부분적으로 병합한다.</p>

<p>set 함수의 <strong>타입</strong><br />
<code class="language-plaintext highlighter-rouge">set: (partial: Partial&lt;State&gt; | (state: State) =&gt; Partial&lt;State&gt;, replace?: boolean) =&gt; void</code></p>

<p><strong>partial</strong>은 상태 객체나 상태를 변경하는 함수를 받는다. (useState와 방식이 동일함)</p>

<ul>
  <li>직접 상태 객체 입력 : <code class="language-plaintext highlighter-rouge">set({count: 5});</code></li>
  <li>이전 상태를 기반으로 새 객체 반환 : <code class="language-plaintext highlighter-rouge">set(state =&gt; ({count: state.count + 1}));</code></li>
</ul>

<p><strong>replace</strong> 옵션은 기존 상태와 병합하지 않고 ‘완전히 새 상태로 교체’ 한다.</p>

<hr />

<p>+ <code class="language-plaintext highlighter-rouge">{}</code>를 <code class="language-plaintext highlighter-rouge">()</code>로 감싸는 이유는 객체를 반환하기 위해서. <code class="language-plaintext highlighter-rouge">()=&gt;{}</code>로 사용하면 단순한 함수가 된다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{};</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">b</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({});</span>
</code></pre></div></div>

<h2 id="middlewares">Middlewares</h2>

<h3 id="combine">combine</h3>

<p><code class="language-plaintext highlighter-rouge">combine</code>은 상태(state)와 액션(actions)를 분리해 구성할 때 사용하는 함수다.<br />
두 개의 객체(<strong>초기 상태 객체</strong>, <strong>액션 함수 객체</strong>)를 하나로 결합해 <code class="language-plaintext highlighter-rouge">stateCreatorFn</code>으로 만들어준다.<br />
액션 함수는 <code class="language-plaintext highlighter-rouge">set</code>을 사용해 기존 상태를 업데이트 한다.</p>

<p><code class="language-plaintext highlighter-rouge">combine</code>은 상태와 액션을 결합하며 <strong>타입을 자동 추론</strong>하기 때문에 제네릭을 통한 명시가 필요 없어진다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">CountState</span> <span class="o">=</span> <span class="p">{</span> <span class="na">count</span><span class="p">:</span> <span class="kr">number</span> <span class="p">};</span>
<span class="kd">type</span> <span class="nx">CountActions</span> <span class="o">=</span> <span class="p">{</span> <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span> <span class="p">};</span>
<span class="kd">type</span> <span class="nx">CountStore</span> <span class="o">=</span> <span class="nx">CountState</span> <span class="o">&amp;</span> <span class="nx">CountActions</span><span class="p">;</span>

<span class="c1">// combine을 사용하지 않음</span>
<span class="kd">const</span> <span class="nx">useStore1</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">CountStore</span><span class="o">&gt;</span><span class="p">((</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">count</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
  <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">})),</span>
<span class="p">}));</span>

<span class="c1">// combine을 사용해 상태와 액션을 분리함</span>
<span class="kd">const</span> <span class="nx">useStore2</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">CountStore</span><span class="o">&gt;</span><span class="p">(</span> <span class="c1">// 제네릭 &lt;CountStore&gt; 불필요함</span>
  <span class="nx">combine</span><span class="p">({</span> <span class="na">count</span><span class="p">:</span> <span class="mi">0</span> <span class="p">},</span> <span class="p">(</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">})),</span>
  <span class="p">}))</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="subscribe-subscribewithselector">subscribe, subscribeWithSelector</h3>

<p>스토어의 상태 변경사항을 감지해 등록된 리스너를 실행시켜주는 리스너 구독 함수이다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 리스너 함수 구독</span>
<span class="kd">const</span> <span class="nx">unsubscribe</span> <span class="o">=</span> <span class="nx">useStore</span><span class="p">.</span><span class="nx">subscribe</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">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">state has changed</span><span class="dl">'</span><span class="p">,</span> <span class="nx">state</span><span class="p">);</span>
<span class="p">});</span>
<span class="c1">// 리스너 구독 해제</span>
<span class="nx">unsubscribe</span><span class="p">();</span>
</code></pre></div></div>

<p><strong>특정 상태값만 감시</strong>하고 싶거나 <strong>이전 상태와 변경된 상태를 비교</strong>할 때는 <code class="language-plaintext highlighter-rouge">subscribeWithSelector</code>미들웨어를 사용한다.</p>

<p><code class="language-plaintext highlighter-rouge">create</code>로 스토어를 생성하며 <code class="language-plaintext highlighter-rouge">subscribeWithSelector</code> 미들웨어 등록해준다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">useStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">CountState</span><span class="o">&gt;</span><span class="p">()(</span>
  <span class="nx">subscribeWithSelector</span><span class="p">((</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
    <span class="na">count</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
    <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">})),</span>
    <span class="na">decrease</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">})),</span>
  <span class="p">}))</span>
<span class="p">);</span>
</code></pre></div></div>

<p>미들웨어를 사용하면 <code class="language-plaintext highlighter-rouge">subscribe</code> 사용방식이 변화한다.</p>

<ul>
  <li>selector : 전체 상태에서 특정 값만 선택하는 <strong>선택자 함수</strong>
    <ul>
      <li>선택된 상태값의 변경에만 리스너가 실행된다.</li>
    </ul>
  </li>
  <li>listener : 상태 변경시 실행되는 <strong>리스너 함수</strong>
    <ul>
      <li>이전값과 변경값을 인자로 받는다.</li>
    </ul>
  </li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 특정 상태값만 선택해주는 선택자</span>
<span class="kd">const</span> <span class="nx">selector</span><span class="p">:</span> <span class="p">(</span><span class="nx">state</span><span class="p">:</span> <span class="nx">CountState</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">number</span> <span class="o">=</span> <span class="p">(</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="p">;</span>
<span class="c1">// 이전값과 변경값을 비교해주는 리스너</span>
<span class="kd">const</span> <span class="nx">listener</span> <span class="o">=</span> <span class="p">(</span><span class="nx">newCount</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">oldCount</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">newCount</span><span class="p">}</span><span class="s2"> -&gt; </span><span class="p">${</span><span class="nx">oldCount</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">};</span>

<span class="c1">// 선택자, 리스너 구독</span>
<span class="kd">const</span> <span class="nx">unsubscribe</span> <span class="o">=</span> <span class="nx">useStore</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span> <span class="nx">listener</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="persist">persist</h3>

<p><code class="language-plaintext highlighter-rouge">persist</code>는 스토어의 상태를 브라우저/디바이스의 로컬 저장소(<code class="language-plaintext highlighter-rouge">localStorage</code> 등)에 자동으로 저장하고 복원시켜주는 미들웨어이다.<br />
<strong>복원(Rehydration)</strong> : 로컬 저장소의 데이터를 앱 실행시 자동으로 store에 불러오는 과정</p>

<p>인자로 <code class="language-plaintext highlighter-rouge">stateCreatorFn</code>과 <code class="language-plaintext highlighter-rouge">persistOptions</code>를 받는다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">useStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">CountStore</span><span class="o">&gt;</span><span class="p">()(</span>
  <span class="nx">persist</span><span class="p">(</span>
    <span class="p">(</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="na">count</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
      <span class="na">increase</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kd">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="na">count</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">})),</span>
    <span class="p">}),</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">count-storage</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// localStorage 키 이름</span>
    <span class="p">}</span>
  <span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<h4 id="persistoptions-주요-옵션">persistOptions 주요 옵션</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">name</code> : 필수, 저장소의 키 이름</li>
  <li><code class="language-plaintext highlighter-rouge">storage</code> : 저장소 선택 (기본값 : <code class="language-plaintext highlighter-rouge">createJSONStorage(()=&gt;localStorage)</code>) -&gt; MMKV 사용</li>
  <li><code class="language-plaintext highlighter-rouge">partialize</code> : 저장할 필드(상태)만 선택적으로 지정</li>
  <li><code class="language-plaintext highlighter-rouge">onRehydrateStorage</code> : 복원 이벤트 핸들러 설정</li>
  <li><code class="language-plaintext highlighter-rouge">version</code> : 상태 버전 지정 (마이그레이션)</li>
  <li><code class="language-plaintext highlighter-rouge">migrate</code> : 버전 불일치 시, 저장 상태를 새로운 구조로 변환</li>
  <li><code class="language-plaintext highlighter-rouge">merge</code> : 복원 시 저장 상태와 초기 상태를 어떻게 병합할지 지정</li>
  <li><code class="language-plaintext highlighter-rouge">skipHydration</code> : 자동으로 상태를 복원하지 않음</li>
</ul>

<hr />

<h4 id="zustand-persist--mmkv">Zustand persist + MMKV</h4>

<p>persist 미들웨어를 이용해 Zustand와 MMKV 로컬 스토리지를 연결해 앱 실행시 자동으로 데이터를 불러올 수 있다.</p>

<ul>
  <li>MMKV 사용을 위한<code class="language-plaintext highlighter-rouge">mmkvStorage</code> 인스턴스를 만든다.</li>
  <li>Zustand의 <code class="language-plaintext highlighter-rouge">persist</code> 미들웨어에 사용할 수 있는<code class="language-plaintext highlighter-rouge">zustandStorage</code> 인터페이스 객체를 만든다.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">createJSONStorage()</code>를 통해 JSON처리와 티입 호환을 맞춘다.</li>
    </ul>
  </li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">MMKV</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-native-mmkv</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">mmkvStorage</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MMKV</span><span class="p">();</span>

<span class="kd">const</span> <span class="nx">zustandStorage</span> <span class="o">=</span> <span class="nx">createJSONStorage</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">setItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
  <span class="p">},</span>
  <span class="na">getItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">value</span> <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>
  <span class="p">},</span>
  <span class="na">removeItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">mmkvStorage</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">name</span><span class="p">);</span>
  <span class="p">},</span>
<span class="p">}));</span>
</code></pre></div></div>

<p>이제 <code class="language-plaintext highlighter-rouge">create</code>를 통해 저장할 데이터에 맞는 커스텀 훅을 생성해 사용한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">UserStore</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">userData</span><span class="p">:</span> <span class="nx">UserDataType</span><span class="p">;</span>
  <span class="nl">setUserData</span><span class="p">:</span> <span class="p">(</span><span class="na">data</span><span class="p">:</span> <span class="nb">Partial</span><span class="o">&lt;</span><span class="nx">UserDataType</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
  <span class="c1">// Partial : 부분적인 업데이트를 위해서 UserDataType의 부분만 가진 객체를 받는다.</span>
<span class="p">};</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">useUserStore</span> <span class="o">=</span> <span class="nx">create</span><span class="o">&lt;</span><span class="nx">UserStore</span><span class="o">&gt;</span><span class="p">()(</span>
  <span class="nx">persist</span><span class="p">(</span>
    <span class="p">(</span><span class="kd">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="na">userData</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">userID</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
        <span class="na">nickname</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
        <span class="na">rank</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
        <span class="na">topScore</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="na">setUserData</span><span class="p">:</span> <span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span>
        <span class="kd">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="p">...</span><span class="nx">state</span><span class="p">,</span>
          <span class="p">...</span><span class="nx">data</span><span class="p">,</span>
        <span class="p">})),</span>
    <span class="p">}),</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user-store</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">storage</span><span class="p">:</span> <span class="nx">zustandStorage</span><span class="p">,</span>
      <span class="na">partialize</span><span class="p">:</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="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">userData</span> <span class="p">}),</span> <span class="c1">// userData만 저장, setUserData 저장X</span>
    <span class="p">}</span>
  <span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>]]></content><author><name>itismilob</name></author><category term="React" /><category term="Zustand" /><category term="Typescript" /><summary type="html"><![CDATA[Zustand 사용방법 정리 + MMKV 저장소와 연결]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/react_logo.webp" /><media:content medium="image" url="https://itismilob.github.io/images/logos/react_logo.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GooApp - 퍼즐 교착상태 문제점</title><link href="https://itismilob.github.io/GooApp_%ED%8D%BC%EC%A6%90_%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C_%EB%AC%B8%EC%A0%9C%EC%A0%90/" rel="alternate" type="text/html" title="GooApp - 퍼즐 교착상태 문제점" /><published>2025-06-02T00:00:00+00:00</published><updated>2025-06-02T00:00:00+00:00</updated><id>https://itismilob.github.io/GooApp_%ED%8D%BC%EC%A6%90_%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C_%EB%AC%B8%EC%A0%9C%EC%A0%90</id><content type="html" xml:base="https://itismilob.github.io/GooApp_%ED%8D%BC%EC%A6%90_%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C_%EB%AC%B8%EC%A0%9C%EC%A0%90/"><![CDATA[<h2 id="상황">상황</h2>

<p>퍼즐을 만들면서 교착상태에 빠져 더이상 진행할 수 없는 문제가 발생했다.</p>

<p>원하는 기능은 5:5로 문제와 정답을 연결하는 기능이다.</p>

<ul>
  <li>문제, 정답이 이전과 다른 랜덤한 위치에 떠야 한다.</li>
  <li>항상 연결할 수 있는 문제-정답 조합이 하나 이상 남아있어야 한다.</li>
</ul>

<p><img src="/images/posts/gooapp/퀴즈.png" style="max-width:300px" /></p>

<h3 id="첫-접근-방식">첫 접근 방식</h3>

<p>처음에는 문제-정답이 모두 이어지는 그룹을 계속 쌓아가는 스택 방식을 생각했다.</p>

<ul>
  <li>버튼의 개수대로 문제들을 모아놓은 그룹을 만든다. - 이 그룹이 1개 층이 된다.</li>
  <li>문제를 맞추면 다음 스택에 있는 문제가 올라온다.</li>
  <li>윗층을 모두 맞추면 아래층으로 완전히 넘어가면서 항상 조합이 나오도록 유지된다.</li>
</ul>

<hr />

<p>시각화 예시</p>

<ul>
  <li>맞춘 문제는 사라진다.</li>
  <li>다음 스택의 문제들이 보여진다.</li>
</ul>

<p><img src="/images/posts/gooapp/try1.png" style="max-width:300px" /></p>

<h3 id="예시-상황">예시 상황</h3>

<p>하나의 정답에 연결될 수 있는 두 개의 정답이 존재할 수 있다.</p>

<p><img src="/images/posts/gooapp/두 문제 - 정답 하나.png" style="max-width:300px" /></p>

<p>다른 층의 조합을 연결한다면 교착상태가 발생해 더이상 진행할 수 없어진다.</p>

<p><img src="/images/posts/gooapp/교착.png" style="max-width:300px" /></p>

<p>문제와 정답에 중복이 없다면 교착이 발생하지 않지만 구구단의 특성상 중복을 제거할수는 없다.<br />
ex) 6x4, 4x6, 8x3, 3x8 = 24</p>

<h2 id="해결">해결</h2>

<p><strong>Queue</strong>에서 문제를 생성해서 <strong>List</strong>로 이동해 보여지는 방식을 떠올렸다.<br />
<strong>문제Queue, 정답Queue</strong> / <strong>문제List, 정답List</strong> 두 종류씩 만든다.</p>

<h3 id="알고리즘">알고리즘</h3>

<p>문제List 채우기</p>

<ol>
  <li>문제List 빈 칸에 문제Queue의 문제를 채운다. (<strong>문제Q -&gt; 문제L</strong>)</li>
  <li>빈 칸으로 이동시킬 문제가 없다면 새로운 문제를 생성하고, 정답을 정답Queue 랜덤 위치에 추가한다.<br />
(<strong>문제L : 문제 생성 / 정답Q : 정답 생성</strong>)</li>
</ol>

<p>정답List 채우기</p>

<ol>
  <li>정답List 빈 칸에 정답Queue의 정답을 채운다. (<strong>정답Q -&gt; 정답L</strong>)</li>
  <li>빈 칸으로 이동시킬 정답이 없다면 새로운 문제를 생성하고, 문제는 문제Queue 랜덤 위치에 추가한다.<br />
(<strong>정답L : 정답 생성 / 문제Q : 문제 생성</strong>)</li>
</ol>

<p>-&gt; 문제List, 정답List가 모두 채워졌고, List들을 화면에 보여준다.</p>

<p>게임을 진행하며 반복한다.</p>

<hr />

<h3 id="설명">설명</h3>

<p>먼저 문제List를 모두 채우고, 조합이 생성될 때 정답Queue를 랜덤하게 채운다.<br />
그 다음 정답List를 모두 채우고 마찬가지로 조합이 생성될 때 문제Queue를 랜덤하게 채운다.</p>

<ul>
  <li>초기 상태<br />
<img src="/images/posts/gooapp/초기 상태.png" alt="" /></li>
</ul>

<ol>
  <li>
    <p>문제 이동<br />
<img src="/images/posts/gooapp/문제 이동.png" alt="" /></p>
  </li>
  <li>
    <p>문제 채우기<br />
<img src="/images/posts/gooapp/문제 생성.png" alt="" /></p>
  </li>
  <li>
    <p>정답 이동<br />
<img src="/images/posts/gooapp/정답 이동.png" alt="" /></p>
  </li>
  <li>
    <p>정답 채우기<br />
<img src="/images/posts/gooapp/정답 채우기.png" alt="" /></p>
  </li>
</ol>

<h3 id="코드">코드</h3>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">queueAlgorithm</span> <span class="o">=</span> <span class="p">(</span>
  <span class="nx">questQueue</span><span class="p">:</span> <span class="nx">QuestArray</span><span class="p">[],</span>
  <span class="nx">questList</span><span class="p">:</span> <span class="nx">QuestArray</span><span class="p">[]</span>
<span class="p">):</span> <span class="p">[</span><span class="nx">QuestArray</span><span class="p">[],</span> <span class="nx">QuestArray</span><span class="p">[]]</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// 큐와 리스트를 분할해 사용함</span>
  <span class="kd">const</span> <span class="nx">QQueue</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">questQueue</span><span class="p">[</span><span class="mi">0</span><span class="p">]];</span>
  <span class="kd">const</span> <span class="nx">AQueue</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">questQueue</span><span class="p">[</span><span class="mi">1</span><span class="p">]];</span>
  <span class="kd">const</span> <span class="nx">QList</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">questList</span><span class="p">[</span><span class="mi">0</span><span class="p">]];</span>
  <span class="kd">const</span> <span class="nx">AList</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">questList</span><span class="p">[</span><span class="mi">1</span><span class="p">]];</span>

  <span class="c1">// 문제_리스트의 빈칸을 모두 채운다.</span>
  <span class="nx">QList</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">Q</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">Q</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">QQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// 문제가 비어있고 문제_큐의 해당 칸이 비어있다면 - 문제를 생성한다.</span>
      <span class="kd">const</span> <span class="p">[</span><span class="nx">newQuest</span><span class="p">,</span> <span class="nx">newAnswer</span><span class="p">]</span> <span class="o">=</span> <span class="nx">questGenerator</span><span class="p">();</span>
      <span class="c1">// 문제의 정답을 정답_큐의 랜덤 위치에 추가한다.</span>
      <span class="nx">insertRandom</span><span class="p">(</span><span class="nx">AQueue</span><span class="p">,</span> <span class="nx">newAnswer</span><span class="p">);</span>
      <span class="nx">QList</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">newQuest</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">Q</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">QQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// 문제가 비어있고 문제_큐의 해당 칸이 존제한다면 - 문제를 채운다.</span>
      <span class="kd">const</span> <span class="nx">temp</span> <span class="o">=</span> <span class="nx">QQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
      <span class="nx">QQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
      <span class="nx">QList</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">temp</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">});</span>

  <span class="c1">// 정답_리스트의 빈칸을 모두 채운다.</span>
  <span class="nx">AList</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">A</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">A</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">AQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// 정답이 비어있고 정답_큐의 해당 칸이 비어있다면 - 문제를 생성한다.</span>
      <span class="kd">const</span> <span class="p">[</span><span class="nx">newQuest</span><span class="p">,</span> <span class="nx">newAnswer</span><span class="p">]</span> <span class="o">=</span> <span class="nx">questGenerator</span><span class="p">();</span>
      <span class="c1">// 문제를 문제_큐의 랜덤 위치에 추가한다.</span>
      <span class="nx">insertRandom</span><span class="p">(</span><span class="nx">QQueue</span><span class="p">,</span> <span class="nx">newQuest</span><span class="p">);</span>
      <span class="nx">AList</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">newAnswer</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">A</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="nx">AQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// 정답이 비어있고 정답_큐의 해당 칸이 존제한다면 - 정답을 채운다.</span>
      <span class="kd">const</span> <span class="nx">temp</span> <span class="o">=</span> <span class="nx">AQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
      <span class="nx">AQueue</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
      <span class="nx">AList</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">temp</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">});</span>

  <span class="c1">// 큐와 리스트 다시 병합해서 반환함</span>
  <span class="kd">const</span> <span class="nx">newQueue</span> <span class="o">=</span> <span class="p">[</span><span class="nx">QQueue</span><span class="p">,</span> <span class="nx">AQueue</span><span class="p">];</span>
  <span class="kd">const</span> <span class="nx">newList</span> <span class="o">=</span> <span class="p">[</span><span class="nx">QList</span><span class="p">,</span> <span class="nx">AList</span><span class="p">];</span>
  <span class="k">return</span> <span class="p">[</span><span class="nx">newQueue</span><span class="p">,</span> <span class="nx">newList</span><span class="p">];</span>
<span class="p">};</span>
</code></pre></div></div>

<h2 id="결론">결론</h2>

<p>알고리즘을 만들 때는 개발을 편하게 하기 위해서 노트에 그림을 그려가며 흐름을 정해놔야겠다.<br />
그래도 문제를 빨리 발견해서 바로 해결할 수 있었다.</p>

<p>문제가 생기고 막힐 때는 잠시 쉬어가며, 어쩌다 이 지경이 되었는지 복기해보자.<br />
일단 밥을 먹다보면 해결 방법이 떠오를 것이다.</p>]]></content><author><name>itismilob</name></author><category term="React_Native" /><category term="GooApp" /><summary type="html"><![CDATA[퍼즐을 만들며 발생한 문제점 해결과정]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/gooapp_logo.png" /><media:content medium="image" url="https://itismilob.github.io/images/logos/gooapp_logo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">React Native MMKV</title><link href="https://itismilob.github.io/MMKV_Storage/" rel="alternate" type="text/html" title="React Native MMKV" /><published>2025-05-26T00:00:00+00:00</published><updated>2025-05-26T00:00:00+00:00</updated><id>https://itismilob.github.io/MMKV_Storage</id><content type="html" xml:base="https://itismilob.github.io/MMKV_Storage/"><![CDATA[<h2 id="mmkv">MMKV</h2>
<p><a href="https://github.com/mrousavy/react-native-mmkv">react-native-mmkv -&gt;</a></p>

<p>ReactNative를 위한 로컬 저장소<br />
WeChat앱의 저장소로 쓰이는 <a href="https://github.com/Tencent/MMKV">Tencent/MMKV</a>를 기반으로 만들어졌다.</p>

<ul>
  <li>C++을 기반으로 만들어져 속도가 매우 빠르다.</li>
  <li>동기적으로 작동해서 프로미스를 사용하지 않는다!</li>
  <li>암호화 (secure storage)를 지원한다.</li>
  <li>저장소 위치를 지정할 수 있다.</li>
</ul>

<hr />

<h2 id="시작하기">시작하기</h2>

<p><code class="language-plaintext highlighter-rouge">npm install react-native-mmkv</code></p>

<p>재사용 가능한 MMKV의 인스턴스를 생성해서 사용한다.<br />
기본 MMKV 스토리지 아이디로 인스턴스가 생성된다. storage ID : <code class="language-plaintext highlighter-rouge">mmkv.default</code></p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">MMKV</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-native-mmkv</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">storage</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MMKV</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="커스텀-스토리지">커스텀 스토리지</h3>

<p>커스텀 스토리지 아이디를 통해 커스텀 스토리지를 생성한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span><span class="nx">MMKV</span><span class="p">,</span> <span class="nx">Mode</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-native-mmkv</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">storage</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MMKV</span><span class="p">({</span>  
  <span class="na">id</span><span class="p">:</span> <span class="s2">`user-</span><span class="p">${</span><span class="nx">userId</span><span class="p">}</span><span class="s2">-storage`</span><span class="p">,</span>
  <span class="na">path</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">USER_DIRECTORY</span><span class="p">}</span><span class="s2">/storage`</span><span class="p">,</span>
  <span class="na">encryptionKey</span><span class="p">:</span> <span class="dl">'</span><span class="s1">hunter2</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">mode</span><span class="p">:</span> <span class="nx">Mode</span><span class="p">.</span><span class="nx">MULTI_PROCESS</span><span class="p">,</span>
  <span class="na">readOnly</span><span class="p">:</span> <span class="kc">false</span>
<span class="p">});</span>
</code></pre></div></div>

<ul>
  <li>id : 스토리지 인스턴스의 ID</li>
  <li>path : 스토리지의 루트 위치. 기본으로는 <code class="language-plaintext highlighter-rouge">$(Documents)/mmkv/~</code> 위치로 저장된다.</li>
  <li>encryptionKey : 암호/복호화 키. 기본으로 일반 텍스트로 저장, 암호화를 하지 않는다.</li>
  <li>mode : MMKV 프로세스의 동작. <code class="language-plaintext highlighter-rouge">MULTI_PROCESS</code>로 설정하면 외부 앱에서 데이터를 수정할 수 있다고 본다.</li>
  <li>readOnly : 읽기 전용 모드</li>
</ul>

<p>앱을 다시시작할 때 항상 같은 키로 저장소를 초기화 해야 데이터를 읽을 수 있다.<br />
암호화 키는 Android Keystore, IOS Keychain 등을 사용</p>

<hr />

<h2 id="사용하기">사용하기</h2>

<p>저장할 수 있는 데이터 타입은 <strong>String, Number, Boolean</strong> 3가지가 있다.</p>

<h3 id="set">Set</h3>

<p>데이터 저장하기 (저장할 때는 타입 명시 X)</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">storage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.name</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Marc</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">storage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.age</span><span class="dl">'</span><span class="p">,</span> <span class="mi">21</span><span class="p">)</span>
<span class="nx">storage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">is-mmkv-fast-asf</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="get">Get</h3>

<p>데이터 불러오기 (특정 타입을 명시해 불러와야 한다.)</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">username</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.name</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// 'Marc'</span>
<span class="kd">const</span> <span class="nx">age</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getNumber</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.age</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// 21</span>
<span class="kd">const</span> <span class="nx">isMmkvFastAsf</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getBoolean</span><span class="p">(</span><span class="dl">'</span><span class="s1">is-mmkv-fast-asf</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// true</span>
</code></pre></div></div>

<h3 id="hooks">Hooks</h3>

<p>React Hook과 연결된 방식으로 사용한다.<br />
<code class="language-plaintext highlighter-rouge">[불러올 State 변수, 저장하는 함수]</code></p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">[</span><span class="nx">username</span><span class="p">,</span> <span class="nx">setUsername</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useMMKVString</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.name</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">age</span><span class="p">,</span> <span class="nx">setAge</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useMMKVNumber</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.age</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">isMmkvFastAsf</span><span class="p">,</span> <span class="nx">setIsMmkvFastAf</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useMMKVBoolean</span><span class="p">(</span><span class="dl">'</span><span class="s1">is-mmkv-fast-asf</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="keys">Keys</h3>

<p>키를 통해서 값을 확인하고 삭제할 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 특정 키가 있는지 확인</span>
<span class="kd">const</span> <span class="nx">hasUsername</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.name</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// 모든 키 가져오기</span>
<span class="kd">const</span> <span class="nx">keys</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getAllKeys</span><span class="p">()</span> <span class="c1">// ['user.name', 'user.age', 'is-mmkv-fast-asf']</span>

<span class="c1">// 특정 키와 연결된 값 삭제</span>
<span class="nx">storage</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="dl">'</span><span class="s1">user.name</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// 모든 키/데이터 삭제</span>
<span class="nx">storage</span><span class="p">.</span><span class="nx">clearAll</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="objects">Objects</h3>

<p>Object형 데이터를 저장하기 위해 JSON.stringify을 사용해 String으로 변환해서 저장한다.<br />
불러올 때도 JSON.parse로 변환해 사용한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">username</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Marc</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">age</span><span class="p">:</span> <span class="mi">21</span>
<span class="p">}</span>

<span class="c1">// JSON string으로 변환</span>
<span class="nx">storage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</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">user</span><span class="p">))</span>

<span class="c1">// string 값을 불러오며 다시 오브젝트로 변환</span>
<span class="kd">const</span> <span class="nx">jsonUser</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// { 'username': 'Marc', 'age': 21 }</span>
<span class="kd">const</span> <span class="nx">userObject</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">jsonUser</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="encryption">Encryption</h3>

<p>이미 존제하는 스토리지의 암호 재설정 및 암호 제거<br />
보안 정책 변경, 키 교체 등에 사용함</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 모든 값을 프라이빗 키로 암호화</span>
<span class="nx">storage</span><span class="p">.</span><span class="nx">recrypt</span><span class="p">(</span><span class="dl">'</span><span class="s1">hunter2</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// 암호 삭제</span>
<span class="nx">storage</span><span class="p">.</span><span class="nx">recrypt</span><span class="p">(</span><span class="kc">undefined</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="buffers">Buffers</h3>

<p>성능과 압축이 필요할 때 Buffer를 사용하지만 주로 JSON 문자열을 사용해 저장한다.</p>

<p>ArrayBuffer : 원시 바이너리 데이터 버퍼를 표현한다.<br />
<code class="language-plaintext highlighter-rouge">ArrayBuffer(버퍼 크기)</code></p>

<p>Uint8Array : TypedArray로 8비트의 부호 없는 정수 배열이다.<br />
길이, 배열, TypedArray, ArrayBuffer 등으로 만들 수 있다.</p>

<p>TypedArray : 타입이 있는 배열, 모든 원소는 동일한 타입이고 만들면 길이를 수정할 수 없다.<br />
배열의 읽기/쓰기를 위해서 사용한다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 3 크기의 ArrayBuffer 생성</span>
<span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">ArrayBuffer</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="c1">// ArrayBuffer로 Uint8Array 생성</span>
<span class="kd">const</span> <span class="nx">dataWriter</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">buffer</span><span class="p">)</span>
<span class="c1">// dataWriter를 사용하여 buffer 값 작성</span>
<span class="nx">dataWriter</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
<span class="nx">dataWriter</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">100</span>
<span class="nx">dataWriter</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="mi">255</span>
<span class="c1">// 스토리지에 데이터 저장</span>
<span class="nx">storage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">someToken</span><span class="dl">'</span><span class="p">,</span> <span class="nx">buffer</span><span class="p">)</span>

<span class="c1">//---------</span>
<span class="kd">const</span> <span class="nx">buffer</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getBuffer</span><span class="p">(</span><span class="dl">'</span><span class="s1">someToken</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">buffer</span><span class="p">)</span> <span class="c1">// [1, 100, 255]</span>
</code></pre></div></div>

<hr />

<h3 id="size">Size</h3>

<p>스토리지 크기 확인</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 스토리지 크기를 byte로 가져옴</span>
<span class="kd">const</span> <span class="nx">size</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">size</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">size</span> <span class="o">&gt;=</span> <span class="mi">4096</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 사용하지 않는 키와 캐시를 삭제함</span>
  <span class="nx">storage</span><span class="p">.</span><span class="nx">trim</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="zustand와-연동해서-사용">Zustand와 연동해서 사용</h2>

<p><a href="https://github.com/mrousavy/react-native-mmkv/blob/main/docs/WRAPPER_ZUSTAND_PERSIST_MIDDLEWARE.md">Zustand persist-middleware -&gt;</a><br />
Zustand와 연동해서 하나의 storage로 편하게 관리할 수 있다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">StateStorage</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">zustand/middleware</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">MMKV</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-native-mmkv</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">storage</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MMKV</span><span class="p">()</span>

<span class="kd">const</span> <span class="nx">zustandStorage</span><span class="p">:</span> <span class="nx">StateStorage</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">setItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">storage</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span>
  <span class="p">},</span>
  <span class="na">getItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">getString</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span>
    <span class="k">return</span> <span class="nx">value</span> <span class="o">??</span> <span class="kc">null</span>
  <span class="p">},</span>
  <span class="na">removeItem</span><span class="p">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">storage</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span>
  <span class="p">},</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>itismilob</name></author><category term="React_Native" /><category term="GooApp" /><summary type="html"><![CDATA[React Native에서 로컬 데이터 저장하기]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/react_logo.webp" /><media:content medium="image" url="https://itismilob.github.io/images/logos/react_logo.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">React Navigation</title><link href="https://itismilob.github.io/React_Navigation/" rel="alternate" type="text/html" title="React Navigation" /><published>2025-05-26T00:00:00+00:00</published><updated>2025-05-26T00:00:00+00:00</updated><id>https://itismilob.github.io/React_Navigation</id><content type="html" xml:base="https://itismilob.github.io/React_Navigation/"><![CDATA[<h2 id="react-navigation">React Navigation</h2>

<p><a href="https://reactnavigation.org/">React Navigation</a></p>

<p>React Navigation은 React Native에서 화면 이동(네비게이션)을 쉽게 구현할 수 있도록 도와주는 라이브러리다.<br />
화면 이동, Tab, Drawer, Modal 메뉴, 웹 페이지의 URL params처럼 데이터를 전달하는 기능 등을 지원한다.</p>

<h2 id="네비게이터-종류">네비게이터 종류</h2>

<ul>
  <li>Stack Navigator : 한 화면씩 쌓이는 방식</li>
  <li>Tab Navigator : 하단 탭 메뉴로 화면 전환</li>
  <li>Drawer Navigator : 옆에서 슬라이드해 나오는 메뉴 (햄버거 메뉴)</li>
  <li>Nested Navigator : 하나의 화면에 여러 네비게이터를 중첩해 사용 (Tab + Stack 등)</li>
  <li>Modal : 모달을 띄우는 네비게이터 옵션</li>
</ul>

<h2 id="패키지">패키지</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">@react-navigation/native</code> : 핵심 라이브러리 - 네비게이션 컨텍스트 제공 (필수)</li>
  <li><code class="language-plaintext highlighter-rouge">@react-navigation/native-stack</code> : 네이티브 느낌의 스택 네비게이터</li>
  <li><code class="language-plaintext highlighter-rouge">@react-navigation/bottom-tabs</code> : 탭 네비게이터</li>
  <li><code class="language-plaintext highlighter-rouge">@react-navigation/drawer</code> : 드로어 네비게이터</li>
  <li><code class="language-plaintext highlighter-rouge">@react-navigation/elements</code> : 네비게이션에 필요한 UI 컴포넌트를 모아놓은 패키지</li>
</ul>

<h2 id="static-vs-dynamic">Static vs Dynamic</h2>

<p>React Navigation에는 두가지 사용 방식 Static/Dynamic이 있다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Static API</th>
      <th>Dynamic API</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>선언 방식</td>
      <td>객체 기반</td>
      <td>JSX 기반</td>
    </tr>
    <tr>
      <td>특징</td>
      <td>선언적, 구조 고정</td>
      <td>유연함, 동적 처리 가능</td>
    </tr>
    <tr>
      <td>버전</td>
      <td>v7부터 추가</td>
      <td>기존부터 사용</td>
    </tr>
    <tr>
      <td>목적</td>
      <td>정적인 네비게이션 트리 명시</td>
      <td>동적인 렌더링 유연성</td>
    </tr>
  </tbody>
</table>

<h3 id="장단점">장단점</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Static API</th>
      <th>Dynamic API</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>장점</td>
      <td>구조가 한눈에 보여 유지보수 쉬움</td>
      <td>JSX 문법을 사용해 컴포넌트처럼 조작 가능</td>
    </tr>
    <tr>
      <td> </td>
      <td>타입 추론이 강력함</td>
      <td>조건부 렌더링 가능</td>
    </tr>
    <tr>
      <td> </td>
      <td>딥 링크 자동 생성</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td>정적 분석 (Typescript, Lint) 등이 유리함</td>
      <td> </td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>단점</td>
      <td>조건부 렌더링 등 동적인 사용이 어려움</td>
      <td>타입 추론 제한적</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td>복잡해지면 구조 파악이 어려움</td>
    </tr>
  </tbody>
</table>

<p>React Navigation에서 추천하는 기본적인 방식은 Static 방식이고 동적 구성이 필요하다면 Dynamic 방식을 사용하면 된다.</p>

<hr />

<h2 id="tutorial">Tutorial</h2>

<p><a href="https://reactnavigation.org/docs/getting-started">React Navigation - Getting started</a></p>

<h3 id="설치-및-네이티브-세팅">설치 및 네이티브 세팅</h3>

<p>React Navigation 설치 : <code class="language-plaintext highlighter-rouge">npm install @react-navigation/native</code><br />
추가 유틸리티 설치 : <code class="language-plaintext highlighter-rouge">npm install react-native-screens react-native-safe-area-context</code></p>

<hr />

<p><a href="https://reactnavigation.org/docs/getting-started/#installing-dependencies-into-a-bare-react-native-project">네이티브 세팅</a></p>

<p>IOS : <code class="language-plaintext highlighter-rouge">npx pod-install ios</code><br />
Android : <code class="language-plaintext highlighter-rouge">android/app/src/main/java/&lt;your package name&gt;/</code> 수정</p>

<h3 id="stack-navigator">Stack Navigator</h3>

<p><code class="language-plaintext highlighter-rouge">createNativeStackNavigator</code> : Stack Navigator 생성 (Drawer, Tab 등 사용 가능)<br />
<code class="language-plaintext highlighter-rouge">createStaticNavigation</code> : Navigator를 가져와 앱에 랜더링하는 컴포넌트를 반환, 앱에서 한 번만 호출</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createStaticNavigation</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-navigation/native</span><span class="dl">'</span><span class="p">;</span>  
<span class="k">import</span> <span class="p">{</span> <span class="nx">createNativeStackNavigator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@react-navigation/native-stack</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">HomeScreen</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./screens/HomeScreen</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// Stack Navigator 생성</span>
<span class="kd">const</span> <span class="nx">RootStack</span> <span class="o">=</span> <span class="nx">createNativeStackNavigator</span><span class="p">({</span>
	<span class="na">screens</span><span class="p">:</span> <span class="p">{</span>
		<span class="na">Home</span><span class="p">:</span> <span class="nx">HomeScreen</span><span class="p">,</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="c1">// 네비게이션 컴포넌트 생성</span>
<span class="kd">const</span> <span class="nx">Navigation</span> <span class="o">=</span> <span class="nx">createStaticNavigation</span><span class="p">(</span><span class="nx">RootStack</span><span class="p">);</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">App</span><span class="p">(){</span>
	<span class="k">return</span> <span class="o">&lt;</span><span class="nx">Navigation</span> <span class="o">/&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="화면-이동">화면 이동</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">navigation.navigate('Details')</code> : 해당 스크린으로 <strong>이동</strong>, 이미 스택에 있으면 <strong>기존 화면으로 이동</strong> (중복 방지)</li>
  <li><code class="language-plaintext highlighter-rouge">navigation.push('Details')</code> : 해당 스크린을 <strong>새로 하나 더 스택에 쌓음</strong> (중복 가능)</li>
  <li><code class="language-plaintext highlighter-rouge">navigation.goBack()</code> : 뒤로가기, 히스토리 pop</li>
  <li><code class="language-plaintext highlighter-rouge">navigation.popTo('Home')</code> : 히스토리에 Home 화면으로 이동</li>
  <li><code class="language-plaintext highlighter-rouge">navigation.popToTop()</code> : 최상위로 이동</li>
</ul>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 화면 이동에 담을 props 데이터의 타입을 지정</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">RootStackParams</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">Home</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">;</span> <span class="c1">//param</span>
	<span class="nl">Details</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">;</span> <span class="c1">//param</span>
<span class="p">};</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">HomeScreen</span><span class="p">(){</span>
	<span class="c1">// 네비게이션 생성</span>
	<span class="c1">// 네비게이션에서 전달할 props 데이터 타입을 지정, 타입을 사용하는 화면 명시</span>
	<span class="kd">type</span> <span class="nx">NavigationProp</span> <span class="o">=</span> <span class="nx">NativeStackNavigationProp</span><span class="o">&lt;</span><span class="nx">RootStackProps</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Home</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>
	<span class="kd">const</span> <span class="nx">navigation</span> <span class="o">=</span> <span class="nx">useNavigation</span><span class="o">&lt;</span><span class="nx">NavigationProp</span><span class="o">&gt;</span><span class="p">();</span>
	
	<span class="k">return</span> <span class="p">&lt;</span><span class="nc">Pressable</span> <span class="na">onPress</span><span class="p">=</span><span class="si">{</span><span class="p">()</span><span class="o">=&gt;</span><span class="p">{</span>
		<span class="nx">navigation</span><span class="p">.</span><span class="nx">navigate</span><span class="p">(</span><span class="dl">'</span><span class="s1">Details</span><span class="dl">'</span><span class="p">);</span>
	<span class="p">}</span><span class="si">}</span><span class="p">&gt;</span>
		Go Details!
	<span class="p">&lt;/</span><span class="nc">Pressable</span><span class="p">&gt;</span>
<span class="p">}</span>

</code></pre></div></div>

<hr />

<h4 id="params-데이터-전달">Params 데이터 전달</h4>

<p>Params : React Navigation을 통해서 데이터를 전달한다. (웹에서의 URL Params처럼 작동)<br />
Props : JSX 컴포넌트를 통해서 데이터를 전달한다.</p>

<p>React Navigation에서 화면의 Props로 Params 데이터인 <code class="language-plaintext highlighter-rouge">route</code>와 <code class="language-plaintext highlighter-rouge">navigation</code> 데이터를 넣어준다.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 화면 이동에 담을 props 데이터의 타입을 지정</span>
<span class="k">export</span> <span class="kd">type</span> <span class="nx">RootStackParams</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">Home</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">;</span>
	<span class="nl">Details</span><span class="p">:</span> <span class="p">{</span> <span class="na">pageID</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span>
<span class="p">};</span>

<span class="c1">// 해당 화면 컴포넌트의 props 타입 설정</span>
<span class="kd">type</span> <span class="nx">DetailsScreenProps</span> <span class="o">=</span> <span class="nx">NativeStackScreenProps</span><span class="o">&lt;</span><span class="nx">RootStackParams</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Details</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>

<span class="c1">// prop 으로 param route를 받아서 데이터 사용</span>
<span class="kd">function</span> <span class="nx">DetailsScreen</span><span class="p">({</span> <span class="nx">route</span> <span class="p">}:</span> <span class="nx">DetailsScreenProps</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// 네비게이션 생성</span>
	<span class="kd">type</span> <span class="nx">NavigationProp</span> <span class="o">=</span> <span class="nx">NativeStackNavigationProp</span><span class="o">&lt;</span><span class="nx">RootStackParams</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Details</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">;</span>
	<span class="kd">const</span> <span class="nx">navigation</span> <span class="o">=</span> <span class="nx">useNavigation</span><span class="o">&lt;</span><span class="nx">NavigationProp</span><span class="o">&gt;</span><span class="p">();</span>

	<span class="c1">// route 에서 데이터 꺼내서 사용</span>
	<span class="kd">const</span> <span class="p">{</span> <span class="nx">pageID</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">route</span><span class="p">.</span><span class="nx">params</span><span class="p">;</span>

	<span class="k">return</span> <span class="p">&lt;</span><span class="nc">Text</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">pageID</span><span class="si">}</span><span class="p">&lt;/</span><span class="nc">Text</span><span class="p">&gt;</span>
<span class="p">}</span>

<span class="c1">// Stack Navigator에서 Props 기본값 설정</span>
<span class="kd">const</span> <span class="nx">RootStack</span> <span class="o">=</span> <span class="nx">createNativeStackNavigator</span><span class="o">&lt;</span><span class="nx">RootStackParams</span><span class="o">&gt;</span><span class="p">({</span>
	<span class="na">screens</span><span class="p">:</span> <span class="p">{</span>
		<span class="na">Details</span><span class="p">:</span> <span class="p">{</span> 
			<span class="na">screen</span><span class="p">:</span> <span class="nx">DetailsScreen</span><span class="p">,</span>
			<span class="c1">// Props 기본값 설정</span>
			<span class="na">initialParams</span><span class="p">:</span> <span class="p">{</span> <span class="na">pageID</span><span class="p">:</span> <span class="dl">'</span><span class="s1">29809</span><span class="dl">'</span> <span class="p">}</span> 
		<span class="p">},</span>
	<span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<ol>
  <li><code class="language-plaintext highlighter-rouge">navigation.navigate('RouteName', {params object})</code> : navigation 메소드에 param 전달</li>
  <li><code class="language-plaintext highlighter-rouge">navigation.popTo('BeforeScreen', {params object})</code> : popTo를 통해 이전 화면에 param을 전달</li>
  <li><code class="language-plaintext highlighter-rouge">route.params</code> : 전달된 param 불러옴</li>
  <li><code class="language-plaintext highlighter-rouge">navigation.setParams({params object})</code> : 현재 화면의 param 데이터 변경</li>
</ol>

<p>중첩 화면(Nested navigator)에서 스크린을 명시해서 데이터 전달</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// More 화면으로 이동, More 화면 안의 Settings 화면에 데이터 전달</span>
<span class="nx">navigation</span><span class="p">.</span><span class="nx">navigate</span><span class="p">(</span><span class="dl">'</span><span class="s1">More</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">screen</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Settings</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="na">user</span><span class="p">:</span> <span class="dl">'</span><span class="s1">jane</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">})</span>
</code></pre></div></div>

<p>추가 규칙</p>
<ul>
  <li>너무 많은 데이터를 담아서는 안된다.</li>
  <li>Webpage의 URL과 마찬가지로 ID, sort, keywords 등의 간단한 정보만 전달한다.</li>
</ul>

<hr />

<h3 id="nested-navigator---중첩-화면">Nested Navigator - 중첩 화면</h3>

<p>네비게이터 안에 네비게이터를 넣어서 사용할 수 있다. (일반 컴포넌트와 동일)<br />
자식 네비게이터에서 Navigation Actions를 버블업 해서 부모 네비게이터의 Action을 사용할 수 있다.</p>

<ul>
  <li>Stack Navigator
    <ul>
      <li>Home screen</li>
      <li>Drawer Navigator
        <ul>
          <li>User screen</li>
          <li>Setting screen</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>Drawer Navigator 의 화면에서는 Stack Navigator의 메소드 <code class="language-plaintext highlighter-rouge">push</code>, <code class="language-plaintext highlighter-rouge">replace</code> 등을 사용 가능하다.<br />
반대로 Stack Navigator의 화면에서는 <code class="language-plaintext highlighter-rouge">openDrawer</code>, <code class="language-plaintext highlighter-rouge">closeDrawer</code> 등의 Drawer Navigator의 메소드를 사용 불가능하다.</p>

<hr />

<p>만약 부모 네비게이터에서 자식의 액션을 사용하려면 <code class="language-plaintext highlighter-rouge">dispatch</code>를 사용해야 한다.<br />
dispatch : navigation state(상태)를 업데이트 한다.</p>

<p><code class="language-plaintext highlighter-rouge">navigation.dispatch(DrawerActions.openDrawer())</code></p>

<hr />

<h3 id="navigation-lifecycle">Navigation Lifecycle</h3>

<p>화면을 이동해도 기존의 화면이 Unmount 되지 않고 계속 위로 쌓인다. (화면이 추가되어도 이전 화면의 cleanup 함수가 실행되지 않음) 뒤로가기로 이전 화면에 돌아오면 남아있던 화면이 계속 보여진다.</p>

<blockquote>
  <p><strong>Home</strong> (Home Mount) -&gt; <strong>Details</strong> (Details Mount, Home remain) -&gt; <strong>Home</strong> (Details Unmount)</p>
</blockquote>

<p>유저가 특정 화면에 들어오고 나가는 걸 확인하기 위해서 <code class="language-plaintext highlighter-rouge">focus</code>, <code class="language-plaintext highlighter-rouge">blur</code> 이벤트를 확인한다.</p>

<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>  
	<span class="c1">// 화면에 들어올 때 호출</span>
	<span class="kd">const</span> <span class="nx">unsubscribe</span> <span class="o">=</span> <span class="nx">navigation</span><span class="p">.</span><span class="nx">addListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">focus</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>  
		<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">ProfileScreen focused</span><span class="dl">'</span><span class="p">);</span>  
	<span class="p">});</span>
  
	<span class="k">return</span> <span class="nx">unsubscribe</span><span class="p">;</span>  
<span class="p">},</span> <span class="p">[</span><span class="nx">navigation</span><span class="p">]);</span>  

<span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>  
	<span class="c1">// 화면에서 나갈 때 호출</span>
	<span class="kd">const</span> <span class="nx">unsubscribe</span> <span class="o">=</span> <span class="nx">navigation</span><span class="p">.</span><span class="nx">addListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">blur</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>  
		<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">ProfileScreen blurred</span><span class="dl">'</span><span class="p">);</span>  
	<span class="p">});</span>  
	  
	<span class="k">return</span> <span class="nx">unsubscribe</span><span class="p">;</span>  
<span class="p">},</span> <span class="p">[</span><span class="nx">navigation</span><span class="p">]);</span>
</code></pre></div></div>

<p>+ useEffect의 의존성으로 navigation 객체를 넣어야 네비게이션 이동시 변화를 반영할 수 있다.</p>

<hr />

<h3 id="modal">Modal</h3>

<p>화면을 모달로 띄우기 위해서는 네비게이터 설정에서 화면의 <code class="language-plaintext highlighter-rouge">presentation</code> 옵션으로 <code class="language-plaintext highlighter-rouge">modal</code>을 설정해준다.<br />
만약 배경이 투명한 모달을 사용하고 싶다면 <code class="language-plaintext highlighter-rouge">transparentModal</code>로 설정하면 된다.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">RootStack</span> <span class="o">=</span> <span class="nx">createStackNavigator</span><span class="p">({</span>
	<span class="na">Modal</span><span class="p">:</span> <span class="p">{</span>
		<span class="na">screen</span><span class="p">:</span> <span class="nx">ModalScreen</span><span class="p">,</span>
		<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
			<span class="na">presentation</span><span class="p">:</span> <span class="dl">'</span><span class="s1">modal</span><span class="dl">'</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>]]></content><author><name>itismilob</name></author><category term="React_Native" /><category term="React_Navigation" /><category term="GooApp" /><summary type="html"><![CDATA[React Native 라우팅을 위한 Navigation 라이브러리]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/react_logo.webp" /><media:content medium="image" url="https://itismilob.github.io/images/logos/react_logo.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">React Native CLI vs Expo</title><link href="https://itismilob.github.io/React_Native_CLI_vs_Expo/" rel="alternate" type="text/html" title="React Native CLI vs Expo" /><published>2025-05-19T00:00:00+00:00</published><updated>2025-05-19T00:00:00+00:00</updated><id>https://itismilob.github.io/React_Native_CLI_vs_Expo</id><content type="html" xml:base="https://itismilob.github.io/React_Native_CLI_vs_Expo/"><![CDATA[<p>React Native CLI와 Expo를 비교하면서 어떤걸 사용할지 정해보자!<br />
이전에 GooApp을 React Native의 테스트 프로젝트로 진행하면서 Expo를 사용했다.<br />
Expo를 사용하면서 느꼈던 장단점을 CLI와 비교하면서 사용할 프레임워크를 고민했다.</p>

<hr />

<h2 id="장단점-정리">장단점 정리</h2>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>React Native CLI</th>
      <th>Expo</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>설정 및 시작 속도</strong></td>
      <td>초기 설정 복잡</td>
      <td>✅ 매우 빠름</td>
    </tr>
    <tr>
      <td><strong>로컬 빌드 지원</strong></td>
      <td>✅ 완벽하게 지원</td>
      <td>기본 미지원 (EAS CLI)</td>
    </tr>
    <tr>
      <td><strong>Native 코드 수정</strong></td>
      <td>✅ 자유롭게 가능</td>
      <td>기본 불가능 (eject 필요)</td>
    </tr>
    <tr>
      <td><strong>서드파티 라이브러리 호환성</strong></td>
      <td>✅ 모든 라이브러리 사용 가능</td>
      <td>제한적</td>
    </tr>
    <tr>
      <td><strong>앱 크기 최적화</strong></td>
      <td>✅ 필요 기능만 설치 가능</td>
      <td>기본 SDK 포함 기능이 많음</td>
    </tr>
    <tr>
      <td><strong>OTA 업데이트</strong></td>
      <td>직접 구현 필요 (CodePush 등)</td>
      <td>✅ 기본 지원 (Expo Updates)</td>
    </tr>
    <tr>
      <td><strong>앱 테스트 용이성</strong></td>
      <td>직접 빌드하거나 에뮬레이터 필요</td>
      <td>✅ Expo Go 앱으로 즉시 테스트 가능</td>
    </tr>
    <tr>
      <td><strong>CI/CD 연동</strong></td>
      <td>✅ 자유롭게 가능</td>
      <td>유료 플랜</td>
    </tr>
    <tr>
      <td><strong>백그라운드 기능 (Task 등)</strong></td>
      <td>✅ 완전 지원</td>
      <td>제한된 백그라운드 기능</td>
    </tr>
    <tr>
      <td><strong>푸시 알림 커스터마이징</strong></td>
      <td>✅ 자유롭게 구성 가능</td>
      <td>Expo Push (커스터마이징 제한)</td>
    </tr>
    <tr>
      <td><strong>기존 Native 앱 통합</strong></td>
      <td>✅ 가능 (기존 앱에 RN 추가 가능)</td>
      <td>불가능</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="프로젝트에-적용될-장단점">프로젝트에 적용될 장단점</h2>

<h3 id="react-native-cli">React Native CLI</h3>

<p><strong>장점</strong></p>
<ul>
  <li>내 컴퓨터를 사용해 로컬 빌드가 가능하다.</li>
  <li>필요한 것들만 설치해서 빌드해 앱 용량 최적화에 유리하다.</li>
  <li>일부 Expo SDK 라이브러리를 사용 가능하다.</li>
</ul>

<p><strong>단점</strong></p>
<ul>
  <li>초기 설정이 복잡하고 추가 툴(Xcode, Android Studio)가 필요하다.</li>
  <li>테스트를 위해 빌드와 설치 과정이 필요하다.</li>
</ul>

<h3 id="expo">Expo</h3>

<p><strong>장점</strong></p>

<ul>
  <li>Expo Go 앱을 통해 편리한 테스트가 가능하다.</li>
  <li>Expo SDK에서 다양한 모듈, 라이브러리를 지원한다.</li>
  <li>Expo Updates를 통한 OTA를 기본 지원한다.</li>
</ul>

<p><strong>단점</strong></p>
<ul>
  <li>EAS를 통한 클라우드 서버를 통해서만 빌드가 가능하다. (대기열)</li>
  <li>ReactNative의 세부 구조를 알기 어렵다.</li>
</ul>

<hr />

<h2 id="cli를-선택한-이유">CLI를 선택한 이유</h2>

<h3 id="react-native를-제대로-이해하고-구조를-파악하기-위해서-cli를-선택했다">React Native를 제대로 이해하고 구조를 파악하기 위해서 CLI를 선택했다.</h3>

<ol>
  <li>초기 설정을 직접 할 수 있다.
    <ul>
      <li>Expo에서는 설정이 되어있어서 알기 어렵지만, CLI는 필요한 설정을 직접 구성하면서 구조를 익힐 수 있다.</li>
    </ul>
  </li>
  <li>React Native의 기본 API들을 사용한다.
    <ul>
      <li>Expo SDK는 API를 수정한 버전을 사용하기 때문에 원래의 RN 기능을 다루기 위해서는 CLI를 사용해야 한다.</li>
    </ul>
  </li>
  <li>확장성에 제한이 없다.
    <ul>
      <li>Expo는 일부 네이티브 모듈에 사용 제약이 있지만 CLI는 자유롭게 연동 가능하다.</li>
    </ul>
  </li>
  <li>로컬에서 직접 빌드가 가능하다.
    <ul>
      <li>Expo는 빌드 서버에서 대기열이 발생하지만 CLI는 직접 컴퓨터에서 바로 빠르게 빌드할 수 있다.</li>
    </ul>
  </li>
  <li>앱 용량 최적화
    <ul>
      <li>Expo는 여려 기능을 기본적으로 포함해 앱 용량이 커질 수 있다. CLI는 필요한 모듈만 설치해 가볍고 단순한 앱을 만들 수 있다.</li>
    </ul>
  </li>
</ol>

<hr />

<p>Expo는 빠른 개발과 테스트에 유리하지만, React Native의 구조와 작동 방식을 깊이 있게 이해하기에는 한계가 있었다. CLI 환경에서는 직접 구조를 설계하고 다양한 네이티브 기능을 다뤄볼 수 있어, React Native를 더 깊이 배우기에 적합하다고 느꼈다.Expo Go를 사용하지 못하는게 최대 단점… 😅</p>]]></content><author><name>itismilob</name></author><category term="React_Native" /><category term="Expo" /><category term="GooApp" /><summary type="html"><![CDATA[React Native CLI와 Expo를 비교하면서 어떤걸 사용할지 정해보자!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://itismilob.github.io/images/logos/react_logo.webp" /><media:content medium="image" url="https://itismilob.github.io/images/logos/react_logo.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>