<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>giwan_dev.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 25 Apr 2023 13:44:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. giwan_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/giwan_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[useEffect 없이 비동기로 가져오는 데이터를 사용하기]]></title>
            <link>https://velog.io/@giwan_dev/fetch-async-data-without-use-effect</link>
            <guid>https://velog.io/@giwan_dev/fetch-async-data-without-use-effect</guid>
            <pubDate>Tue, 25 Apr 2023 13:44:13 GMT</pubDate>
            <description><![CDATA[<h2 id="정석">정석</h2>
<p>React에서 데이터를 비동기로 가져올 때 <code>useEffect</code>를 사용한다.</p>
<pre><code class="language-tsx">function GoodOldWayToFetch() {
  const [message, setMessage] = useState&lt;string&gt;()

  useEffect(() =&gt; {
    getMessage()
      .then((message) =&gt; {
      setMessage(message)
    })
  }, [])

  if (message === undefined) {
    return &lt;Loading /&gt;
  }
  return &lt;Message message={message} /&gt;
}</code></pre>
<p>이건 <a href="https://react.dev/learn/you-might-not-need-an-effect#fetching-data">react.dev에서 <code>useEffect</code>를 쓰는 것이라고 단언하는</a> 너무나 자연스러운 정석 코드이다. 하지만 이 방법은 <code>useEffect</code>를 사용하므로 첫 렌더링 이후에 API를 요청한다.</p>
<h2 id="고민">고민</h2>
<p>요청을 좀 더 빨리할 수 없을까? 물론 클라이언트에서 렌더링하는 어플리케이션에서 데이터 조회 이전 상태도 UI 대응이 분명히 필요한 상태이지만, 만약 이전 상태가 정말로 아무 의미를 가지지 않는다면 초기 상태 UI를 대응하는 것이 불필요한 일 아닐까?</p>
<p>그리고 한 번 조회하고 말 데이터를 항상 <code>useState</code>와 <code>useEffect</code> 쌍으로 관리하는 게 장황하게도 느껴졌다. 어플리케이션의 설정 값을 리모트에서 관리한다면 변하지 않을 그 값을 위해 <code>useState</code>와 <code>useEffect</code> 쌍이 컴포넌트 안에 등장하는 게 오히려 어색하다. 거의 불변의 설정인데 이것이 &quot;상태&quot;이고 동기화해야 할 Side &quot;Effect&quot;가 발생하는가? 데이터를 조회하는 코드가 컴포넌트 외부에 있고 조회한 데이터를 컴포넌트의 prop으로 사용할 수는 없을까?</p>
<p>사실 이건 아주 오래된 고민이었다. 오랫동안 다음 코드 같은 구조를 만들어보고 싶었지만 더 자세한 구현이 생각이 나지 않아 진전이 없었다.</p>
<pre><code class="language-tsx">const promise = getMessage()

function PromisedMessage() {
  // promise를 저글링
}</code></pre>
<p>그런데 며칠 전에 React의 <code>Suspense</code>와 <code>lazy</code>를 공부하면서 내 고민에 쓸만하다는 생각이 들었다.
다음은 <code>lazy</code>와 <code>Suspense</code>로 컴포넌트 모듈을 동적으로 로드하는 예제이다.</p>
<pre><code class="language-tsx">const Lazified = lazy(() =&gt; import(&#39;./SomeComponent&#39;)) 

function Container() {
  return (
      &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
      &lt;Lazified /&gt;
    &lt;/Suspense&gt;
  )
}</code></pre>
<p>보통은 모듈을 동적으로 가져와서 해당 모듈에서 기본 내보내기한 컴포넌트를 동적으로 사용할 때 <code>lazy</code>와 <code>Suspense</code>를 사용한다.</p>
<h2 id="약간은-hacky한-느낌">약...간은 Hacky한 느낌</h2>
<p>하지만 <code>lazy</code>의 파라미터는 그냥 Promise를 반환하는 함수이다. 그럼 이 함수 안에서 일반적인 비동기 코드도 실행할 수 있는 것 아닐까?</p>
<pre><code class="language-tsx">const LazifiedMessage = lazy(async () =&gt; {
  const message = await getMessage()

  const Component = () =&gt; &lt;Message message={message} /&gt;

  return { default: Component }
})</code></pre>
<p><img src="https://velog.velcdn.com/images/giwan_dev/post/1c5684cc-7c94-4476-a800-4f547714d1a5/image.gif" alt=""></p>
<p><a href="https://codesandbox.io/p/sandbox/lazy-suspense-promise-ilnju5">잘 되는 것 같다!</a> 그럼 이제 이걸 추상화해서 쉽게 쓸 수 있게 바꿔보자.</p>
<pre><code class="language-tsx">function lazify&lt;C extends FC&lt;any&gt;, P&gt;(
  Component: C,
  getProps: () =&gt; Promise&lt;P&gt;
): LazyExoticComponent&lt;C extends FC&lt;infer OP&gt; ? FC&lt;Omit&lt;OP, keyof P&gt;&gt; : never&gt; {
  return lazy(async () =&gt; {
    const asyncProps = await getProps();

    const LazifiedComponent = ((props) =&gt; {
      const finalProps = {
        ...asyncProps,
        ...props,
      } as C extends FC&lt;infer OP&gt; ? OP : never;

      return &lt;Component {...finalProps} /&gt;;
    }) as C extends FC&lt;infer OP&gt; ? FC&lt;Omit&lt;OP, keyof P&gt;&gt; : never;
    LazifiedComponent.displayName = `Lazified${Component.displayName}`;

    return { default: LazifiedComponent };
  });
}</code></pre>
<p><code>lazify</code> 함수는 <code>lazy</code> 함수와 마찬가지로 일종의 <a href="https://ko.legacy.reactjs.org/docs/higher-order-components.html">Higher Order Function (HOC)</a>이다. 문서 상단에 붉은 안내 문구처럼 class 컴포넌트가 있던 시절에 많이 사용하던 방법론이다. 컴포넌트의 코드를 캡슐화할 때 새로운 컴포넌트를 만들어 반환하는 대신 컴포넌트 안에 함수 호출 하나를 추가하게 되면서 HOC는 많이 쓰이지 않게 되었다. 나도 React Hook을 쓰면서 HOC는 거의 작성하지 않게 되었다. 오히려 HOC를 만들면서 했던 경험을 함수를 만드는 함수(팩토리 함수? 정확한 용어는 모르겠다.)를 작성하는데 썼다.</p>
<p><code>lazify</code> 함수는 컴포넌트와 그 컴포넌트가 받는 props 일부를 계산하는 비동기 함수, 두 개의 파라미터를 받는다. 이를 바탕으로 새로운 컴포넌트를 만들어주는데, 새로운 컴포넌트는 비동기 함수가 제공하지 않는 props를 가지는 컴포넌트이다. 다음 코드처럼 쓸 수 있다.</p>
<pre><code class="language-tsx">const LazifiedComponentWithRemoteConfig = lazified(Component, async () =&gt; {
  const [config1, config2] = await Promise.all([getConfig1(), getConfig2()])
  return { config1, config2 }
})

function Parent() {
  return (
      &lt;Suspense fallback={&lt;Loading /&gt;}&gt;
      &lt;span&gt;
        여기 있는 모든 내용은 설정 정보를 모두 가져와서
        Lazified 컴포넌트를 렌더링할 수 있게 되기 전까지 보이지 않습니다.
      &lt;/span&gt;

      &lt;LazifiedComponentWithRemoteConfig config3=&quot;foo&quot; /&gt;
    &lt;/Suspense&gt;
  )
}</code></pre>
<h2 id="다시-고민">다시 고민</h2>
<p>이 방법을 모든 API 요청 함수에 사용할 필요는 없는 것 같다. API에서 가져온 데이터는 사용자가 UI와 상호작용하면서 다른 파라미터로 데이터를 요청하게 될 수도 있고, 최신 상태로 업데이트가 필요할 수도 있다. 그런 상황에서 <code>lazify</code>는 대응할 수 있는 방법이 없기 때문에 왠만하면 원래의 <code>useEffect</code>를 사용하는 것이 좋다. 정말로 동기화해야 할 상태 변화가 있는 거니까.
만약 한 번 가져오면 어플리케이션 주기동안 새로고침할 필요없는 정적인 정보이면서 어플리케이션 UI를 그리는데 중요한 정보라면 이를 적용해 볼 수 있겠다. 해당 정보가 없다면 컴포넌트를 그리는 것이 의미가 없고, 정보를 로드하는데 실패하면 해당 컴포넌트를 아예 사용할 수 없어야 하는 그런 데이터... 지금은 인증이나 필수 사용자 정보 같은 것이 떠오르는데 실제로 적용해 본 적은 없어서 판단하기 힘들다.</p>
<p>필요한 지점을 적고 보니 굉장히 Next.js의 <code>getServerSideProps</code>에서 가져오는 데이터의 특성과 비슷한 느낌인데...</p>
<blockquote>
<p>데이터를 조회하는 코드가 컴포넌트 외부에 있고 조회한 데이터를 컴포넌트의 prop으로 사용할 수는 없을까?</p>
</blockquote>
<p>애초에 했던 고민이 <code>getServerSideProps</code>의 작동 방식과 정확히 일치한다. 사실 내 고민이 그냥 Next.js를 사용하지 않아서 생기는 것이었을까? 🤔</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://web.dev/code-splitting-suspense">React.lazy 및 Suspense를 사용한 코드 분할 - web.dev</a></li>
</ul>
<h2 id="후속-조사">후속 조사</h2>
<p>글을 다 쓰고 나서야 이런 시도가 없는지 검색했다.</p>
<ul>
<li><a href="https://medium.com/@darren_45987/react-lazy-making-lazy-less-lazy-8ce1a1986622">React.lazy — Making lazy less lazy - Darren Cresswell</a>: fetch 함수를 호출해서 응답을 파라미터로 컴포넌트를 실행해버리는 구조...? 근데 코드가 뭔가 이상하다.</li>
<li><a href="https://blog.logrocket.com/async-rendering-react-suspense">React Suspense: Async rendering in React - Yomi Eluwande</a>: 처음 상상했던 대로 모듈 단위에서 비동기 함수를 호출하고 컴포넌트에서 promise(-ish)를 다루는 구조</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Webpack을 사용하는 코드에서 Web Worker의 스크립트 파일의 경로 설정 문제]]></title>
            <link>https://velog.io/@giwan_dev/Webpack%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%BD%94%EB%93%9C%EC%97%90%EC%84%9C-Web-Worker%EC%9D%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@giwan_dev/Webpack%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%BD%94%EB%93%9C%EC%97%90%EC%84%9C-Web-Worker%EC%9D%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 22 Mar 2023 13:17:30 GMT</pubDate>
            <description><![CDATA[<h2 id="tl-dr">TL; DR</h2>
<p>Webpack에서 Worker API를 사용할 때는 Worker 생성과 URL 생성을 모두 인라인으로 작성해야 한다.</p>
<pre><code class="language-ts">const worker = new Worker(new URL(&quot;./script.ts&quot;, import.meta.url));</code></pre>
<h2 id="배경">배경</h2>
<p><a href="https://github.com/giwan-dev/my-record-collection">사이드 프로젝트</a>에서 많은 계산을 요하는 작업이 있어서 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API">Web Workers API</a>를 사용했다. 시키는 대로 막 구현한 구조는 다음과 같다.</p>
<pre><code class="language-ts">// my-module/index.ts
export function runHeavyTaskUsingWorker(input: Input): Promise&lt;Output&gt; {
  return new Promise((resolve, reject) =&gt; {
    const worker = new Worker(new URL(&quot;./script.ts&quot;, import.meta.url));

    worker.onmessage = (event: MessageEvent&lt;Output&gt;) =&gt; {
      resolve(event.data.palette);
      worker.terminate();
    };

    worker.postMessage(input);
  });
}

// my-module/script.ts
addEventListener(
  &quot;message&quot;,
  (event: MessageEvent&lt;Input&gt;) =&gt; {
    postMessage(heavyTask(event.data));
  },
);

function heavyTask(input: Input): Output {
  // 구현 생략
}</code></pre>
<p>코드를 보다 문득 Web Worker와 관련된 부분을 추상화 하고 싶은 생각이 들었다. CPU를 많이 사용하는 함수를 넣어서 Worker로 실행하도록 해주는 함수를 만들고 싶었다. 상상한 인터페이스는 다음과 같다.</p>
<pre><code class="language-ts">const runWorker = generateWorkerRunner(heavyTask)
// runWorker의 타입: function runWorker(input: Input): Promise&lt;Output&gt;</code></pre>
<p>이리저리 바꾸면서 시간을 보내다가 결국 완성은 못했지만, Webpack에 대해 재미있는 사실을 배웠다.</p>
<h2 id="web-worker에-전달하는-스크립트-주소">Web Worker에 전달하는 스크립트 주소</h2>
<p>worker를 생성할 때 원래는 HTML의 <code>script</code> 태그처럼 worker가 실행할 스크립트의 주소를 넣어줘야 한다. <code>const worker = new Worker(&quot;/my-script.js&quot;);</code> 형태로 말이다. 그런데 내 사이드프로젝트를 포함한 Webpack 기반의 번들링 환경에선 Webpack이 최종 파일의 URL을 만드니까 스크립트의 정확한 주소를 하드코딩할 수 없다. 현재 모듈 기반의 상대 경로를 지정해주면 Webpack이 실제 파일 경로로 바꿔줘야 한다. 이것이 다음 코드의 존재 이유다.</p>
<pre><code class="language-ts">new URL(&quot;./script.ts&quot;, import.meta.url)</code></pre>
<p><code>URL</code>에 들어가는 첫 번째 파라미터는 현재 모듈에서 스크립트 파일로 가는 상대 경로, 두 번째 파라미터는 base로 이용할 현재 모듈의 경로이고, URL 함수가 그걸 조합해서 스크립트 모듈 주소를 반환하는 줄 알았다. 그래서 다음 코드가 당연히 가능할 거라고 생각했다.</p>
<pre><code class="language-ts">const url = new URL(&quot;./script.ts&quot;, import.meta.url);
// 여러 코드 맥락...
const worker = new Worker(url);</code></pre>
<p>하지만 이 코드는 잘못된 URL을 생성한다. <code>script.ts</code>를 assets/media에서 가져오려고 하고 비디오 형식의 파일로 생각한다. 여러 번의 실험 끝에 Worker 생성과 URL 생성을 모두 인라인으로 작성해야 한다는 것을 깨달았다. 찾아보니 <a href="https://github.com/webpack/webpack/discussions/14093#discussioncomment-1257149">나처럼 삽질한 사람</a>도 있었고, <a href="https://webpack.js.org/guides/web-workers">Webpack의 공식 설명</a>도 있었다. Webpack이 Worker + URL의 조합을 코드에서 찾고, 그걸 실제 스크립트의 주소로 대치하는 걸로 보인다.</p>
<h2 id="후기">후기</h2>
<p>Webpack의 번들링을 공기처럼 당연하게 느껴서 Webpack이 코드를 바꾼다는 생각을 안 하게 되었다. 내가 관리하는 원본 소스코드의 형상이 Webpack을 통과하면 달라진다는 것을 쉽게 잊게 된다. 만약 Vite처럼 native ECMAScript 모듈을 사용한다면 원본의 형상을 그대로 사용하니, url을 따로 선언해도 문제가 없을 걸로 보인다. 하지만, 한 번 해보니 모듈을 잘 가져오긴 하는데 import, export 구문을 사용하면 오류(<code>SyntaxError: import declarations may only appear at top level of a module</code>)가 발생한다. 공부해야 할 건 정말 끝이 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022년 회고]]></title>
            <link>https://velog.io/@giwan_dev/2022%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@giwan_dev/2022%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 31 Dec 2022 09:01:24 GMT</pubDate>
            <description><![CDATA[<p>2022년은 여러 모습이 필요했던 해였다. 만 4년 경력의 직장인이었다가 대학에서 마지막 해를 보내는 복학생이 되었다. 그리고 다시 회사를 구하는 취준생이기도 했다. 한편, 10개월에 걸쳐 천천히 크로스핏터가 되었고 위스키를 마시는 술꾼이 되기도 했다. 그만큼 다양한 경험을 했고 기억하고 싶은 일도 많았다.</p>
<p>복학생이다 보니 휴학 이전에 내가 어떤 삶을 살았었는지 궁금할 때가 많았다. 밥은 뭘 먹었고, 과제와 공부는 어떻게 했고, 어떤 생각을 하고 살았었는지 지금의 나와 비교하고 싶은 순간이 많았다. 하지만 그때의 상황을 알만한 자료가 남아있는 게 없었다. 올해의 기억은 지금 가장 선명하지만, 이 또한 휴학 이전의 내 삶처럼 시간이 지나면 희미해질 것이다. 그때가 되어 내가 2022년에 어떤 생각을 하고 살았는지 알 수 있도록 회고를 작성한다.</p>
<p>올 한 해 나의 가장 큰 정체성은 복학생이었다. 학부 졸업 이후 대학원에 갈 것도 아니고 산업기능요원으로 일하며 쌓은 경력으로 취직할 예정인 나는 높은 학점이 필요하지 않았다. 졸업만 하면 되는 복학생. 그게 나의 목표였다. 하지만 힘을 빼고 학기를 보낸다는 게 생각보다 쉽지 않았다. 수업도 열심히 듣고 과제도 나름대로 최선을 다했다. 정말 죽을 만큼 노력했냐 하면 그렇진 않지만, 사실 나는 어떤 일에도 그 정도 노력하진 않는 사람이다. 기왕이면 좋은 학점을 받아서 나의 지성을 인정받고 싶기도 했고, 긴 시간을 쏟는 만큼 많은 것을 얻고 싶기도 했다. 결과에 연연하지 않을 수 있었기 때문에 즐겁게 두 학기를 다녔다.</p>
<p>두 학기 동안 전공과목으로 OS와 동시성 프로그래밍, 요구공학, 데이터베이스 개론, 그래프 기계학습 및 마이닝 수업을 들었다. 먼저 OS는 전산의 꽃이라고 하는 만큼 멋지고 흥미로운 개념을 다룰 수 있었다. 핀토스 프로젝트도 큰 규모의 프로그램 구조를 파악하고 기능을 추가하는 경험이어서 유익했다. 여기서 동시성의 개념을 처음 다뤘고, 이후 동시성 프로그래밍 수업에서 좀 더 깊은 개념을 공부했다. 지금까지 프론트엔드에서 동시성을 다룰 일이 없었는데 그래서 그런지 동시성을 다루는 데이터 구조와 알고리즘이 너무나 매력적이었다. Rust와 친해질 기회이기도 했다. 한편, 요구공학 수업은 요구사항을 잘 관리하는 것이 효율적인 소프트웨어 엔지니어링과 연결되어 있다는 인사이트를 주었다. 앞으로 계속 회사에서 일하면서 그런 능력을 기를 텐데 필요하다면 요구공학 수업에서 다룬 방법론을 좀 더 공부해 두는 것도 좋겠다. 데이터베이스 개론 수업은 서비스를 개발한다면 거의 필수인 데이터베이스를 알아보고 싶어서 신청했다. SQL의 전반적인 내용을 훑을 수 있었고, DB를 고도화할 수 있는 방법론인 normalization에 대해 알아두게 되었다. 마지막으로 그래프 기계학습 및 마이닝 수업은 기계학습 이론이 개념적으로 이해하긴 쉽지만, 수학적으로 이해하기엔 내 수학 실력이 매우 부족하다는 것을 일깨워 주었다. 이렇게 모든 과목이 소중한 경험이었다.</p>
<p>한편 교양 과목으로는 과학사와 한국사회경제사, 영미 과학 영화, 한국문학의 이해, 스타트업 101 수업을 들었다. 과학사 수업은 과학도로서 한 번쯤은 공부해야 할 과학의 발전을 다뤘다는 점에서 뿌듯했다. 수강 인원 중에 22학번이 몇몇 있었는데 나도 1학년 때 이런 수업을 들을 수 있었다면 좋았을 것 같았다. 추가로 과학적 사건에서 비롯된 여러 주제에 대해 내 생각을 간단히 정리해 볼 수 있는 기회이기도 했다. 한국사회경제사 수업은 지금까지 배웠던 역사관과는 다른 현실적이고 실용적인 역사관을 접할 기회였다. 수업 내내 여러 역사적 사건의 배경과 영향에 대해 설득력 있게 분석하는 방법을 연습했고, 앞으로 사회, 정치적 사건을 내 나름대로 분석할 수 있는 안목이 태동하는 것이 느껴졌다. 영미 과학 영화 수업은 평소에 좋아하던 SF 영화를 좀 더 깊이 있게 해석할 수 있는 시간이었다. 중간, 기말 에세이 빼고는 학기 중에 해야 할 것이 거의 없어서 영화 썰 듣는 기분으로 수업을 들을 수 있어서 좋았다. 한국문학의 이해 수업은 평생 읽어 볼 것 같지 않은 근대소설을 거의 매주 한 편씩 읽었다. 학기 끝자락에 와선 정이 들어서 앞으로도 계속 읽고 싶다는 생각마저 들었다. 마지막으로 스타트업 101 수업은 창업에 대해 더 가까워질 수 있는 계기였다. 하지만 리스크가 큰 만큼 혼자 사이드프로젝트로 시작해야 할 것 같다는 생각도 했다. 교양 수업들을 들으며 내 삶이 더 풍요로워지는 기분이었고, 이것을 앞으로도 계속 지켜가고 싶었다..</p>
<p>2년간 코로나를 핑계로 운동을 하지 않아 살찐 내 몸을 건강하게 만들기 위해 운동이 필요했다. 4시에 수업이 끝나는 여유로운 일정을 이용해 5시에 크로스핏을 했다. 2월 말에 한 달 정도 먼저 시작했던 형의 추천을 받고 그날 바로 체험하러 갔다. 그날 와드가 파워클린 &amp; 저크 30개였다. 올림픽 역도 경기에서나 보던 동작을 내가 직접 하다니! 그리고 바닥에 바벨을 그냥 떨어뜨릴 수 있다니! 활기차고 재밌는 운동인 것 같아서 바로 등록했다. 아마 크로스핏의 악명을 미리 알았더라면 이렇게 선뜻 등록하지 않았을 것 같다. 잘 모르고 일단 경험한 것이 크게 작용했다.</p>
<p>운동을 하면 할수록 헬스에 비해 크로스핏이 내게 더 잘 맞았다. 활기차고 다양한 운동을 접할 수 있다는 점이 매력적이다. 헬스가 수도원에 들어가서 신학 공부를 하는 것이라면 크로스핏은 일요 예배에 참석해서 신나는 찬송가를 부르는 것이다. 둘 다 운동을 향한 신앙을 키워주지만, 방향이 조금 다르다. 와드를 하는 동안 모든 생각을 비우고, 죽을 것 같은 순간이 지나면 성취감 세례를 받는다. 내년에도 크로스핏은 계속 꾸준히 하고 싶다..</p>
<p>운동으로 채운 신체 건강을 까먹는 취미도 하나 생겼는데 위스키를 마시게 되었다. 올해 초에는 칵테일에 관심이 더 많았는데 점점 위스키로 옮겨왔다. 그리고 지금은 와인에도 조금씩 손을 대고 있다. 이전부터 내가 냄새에 민감하다고 생각할 때가 있었다. 옆자리에 담배 피운 사람이 앉는다던가, 길빵하는 사람을 만날 때… 사실 민감하기보다 좋은 향을 맡을 때를 좋아하는 것 같다. 그런 측면에서 위스키의 다양한 향과 맛을 즐기는 과정이 즐겁다. 편하게 앉아서 잔에서 올라오는 향을 맡으며 향기와 알코올이 혈관을 타고 흐르게 만들면 마음이 잔잔해진다. 아직 내 감각이 날카롭지 못해서 전문가처럼 섬세한 분석을 하진 못하지만, 조금씩 스펙트럼을 넓히면서 맛과 향의 해상도를 올리고 싶다.</p>
<p>과제를 해야 하는 수업이 적어 상대적으로 널널했던 가을학기를 바쁘게 만든 것은 취업 준비였다. 중간고사 이후로 회사 면접을 보러 다닐 생각이었는데 이전에 같이 일했던 분께 지원해보라는 연락이 와서 개강하기도 전에 절차가 시작되었다. 그래서 부랴부랴 몇 개 회사를 더 지원했다. 그리고 중간고사 이후에 한 무리의 회사를 더 지원했고 채용 과제, 코딩 테스트와 면접으로 정신없는 10월, 11월을 보냈다. 어떤 주는 월, 화, 수, 목, 금 매일 면접이 있었다. 대면 면접에 참석하러 서울을 올라가는 일도 잦았다. 일정과 해야 할 일을 저글링 하는 것이 어려웠지만 무너지지 않고 잘 해내서 다행이다.</p>
<p>결과적으로 많이 떨어지고 많이 붙었다. 6개 회사에서 오퍼를 받았고 8개 회사에서 떨어졌다. 중간고사 이전에 지원했던 회사는 크고 유명한 회사나 뭔가 기술적으로 있어 보이는 회사였는데 전부 떨어졌다. 기술적으로 배울 점이 많이 있을 거라 예상했고, 그래서 가고 싶은 회사들이라 더 아쉬웠다. 면접도 못 본 회사는 잘 모르겠지만, 면접 본 크고 유명한 회사들은 정해놓은 기술 질문이 있고 그 기술 질문의 답을 외우고 있는지로 평가하는 것처럼 느껴졌다. 이후에 좋은 평가를 받고 최종 합격했던 회사의 면접에선 내 경험과 생각에 대한 이야기를 더 많이 했었고, 그래서 면접도 재밌었는데 크고 유명한 회사들은 나의 역량보단 내 머리의 용량을 평가받는 기분이었다. 앞으로도 크고 유명한 회사들의 기술 면접의 문턱을 넘지 못할 것 같다. 시험공부 하듯이 평소에 쓰지 않던 것들을 외워가야 하는데 그렇게까지 하고 싶지 않다.</p>
<p>떨어진 회사만큼 나를 좋게 봐준 회사도 많아서 행복했다. 그동안 고민했던 것이 헛된 것이 아니었다고 느꼈고, 앞으로 내가 하는 생각과 결정들도 큰 가치를 만들 수 있지 않을까 하는 용기도 얻었다. 이번에 가게 된 회사에서 즐겁게 일하면서 좋은 서비스를 만드는 데 기여하고 싶다. 전 회사에서 그랬던 것처럼 신뢰받는 동료가 되고 싶다. 배울 기회를 소중히 여기고 내가 가진 능력을 최대한 나누고 싶다. 그런 노력이 결실을 보아 다시 내게 돌아오면 좋겠다.</p>
<p>직장인이었다가 학생이었다가 취준생이 된 변화무쌍한 한 해였지만 큰 문제 없이 잘 마무리해서 뿌듯하다. 2022년을 마무리하면서 내 삶도 새로운 분기점을 지나게 되었다. 대학을 졸업하고 이제 어엿한 경제활동 인구가 되어 내 삶의 모든 것을 설계하고 완성해 나가야 한다. 2023년에 어떤 일들이 벌어질지 모르겠고, 나도 아무런 계획이 없지만 아무튼 행복했으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[macOS 업그레이드 후 오류 대처]]></title>
            <link>https://velog.io/@giwan_dev/macOS-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%9B%84-%EC%98%A4%EB%A5%98-%EB%8C%80%EC%B2%98</link>
            <guid>https://velog.io/@giwan_dev/macOS-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%ED%9B%84-%EC%98%A4%EB%A5%98-%EB%8C%80%EC%B2%98</guid>
            <pubDate>Sun, 30 Oct 2022 13:38:23 GMT</pubDate>
            <description><![CDATA[<p>macOS를 Ventura로 업그레이드한 후 git 명령을 실행하니 다음과 같은 오류가 발생했다.</p>
<pre><code>xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun</code></pre><pre><code>xcode-select --install</code></pre><p>로 개발자 도구를 재설치해서 해결했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[화면을 가리는 UI 표시 여부 관리 방법]]></title>
            <link>https://velog.io/@giwan_dev/next-overlay</link>
            <guid>https://velog.io/@giwan_dev/next-overlay</guid>
            <pubDate>Tue, 19 Jul 2022 15:34:36 GMT</pubDate>
            <description><![CDATA[<p>화면을 가리는 UI (팝업, 다이얼로그, 액션시트 등등...)를 표시할 때 한 가지 문제가 있었다. 단순히 useState를 이용한 상태로 관리하면 안드로이드 기기에서 뒤로가기 버튼을 눌렀을 때 페이지가 뒤로 가버린다. 일반적으로 안드로이드 사용자는 앱에서 화면을 가리는 UI를 닫을 때 뒤로가기 버튼을 사용하기 때문에 웹 환경에서도 뒤로 가기 버튼을 이용해 해당 UI를 닫을 수 있다면 좋을 것이다. 어떻게 하면 좋을까?
일단 브라우저의 뒤로 가기 액션을 막을 수 있는 방법은 마땅히 존재하지 않는다. 뒤로 가기 액션이 발생했을 때 그것이 작동하지 않도록 막고, 나만의 코드를 실행할 수는 없는 것이다.
해시를 URL에 추가해서 history API에 기록하는 방법이 있다. 어떤 값을 해시로 사용해야할까? 매번 개발자가 임의의 값을 하드코딩하는 방법도 있겠지만, 좀 더 쉽게 사용할 수 있도록 react@18에서 추가된 <code>useId</code>를 사용했다.
state가 바뀌면 이를 해시와 동기화하고, 해시가 바뀌면 이를 state와 동기화하는 <code>useEffect</code> 코드를 작성했다.
한 가지 문제가 있다면 history API를 이용하기 때문에 UI가 열려있는 상태에서 history API를 조작하여 UI를 닫았는데 닫히지 않거나, 해시가 완전히 닫히기 전에 history를 조작하여 race condition이 생기는 문제가 있을 수 있다. UI가 열려있는 상태에서 route가 바뀔 때 경고문을 출력하도록 처리하고, UI를 완전히 닫히는 것을 기다릴 수 있도록 닫는 함수가 promise를 반환하도록 처리했다.</p>
<p>이런 아이디어를 모아 <a href="https://github.com/zprime0920/next-overlay">next-overlay</a>라는 라이브러리를 만들어봤다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SPA에서 devDependency]]></title>
            <link>https://velog.io/@giwan_dev/dev-dependency-in-spa</link>
            <guid>https://velog.io/@giwan_dev/dev-dependency-in-spa</guid>
            <pubDate>Mon, 04 Jul 2022 13:46:40 GMT</pubDate>
            <description><![CDATA[<p><code>devDependencies</code>. 개발 환경에서 사용하는 패키지 목록이다. 배포된 코드가 사용하지 않는 의존성이라면 <code>devDependencies</code>에 넣어서 불필요한 설치를 막을 수 있다.
리액트 개발 환경에서 이 <code>devDependencies</code>의 분류에 의문이 들었다. SPA 코드는 번들링 되기 때문에 모든 의존성이 몇 개의 JavaScript 파일로 묶인다. <code>node_modules</code>를 주렁주렁 달고 배포하는 게 아니라 번들링 된 파일 몇 개만 배포하면 된다. 그렇기 때문에 <code>dependencies</code>의 의존성만 설치하는 일 자체를 하지 않아도 된다.
그동안 빌드 이후에도 필요한 의존성을 <code>dependencies</code>에 넣고, 그렇지 않다면 <code>devDependecies</code>에 넣어 왔다. 빌드된 파일을 배포하고 다른 패키지에서 설치하여 사용하는 일반적인 패키지에서는 유효한 분류였다. 하지만 빌드 이후 사용하는 의존성이 없는 SPA는 이 분류에 따르면 모든 의존성이 <code>devDependencies</code>에 들어가야 한다. 심지어 <code>react</code>까지도. <code>devDependencies</code>에 있는 <code>react</code>는 너무 어색하지 않은가?
반대로 빌드에 필요한 파일을 <code>dependencies</code>로 분류하면 어떨까? 린트니 테스트니 개발 과정에서 필요한 것이고, SPA의 본질은 빌드니까 말이다. 그럼 그동안 열심히 <code>devDependencies</code>로 분류해왔던 <code>typescript</code>와 <code>webpack</code>이 <code>dependencies</code>가 된다. 너무 낯설어서 아직 적응이 안 된다.
Node.js 패키지 의존성을 트리로 나타낸다면 SPA들은 leaf node이다. leaf 노드이기 때문에 다른 패키지에서 사용하는 방법론이 조금 안 맞는 것 같다. 이런 leaf node만을 위한 패키지 매니저를 만들어보면 어떨까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 쓴 코드의 사용자는 둘이다]]></title>
            <link>https://velog.io/@giwan_dev/teammate-is-your-user</link>
            <guid>https://velog.io/@giwan_dev/teammate-is-your-user</guid>
            <pubDate>Mon, 21 Feb 2022 08:38:29 GMT</pubDate>
            <description><![CDATA[<p><a href="https://kentcdodds.com/blog/avoid-the-test-user">웹 프론트엔드 개발자가 작성하는 코드를 사용하는 집단은 둘이다.</a> 서비스 사용자와 다른 프론트엔드 개발자. 이를 바탕으로 프론트엔드 개발자의 업무 범위를 3가지로 나눌 수 있다.</p>
<p>첫 번째는 서비스 사용자만 쓰는 코드이다. 웹 페이지나 어드민 페이지를 구성하는 코드가 여기에 해당한다. 트리플에선 “서비스 프로젝트&quot;라는 이름으로 불리기도 하며, 작은 단위로 저장소를 나눠서 <code>-web</code> 접미사를 붙여 관리하고 있다.</p>
<p>두 번째는 서비스 사용자와 프론트엔드 개발자 모두 사용하는 코드이다. 여러 페이지에서 공통으로 쓰이는 UI나 비즈니스 로직을 담은 모듈이 여기에 해당한다. 이들 코드는 첫 번째 코드를 작성하는 프론트엔드 개발자가 불러와 사용하고, 완성된 웹 페이지와 상호작용하면서 서비스 사용자도 쓰게 된다. 트리플에선 각 서비스 프로젝트를 별개의 저장소로 관리하고 있기 때문에 공통 모듈을 <a href="https://github.com/titicacadev/triple-frontend">triple-frontend</a>라는 이름의 모노레포로 관리한다.</p>
<p>세 번째는 프론트엔드 개발자만 사용하는 코드이다. 코드를 정적 분석하는 도구나 CI/CD, 또는 서비스 프로젝트의 보일러플레이트를 생성하는 코드 등이 있다. 트리플에서의 몇가지 예를 들면 <a href="https://github.com/titicacadev/eslint-config-triple">eslint-config-triple</a>이나 <a href="https://github.com/titicacadev/create-triple-app">create-triple-app</a>이 있다. triple-frontend에서 CI/CD를 담당하는 yaml 파일도 이에 해당한다고 볼 수 있다.</p>
<h3 id="서비스-사용자의-코드">서비스 사용자의 코드</h3>
<p>웹 페이지를 만드는 것은 프론트엔드 개발자의 가장 기본적인 역량이다. 디자인을 작동하는 페이지로 옮기는 과정에서 기본적인 HTML, CSS가 필요하고, 자바스크립트 지식이 필요하다. 한 번만 쓰고 버리는 페이지가 아니라면 계속 변경해야하기 때문에 유연한 구조를 설계할 수 있어야 한다.</p>
<h3 id="서비스-사용자와-프론트엔드-개발자의-코드">서비스 사용자와 프론트엔드 개발자의 코드</h3>
<p>서비스가 많아지면 일일히 새로운 페이지를 만드는 것은 비효율적이다. 작은 단위의 코드를 재사용하여 더 효율적으로 페이지를 만들 수 있다. 이때부터 작성한 코드를 다른 프론트엔드 개발자가 사용하기 때문에 인터페이스에 대한 고민이 필요하다.</p>
<p>공통 모듈을 만들 때에도 기본적인 HTML, CSS, JavaScript 능력이 필요하다. 더불어, 코드의 내용을 읽지 않고도 코드의 역할을 알 수 있는 인터페이스를 설계하는 능력이 필요하다. 이를 위해선 적절한 단계의 추상화와 직관적인 작명 등을 할 수 있어야 한다. 그리고 테스트 코드도 작성해야한다. 여러 프로젝트에서 사용하는 만큼 여러 개발자가 관여하기 때문에 원하는 기능에 대한 테스트를 잘 작성해둬야 새로운 기능을 추가하더라도 기존 기능에 문제가 없을 것이다. 이를 통해 개발자가 매번 모든 케이스를 테스트하지 않아도 된다는 안도감과, 내 마음대로 수정해도 문제가 없다는 자신감을 심어 줄 수 있다.</p>
<h3 id="프론트엔드-개발자의-코드">프론트엔드 개발자의 코드</h3>
<p>프론트엔드 개발자의 반복되는 업무를 자동화하여 더 효율적으로 코드를 생산할 수 있다. 자주 나오는 리뷰 내용을 린트 규칙으로 만들고, 커밋 푸시마다 빌드와 테스트를 반복할 수 있도록 자동화하며, 매번 생성해줘야하는 보일러플레이트를 한 번에 만드는 도구를 만들어 프론트엔드 개발자의 업무 환경을 개선할 수 있다. 이를 통해 개발자 개인의 실수를 막을 수 있고, 개발자의 시간과 에너지를 아낄 수 있다. 또한 암묵적인 규칙을 코드로 명확하게 관리할 수 있다.</p>
<p>이를 위해선 자신의 개발 환경을 관찰하는 습관이 필요하다. 자신의 업무 중에서 반복되는 내용은 무엇인지, 그리고 그것을 자동화할 수 있는지 고민하고, 만약 자동화할 수 있다면 늦기 전에 작업해놓는 것이 좋다. 당장 작업할 시간이 없다고 미루면 더 큰 시간을 손해보게 된다.</p>
<h3 id="결론">결론</h3>
<p>대부분의 업무에서 프론트엔드 개발자는 서비스 사용자만 신경 쓰게 된다. 좋은 코드를 평가하는 가장 쉬운 기준은 “이 코드가 작동하는가&quot;이기 때문이다. 하지만 프론트엔드 개발자의 생산성을 높이면 새로운 기능을 더 빠르게 서비스 사용자에게 전달할 수 있다. 옆 팀원이 내가 작성한 코드의 “사용자&quot;라는 것을 염두하여 더 나은 코드를 만들자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[{} 타입 안 쓰기]]></title>
            <link>https://velog.io/@giwan_dev/%EB%B9%88-%EA%B0%9D%EC%B2%B4-%ED%83%80%EC%9E%85-%EC%95%88%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@giwan_dev/%EB%B9%88-%EA%B0%9D%EC%B2%B4-%ED%83%80%EC%9E%85-%EC%95%88%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Fri, 14 Jan 2022 07:55:52 GMT</pubDate>
            <description><![CDATA[<p>children prop을 가지는 컴포넌트를 만들 때 React에서 제공하는 <code>PropsWithChildren</code> 타입을 애용했다. 예를 들어, children과 foo를 prop으로 가지는 컴포넌트는 다음과 같다.</p>
<pre><code class="language-tsx">function Foo({ foo, children }: PropsWithChildren&lt;{ foo: string }&gt;) {
  return (
    &lt;div&gt;
      {foo}
      {children}
    &lt;/div&gt;
  )
}</code></pre>
<p>children만 prop으로 가지는 컴포넌트는 <code>PropsWithChidren&lt;{}&gt;</code>을 사용했다.
참고로 <code>PropsWithChildren</code>은 이렇게 생겼다.</p>
<pre><code class="language-ts">type PropsWithChildren&lt;P&gt; = P &amp; {
    children?: ReactNode | undefined;
}</code></pre>
<p>그런데, <a href="https://github.com/typescript-eslint/typescript-eslint/blob/v5.6.0/packages/eslint-plugin/docs/rules/ban-types.md">@typescript-eslint/ban-types</a> 규칙이 <code>{}</code>를 쓰지 말라고 했다. 나는 이 타입이 빈 객체를 의미하는 타입일 줄 알았는데... 실제 작동은 null이 아닌 값 전체를 나타낸다고 한다. null이 아닌 값 전체와 <code>{ children }</code>의 intersection은 <code>{ children }</code>이니, 유효했던 것이었다. 그래도 <code>{}</code>와 이별하기 위해 <code>PropsWithChildren</code>에 넣을 다른 값을 알아내는 여정을 시작했다.</p>
<p>첫 번째 시도는 엄격한 빈 객체 타입이다. <code>Record&lt;string, never&gt;</code>로 하면 진짜 빈 객체를 나타내니 문제가 쉽게 해결될 것이다. 하지만 실제로는 사용할 수 없는 컴포넌트가 된다. 컴포넌트 선언에선 문제가 없지만, 컴포넌트를 사용할 때 index signature와 children의 타입이 호환이 되지 않는다는 오류가 발생한다. string인 index 시그니처에 들어가는 값은 never라서 ReactNode를 할당할 수 없는 것이다.</p>
<pre><code class="language-tsx">function Type1({ children }: PropsWithChildren&lt;Record&lt;string, never&gt;&gt;) {
    return &lt;div&gt;{children}&lt;/div&gt;
}

&lt;Type1&gt;asdf&lt;/Type1&gt; // { children: string }에는 index signature와 호환이 안되는 문제 발생</code></pre>
<p>두 번째 시도는 임의의 객체였다. <code>Record&lt;string, unknown&gt;</code>을 사용하면 인덱스 시그니처가 호환될 것이다. 하지만 이 컴포넌트는 다른 문제가 있었다. 임의의 속성을 갖는 객체를 사용했으므로 임의의 prop을 사용할 수 있게 되어버렸다. 타입 시스템을 해치는 일이기 때문에 다른 타입을 찾게 되었다.</p>
<pre><code class="language-tsx">function Type2({ children }: PropsWithChildren&lt;Record&lt;string, unknown&gt;&gt;) {
    return &lt;div&gt;{children}&lt;/div&gt;
}

&lt;Type2&gt;asdf&lt;/Type2&gt; // OK
&lt;Type2 foo=&quot;HACK&quot;&gt;asdf&lt;/Type2&gt; // OK...이지 않았으면 한다.</code></pre>
<p>파라미터로 넣는 타입과 <code>{ children }</code>과 intersection을 한 뒤에 <code>{ children }</code>이 남으려면 어떤 값이어야 할까? A 교집합 B를 했는데 A가 나오면 B는...? 전체 집합이다. TypeScript 세계에선 그걸 <code>any</code>라고 부른다. 그런데 <code>any</code>도 친해지면 안 되니까 <code>unknown</code>을 썼다. <code>unknown</code>을 쓰면 children을 문제 없이 사용할 수 있으면서도 임의의 prop을 허용하지 않는 컴포넌트를 만들 수 있다.</p>
<pre><code class="language-tsx">function Type3({ children }: PropsWithChildren&lt;unknown&gt;) {
    return &lt;div&gt;{children}&lt;/div&gt;
}

&lt;Type3&gt;asdf&lt;/Type3&gt; // OK
&lt;Type3 foo=&quot;HACK&quot;&gt;asdf&lt;/Type3&gt; // Not OK</code></pre>
<p><a href="https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDGMA0cDecAKUEYAzgOrAwAWAwlcADYAmUSAdnAL5wBmRIcAOSt0MQQChxPAK5sMwCBwAqATzBIAjAAo8aes1YdOALgJFSFanUYt2AHmRpoTOyRhRgbAObY2SAG5IUAB8wQCUuOJw0XCsMNJQHHbBOHo2hpx2APTB4pySMnIwCspqSABMOnBpBuxcpoTE5JS0+rZsDkhOUC5uHt7YsgDWbBAA7myhEThRMXEJSSk17Zk5eQWy8opwquoAzFXLhvVmTZat6fbDoxPhkTGxSPGJcMmpbRnZufmSTmxucBQ0QAvHAtLNoskIQ9XrtNMEUCQmDxsnCNLkYTE7HDygikSisjiMZjYWVyoD8cCAESI5FUvHI1Fk4mY7FlPYMglwjnQh5s-YU5HU2k8ekipn7FmvNZhIA">TS Playground</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[패키지 의존성을 뒤집어보자]]></title>
            <link>https://velog.io/@giwan_dev/dependency-inversion</link>
            <guid>https://velog.io/@giwan_dev/dependency-inversion</guid>
            <pubDate>Wed, 05 Jan 2022 04:28:34 GMT</pubDate>
            <description><![CDATA[<p>라우터 패키지의 링크 컴포넌트는 로그인이 필요하거나 앱 전환이 필요할 때 모달을 띄우는 기능이 있다. 그래서 모달 패키지를 의존하고 있었다.</p>
<p>그런데 모달 패키지에서 라우터 패키지의 코드를 사용하고 싶어졌다. 로그인 모달에서 확인을 누르면 로그인 페이지로 이동하는데 이 부분을 라우터 패키지 코드로 대체하고 싶었다.
서로에게 의존하는 한 쌍이라니 감동적인 이야기지만 코드의 세계에선 성가신 문제다.</p>
<p>모달 패키지보다 라우터 패키지가 더 낮은 수준의 모듈이라고 생각했다. 왜냐하면 모달 패키지가 필요한 부분이 얼마든지 바뀔 수 있기 때문이다. 지금은 모달을 띄우지만 모달이 아닌 다른 UI를 표시하고 싶을 수도 있고, 그냥 어떤 코드를 바로 실행하고 싶을 수도 있다. 본질은 어떤 코드를 실행하는 것이지 모달이 아니었다.</p>
<p>그래서 라우터 패키지에서 모달 의존성을 제거하고 싶었다. 하지만 어떻게? 링크 컴포넌트를 사용할 때마다 모달을 호출하는 함수를 넘겨주고 싶지 않았다. 그러면 Context API를 사용해서 로그인이 필요할 때 실행하는 함수를 전달하면 되겠다! 링크 컴포넌트 바깥에서 자유롭게 액션을 조절할 수 있을 것이다.</p>
<p>한편, Context API의 특징 덕분에 확장이 쉽다. 기존 Provider 아래에 새로운 Provider를 마운트하면 행동을 덮어쓸 수 있다. 이를 이용하면 특정한 부분만 다른 행동을 하게 만드는 것도 가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[타입스크립트 인터페이스를 임의의 객체에 할당하지 못하는 문제]]></title>
            <link>https://velog.io/@giwan_dev/til-typescript-interface-object</link>
            <guid>https://velog.io/@giwan_dev/til-typescript-interface-object</guid>
            <pubDate>Tue, 07 Dec 2021 01:29:40 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>인터페이스로 정의한 객체를 임의의 객체(<code>Record&lt;string, unknown&gt;</code>)에 할당할 수 없었다. string 타입의 index signature가 인터페이스엔 없다는 오류가 발생했다.</p>
<pre><code class="language-ts">interface FooInterface {
  foo: string
}

declare function returnFoo(): FooInterface
declare function acceptObject(obj: Record&lt;string, unknown&gt;): void

acceptObject(returnFoo())
// Argument of type &#39;FooInterface&#39; is not assignable to parameter of type &#39;Record&lt;string, unknown&gt;&#39;.</code></pre>
<p>이를 간소화하면,</p>
<pre><code class="language-ts">interface FooInterface {
  foo: string
}

const bar: Record&lt;string, unknown&gt; = { foo: &#39;&#39; } as FooInterface
// Type &#39;FooInterface&#39; is not assignable to type &#39;Record&lt;string, unknown&gt;&#39;.
// Index signature for type &#39;string&#39; is missing in type &#39;FooInterface&#39;.(2322)</code></pre>
<h2 id="해결책">해결책</h2>
<p>인터페이스에 index signature를 정의하거나 type으로 객체를 정의하면 된다.</p>
<pre><code class="language-ts">interface FooInterface {
  [key: string]: string | undefined // 명시적 속성의 모든 타입을 가지고 있어야 함
  foo: string
}

const bar: Record&lt;string, unknown&gt; = { foo: &#39;&#39; } as FooInterface // OK</code></pre>
<p>하지만 이렇게 하면 <code>FooInterface</code>로 정의한 객체는 아무 속성이나 접근이 가능해진다.</p>
<pre><code class="language-ts">type FooType = {
  foo: string
}

const bar: Record&lt;string, unknown&gt; = { foo: &#39;&#39; } as FooType // OK</code></pre>
<h2 id="정리">정리</h2>
<p>엄격한 타입스크립트 세계에서 생각하면 <code>Record&lt;string, unknown&gt;</code>를 쓰는 일 따위는 하지 말아야 한다. 모든 파라미터와 리턴 값은 명확한 타입으로 정의해서 안전한 타입 시스템을 구축해야한다.</p>
<p>하지만 현실 세계에선 어떤 값이 있는지 모르는 객체를 다룰 일이 많다. 해당 객체를 타이핑할 수 없다면 인터페이스에 인덱스 시그니처를 명시하는 건 어떨까? 리팩토링하고 싶은 욕구가 드는 흔적을 남길 수 있다. 나중에 코드를 보게 될 미래의 내가 리팩토링 하겠지.</p>
<p><a href="https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgGIHt0ElzXk5AbwChlkZMAuZAZzClAHNiBfY4mAVxATGHRDIoEMJyggM6ABQBKapJyRYiFITZcefAckRIADmADyAIwBWEXlPRnqAJQvooAEwA8dBiEYAaZNwDWIOgA7iAAfHLIAG7owE5EbMS6EAYm5pbCouKSsjLs7AgCdDp2Ds5u9Ew+-oEhocgAvPE6NGiYingqecSgSvgoCrjKSABMRKTIANp+EACe1O5MALrzFZ7IAD6+IE4QMKAQTuMU6CsezAkFIEXGJQVlC55VIAHBYQ1E5FTIAOTfyCzNVrYQZ9YbES5FBDyNoglTvQifE4-P4JYwAOjgNCcMHBGKxOLyYBmen6mAAKsSUI0SGRjqcmKw8hCwMgnLdHK4Ht4ti9avCAZigRSScQgA">TS Playground</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[async/await 문법으로 예외 처리를 할 때 저지른 실수]]></title>
            <link>https://velog.io/@giwan_dev/mistake-with-async-await</link>
            <guid>https://velog.io/@giwan_dev/mistake-with-async-await</guid>
            <pubDate>Thu, 16 Sep 2021 15:31:50 GMT</pubDate>
            <description><![CDATA[<p>async/await 문법과 try/catch 구문을 섞어 쓰면서 했던 실수를 기록한다.</p>
<p>에러가 발생할 수 있는 비동기 함수가 있고, 그 함수에서 발생하는 에러를 처리하고 싶었다. 그래서 다음과 같은 코드를 작성했다.</p>
<pre><code class="language-js">async function getValue() {
  try {
    return myAsyncFunction();
  } catch (error) {
    console.error(error);
    return undefined;
  }
}</code></pre>
<p>이렇게 하면 <code>myAsyncFunction</code> 실행중 발생한 에러가 잡혀서 <code>getValue</code> 함수는 undefined를 반환할 것이라고 생각했다. 하지만 <code>getValue</code> 함수 바깥으로 계속 에러가 던져지고 있었다.</p>
<p>코드를 좀 더 유심히 바라보다 원인을 알게 되었는데, 함수의 실행 결과를 바로 리턴하는 것이 문제였다. <code>myAsyncFunction</code>은 비동기 함수이니까 Promise를 반환할 테고, <code>getValue</code>도 같은 Promise 객체를 반환할 뿐이다. Promise의 최종 계산은 <code>getValue</code>를 await하고 있는 지점에서 이뤄지니까 에러는 잡히지 않는다.</p>
<p>이를 원하는 대로 작동하게 만들려면 <code>getValue</code>에서 <code>myAsyncFunction</code>의 Promise를 resolve 해줘야 한다.</p>
<pre><code class="language-js">async function getValue() {
  try {
    const value = await myAsyncFunction();
    return value;
  } catch (error) {
    console.error(error);
    return undefined;
  }
}</code></pre>
<p>Promise의 &quot;await&quot;을 위한 변수 선언이 눈에 띈다. 왜냐면 <code>return await</code>은 린트 규칙으로 막고 있었거든.</p>
<p><strong>(2021년 10월 13일 업데이트)</strong>
<a href="https://eslint.org/docs/rules/no-return-await"><code>no-return-await</code> 규칙</a>이 <code>return await</code>를 무조건 막는 줄 알았는데 아니었다. try/catch 안의 <code>return await</code>은 허용하고 있었다. 그냥 내가 이렇게 코딩해야하는 것을 모르고 있었을 뿐...
따라서 다음과 같은 형태를 쓸 수 있다.</p>
<pre><code class="language-js">async function getValue() {
  try {
    return await myAsyncFunction();
  } catch (error) {
    console.error(error);
    return undefined;
  }
}</code></pre>
<p>에러는 항상 발생하는 것이 아니라 잘못 코딩한 것을 지나치기 쉽다. 테스트 코드를 잘 짜던지, 아니면 더글라스 크락포드 형님이 말씀하셨듯 정말 예상치 못한 상황에서만 에러를 발생하게 함수를 설계해야겠다.</p>
<hr>
<p>아예 async/await 문법을 쓰지 말아볼까...?</p>
<pre><code class="language-js">myAsyncFunction({
  onSuccess: (value) =&gt; {
    handleValue(value)
  },
  onFail: (error) =&gt; {
    console.error(error)
    handleValue(undefined)
  },
})</code></pre>
<p>결과를 기다리기 위한 변수 선언이 없고, 모든 걸 함수로 표현한다는 장점이 느껴진다. 하지만 콜백 헬이 두렵기도 하고... 아직 async/await이 편해보인다. 🤔</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비동기 이벤트 핸들러를 여러 번 호출할 때 처리 방법]]></title>
            <link>https://velog.io/@giwan_dev/async-event-handler-hof</link>
            <guid>https://velog.io/@giwan_dev/async-event-handler-hof</guid>
            <pubDate>Wed, 28 Jul 2021 12:27:37 GMT</pubDate>
            <description><![CDATA[<h3 id="쉽게-쓰여진-form">쉽게 쓰여진 form</h3>
<p>작성한 데이터를 서버로 보내는 form이 있다.</p>
<pre><code class="language-tsx">function MyForm() {
  const handleSubmit: FormEventHandler&lt;HTMLFormElement&gt; = async (e) =&gt; {
    e.preventDefault()

    await fetch(&#39;/api/my-awesome-api&#39;, { method: &#39;POST&#39; })
  }

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      {/* 입력 UI */}
      &lt;button type=&quot;submit&quot;&gt;전송&lt;/button&gt;
    &lt;/form&gt;
  )
}</code></pre>
<p>이 form에서 버튼을 여러 번 누르면 <code>handleSubmit</code> 함수를 여러 번 호출할 것이다. 하지만 새로운 데이터를 DB에 저장하는 form이라면 API를 여러 번 호출하는 것을 막아야 한다. 물론 서버 쪽에서도 대응을 하겠지만, 클라이언트에 뭔지 모를 API 에러가 쌓이는 것보다 깔끔하게 한 번 요청하는 것이 좋을 것 같다.</p>
<h3 id="-잠시-기다려주세요">???: &quot;잠시 기다려주세요...&quot;</h3>
<p>가장 먼저 비동기 요청이 진행중일 때 핸들러가 작동하지 않도록 막았다. <code>submitting</code> 상태를 정의하여 비동기 함수가 전후로 상태를 변경해준다. 그리고 상태 값이 true이면 핸들러를 막는다.</p>
<pre><code class="language-tsx">function MyForm() {
  const [submitting, setSubmitting] = useState(false)

  const buttonDisabled = submitting

  const handleSubmit: FormEventHandler&lt;HTMLFormElement&gt; = async (e) =&gt; {
    e.preventDefault()

    if (submitting) {
      return
    }

    setSubmitting(true)

    await fetch(&#39;/api/my-awesome-api&#39;, { method: &#39;POST&#39; })

    setSubmitting(false)
  }

  return (
    &lt;form onSubmit={handleSubmit}&gt;
      {/* 입력 UI */}
      &lt;button type=&quot;submit&quot; disabled={buttonDisabled}&gt;전송&lt;/button&gt;
    &lt;/form&gt;
  )
}</code></pre>
<h3 id="setstate의-함정"><code>setState</code>의 함정</h3>
<p>리액트의 <code>setState</code> 함수는 동기적으로 작동하지 않는다. <code>setSubmitting(true)</code>를 호출했다고 해서 그 즉시 <code>submitting</code>의 값이 true가 되는 것이 아니다. Reconciliation이 일어나고 상태가 반영되어 새로운 <code>handleSubmit</code> 함수가 만들어지기 전까진 클로저에 이전 <code>submitting</code>를 가진 <code>handleSubmit</code> 함수가 호출된다. 이 순간은 아주 짧겠지만, 그래도 버튼을 충분히 빠르게 누르면 방어 코드를 통과할 수 있다.</p>
<h3 id="debounce"><code>debounce</code>...?</h3>
<p>이것은 debounce를 통해 막을 수 있다. 이벤트가 발생하면 일정 시간동안 기다렸다가 핸들러를 호출하는 방법인데, 기다리는 동안 새로운 이벤트가 발생하면 이전 이벤트 처리는 취소하고 새로운 이벤트로 다시 기다린다. 기다리는 시간동안 이벤트가 발생하지 않으면 핸들러를 호출한다.</p>
<p>이것을 <code>handleSubmit</code> 함수에 적용하려고 했다.</p>
<pre><code class="language-tsx">const handleSubmit = debounce((e) =&gt; {
  e.preventDefault()

  if (submitted) {
      return
  }

  setSubmitted(true)

  await fetch(&#39;/api/my-awesome-api&#39;, { method: &#39;POST&#39; })
}, 500)</code></pre>
<p>그런데 이렇게 하니 문제가 있었다. 디바운스 되어 이전에 누른 이벤트를 처리하진 않으니 <code>preventDefault()</code>를 호출하지 않아서 form의 기본 동작이 일어났다. 기본 동작을 무시하고 자체 이벤트 핸들러만 실행하고 싶었기 때문에 <code>handleSubmit</code> 함수는 그대로 두고, 내용 부분을 디바운스해야 했다.</p>
<h3 id="필요한-기능-정리">필요한 기능 정리</h3>
<p>지금까지 필요한 기능을 정리해봤다. 디바운스를 통해 여러번의 이벤트가 발생해도 핸들러는 한 번만 호출되어야 하며, 핸들러가 작동하는 동안은 새로운 핸들러가 작동하지 말아야 한다. 이를 그림으로 그리면 다음과 같다.</p>
<p><img src="https://images.velog.io/images/giwan_dev/post/ea475313-cc34-4e9d-9bc4-3f9359370e7e/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-07-28%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.08.44.png" alt=""></p>
<p>초록색 체크 표시가 된 이벤트만 처리되고, 나머지 이벤트는 모두 무시하는 것이다.</p>
<h3 id="makeasyncfunctiontransactional"><code>makeAsyncFunctionTransactional</code></h3>
<p>최대한 작은 단위로 기능을 개발해야 버그도 없고 재사용성도 높아진다고 생각한다. 이런 생각으로 비동기 함수를 변환하는 Higher Order Function을 상상했다. <code>makeAsyncFunctionTransactional</code>이라고 이름지은 이 함수를 대상 함수가 통과하면 여러 번 호출해도 한 번만 작동하고, 비동기 작업이 끝나기 전까진 호출해도 작동하지 않게 변한다. 이런 함수를 구현하기로 했다.</p>
<h3 id="디바운스부터">디바운스부터</h3>
<p>먼저 가장 복잡한 디바운스부터 구현했다. 디바운스를 적용하더라도 원래 함수의 응답을 반환해야 한다. 그래서 Promise를 반환하는 함수를 작성했다. Promise 안에서 <code>setTimeout</code>을 실행하고, 그 안에서 파라미터로 받은 함수를 실행한다. <code>then</code>, <code>catch</code> 체인에 각각 <code>resolve</code>, <code>reject</code> 넣어서 원본 함수의 응답을 변환 함수로 전달할 수 있다.
변환한 함수를 사용할 때 디바운스로 취소되면 예외로 취급하고 싶었다. 그래서 timeout을 취소할 때 에러를 냈다. 그리고 이 에러를 판단할 수 있는 메서드를 변환 함수에 같이 제공했다. 변환된 함수를 사용할 때 try/catch 문과 메서드를 이용해 이 함수가 낸 에러만 특정하여 조용히 넘어갈 수 있다.</p>
<pre><code class="language-tsx">function makeAsyncFunctionTransactional&lt;
  Params extends any[],
  Response extends any,
&gt;(
  asyncFunction: (...params: Params) =&gt; Promise&lt;Response&gt;,
): { (...params: Params): Promise&lt;Response&gt;; isOwnError(error: any): boolean } {
  const REJECTED_CALL_ERROR = &#39;REJECTED_CALL_ERROR&#39;

  // 이전 호출의 타임아웃을 제거하는 함수를 외부 함수에서 관리하여 상태를 유지할 수 있다.
  let clearPreviousTimeout: (() =&gt; void) | undefined

  const transactionFunction: {
    (...params: Params): Promise&lt;Response&gt;
    isOwnError(error: any): boolean
  } = (...params) =&gt; {
    if (clearPreviousTimeout !== undefined) {
      clearPreviousTimeout()
    }

    // Promise를 이용해 원본 함수의 응답을 변환 함수가 사용할 수 있도록 할 수 있다.
    return new Promise((resolve, reject) =&gt; {
      const timeout = setTimeout(() =&gt; {
        clearPreviousTimeout = undefined

        asyncFunction(...params)
          .then(resolve)
          .catch(reject)
      }, 500)

      clearPreviousTimeout = () =&gt; {
        clearTimeout(timeout)
        reject(new Error(REJECTED_CALL_ERROR))
      }
    })
  }

  // 이 함수에서 일으킨 에러인지 아닌지 검증할 수 있다.
  transactionFunction.isOwnError = (error: any): boolean =&gt; {
    return error?.message === REJECTED_CALL_ERROR
  }

  return transactionFunction
}</code></pre>
<pre><code class="language-tsx">const transactionFetchForm = makeAsyncFunctionTransactional(fetchForm)

const handleSubmit = () =&gt; {

  try {
    // handleSubmit이 빠르게 여러 번 실행되어도 transactionFetchForm은 디바운스 된다.
    const response = await transactionFetchForm(params)
    return response
  } catch (error) {
    if (transactionFetchForm.isOwnError(error)) {
      // 디바운스 되었을 때 에러가 발생하고, 여기서 조용히 넘어간다.
      return
    }

    throw error
  }
}</code></pre>
<h3 id="실행중이니-호출-무시">실행중이니 호출 무시</h3>
<p>다음은 비동기 함수가 실행 중일 때 발생하는 요청을 무시하는 코드를 추가했다. <code>asyncFunction</code> 실행 전후에 클로저로 접근할 수 있는 &quot;실행중&quot; 상태 값을 변경해주고, <code>transactionalFunction</code>을 실행했을 때 값을 보고 더 진행할지 말지 결정한다. 그리고 위와 마찬가지로 비동기 함수가 실행 중일 때 에러를 낸다.</p>
<pre><code class="language-tsx">function makeAsyncFunctionTransactional&lt;
  Params extends any[],
  Response extends any,
&gt;(
  asyncFunction: (...params: Params) =&gt; Promise&lt;Response&gt;,
): { (...params: Params): Promise&lt;Response&gt;; isOwnError(error: any): boolean } {
  const REJECTED_CALL_ERROR = &#39;REJECTED_CALL_ERROR&#39;
  const FUNCTION_EXECUTING_ERROR = &#39;FUNCTION_EXECUTING_ERROR&#39;

  let clearPreviousTimeout: (() =&gt; void) | undefined
  let isExecuting = false

  const transactionFunction: {
    (...params: Params): Promise&lt;Response&gt;
    isOwnError(error: any): boolean
  } = (...params) =&gt; {
    // 이미 실행중이라면 예외 처리한다.
    if (isExecuting) {
      throw new Error(FUNCTION_EXECUTING_ERROR)
    }

    if (clearPreviousTimeout !== undefined) {
      clearPreviousTimeout()
    }

    return new Promise((resolve, reject) =&gt; {
      const timeout = setTimeout(() =&gt; {
        clearPreviousTimeout = undefined

        isExecuting = true
        asyncFunction(...params)
          .then(resolve)
          .catch(reject)
          .finally(() =&gt; {
            // 원본 함수가 성공했든, 실패했든 실행중 상태값이 변해야 한다.
            isExecuting = false
          })
      }, 500)

      clearPreviousTimeout = () =&gt; {
        clearTimeout(timeout)
        reject(new Error(REJECTED_CALL_ERROR))
      }
    })
  }

  transactionFunction.isOwnError = (error: any): boolean =&gt; {
    return (
      error?.message === REJECTED_CALL_ERROR ||
      error?.message === FUNCTION_EXECUTING_ERROR
    )
  }

  return transactionFunction
}</code></pre>
<h3 id="마무리">마무리</h3>
<p>이렇게 여러번 호출해도 한 번 작동하는 것을 보장하게 만들어주는 <code>makeAsyncFunctionTransactional</code> 함수를 구현했다. 클로저를 이용해 현재 상태를 저장하는 것이 가장 중요했다. </p>
<p>완성된 함수는 <a href="https://codesandbox.io/s/transactional-async-function-hof-j3r4q?file=/src/async-fn-hof.ts">여기</a>서 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[생성자 함수와 리액트 컴포넌트]]></title>
            <link>https://velog.io/@giwan_dev/%EC%83%9D%EC%84%B1%EC%9E%90-%ED%95%A8%EC%88%98%EC%99%80-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@giwan_dev/%EC%83%9D%EC%84%B1%EC%9E%90-%ED%95%A8%EC%88%98%EC%99%80-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Tue, 20 Jul 2021 03:16:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://rinae.dev/posts/how-javascript-works-summary">&quot;&#39;자바스크립트는 왜 이모양인가&#39; 읽기&quot;</a>를 읽고</p>
</blockquote>
<p>독후감을 읽고 나서 작성하는 독후감.. &quot;자바스크립트는 왜 그모양일까?&quot; 책은 꼭 읽어봐야겠다.</p>
<p>자바스크립트는 클로저로 함수 안에 state를 가질 수 있기 때문에 클래스가 필요없다는 이야기를 읽었다. 마침 작업중이던 코드에 클래스를 썼어서 생성자 함수로 바꿔봤다.</p>
<p>결과는 아주 깔끔했다. 생성자 함수의 지역 변수가 클래스의 private 속성처럼 작동하고, 파라미터도 쉽게 참조할 수 있고, 심지어 이전에 발견 못했던 분기에 따른 버그도 발견했다.</p>
<p>바꿔보니 구조가 뭔가 익숙했는데 바로 리액트 함수 컴포넌트랑 같은 모양이었다. props를 파라미터로, state를 지역변수로 생각하면 JSX 덩어리를 생성하는 생성자 함수가 리액트 컴포넌트의 정체였다.
복선(?)도 깔려있었는데 리액트 컴포넌트 네이밍 컨벤션은 PascalCase이고, 생성자 또한 PascalCase이다.</p>
<p>이제야 자바스크립트가 클래스 없이 더 간결할 수 있다는 것을 이해했다. 리액트 팀이 함수 컴포넌트를 만든 것도 클래스 없이 더 간결한 구조가 되기 위해서일 것이다. 앞으로 갈 길이 멀다는 것을 다시 한 번 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로지텍 MX Vertical]]></title>
            <link>https://velog.io/@giwan_dev/%EB%A1%9C%EC%A7%80%ED%85%8D-MX-Vertical</link>
            <guid>https://velog.io/@giwan_dev/%EB%A1%9C%EC%A7%80%ED%85%8D-MX-Vertical</guid>
            <pubDate>Fri, 28 May 2021 02:48:42 GMT</pubDate>
            <description><![CDATA[<p>출근하자마자 오른쪽 손목이 시큰거리는 것 같아 충동적으로 <a href="https://www.logitech.com/ko-kr/products/mice/mx-vertical-ergonomic-mouse.910-005450.html">로지텍 MX Vertical</a> 마우스를 샀다. 아침에 주문했는데 저녁 때 도착하는 쿠팡의 엄청난 서비스에 감탄했다.</p>
<p>어제 밤엔 연결만 해놓고 오늘 아침부터 본격적으로 써보는데 몇 가지가 아쉽다.</p>
<p>그동안 트랙패드를 사용하면서 얼마나 가로 스크롤에 익숙해졌는지 깨달았다. 피그마 페이지를 확인할 때도, 노션에서 작업 보드를 확인할 때도, 그리고 이메일을 읽음 처리할 때도 자연스럽게 가로 스크롤을 하고 있었다. 그래서 키보드 왼쪽에 트랙패드를 두고 꼭 필요할 때 사용하게 됐다. 이러면 오른쪽 대신 왼쪽 손목이 아프게 되는 건 아닐까...</p>
<p>그리고 키보드를 쓰다가 마우스를 잡을 때 빠르게 잡히지 않는다. 키보드를 쓸 때는 손이 누워있는데 마우스는 세로로 잡아야 하니 팔목을 돌리면서 손목도 돌려야 한다. 트랙패드를 쓸 때보다 더 품이 많이 드는 느낌이다.</p>
<p>버티컬 마우스에 익숙하지 않아서인지 트랙패드를 너무 오래써서인지 모르겠지만 목표 지점으로 갈 때 너무 부정확하다. 내가 이렇게 손을 떨었던가 싶을 정도로 덜덜 떨리는 포인터를 보고 있자니 답답했다. 마우스를 계속 잡고 있는 게 아니라 키보드랑 마우스를 왔다갔다 하니 부정확한 움직임이 더 심한 것 같다.</p>
<p>결정적으로 손목도 극적으로 괜찮아지지 않는 느낌이다. 아직 하루도 안 됐지만 오히려 꺾이지 않던 부분으로 꺾이는 느낌도 든다. 적응하는데 시간이 더 필요하지 싶다.</p>
<p>MX마스터는 가로 스크롤 하는 휠도 있었던 거 같은데, 좀 더 알아보고 살 걸 그랬나하는 후회가 든다. 한 편으로는 이참에 아예 키보드만 쓰는 습관을 들여야 하나 고민 중이다.</p>
<p>잘 잡으면 손에 착 감기는 느낌이 좋고, 마우스 움직임이나 표면의 감촉도 정말 부드러워서 마음에 든다. 이렇게 계속 손이 가다보면 언젠가 적응할 수 있지 않을까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[package-lock 버전과 postinstall]]></title>
            <link>https://velog.io/@giwan_dev/package-lock-%EB%B2%84%EC%A0%84%EA%B3%BC-postinstall</link>
            <guid>https://velog.io/@giwan_dev/package-lock-%EB%B2%84%EC%A0%84%EA%B3%BC-postinstall</guid>
            <pubDate>Tue, 18 May 2021 04:05:40 GMT</pubDate>
            <description><![CDATA[<p>CI에서 Sentry 바이너리 파일을 찾을 수 없다는 에러를 마주했다.</p>
<pre><code>Sentry CLI Plugin: spawn /app/node_modules/@sentry/cli/sentry-cli ENOENT</code></pre><p>Sentry CLI가 사용하는 바이너리 파일(<code>sentry-cli</code>)을 찾지 못했다는 에러이다. 해당 파일은 <code>@sentry/cli</code>를 설치할 때 <code>postinstall</code> 스크립트로 만들어진다. 모종의 이유로 <code>postinstall</code> 스크립트가 돌지 않은 것이다.
npm@7부터 <a href="https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json#packages"><code>package-lock</code>에 패키지의 <code>hasInstallScript</code> 속성</a>이 true여야 패키지의 <code>postinstall</code>을 돌리게 되었다. 그런데 이 속성은 <code>package-lock</code> 버전 2부터 추가되었다. npm@6으로 패키지를 설치했다면 해당 속성이 없는 것이다. 그런데 npm@7으로 단순 설치(<code>npm install</code>)는 <code>hasInstallScript</code>를 추가하지 않는다. 패키지를 직접 제거했다가 다시 설치하는 과정을 거쳐야 해당 속성을 추가한다.
정리하면, npm@6을 사용해 <code>postinstall</code>을 사용하는 패키지를 설치한 다음 npm@7을 사용하게 되면 <code>postinstall</code> 스크립트가 작동하지 않아서 문제가 생길 수 있다는 것이다. 그리고 이 문제는 <code>hasInstallScript</code> 속성이 없어서 생기는 문제이며, 해당 속성을 넣어주기 위해 <code>postinstall</code>을 사용하는 패키지를 삭제 후 재설치하여 해결할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vim 클립보드]]></title>
            <link>https://velog.io/@giwan_dev/Vim-%ED%81%B4%EB%A6%BD%EB%B3%B4%EB%93%9C</link>
            <guid>https://velog.io/@giwan_dev/Vim-%ED%81%B4%EB%A6%BD%EB%B3%B4%EB%93%9C</guid>
            <pubDate>Fri, 07 May 2021 05:36:57 GMT</pubDate>
            <description><![CDATA[<p>커맨드+c, 커맨드+v만 사용하다가 Vim의 레지스터로 복사 붙여넣기 기능을 사용하니 신세계이다.</p>
<p>다음과 같이 객체의 속성을 코드 이곳저곳에 추가한다고 해보자</p>
<pre><code>const nestedObject = {
    headers: {
        &#39;my-custom-header&#39;: &#39;value&#39;, // 이 부분을 추가하는 것이 포인트
    },
}</code></pre><p>코드 어떤 곳에는 전체 객체를 통째로 넣어야 하고, 어떤 곳에는 객체가 이미 들어가고 있어서 headers만 추가해줘야 하고, 또 어떤 곳은 그냥 속성만 추가해줘도 될 수 있다.
이전까지는 캐시 정책(?)에 입각하여 마지막 붙여넣기한 코드를 복사해서 다음 코드에 붙여넣고 수정하는 것을 반복했다.
하지만 Vim 바인딩을 쓰면서 복사한 내용을 레지스터에 할당할 수 있다는 것을 알았다.
키를 누르는 순서는 다음과 같다.</p>
<pre><code>shift + &#39; (&quot;)
a (레지스터)
y (복사)</code></pre><p>이걸 이용해 <code>a</code>에는 객체 전체를 할당하고, <code>s</code>에는 <code>headers</code> 객체, 속성 라인은 <code>d</code>에 할당한 다음 코드를 맞닥뜨렸을 때 원하는 레지스터의 값을 불러다가 붙여넣을 수 있다.</p>
<pre><code>&quot;
a | s | d
p</code></pre><p>캐시 미스(?) 날 일 없는 효율적인 복붙 🎉</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 시스템에서 상태 관리 패턴]]></title>
            <link>https://velog.io/@giwan_dev/Next.js-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@giwan_dev/Next.js-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Wed, 05 May 2021 06:03:32 GMT</pubDate>
            <description><![CDATA[<ul>
<li>가장 먼저 <code>useState</code>랑 <code>useEffect</code> 짝으로 관리.<ul>
<li>만약 관련된 코드가 커지거나 재사용이 필요하다면 둘을 묶어서 훅으로 만들 수 있음.</li>
</ul>
</li>
<li>데이터를 여러 컴포넌트에서 사용하게 되면 Context API를 도입하면 됨.<ul>
<li>이 때 Context API의 Provider 컴포넌트를 따로 정의해서 state랑 fetch 코드를 집어 넣으면 구조가 심플함.</li>
</ul>
</li>
<li>데이터를 SSR해야 한다면 Next.js의 <code>getServerSideProps</code>에서 fetch 해야 함.<ul>
<li><code>Provider</code>의 prop으로 초기 값을 전달 받고, 이후 새로고침하는 로직을 Context API로 노출해야할 수도 있음</li>
</ul>
</li>
<li>페이지를 이동하더라도 fetch를 반복하지 않으려면, state가 <code>_app</code> 수준에 있어야 함.<ul>
<li>Context API의 Provider를 <code>_app</code>에서 마운트</li>
<li><code>_app</code>의 <code>getInitialProps</code>에서 데이터를 fetching할 수 있음.<ul>
<li>근데 이러면 데이터를 안 쓰는 페이지에서도 요청하게 됨.</li>
<li>static 페이지를 빌드할 수 없어 Next.js의 장점을 깎아 먹음.</li>
</ul>
</li>
<li><code>_app</code>의 prop 중에 각 페이지의 초기 props를 담는 <code>pageProps</code>가 있다. 여기서 원하는 데이터를 뽑아서 Provider에 넣어주면 좋다.</li>
</ul>
</li>
</ul>
<hr>
<p>Apollo Client를 Next.js와 연동하면서 마지막 방법을 알게 되어 총 정리해 봄. </p>
]]></description>
        </item>
    </channel>
</rss>