Jekyll2019-10-13T14:24:00+00:00https://jonir227.github.io/feed.xmlMemoReact, Typescript, 취미에 대한 글을 씁니다.Typescript에서 redux-actions대체하기2019-10-13T10:03:12+00:002019-10-13T10:03:12+00:00https://jonir227.github.io/develop/2019/10/13/Typescript%EC%97%90%EC%84%9C-redux-actions-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0<h1 id="redux-actions"><code class="highlighter-rouge">Redux-Actions</code></h1> <p>리덕스 액션즈는 리덕스에서 귀찮은 일들을 상당부분 줄여주는 유틸리티 라이브러리이다. 많은 기능들이 있지만 필자가 가장 즐겨 쓰는 (사실은 그것만 사용하는)함수는 두개. <code class="highlighter-rouge">createAction</code>과 <code class="highlighter-rouge">handleActions</code>이다.</p> <p>이 두가지 함수는 리덕스에서 상당히 귀찮은 작업인 <code class="highlighter-rouge">ActionCreator</code>를 만드는 작업과 <code class="highlighter-rouge">Reducer</code>를 만드는 작업의 보일러플레이트를 굉장히 편하게 줄여준다. 그래서 개인적으로 작업을 하든, 업무를 할때든 리덕스를 사용할때면 거의 항상 사용하곤 했다.</p> <p>아래는 라이브러리 사용을 간단하게 작성해 보았다. 하기의 사용 예 말고 다양한 사용 사례가 많지만 개인적으로는 대부분 이 사용 방법에서 벗어나지 않게 사용했던것 같다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 액션</span> <span class="kd">const</span> <span class="nx">CHANGE_NAME</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">CHANGE_NAME</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">CHANGE_AGE</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">CHANGE_AGE</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">defaultStae</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">john</span><span class="dl">'</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="mi">27</span> <span class="p">};</span> <span class="c1">// ------ Before ----------</span> <span class="c1">// 액션 크리에이터</span> <span class="kd">const</span> <span class="nx">changeName</span> <span class="o">=</span> <span class="nx">name</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">CHANGE_NAME</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="p">});</span> <span class="kd">const</span> <span class="nx">changeAge</span> <span class="o">=</span> <span class="nx">age</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">CHANGE_NAME</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">age</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="p">(</span><span class="nx">state</span> <span class="o">=</span> <span class="nx">defaultState</span><span class="p">,</span> <span class="nx">action</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">switch</span> <span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span> <span class="k">case</span> <span class="na">CHNAGE_NAME</span><span class="p">:</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="p">...</span><span class="nx">state</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">name</span> <span class="p">};</span> <span class="p">}</span> <span class="k">case</span> <span class="na">CHANGE_AGE</span><span class="p">:</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="p">...</span><span class="nx">state</span><span class="p">,</span> <span class="na">age</span><span class="p">:</span> <span class="nx">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">age</span> <span class="p">};</span> <span class="p">}</span> <span class="nl">default</span><span class="p">:</span> <span class="k">return</span> <span class="nx">state</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span> <span class="c1">// -------- after -------</span> <span class="c1">// 액션 크리에이터</span> <span class="kd">const</span> <span class="nx">changeName</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_NAME</span><span class="p">,</span> <span class="p">(</span><span class="nx">name</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}));</span> <span class="kd">const</span> <span class="nx">changeAge</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_AGE</span><span class="p">,</span> <span class="p">(</span><span class="nx">age</span> <span class="o">=</span> <span class="p">{</span> <span class="nx">age</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="nx">handleActions</span><span class="p">({</span> <span class="p">[</span><span class="nx">CHANGE_NAME</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">payload</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="na">name</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">name</span> <span class="p">}),</span> <span class="p">[</span><span class="nx">CHANGE_AGE</span><span class="p">]:</span> <span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">action</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="na">age</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">age</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div></div> <p>확실히 구문이 읽기 편하고 깔끔해졌다. 여기까지 봐서는 안쓸 이유가 없어보인다. 하지만 여기에는 문제가 있다. 이 라이브러리는 타입스크립트 지원이 굉장히 허술하게 되어있다.</p> <h1 id="무엇이-문제인가">무엇이 문제인가?</h1> <p>여러가지 문제가 있지만, 가장 큰 문제는 <code class="highlighter-rouge">createAction</code>이 뱉어내는 액션 크리에이터의 타입은 무조건 <code class="highlighter-rouge">string</code>으로 고정되어 있다는 사실이다. <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/redux-actions/index.d.ts">타입 정의</a>를 한번 보자.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// FSA-compliant action.</span> <span class="c1">// See: https://github.com/acdlite/flux-standard-action</span> <span class="k">export</span> <span class="kr">interface</span> <span class="nx">BaseAction</span> <span class="p">{</span> <span class="nl">type</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="p">}</span> <span class="k">export</span> <span class="kr">interface</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">Payload</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="nx">BaseAction</span> <span class="p">{</span> <span class="nl">payload</span><span class="p">:</span> <span class="nx">Payload</span><span class="p">;</span> <span class="nx">error</span><span class="p">?:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>이 라이브러리를 타입스크립트에서 사용하려고 시도해 보았던 사람들은 알 것이다. <code class="highlighter-rouge">string</code> 타입으로 고정해서 내놓는 것이 얼마나 끔찍한 일인지.. 우선 이 액션 크리에이터가 뱉어내는 액션은 기본적으로 <code class="highlighter-rouge">string</code>타입이기 때문에 액션 크리에이터가 리턴하는 인터페이스를 따로 짜주어야 한다. 이렇게 말이다.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">changeName</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_NAME</span><span class="p">,</span> <span class="nx">name</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">name</span> <span class="p">}));</span> <span class="kr">interface</span> <span class="nx">ChangeName</span> <span class="p">{</span> <span class="nl">type</span><span class="p">:</span> <span class="k">typeof</span> <span class="nx">CHANGE_NAME</span><span class="p">;</span> <span class="nl">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nl">name</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="p">};</span> <span class="p">}</span> </code></pre></div></div> <p><a href="https://jonir227.github.io/develop/2019/06/04/redux-saga%EC%99%80-typescript-%ED%8E%B8%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0.html">이전 글</a>에서 소개했던 <code class="highlighter-rouge">ReturnType</code>을 이용하는 방법도 어림없다. 왜냐하면 <code class="highlighter-rouge">changeName</code>이 리턴하는 함수는 무조건 <code class="highlighter-rouge">type</code>이 <code class="highlighter-rouge">string</code>이기 때문에 올바르게 액션을 만들어내지 못하기 때문이다.</p> <p>또 다른 문제는 <code class="highlighter-rouge">handleActions</code>에도 있다. 이 함수의 문제 역시 꽤 많다. 우선 가장 기본이 되는 타입 정의부터 살펴보자.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="nx">type</span> <span class="nx">Reducer</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span> <span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="nx">action</span><span class="p">:</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">Payload</span><span class="o">&gt;</span> <span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">State</span><span class="p">;</span> <span class="k">export</span> <span class="nx">type</span> <span class="nx">ReducerMeta</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="p">,</span> <span class="nx">Meta</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span> <span class="nx">state</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="nx">action</span><span class="p">:</span> <span class="nx">ActionMeta</span><span class="o">&lt;</span><span class="nx">Payload</span><span class="p">,</span> <span class="nx">Meta</span><span class="o">&gt;</span> <span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">State</span><span class="p">;</span> <span class="k">export</span> <span class="nx">type</span> <span class="nx">ReducerMapValue</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span> <span class="o">=</span> <span class="o">|</span> <span class="nx">Reducer</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">ReducerNextThrow</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span> <span class="o">|</span> <span class="nx">ReducerMap</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span><span class="p">;</span> <span class="k">export</span> <span class="kr">interface</span> <span class="nx">ReducerMap</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span> <span class="p">{</span> <span class="p">[</span><span class="na">actionType</span><span class="p">:</span> <span class="nx">string</span><span class="p">]:</span> <span class="nx">ReducerMapValue</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="k">export</span> <span class="kd">function</span> <span class="nx">handleActions</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span><span class="p">(</span> <span class="na">reducerMap</span><span class="p">:</span> <span class="nx">ReducerMap</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span><span class="p">,</span> <span class="na">initialState</span><span class="p">:</span> <span class="nx">State</span><span class="p">,</span> <span class="nx">options</span><span class="p">?:</span> <span class="nx">Options</span> <span class="p">):</span> <span class="nx">ReduxCompatibleReducer</span><span class="o">&lt;</span><span class="nx">State</span><span class="p">,</span> <span class="nx">Payload</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p>여기서도 보이는 문제는 액션이 <code class="highlighter-rouge">type</code>을 <code class="highlighter-rouge">string</code>으로 간주한다는 점이다. 액션 타입에 대하여 제네릭이든지 어떤 타입 추론에 도움이 될만한 정보가 존재하지 않는다. 이 끔찍한 가정은 <code class="highlighter-rouge">handleActions</code>에서 두번째 제네릭의 인자로 “액션”이 아닌 “페이로드”만 받는것과도 상통한다. 그러면 이 함수를 어떻게 써야할까?</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 페이로드를 가져오기 위한 헬퍼 타입</span> <span class="nx">type</span> <span class="nx">GetPayload</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="nx">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">any</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">ReturnType</span><span class="o">&lt;</span><span class="nx">A</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">infer</span> <span class="nx">P</span><span class="o">&gt;</span> <span class="p">?</span> <span class="nx">P</span> <span class="p">:</span> <span class="nx">never</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">changeName</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_NAME</span><span class="p">,</span> <span class="p">(</span><span class="nx">name</span><span class="p">:</span> <span class="nx">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">name</span> <span class="p">}));</span> <span class="kd">const</span> <span class="nx">changeAge</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_AGE</span><span class="p">,</span> <span class="p">(</span><span class="nx">age</span><span class="p">:</span> <span class="nx">number</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">age</span> <span class="p">}));</span> <span class="nx">type</span> <span class="nx">UserPayloads</span> <span class="o">=</span> <span class="nx">GetPayload</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">changeAge</span> <span class="o">|</span> <span class="k">typeof</span> <span class="nx">changeName</span><span class="o">&gt;</span><span class="p">;</span> <span class="kr">interface</span> <span class="nx">User</span> <span class="p">{</span> <span class="nl">age</span><span class="p">:</span> <span class="nx">number</span><span class="p">;</span> <span class="nl">name</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">defaultState</span> <span class="o">=</span> <span class="p">{</span> <span class="na">age</span><span class="p">:</span> <span class="mi">27</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">john</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">handleActions</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">,</span> <span class="nx">UserPayloads</span><span class="o">&gt;</span><span class="p">(</span> <span class="p">{</span> <span class="p">[</span><span class="nx">CHANGE_NAME</span><span class="p">]:</span> <span class="p">(</span><span class="nx">state</span><span class="p">,</span> <span class="nx">action</span><span class="p">)</span> <span class="o">=&gt;</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">action</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">state</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="p">{</span> <span class="p">...</span><span class="nx">state</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="p">};</span> <span class="p">}</span> <span class="p">},</span> <span class="nx">defaultState</span> <span class="p">);</span> </code></pre></div></div> <p>무언가 머리가 아픈 부분이 발견되었는가? <code class="highlighter-rouge">CHANGE_NAME</code>이 받는 함수의 <code class="highlighter-rouge">action</code>은 <code class="highlighter-rouge">Typescript</code>의 <code class="highlighter-rouge">switch/case</code>문 안에서의 <code class="highlighter-rouge">Type narrowing</code>의 효과를 전혀 받지 못한다. 모든 액션의 페이로드가 섞여있기 때문에 각 액션 타입에 맞는 페이로드를 걸러낸 다음 사용해야 한다. 이렇게 되면 사실상 <code class="highlighter-rouge">Typescript</code>를 사용하는 이점이 많이 사라지기 때문에 타입스크립트를 사용하면서 이 라이브러리를 사용하기 않게 되었다.</p> <h1 id="대안">대안?</h1> <p>물론 대안을 찾아보지 않은 것은 아니다. 문제를 해결하기 위한 유명한 라이브러리가 있는데 <a href="https://github.com/piotrwitek/typesafe-actions"><code class="highlighter-rouge">typesafe-actions</code></a>라는 라이브러리이다. 정말 좋은 라이브러리이지만 이 라이브러리의 사용법이 익숙하지 않고, <code class="highlighter-rouge">redux-acton</code>의 액션-핸들액션 맵핑 방식이 가독성이 매우 뛰어나다고 생각하여 직접 만들기로 했다.</p> <blockquote> <p>수정사항: <code class="highlighter-rouge">typesafe-actions</code>를 과거에 확인했을때는 이런 객체 맵핑 방식이 지원되지 않았었는데, 올해 4월에 업데이트된 4.1버전부터 지원되는 것을 확인했다. 미리 확인해볼걸 하는 후회가 든다..</p> </blockquote> <h1 id="직접-만들기">직접 만들기</h1> <p>앞서서 <code class="highlighter-rouge">redux-actions</code>의 타입 정의가 엉망이라고 했지만 사실 과거에는 어쩔 수 없었다고 생각한다. <code class="highlighter-rouge">string</code>타입으로만 문자열을 사용할 수 있었으니까. 하지만 우리에게는 <code class="highlighter-rouge">3.4</code>버전부터 추가된 <code class="highlighter-rouge">as const</code>가 있다. 그걸 사용해서 비슷하지만, 타입정의를 정확하게 해주는 대체제를 만들어보자</p> <h2 id="handleactions"><code class="highlighter-rouge">handleActions</code></h2> <p>코드와 함께 보자.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 리덕스이 표준 액션 객체를 사용한다.</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">redux</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// `type`를 `string`이 아닌 제네릭으로 두어서 `type`의 타입을 유지하여 리턴한다.</span> <span class="c1">// 정확한 액션 크리에이터 함수를 만들기 위해서 함수의 시그니쳐를 오버로딩한다.</span> <span class="c1">// 1. 페이로드 크리에이터 함수가 정의되어 있는 경우</span> <span class="c1">// 2. 페이로드 크리에이터 함수가 정의되어있지 않아 페이로드가 없는 경우</span> <span class="kd">function</span> <span class="nx">createAction</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">,</span> <span class="nx">P</span> <span class="kd">extends</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="nx">any</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">any</span><span class="o">&gt;</span><span class="p">(</span> <span class="nx">type</span><span class="p">:</span> <span class="nx">T</span><span class="p">,</span> <span class="nx">payloadCreator</span><span class="p">:</span> <span class="nx">P</span> <span class="p">):</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="nx">Parameters</span><span class="o">&lt;</span><span class="nx">P</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="o">&amp;</span> <span class="p">{</span> <span class="na">payload</span><span class="p">:</span> <span class="nx">ReturnType</span><span class="o">&lt;</span><span class="nx">P</span><span class="o">&gt;</span> <span class="p">};</span> <span class="kd">function</span> <span class="nx">createAction</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">type</span><span class="p">:</span> <span class="nx">T</span><span class="p">):</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">function</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">type</span><span class="p">:</span> <span class="nx">any</span><span class="p">,</span> <span class="nx">payloadCreator</span><span class="p">?:</span> <span class="nx">any</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="nx">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">type</span><span class="p">,</span> <span class="p">...(</span><span class="nx">payloadCreator</span> <span class="o">&amp;&amp;</span> <span class="p">{</span> <span class="na">payload</span><span class="p">:</span> <span class="nx">payloadCreator</span><span class="p">(...</span><span class="nx">args</span><span class="p">)</span> <span class="p">})</span> <span class="p">});</span> <span class="p">}</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">createAction</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">changeName</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_NAME</span><span class="p">,</span> <span class="p">(</span><span class="nx">name</span><span class="p">:</span> <span class="nx">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">name</span> <span class="p">}));</span> <span class="kd">const</span> <span class="nx">changeAge</span> <span class="o">=</span> <span class="nx">createAction</span><span class="p">(</span><span class="nx">CHANGE_AGE</span><span class="p">,</span> <span class="p">(</span><span class="nx">age</span><span class="p">:</span> <span class="nx">number</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">age</span> <span class="p">}));</span> <span class="nx">type</span> <span class="nx">UserActions</span> <span class="o">=</span> <span class="nx">ReturnType</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">changeName</span> <span class="o">|</span> <span class="k">typeof</span> <span class="nx">changeAge</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p>한결 간결해지고, 단순히 페이로드만의 타입이 아닌, 액션의 타입 전부를 기억하고있는 액션 타입이 만들어졌다. 그 다음은 이것을 처리하는 <code class="highlighter-rouge">handleActions</code>의 차례이다.</p> <h2 id="handleactions-1"><code class="highlighter-rouge">handleActions</code></h2> <p>처음 타입스크립트를 사용하려고 했을때 이 함수를 어떻게든 만들어보려고 했었다. 결과는 실패로 돌아갔는데 <code class="highlighter-rouge">as const</code>의 은총으로 액션 타입에 대한 정확한 정의가 가능해진 지금은 정복이 가능해졌다.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 역시 리덕스의 표준 액션 인터페이스를 사용한다.</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Action</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">redux</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// 인자로 받을 리듀서의 키 밸류 맵이다.</span> <span class="c1">// 리덕스의 액션 타입의 기본은 any로 되어있기 때문에 string으로 만든 액션을 상속받아서 사용한다.</span> <span class="nx">type</span> <span class="nx">ReducerMap</span><span class="o">&lt;</span><span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">string</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">S</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// 액션을 상속받은 A의 `type`의 타입들을 키로 사용한다.</span> <span class="c1">// 리듀서 함수의 인자로 넘어가는 액션은 그 키 타입에 맞는 액선을 끄집어내서(MatchedAction) 사용한다.</span> <span class="p">[</span><span class="nx">AT</span> <span class="k">in</span> <span class="nx">A</span><span class="p">[</span><span class="dl">"</span><span class="s2">type</span><span class="dl">"</span><span class="p">]]?:</span> <span class="p">(</span><span class="na">state</span><span class="p">:</span> <span class="nx">S</span><span class="p">,</span> <span class="na">action</span><span class="p">:</span> <span class="nx">MatchedAction</span><span class="o">&lt;</span><span class="nx">A</span><span class="p">,</span> <span class="nx">AT</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">S</span><span class="p">;</span> <span class="p">}</span> <span class="o">&amp;</span> <span class="p">{</span> <span class="p">[</span><span class="na">key</span><span class="p">:</span> <span class="nx">string</span><span class="p">]:</span> <span class="p">(</span><span class="na">state</span><span class="p">:</span> <span class="nx">S</span><span class="p">,</span> <span class="na">action</span><span class="p">:</span> <span class="nx">Action</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">S</span> <span class="p">};</span> <span class="c1">// 객체의 키 접근을 위한 인덱스 시그니쳐</span> <span class="c1">// 인자로 받은 액션들(A) 중에 액션타입(T)이 일치하는 타입만 내보내고 나머지는 지운다.</span> <span class="nx">type</span> <span class="nx">MatchedAction</span><span class="o">&lt;</span><span class="nx">A</span><span class="p">,</span> <span class="nx">T</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span> <span class="p">?</span> <span class="nx">A</span> <span class="p">:</span> <span class="nx">never</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">handleActions</span> <span class="o">=</span> <span class="o">&lt;</span><span class="nx">S</span><span class="p">,</span> <span class="nx">A</span> <span class="kd">extends</span> <span class="nx">Action</span><span class="o">&lt;</span><span class="nx">string</span><span class="o">&gt;&gt;</span><span class="p">(</span> <span class="na">reducerMap</span><span class="p">:</span> <span class="nx">ReducerMap</span><span class="o">&lt;</span><span class="nx">A</span><span class="p">,</span> <span class="nx">S</span><span class="o">&gt;</span><span class="p">,</span> <span class="na">defaultState</span><span class="p">:</span> <span class="nx">S</span> <span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="nx">state</span> <span class="o">=</span> <span class="nx">defaultState</span><span class="p">,</span> <span class="nx">action</span><span class="p">?:</span> <span class="nx">A</span><span class="p">):</span> <span class="nx">S</span> <span class="o">=&gt;</span> <span class="nx">action</span> <span class="o">&amp;&amp;</span> <span class="nx">reducerMap</span><span class="p">[</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">]</span> <span class="p">?</span> <span class="nx">reducerMap</span><span class="p">[</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">](</span><span class="nx">state</span><span class="p">,</span> <span class="nx">action</span><span class="p">)</span> <span class="p">:</span> <span class="nx">state</span><span class="p">;</span> <span class="k">export</span> <span class="k">default</span> <span class="nx">matchAction</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="nx">handleActions</span><span class="o">&lt;</span><span class="nx">UserActions</span><span class="p">,</span> <span class="nx">User</span><span class="o">&gt;</span><span class="p">(</span> <span class="p">{</span> <span class="c1">// 정의된 액션들에 맞는 타입만 객체 키값으로 사용가능하고, 인자로 넘겨지는 액션도</span> <span class="c1">// 그 키에 맞는 액션이 전달되기때문에 타입에 안전하다.</span> <span class="p">[</span><span class="nx">CHANGE_NAME</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">payload</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="na">name</span><span class="p">:</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">name</span> <span class="p">})</span> <span class="p">},</span> <span class="nx">defaultState</span> <span class="p">);</span> </code></pre></div></div> <p>여기까지 간단하게 <code class="highlighter-rouge">redux-actions</code>의 대체 함수를 만들어보았다. 사실 구현되지 않은 기능들이 리덕스 액션에 많이 존재하지만 간단한 사용 용도로는 이정도 구현도 적절하리라 생각한다. 필요에 따라서 추가로 구현하거나 하면 될 것 같다. 타입스크립트를 사용하면서 <code class="highlighter-rouge">redux-actions</code>를 그리워 하는 사람들에게 도움이 되었으면 좋겠다.</p>Redux-Actionsmobx-react v6 마이그레이션하기2019-09-09T03:02:48+00:002019-09-09T03:02:48+00:00https://jonir227.github.io/develop/2019/09/09/mobx-react-v6-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98%ED%95%98%EA%B8%B0<p><code class="highlighter-rouge">mobx-react</code>는 <code class="highlighter-rouge">observable</code>한 상태들, <code class="highlighter-rouge">computed</code>로 계산된 결과를 메모이즈 해둘 수 있는 기능들 등 <code class="highlighter-rouge">redux</code>와는 다른 매력을 가지고 있는 라이브러리이다. 하지만 <code class="highlighter-rouge">react</code>가 16.8버전에 도입된 <code class="highlighter-rouge">hooks</code>와 호환이 안되는 문제가 발생하게 되었는데, 이 글에서는 그 문제가 무엇인지, <code class="highlighter-rouge">mobx-react</code>는 어떻게 그 방법을 해결했는지를 작성해보고 실제 코드에서 어떻게 마이그레이션 할지를 고민해본 내용을 작성해 볼 생각이다.</p> <h1 id="mobx-react-v5의-문제점"><code class="highlighter-rouge">mobx-react</code> v5의 문제점</h1> <p>이 버전이 가지고 있는 가장 큰 문제는 컴포넌트들의 상태 공유를 하기 위하여 <code class="highlighter-rouge">legacy context</code>를 사용한다는 점이다. 레거시 컨텍스트는 이후 <code class="highlighter-rouge">react</code>에서 제거될 기능이기 때문에 계속해서 사용한다면 리액트 버전을 최신으로 유지하는 데에 문제가 될 여지가 있다.</p> <p>두번째 문제점은 <code class="highlighter-rouge">observer</code>함수 자체가 클래스를 위해서 만들어진 함수이기 때문에 현재 <code class="highlighter-rouge">hook</code>과는 호환되지 않는 문제점이다.</p> <p><code class="highlighter-rouge">hook</code>을 사용하고싶어서 함수 컴포넌트로 코드를 작성하고 난 다음, 코드를 실행시키면 클래스 컴포넌트에서 <code class="highlighter-rouge">hook</code>을 사용할 수 없다는 에러 메시지를 본 것은 필자 뿐만 아니라 많은 사람들이 겪었을 문제라고 생각한다. <code class="highlighter-rouge">mobx</code>에서는 아래의 코드 처럼 함수일 경우에는 클래스로 래핑하여 재귀적으로 호출함으로서 해결하였다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">observer</span><span class="p">(</span><span class="nx">arg1</span><span class="p">,</span> <span class="nx">arg2</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ... 기타 내용들</span> <span class="c1">// Stateless function component:</span> <span class="c1">// If it is function but doesn't seem to be a react class constructor,</span> <span class="c1">// wrap it to a react class automatically</span> <span class="k">if</span> <span class="p">(</span> <span class="k">typeof</span> <span class="nx">componentClass</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">function</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="o">!</span><span class="nx">componentClass</span><span class="p">.</span><span class="nx">prototype</span> <span class="o">||</span> <span class="o">!</span><span class="nx">componentClass</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">render</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">componentClass</span><span class="p">.</span><span class="nx">isReactClass</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">Component</span><span class="p">.</span><span class="nx">isPrototypeOf</span><span class="p">(</span><span class="nx">componentClass</span><span class="p">)</span> <span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">observerComponent</span> <span class="o">=</span> <span class="nx">observer</span><span class="p">(</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Component</span> <span class="p">{</span> <span class="kd">static</span> <span class="nx">displayName</span> <span class="o">=</span> <span class="nx">componentClass</span><span class="p">.</span><span class="nx">displayName</span> <span class="o">||</span> <span class="nx">componentClass</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span> <span class="kd">static</span> <span class="nx">contextTypes</span> <span class="o">=</span> <span class="nx">componentClass</span><span class="p">.</span><span class="nx">contextTypes</span><span class="p">;</span> <span class="kd">static</span> <span class="nx">propTypes</span> <span class="o">=</span> <span class="nx">componentClass</span><span class="p">.</span><span class="nx">propTypes</span><span class="p">;</span> <span class="kd">static</span> <span class="nx">defaultProps</span> <span class="o">=</span> <span class="nx">componentClass</span><span class="p">.</span><span class="nx">defaultProps</span><span class="p">;</span> <span class="nx">render</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">componentClass</span><span class="p">.</span><span class="nx">call</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">hoistStatics</span><span class="p">(</span><span class="nx">observerComponent</span><span class="p">,</span> <span class="nx">componentClass</span><span class="p">);</span> <span class="k">return</span> <span class="nx">observerComponent</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// 기타 내용들</span> <span class="p">}</span> </code></pre></div></div> <p>리액트에 <code class="highlighter-rouge">hooks</code>이 들어온 지금 <code class="highlighter-rouge">Stateless Function Component</code>는 더이상 맞지 않는 주석이다. 함수 컴포넌트를 클래스의 랜더 함수에서 실행시켜 리턴하니 경고가 나오고 클래스의 안쪽이므로 자연스럽게 <code class="highlighter-rouge">hook</code>을 사용할 수 없게 된다.</p> <p>이런 문제점은 <code class="highlighter-rouge">mobx</code>커뮤니티에서 문제가 되었고 이런 구조들을 개선하기 위해서 라이브러리 전체를 다시 쓰게 되는데, 그래서 나온 것이 <code class="highlighter-rouge">mobx-react-lite</code>이다.</p> <h1 id="mobx-react-lite"><code class="highlighter-rouge">mobx-react-lite</code></h1> <p>이 라이브러리는 완전히 다시 쓰여진 모브엑스의 새 버전이다. 그렇다면 <code class="highlighter-rouge">mobx-react</code>는 어떻게 되는가? 다음 버전인 v6으로 업데이트 되었지만, 내부는 기존 라이브러리의 호환성을 유지하기 위해서 v5의 함수들을 이 라이브러리로 구현해 둔 것으로 앞으로 주력 개선사항은 <code class="highlighter-rouge">mobx-react-lite</code>에서 개발될 것으로 생각이 된다.</p> <p><code class="highlighter-rouge">mobx-react-lite</code>는 <code class="highlighter-rouge">hooks</code>의 도입과 <code class="highlighter-rouge">legacy context</code>의 제거 이 두가지를 가장 염두에 둔 것으로 보인다. 이 새 라이브러리가 문제를 어떻게 해결했는지 살펴보자.</p> <h2 id="legacy-context"><code class="highlighter-rouge">legacy context</code></h2> <p><code class="highlighter-rouge">redux</code>의 철학은 <code class="highlighter-rouge">single source of truth</code>이다. 따라서 라이브러리에서 관리되는 모든 상태들이 하나의 스토어에 들어있어야 한다. 그렇기 때문에 라이브러리에서 관리되는 프로바이더가 필연적이였을 것이라고 생각한다. 하지만 <code class="highlighter-rouge">mobx</code>는 그렇지 않다. 관심사에 따라서 여러개의 스토어를 만들고 필요에 따라서 컴포넌트위에 프로바이더를 만들어서 사용할 수 있다.</p> <p>새 버전에서는 레거시 컨텍스트를 들어내면서 이 철학에 맞는 가장 간단한 방법을 사용했다.</p> <blockquote> <p>컨텍스트의 컨셉이 너무나도 간단해서 라이브러리에 추가로 필요한 값들이 없었습니다. 만약 모든 앱을 hook으로 관리하도록 마이그레이션 하는데 성공한다면, Mobx에서 제공하는 Provier가 필요하지 않고 심지어 더 많은 제어가 가능해집니다. <a href="https://mobx-react.js.org/recipes-migration">(원문)</a></p> </blockquote> <p>필요하면 직접 만들어 쓰라는 말이다. 실제로 그 방법은 너무나도 간단하다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">UserContext</span> <span class="o">=</span> <span class="nx">createContext</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">UserProvider</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">children</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// class로 개발된 mobx 스토어든, mst로 작성된 스토어든, 여기서 초기화를 시켜준다.</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">userStore</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">new</span> <span class="nx">UserStore</span><span class="p">());</span> <span class="c1">// 스토어 생성 비용이 크지 않다면 이렇게 사용해도 된다.</span> <span class="kd">const</span> <span class="nx">userStore</span> <span class="o">=</span> <span class="nx">useRef</span><span class="p">(</span><span class="k">new</span> <span class="nx">UserStore</span><span class="p">()).</span><span class="nx">current</span><span class="p">;</span> <span class="c1">// 공식 문서에서는 useMemo는 리액트에서 랜덤하게 값을 버려버리기 때문에 사용하지 말라고 권장하고 있다.</span> <span class="c1">// const userStore = useMemo(() =&gt; new UserStore(), []);</span> <span class="k">return</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">UserContext</span><span class="p">.</span><span class="nx">Provider</span> <span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="nx">userStore</span><span class="p">}</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">children</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/UserContext.Provider</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">};</span> <span class="c1">// 값이 필요하다면 그냥 useContext를 사용하면 된다.</span> <span class="kd">const</span> <span class="nx">UserProfile</span> <span class="o">=</span> <span class="p">()</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">useContext</span><span class="p">(</span><span class="nx">UserContext</span><span class="p">);</span> <span class="c1">// useObserver에 대해서는 아래에 설명이 되어있다.</span> <span class="k">return</span> <span class="nx">useObserver</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">userStore</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span> <span class="o">-</span> <span class="p">{</span><span class="nx">userStore</span><span class="p">.</span><span class="nx">age</span><span class="p">}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">));</span> <span class="p">};</span> </code></pre></div></div> <p>이렇게 함으로서 <code class="highlighter-rouge">mobx</code>는 <code class="highlighter-rouge">inject</code>를 사용할 필요가 없어졌다. 간결하게 작성할수 있으며 context를 이용하기 때문에 타입스크립트의 타입 추론도 더 잘 받을수 있게 되었다.</p> <p>사실 <code class="highlighter-rouge">mobx-react-lite</code>가 훅을 어떻게 사용하는지 들여다본다면 <code class="highlighter-rouge">Provider</code>의 존재조차 필요가 없다. 자세한 내용은 <code class="highlighter-rouge">hook</code>을 설명하면서 상세히 들여다 보겠다.</p> <h2 id="hooks"><code class="highlighter-rouge">hooks</code></h2> <p><code class="highlighter-rouge">mobx-react</code>에서 많은 훅이 추가가 되었지만 가장 눈여겨 볼 만한 훅은 <code class="highlighter-rouge">useObserver</code>이다. 사실상 이 훅이 <code class="highlighter-rouge">mobx</code>와 리액트를 이어주는 가장 핵심적인 <code class="highlighter-rouge">hook</code>이기 때문이다.</p> <p>새로 추가된 <code class="highlighter-rouge">useLocalStore</code>같은 함수들은 공식 문서를 보면 사용법이 잘 나와있기 때문에 따로 확인하고 넘어가지 않겠다.</p> <p>핵심적인 내용을 위해서 몇 가지 내용을 제거한 코드를 보자. <code class="highlighter-rouge">mobx</code>의 <code class="highlighter-rouge">reaction</code>을 알고 있다면 꽤 간단하게 짜여져 있다.</p> <p>우선 <code class="highlighter-rouge">mobx</code>의 <code class="highlighter-rouge">Raction</code>에 대해서 짚어보고 넘어가자. 공식 문서에 있는 <code class="highlighter-rouge">reaction</code>과는 살짝 다르다. <code class="highlighter-rouge">reaction</code>은 외부로 노출된 함수이고 <code class="highlighter-rouge">Reaction</code>은 실제로 기능을 하는 클래스라고 생각하면 될 것 같다. 아래는 <code class="highlighter-rouge">Reaction</code>코드의 <a href="https://github.com/mobxjs/mobx/blob/master/src/core/reaction.ts#L35">주석</a>에 쓰여져 있는 동작 과정이다.</p> <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> * The state machine of a Reaction is as follows: * * 1) 인스턴스가 생성된 뒤에는 reaction은 반드시 runReaction을 호출하거나 스케줄링함으로서 시작되어야 합니다. * 2) `onInvalidate`는 `this.track(someFunction)`를 호출하는 함수여야 합니다. * 3) `someFunction`에서 접근되는 모든 옵저버블은 이 reaction에 의해서 관찰되어집니다. * 4) Reaction의 someFunction의 디펜던시가 변경되게 되면 이 다음 실행때 리스케줄됩니다. 디펜던시가 변경되었을때 `isScheduled`가 ture로 변경됩니다. * 5) `onInvalidate`가 실행되고, 1번으로 되돌아갑니다. </code></pre></div></div> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 원본 코드</span> <span class="c1">// https://github.com/mobxjs/mobx-react-lite/blob/master/src/useObserver.ts</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">Reaction</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">mobx</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">useDebugValue</span><span class="p">,</span> <span class="nx">useRef</span><span class="p">,</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">printDebugValue</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./printDebugValue</span><span class="dl">"</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">isUsingStaticRendering</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./staticRendering</span><span class="dl">"</span><span class="p">;</span> <span class="k">export</span> <span class="kd">function</span> <span class="nx">useObserver</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span> <span class="nx">fn</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">T</span><span class="p">,</span> <span class="nx">baseComponentName</span><span class="p">:</span> <span class="nx">string</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">observed</span><span class="dl">"</span> <span class="p">):</span> <span class="nx">T</span> <span class="p">{</span> <span class="c1">// 강제로 업데이트하는 setState를 만들어준다.</span> <span class="c1">// 업데이트의 주체가 리액트가 아닌 상태관리 컴포넌트에서 흔히 사용되는 패턴이다.</span> <span class="kd">const</span> <span class="p">[,</span> <span class="nx">setTick</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">forceUpdate</span> <span class="o">=</span> <span class="nx">useCallback</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">setTick</span><span class="p">(</span><span class="nx">tick</span> <span class="o">=&gt;</span> <span class="nx">tick</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span> <span class="p">},</span> <span class="p">[]);</span> <span class="c1">// reaction을 만들어 준 다음, 초기화를 시켜준다.</span> <span class="c1">// 이때, reaction이 일어날때마다 강제로 리랜더를 시켜준다.</span> <span class="kd">const</span> <span class="nx">reaction</span> <span class="o">=</span> <span class="nx">useRef</span><span class="o">&lt;</span><span class="nx">Reaction</span> <span class="o">|</span> <span class="kc">null</span><span class="o">&gt;</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">reaction</span><span class="p">.</span><span class="nx">current</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reaction</span><span class="p">.</span><span class="nx">current</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Reaction</span><span class="p">(</span><span class="s2">`observer(</span><span class="p">${</span><span class="nx">baseComponentName</span><span class="p">}</span><span class="s2">)`</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">forceUpdate</span><span class="p">();</span> <span class="c1">// 리랜더가 되므로 아래 라인의 track을 다시 호출한다. `onInvalidate`의 조건을 충족시킴</span> <span class="p">});</span> <span class="p">}</span> <span class="c1">// cleanup 함수를 만들어주고 등록해준다.</span> <span class="kd">const</span> <span class="nx">dispose</span> <span class="o">=</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">reaction</span><span class="p">.</span><span class="nx">current</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">reaction</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">isDisposed</span><span class="p">)</span> <span class="p">{</span> <span class="nx">reaction</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">dispose</span><span class="p">();</span> <span class="p">}</span> <span class="p">};</span> <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">dispose</span><span class="p">,</span> <span class="p">[]);</span> <span class="c1">// reaction의 안쪽에 인자로 받은 함수를 실행시켜주고, 스케줄링한다.</span> <span class="c1">// 따라서, 인자로 넘겨주는 함수의 안쪽에 observable이 존재하면 이 값이 변경될때마다 리랜더가 되는 것이다.</span> <span class="c1">// 그렇기 때문에 useObserver로 넘겨주는 값들이 비구조화 할당을 하여 주소가 아닌 값이 된다면 값을 추적하지 못하는 것이다.</span> <span class="c1">// render the original component, but have the</span> <span class="c1">// reaction track the observables, so that rendering</span> <span class="c1">// can be invalidated (see above) once a dependency changes</span> <span class="kd">let</span> <span class="nx">rendering</span><span class="o">!</span><span class="p">:</span> <span class="nx">T</span><span class="p">;</span> <span class="kd">let</span> <span class="nx">exception</span><span class="p">;</span> <span class="nx">reaction</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">track</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="nx">rendering</span> <span class="o">=</span> <span class="nx">fn</span><span class="p">();</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">exception</span> <span class="o">=</span> <span class="nx">e</span><span class="p">;</span> <span class="p">}</span> <span class="p">});</span> <span class="k">if</span> <span class="p">(</span><span class="nx">exception</span><span class="p">)</span> <span class="p">{</span> <span class="nx">dispose</span><span class="p">();</span> <span class="k">throw</span> <span class="nx">exception</span><span class="p">;</span> <span class="c1">// re-throw any exceptions catched during rendering</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">rendering</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>눈치가 빠른 사람들은 왜 <code class="highlighter-rouge">Provider</code>가 필요없는지 알 수 있을 것이다. 업데이트 로직이 훅 안에 전부 존재함으로서 더이상 context를 통해서 상태를 전달받을 필요가 없어졌다. 단지 <code class="highlighter-rouge">useObserver</code>안에 <code class="highlighter-rouge">observable</code>한 값만 존재하면 된다. 이 <code class="highlighter-rouge">hook</code>이 알아서 변경을 추적해서 리랜더를 한다. 이렇게 된다면 <code class="highlighter-rouge">store</code>를 싱글톤으로 만들고, 컴포넌트에서 그냥 가져다가 쓰는 방식으로 사용할 수도 있다. 물론 <code class="highlighter-rouge">mobx</code> 공식 문서에서는 <a href="https://mobx-react.js.org/recipes-context">테스트를 위해서라면 권장하지 않는다</a>고 쓰여져 있다.</p> <h1 id="마이그레이션하기">마이그레이션하기</h1> <p>이제 어느정도 두 버전 사이의 차이를 알아보았으니, 점진적인 마이그레이션을 위해서 <code class="highlighter-rouge">mobx</code>가 어떤 방법을 내놓았는지 보자.</p> <p>눈여겨볼 만한 변경점은 <code class="highlighter-rouge">mobx-react</code> v6의 <code class="highlighter-rouge">Provider</code>의 변화이다. 공식 문서에 써져있던대로 거의 손을 대지 않고 <code class="highlighter-rouge">react</code>의 <code class="highlighter-rouge">context</code>를 그대로 사용하고 있다. 역시 불필요한 코드는 제거했다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 원본 코드</span> <span class="c1">// https://github.com/mobxjs/mobx-react/blob/master/src/Provider.js</span> <span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span> <span class="c1">// 컨텍스트를 만든다.</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">MobXProviderContext</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createContext</span><span class="p">({});</span> <span class="k">export</span> <span class="kd">function</span> <span class="nx">Provider</span><span class="p">({</span> <span class="nx">children</span><span class="p">,</span> <span class="p">...</span><span class="nx">stores</span> <span class="p">})</span> <span class="p">{</span> <span class="c1">// 상위 컨텍스트의 스토어들 가져온다.</span> <span class="kd">const</span> <span class="nx">parentValue</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">useContext</span><span class="p">(</span><span class="nx">MobXProviderContext</span><span class="p">);</span> <span class="c1">// 부모의 컨텍스트 값과 새로운 스토어들을 가져와 합쳐준다.</span> <span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">useRef</span><span class="p">({</span> <span class="p">...</span><span class="nx">parentValue</span><span class="p">,</span> <span class="p">...</span><span class="nx">stores</span> <span class="p">}).</span><span class="nx">current</span><span class="p">;</span> <span class="k">return</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">MobXProviderContext</span><span class="p">.</span><span class="nx">Provider</span> <span class="nx">value</span><span class="o">=</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="nx">children</span><span class="p">}</span> <span class="o">&lt;</span><span class="sr">/MobXProviderContext.Provider</span><span class="err">&gt; </span> <span class="p">);</span> <span class="p">}</span> <span class="nx">Provider</span><span class="p">.</span><span class="nx">displayName</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">MobXProvider</span><span class="dl">"</span><span class="p">;</span> </code></pre></div></div> <p>몇 줄만으로 <code class="highlighter-rouge">Provider</code>의 구현이 끝났다. 마찬가지로 사용도 간단하다. <code class="highlighter-rouge">MobxProviderContext</code>는 상위 컨텍스트의 값을 가져와서 사용하기 때문에 컴포넌트에서 호출만 하면 된다. Inject도, observe로 감싸줄 필요가 없다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">UserProfile</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Inject에 대응하는 부분이다.</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">userStore</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useContext</span><span class="p">(</span><span class="nx">MobxProviderContext</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">userStore</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="dl">"</span><span class="s2">userStore가 없습니다</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// observe에 대응하는 부분이다.</span> <span class="k">return</span> <span class="nx">useObserver</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">(</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span> <span class="p">{</span><span class="nx">userStore</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span> <span class="o">-</span> <span class="p">{</span><span class="nx">userStore</span><span class="p">.</span><span class="nx">age</span><span class="p">}</span> <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span> <span class="p">));</span> <span class="c1">// 혹은, 랜더 시에 필요한 특정 값만 필요하다면 이렇게도 사용할 수 있다.</span> <span class="kd">const</span> <span class="nx">businessLogicProfile</span> <span class="o">=</span> <span class="nx">useObserver</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="o">!</span><span class="nx">userStore</span><span class="p">.</span><span class="kr">public</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="dl">"</span><span class="s2">조회할수 없는 프로필입니다</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">userStore</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2"> - </span><span class="p">${</span><span class="nx">userStore</span><span class="p">.</span><span class="nx">age</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span> <span class="p">});</span> <span class="k">return</span> <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">businessLogicProfile</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/div&gt;</span><span class="err">; </span><span class="p">};</span> </code></pre></div></div> <p><code class="highlighter-rouge">mobx</code>는 그 자유도 때문에 프로젝트 구조가 매우 다양할 것이기 때문에 적절한 hook을 작성하여 스토어를 가져다가 쓰면 될 것 같다. 기본 구현이 매우 간단하기 때문이다.</p> <p>그러면 새로운 코드는 어떻게 짜면 좋을까? 간단하게나마 <a href="https://codesandbox.io/s/friendly-pine-x2fid?fontsize=14">예제</a>를 작성해 보았다. 가장 좋았던 점은 더이상 타입스크립트의 타입 추론을 !를 사용하여 강제시킬 필요가 없다는 점인것같다.</p> <p><code class="highlighter-rouge">useObserver</code>의 사용이 살짝 불편한 감이 없지 않아 있는것 같지만 어쨋든 새로운 기능을 사용해보는 것은 언제나 즐거운 일인 것 같다.</p> <hr /> <p>지금까지 간략하게 <code class="highlighter-rouge">mobx-react</code>의 내부적인 변화를 살펴보고 어떻게 새 변화에 맞추어 기존 코드베이스에 적용할지를 작성해보았다. 마이그레이션 부분은 너무나도 많은 자유도가 있기 때문에 내용이 부족한 부분이 있다고 생각되지만 고민하고 있는 사람들에게 많은 도움이 되었으면 좋겟다.</p>mobx-react는 observable한 상태들, computed로 계산된 결과를 메모이즈 해둘 수 있는 기능들 등 redux와는 다른 매력을 가지고 있는 라이브러리이다. 하지만 react가 16.8버전에 도입된 hooks와 호환이 안되는 문제가 발생하게 되었는데, 이 글에서는 그 문제가 무엇인지, mobx-react는 어떻게 그 방법을 해결했는지를 작성해보고 실제 코드에서 어떻게 마이그레이션 할지를 고민해본 내용을 작성해 볼 생각이다.redux-saga와 typescript 편하게 사용하기2019-06-04T06:02:15+00:002019-06-04T06:02:15+00:00https://jonir227.github.io/develop/2019/06/04/redux-saga%EC%99%80-typescript-%ED%8E%B8%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0<p>나는 리덕스 사가의 팬이다. 작은 단위로 코드를 쪼개놓아 재사용성이 늘어나는 것도 좋고, 개발자의 코드가 개발자가 실행 시점을 정의하는게 아닌 <strong>사가가 실행의 주체</strong>가 된다는 점도 좋다. 작년 말부터 사가를 쓰기 시작해서 어느정도 비동기 호출이 있는 프로젝트에는 항상 사가를 넣어두고 시작할정도로 지금은 사가 없이는 살수 없는 사람이 되어버렸다.</p> <p>그러던 중 <code class="highlighter-rouge">TypeScript</code>를 알게 되었고, <code class="highlighter-rouge">redux-saga</code>와 <code class="highlighter-rouge">TypeScript</code>를 함께 쓰면서 꽤 만족스러운 패턴을 작성하게 되어 이것을 소개하고자 글을 작성하게 되었다.</p> <h2 id="fetchentity"><code class="highlighter-rouge">FetchEntity</code></h2> <p>사가를 쓰면서 가장 즐겨 썼던 패턴은 <code class="highlighter-rouge">redux-saga</code>의 <a href="https://github.com/redux-saga/redux-saga/blob/master/examples/real-world/sagas/index.js">real-world-example</a>에서의 <code class="highlighter-rouge">fetchEntity</code>함수였다. 나는 내가 사용하기 편하게 조금 다른 방식으로 작성했다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fetchEntity</span> <span class="o">=</span> <span class="p">(</span><span class="nx">entitiy</span><span class="p">,</span> <span class="nx">apiFn</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kd">function</span><span class="o">*</span><span class="p">(...</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">entitiy</span><span class="p">.</span><span class="nx">request</span><span class="p">());</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span><span class="nx">apiFN</span><span class="p">,</span> <span class="p">...</span><span class="nx">params</span><span class="p">);</span> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">entity</span><span class="p">.</span><span class="nx">success</span><span class="p">(</span><span class="nx">data</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="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">entitiy</span><span class="p">.</span><span class="nx">failure</span><span class="p">());</span> <span class="p">}</span> <span class="p">};</span> </code></pre></div></div> <p>비동기 호출의 단위를 <code class="highlighter-rouge">entity</code>로 관리하고 이 호출의 루틴을 함수로 만들어 재사용성을 높인 함수이다.</p> <p>여기서 안보이는 <code class="highlighter-rouge">entity</code>는 이렇게 생겼다. 역시 개인적으로 사용하기 편하게 간략하게 작성했다.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="na">request</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER_REQUEST</span><span class="dl">'</span> <span class="p">}),</span> <span class="na">success</span><span class="p">:</span> <span class="nx">user</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER_SUCCESS</span><span class="dl">'</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">user</span> <span class="p">}</span> <span class="p">}),</span> <span class="na">failure</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER_FAILURE</span><span class="dl">'</span> <span class="p">}),</span> <span class="p">};</span> </code></pre></div></div> <p>정리하자면 각 비동기 단계의 <code class="highlighter-rouge">action</code>을 정의한 것이 <code class="highlighter-rouge">entitiy</code>인 것이다. 매번 이렇게 세가지 액션을 만들어주는 것이 귀찮으므로 함수를 작성해서 사용하자.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">createEntityAction</span> <span class="o">=</span> <span class="nx">entity</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">request</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">entity</span><span class="p">.</span><span class="nx">REQUEST</span> <span class="p">}),</span> <span class="na">success</span><span class="p">:</span> <span class="nx">data</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">entitiy</span><span class="p">.</span><span class="nx">SUCCESS</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="nx">data</span> <span class="p">}),</span> <span class="na">failure</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">entity</span><span class="p">.</span><span class="nx">FAILURE</span> <span class="p">}),</span> <span class="p">});</span> </code></pre></div></div> <p>이 패턴을 사용하면 간단한 비동기 호출은 정말 편하게 작성할 수 있다. <code class="highlighter-rouge">user</code>의 정보를 가져온다고 생각해보자.</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fetchUserAPI</span> <span class="o">=</span> <span class="k">async</span> <span class="nx">userId</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="kd">get</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">`</span><span class="p">)).</span><span class="nx">data</span><span class="p">;</span> <span class="c1">// Action Types</span> <span class="kd">const</span> <span class="nx">FETCH_USER</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">FETCH_USER</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="p">{</span> <span class="na">REQUEST</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER_REQUEST</span><span class="dl">'</span><span class="p">,</span> <span class="na">SUCCESS</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER_SUCCESS</span><span class="dl">'</span><span class="p">,</span> <span class="na">FAILURE</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER_FAILURE</span><span class="dl">'</span><span class="p">,</span> <span class="p">};</span> <span class="c1">// action. 컴포넌트에 커넥트 시켜서 사용</span> <span class="kd">const</span> <span class="nx">fetchUser</span> <span class="o">=</span> <span class="nx">id</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">FETCH_USER</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="p">});</span> <span class="c1">// Entity</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">createEntityAction</span><span class="p">(</span><span class="nx">USER</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">fetchUserSaga</span> <span class="o">=</span> <span class="nx">fetchEntity</span><span class="p">(</span><span class="nx">user</span><span class="p">,</span> <span class="nx">fetchUserAPI</span><span class="p">);</span> <span class="kd">function</span><span class="o">*</span> <span class="nx">fetchUserWatcher</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">},</span> <span class="p">}</span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">take</span><span class="p">(</span><span class="nx">FETCH_USER</span><span class="p">);</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span><span class="nx">fetchUserSaga</span><span class="p">,</span> <span class="nx">id</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p><code class="highlighter-rouge">fetchEntity</code>패턴을 사용하면 기본적인 비동기 호출은 대부분 대응이 가능하다. <code class="highlighter-rouge">fetchUserWatcher</code>에서는 <code class="highlighter-rouge">call</code>을 사용했지만, 상황에 따라서 <code class="highlighter-rouge">fork</code>나, 혹은 <code class="highlighter-rouge">watcher</code>자체를 <code class="highlighter-rouge">takeLatest</code>나 <code class="highlighter-rouge">takeLeading</code>으로 사용해도 무방하다.</p> <h2 id="typescript">TypeScript</h2> <blockquote> <p>이하는 타입스크립트 3.4버전 이상을 기준으로 작성되었습니다.</p> </blockquote> <p>위의 패턴에서 타입스크립트를 붙여보자. 개인적으로는 <strong>사용하는 쪽에서 제네릭을 최대한으로 사용하지 않는</strong> 형태로 작성하는 것을 선호한다. 개발자가 통제할 수 없는 부분에서만 제네릭을 넣는 것이 사용성과 생산성을 높이는 것이라고 생각하기 때문이다.</p> <p>우선 <code class="highlighter-rouge">API</code>호출을 하는 함수의 타입 지정을 해보자. 크게 복잡한 타입은 아니지만, 코드 길이를 줄이는데 도움을 준다. 비동기 데이터를 리턴하는 함수의 타입이다.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">type</span> <span class="nx">APIEndpoint</span><span class="o">&lt;</span><span class="nx">P</span> <span class="kd">extends</span> <span class="nx">any</span><span class="p">[],</span> <span class="nx">R</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(...</span><span class="nx">p</span><span class="p">:</span> <span class="nx">P</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">R</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p>그 다음은 api 호출을 하는 함수를 작성 해보자.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fetchUserAPI</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">userID</span><span class="p">:</span> <span class="nx">number</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="k">await</span> <span class="nx">axios</span><span class="p">.</span><span class="kd">get</span><span class="o">&lt;</span><span class="nx">IUser</span><span class="o">&gt;</span><span class="p">(</span><span class="s2">`/user/</span><span class="p">${</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">)).</span><span class="nx">data</span><span class="p">;</span> </code></pre></div></div> <p>인터페이스 <code class="highlighter-rouge">IUser</code>는 여기서 중요한 부분이 아니라고 생각되어 작성하지 않았다. 함수는 간단하다. <code class="highlighter-rouge">userID</code>를 받아서 <code class="highlighter-rouge">IUser</code>를 서버에서 받아온 후 그것을 리턴 하는 함수이다.</p> <p>이제 액션 타입을 작성 해보자.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Action Type</span> <span class="kd">const</span> <span class="nx">GET_USER</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">GET_USER</span><span class="dl">'</span> <span class="k">as</span> <span class="kd">const</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">USER</span> <span class="o">=</span> <span class="p">{</span> <span class="na">REQUEST</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET_USER_REQUEST</span><span class="dl">'</span><span class="p">,</span> <span class="na">SUCCESS</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET_USER_SUCCESS</span><span class="dl">'</span><span class="p">,</span> <span class="na">FAILURE</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET_USER_FAILURE</span><span class="dl">'</span><span class="p">,</span> <span class="p">}</span> <span class="k">as</span> <span class="kd">const</span><span class="p">;</span> </code></pre></div></div> <p>주목해 보아야 할 부분은 <code class="highlighter-rouge">as const</code>의 사용이다. <code class="highlighter-rouge">as const</code>구문은 타입스크립트 3.4버전에서 추가된 기능이다. 상세한 문법의 스펙은 다음과 같다.</p> <ul> <li>리터럴의 타입은 확장될 수 없다(<code class="highlighter-rouge">'hello'</code>에서 <code class="highlighter-rouge">string</code>으로 확장되는 행위 불가능)</li> <li>오브젝트 리터럴은 <code class="highlighter-rouge">readonly</code>속성을 가지게 된다.</li> <li>배열 리터럴은 <code class="highlighter-rouge">readonly</code> 튜플이 된다.</li> </ul> <p>따라서, 이 객체나 액션 타입의 선언은 그 자체로 타입과 비슷하게 동작하게 된다. 이렇게 처리하게 되면 리듀서에서 스위치케이스 문을 작성하는 데에 아주 유용하게 사용할 수 있다.</p> <p>이제 액션 크리에이이터를 작성해 보자. 우선 <code class="highlighter-rouge">getUser</code>를 먼저 작성했다.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">getUser</span> <span class="o">=</span> <span class="p">(</span><span class="nx">userId</span><span class="p">:</span> <span class="nx">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">GET_USER</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">userId</span> <span class="p">});</span> <span class="nx">type</span> <span class="nx">GetUser</span> <span class="o">=</span> <span class="nx">ReturnType</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">getUser</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p><code class="highlighter-rouge">ReturnType</code>은 타입스크립트 2.8버전에서 추가된 타입으로, 함수의 반환값을 잡아준다.</p> <p>3.4버전 <code class="highlighter-rouge">as const</code>가 없었을 때에는 액션크리에이터에 사용이 불가능했지만, <code class="highlighter-rouge">as const</code>로 액션크리에이터의 타입을 잡아주는데 굉장히 편한 타입이 되었다.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GET_USER에 as const 가 없을떄</span> <span class="nx">type</span> <span class="nx">GetUser</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="nl">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nl">userId</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="p">};</span> <span class="p">};</span> <span class="c1">// GET_USER에 as const가 있을때</span> <span class="nx">type</span> <span class="nx">GetUser</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET_USER</span><span class="dl">'</span><span class="p">;</span> <span class="nl">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nl">userId</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="p">};</span> <span class="p">};</span> </code></pre></div></div> <p>그 다음은 <code class="highlighter-rouge">entity</code>를 만들어주는 함수를 작성해보자. 이번에는 자바스크립트로 작성했을때와는 모양이 조금 다르다.</p> <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">IEntity</span><span class="o">&lt;</span><span class="nx">R</span><span class="p">,</span> <span class="nx">S</span><span class="p">,</span> <span class="nx">F</span><span class="o">&gt;</span> <span class="p">{</span> <span class="na">REQUEST</span><span class="p">:</span> <span class="nx">R</span><span class="p">;</span> <span class="nl">SUCCESS</span><span class="p">:</span> <span class="nx">S</span><span class="p">;</span> <span class="nl">FAILURE</span><span class="p">:</span> <span class="nx">F</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// 액션을 만들어 내는 데에 필요한 것은 DATA타입이다.</span> <span class="c1">// SUCCESS액션의 리턴타입에 타입 지정을 해주기 해주기 위해서 반드시 필요하다.</span> <span class="c1">// 이때, api를 모르는 상태에서는 반드시 제네릭으로 넣어주는 것이 필요한데,</span> <span class="c1">// 제네릭을 넣어주는 방법을 우회하기 위해서 api와 액션을 함께 묶어 객체로 만들어주는 방법을 택했다.</span> <span class="kd">const</span> <span class="nx">createEntityAction</span> <span class="o">=</span> <span class="o">&lt;</span><span class="nx">R</span><span class="p">,</span> <span class="nx">S</span><span class="p">,</span> <span class="nx">F</span><span class="p">,</span> <span class="nx">PARAM</span> <span class="kd">extends</span> <span class="nx">any</span><span class="p">[],</span> <span class="nx">DATA</span><span class="o">&gt;</span><span class="p">(</span> <span class="na">entitiy</span><span class="p">:</span> <span class="nx">IEntity</span><span class="o">&lt;</span><span class="nx">R</span><span class="p">,</span> <span class="nx">S</span><span class="p">,</span> <span class="nx">F</span><span class="o">&gt;</span><span class="p">,</span> <span class="na">api</span><span class="p">:</span> <span class="nx">ApiEndpoint</span><span class="o">&lt;</span><span class="nx">DATA</span><span class="p">,</span> <span class="nx">PARAM</span><span class="o">&gt;</span> <span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">ACTION</span><span class="p">:</span> <span class="p">{</span> <span class="na">REQUEST</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">entitiy</span><span class="p">.</span><span class="nx">REQUEST</span> <span class="p">}),</span> <span class="na">SUCCESS</span><span class="p">:</span> <span class="p">(</span><span class="na">data</span><span class="p">:</span> <span class="nx">DATA</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">entitiy</span><span class="p">.</span><span class="nx">SUCCESS</span><span class="p">,</span> <span class="na">payload</span><span class="p">:</span> <span class="nx">data</span> <span class="p">}),</span> <span class="na">FAILURE</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="nx">entitiy</span><span class="p">.</span><span class="nx">FAILURE</span> <span class="p">})</span> <span class="p">},</span> <span class="na">API</span><span class="p">:</span> <span class="nx">api</span> <span class="p">})</span> <span class="c1">// 타입을 끄집어 내기 위한 헬퍼 타입</span> <span class="kr">interface</span> <span class="nx">IEntityAction</span> <span class="p">{</span> <span class="na">ACTION</span><span class="p">:</span> <span class="p">{</span> <span class="na">REQUEST</span><span class="p">:</span> <span class="p">(...</span><span class="na">p</span><span class="p">:</span> <span class="nx">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">any</span><span class="p">;</span> <span class="nl">SUCCESS</span><span class="p">:</span> <span class="p">(...</span><span class="na">p</span><span class="p">:</span> <span class="nx">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">any</span><span class="p">;</span> <span class="nl">FAILURE</span><span class="p">:</span> <span class="p">(...</span><span class="na">p</span><span class="p">:</span> <span class="nx">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">any</span><span class="p">;</span> <span class="p">[</span><span class="na">key</span><span class="p">:</span> <span class="nx">string</span><span class="p">]:</span> <span class="p">(...</span><span class="na">p</span><span class="p">:</span> <span class="nx">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="nx">any</span><span class="p">;</span> <span class="p">};</span> <span class="nl">API</span><span class="p">:</span> <span class="nx">ApiEndpoint</span><span class="o">&lt;</span><span class="nx">any</span><span class="p">,</span> <span class="nx">any</span><span class="o">&gt;</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// 액션 타입 추출을 위한 타입</span> <span class="c1">// ACTION의 각 단계의 리턴타입을 가져왔다.</span> <span class="nx">EntityAction</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">IEntityAction</span><span class="o">&gt;</span> <span class="o">=</span> <span class="nx">ReturnType</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">[</span><span class="dl">'</span><span class="s1">ACTION</span><span class="dl">'</span><span class="p">][</span><span class="nx">keyof</span> <span class="nx">T</span><span class="p">[</span><span class="dl">'</span><span class="s1">ACTION</span><span class="dl">'</span><span class="p">]]</span><span class="o">&gt;</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">userEntity</span> <span class="o">=</span> <span class="nx">createEntityAction</span><span class="p">(</span><span class="nx">USER</span><span class="p">,</span> <span class="nx">getUserApi</span><span class="p">);</span> <span class="nx">type</span> <span class="nx">UserEntity</span> <span class="o">=</span> <span class="nx">EntityAction</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">userEntity</span><span class="o">&gt;</span><span class="p">;</span> </code></pre></div></div> <p>액션이 api에서 리턴하는 타입을 반드시 알고 있어야 하기 때문에 api를 인자로 넣어 주었다. 여러 가지 방법을 시도해 보았지만, 저 세가지 함수를 자동으로 만들어 주기 위해서는 어떤 부분에서 api에서 리턴하는 타입을 반드시 제네릭으로 넣어주어야 했기 때문에 다른 방법을 찾았던 것이 이 방법 이였다.</p> <p><code class="highlighter-rouge">Entity</code>가 api 앤드포인트 정보를 포함함에 따라서 <code class="highlighter-rouge">fetchEntity</code>의 모양도 달라지게 된다.</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">fetchEntity</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">IEntityAction</span><span class="o">&gt;</span><span class="p">({</span> <span class="nx">ACTION</span><span class="p">,</span> <span class="nx">API</span> <span class="p">}:</span> <span class="nx">T</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="kd">function</span><span class="o">*</span><span class="p">(...</span><span class="nx">p</span><span class="p">:</span> <span class="nx">Parameters</span><span class="o">&lt;</span><span class="nx">T</span><span class="p">[</span><span class="dl">'</span><span class="s1">API</span><span class="dl">'</span><span class="p">]</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span> <span class="k">try</span> <span class="p">{</span> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">ACTION</span><span class="p">.</span><span class="nx">REQUEST</span><span class="p">());</span> <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span><span class="nx">API</span><span class="p">,</span> <span class="p">...</span><span class="nx">p</span><span class="p">);</span> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">ACTION</span><span class="p">.</span><span class="nx">SUCCESS</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="k">yield</span> <span class="nx">put</span><span class="p">(</span><span class="nx">ACTION</span><span class="p">.</span><span class="nx">FAILURE</span><span class="p">());</span> <span class="p">}</span> <span class="p">};</span> <span class="p">}</span> </code></pre></div></div> <p><code class="highlighter-rouge">Parameters&lt;T&gt;</code>는 <code class="highlighter-rouge">ReturnType&lt;T&gt;</code>와 비슷하게 동작한다. 함수에서 인자의 타입을 추출하는 유틸리티 타입인데, <code class="highlighter-rouge">API</code>함수에서 받는 인자를 그대로 리턴되는 함수에서 받기 위해서 사용했다.</p> <p>이렇게 작성했으면 이제 사가를 작성하면 된다.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">getUserSaga</span> <span class="o">=</span> <span class="nx">fetchEntity</span><span class="p">(</span><span class="nx">userEntity</span><span class="p">);</span> <span class="kd">function</span><span class="o">*</span> <span class="nx">getUserWatcher</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">{</span> <span class="na">payload</span><span class="p">:</span> <span class="p">{</span> <span class="nx">userId</span> <span class="p">},</span> <span class="p">}:</span> <span class="nx">GetUser</span> <span class="o">=</span> <span class="k">yield</span> <span class="nx">take</span><span class="p">(</span><span class="nx">GET_USER</span><span class="p">);</span> <span class="c1">// fetchEntity가 타입을 제대로 지정해주기 때문에 userId인자의 타입까지 확인해준다.</span> <span class="k">yield</span> <span class="nx">call</span><span class="p">(</span><span class="nx">getUserSaga</span><span class="p">,</span> <span class="nx">userId</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>여기까지 자바스크립트로 작성된 패턴을 타입스크립트로 다시 작성해 보았다. 부족한 실력이지만 이 글을 통해서 사가와 타입스크립트로 프로젝트를 작성하는 사람들에게 도움이 되었으면 좋겠다.</p> <hr /> <p>혹시 오타나 잘못된 내용이 있다면 피드백 주시면 감사하겠습니다.</p> <p>전체 코드는 <a href="https://gist.github.com/Jonir227/b7fc8b5b0646b7a90c26bd73a70c12b9">이곳</a>에서 확인할 수 있습니다.</p>나는 리덕스 사가의 팬이다. 작은 단위로 코드를 쪼개놓아 재사용성이 늘어나는 것도 좋고, 개발자의 코드가 개발자가 실행 시점을 정의하는게 아닌 사가가 실행의 주체가 된다는 점도 좋다. 작년 말부터 사가를 쓰기 시작해서 어느정도 비동기 호출이 있는 프로젝트에는 항상 사가를 넣어두고 시작할정도로 지금은 사가 없이는 살수 없는 사람이 되어버렸다.블로그 첫 글2019-03-18T07:07:15+00:002019-03-18T07:07:15+00:00https://jonir227.github.io/blog/2019/03/18/first-blog-article<p>블로그의 첫 글입니다.</p>블로그의 첫 글입니다.