<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 지식, 트렌드, 경험 공유</title>
    <link>https://mechaniccoder.tistory.com/</link>
    <description>개발자로서 성장하기 위해 배운 지식, 트렌드와 경험을 글로 남기고 이를 공유합니다.</description>
    <language>ko</language>
    <pubDate>Mon, 25 May 2026 19:52:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>mechaniccoder</managingEditor>
    <image>
      <title>개발 지식, 트렌드, 경험 공유</title>
      <url>https://tistory1.daumcdn.net/tistory/5581501/attach/9257477d4df24703b8889b3141b86ab0</url>
      <link>https://mechaniccoder.tistory.com</link>
    </image>
    <item>
      <title>엉켜있던 Peer dependencies 디버깅하기</title>
      <link>https://mechaniccoder.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 개발 환경에서 아래와 같은 에러를 마주했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;No&amp;nbsp;QueryClient&amp;nbsp;set,&amp;nbsp;useQueryClientProvider&amp;nbsp;to&amp;nbsp;set&amp;nbsp;one&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-04-13 at 4.36.12 PM.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qADuv/btsGB9VMz0T/LguLzSHRBzKOMzMPbyBRKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qADuv/btsGB9VMz0T/LguLzSHRBzKOMzMPbyBRKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qADuv/btsGB9VMz0T/LguLzSHRBzKOMzMPbyBRKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqADuv%2FbtsGB9VMz0T%2FLguLzSHRBzKOMzMPbyBRKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;360&quot; data-filename=&quot;Screenshot 2024-04-13 at 4.36.12 PM.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 정보를 기반으로 추론했을 때 useSuspenseQuery를 호출하려 하는데 루트에 QueryProvider가 없어서 발생하는 에러인 것 같습니다. 그런데 이상합니다. 분명 루트에는 QueryProvider가 감싸져 있습니다.&amp;nbsp; 그런데도 계속 에러가 발생하는 것을 보고 packages 관련 문제라는 것을 직감했습니다. 빠르게 방향을 잡을 수 있었던 이유는 과거에 &lt;a href=&quot;https://mechaniccoder.tistory.com/24#Dependency%20Version%20Conflict-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모노레포 환경에서 node_modules packages hoisting 문제를 겪고 해결한 경험&lt;/a&gt;이 있었기 때문입니다. 그때는 yarn v1을 패키지 매니저로 사용했었고 현재는 pnpm을 사용하지만 같은 맥락의 에러라고 판단했습니다. (엄밀히 말하면 위 에러는 node_modules hoisting이 원인은 아니었습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 node_modules/.pnpm에서 설치된 @tanstack/react-query를 확인해 봤습니다. 2개의 @tanstack/react-query가 설치되어 있는 것을 확인했습니다. 위의 @tanstack/react-query는 react-dom@18.2.0, react@18.2.0 두 개의 패키지가 peer dependencies이며 아래의 @tanstack/react-query는 두 개의 패키지에 더해 react-native@0.73.6을 추가로 peer dependencies로 가집니다. (&lt;a href=&quot;https://pnpm.io/how-peers-are-resolved&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;How peers are resolved&lt;/a&gt;)&lt;/p&gt;
&lt;pre id=&quot;code_1713001516578&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;node_modules
  .pnpm
    @tanstack+react-query@4.36.1_react-dom@18.2.0_react@18.2.0
    @tanstack+react-query@4.36.1_react-dom@18.2.0_react-native@0.73.6_react@18.2.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 얻은 정보를 바탕으로 에러의 원인을 추론할 수 있습니다. useSuspenseQuery를 import 할 때 사용한 @tanstack/react-query와 루트에서 QueryProvider를 import 할 때의 @tanstack/react-query가 서로 다르기 때문에 useSuspenseQuery에서 QueryProvider의 prop으로 넘긴 QueryClient를 찾을 수 없어서 에러가 발생한 것입니다. 다이어그램으로 살펴보죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;765&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dN8pkv/btsGC3gsrCX/WTKuD2yUl7lRfFcbF0SWwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dN8pkv/btsGC3gsrCX/WTKuD2yUl7lRfFcbF0SWwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dN8pkv/btsGC3gsrCX/WTKuD2yUl7lRfFcbF0SWwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdN8pkv%2FbtsGC3gsrCX%2FWTKuD2yUl7lRfFcbF0SWwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1720&quot; height=&quot;765&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;765&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 위의 추론이 맞는지 pnpm-lock.yml에서 확인해보겠습니다. useSuspenseQuery를 가지는 @suspensvie/react-query에서 react-dom@18.2.0, react@18.2.0, react-native@0.73.6을 peer dependencies로 가지는 @tanstack/react-query를 사용하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713001527503&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  /@suspensive/react-query@1.18.3(@suspensive/react@1.18.3)(@tanstack/react-query@4.36.1)(react@18.2.0):
    peerDependencies:
      '@suspensive/react': ^1.18.3
      '@tanstack/react-query': ^4
      react: ^16.8 || ^17 || ^18
    dependencies:
      '@suspensive/react': 1.18.3(react@18.2.0)
      '@tanstack/react-query': 4.36.1(react-dom@18.2.0)(react@18.2.0)(react-native@0.73.6) // peer dependencies
      react: 18.2.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트에서 사용한 @tanstack/react-query는 react-native@0.73.6을 peer dependency로 가지지 않기 때문에 위에서 추론한 원인이 사실임을 확인했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713001533919&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      '@tanstack/react-query':
        specifier: ^4.32.6
        version: 4.36.1(react-dom@18.2.0)(react@18.2.0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결하는 과정은 간단했습니다. 아무 패키지나 삭제하고 재설치하면 pnpm이 peer dependencies를 재조정하기 때문에 같은 peer dependencies를 가지도록 수정됐습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 뭔가 이상합니다. peer dependencies로 react, react-dom은 이해하겠는데 react-native는 뭘까요? 모바일이 아닌 웹에서 개발하기 때문에 react-native를 peer dependencies로 가질 이유가 없어보입니다. 원인을 찾기 위해 &lt;a href=&quot;https://github.com/TanStack/query/blob/6532ed3cbe49f04b3cb31f80ea88b5886416d51a/packages/react-query/package.json#L64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@tanstack/react-query package.json에 peer dependency field&lt;/a&gt;를 확인해봤습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713001542426&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;peerDependencies&quot;: {
    &quot;react&quot;: &quot;^16.8.0 || ^17.0.0 || ^18.0.0&quot;,
    &quot;react-dom&quot;: &quot;^16.8.0 || ^17.0.0 || ^18.0.0&quot;,
    &quot;react-native&quot;: &quot;*&quot;
  },
  &quot;peerDependenciesMeta&quot;: {
    &quot;react-dom&quot;: {
      &quot;optional&quot;: true
    },
    &quot;react-native&quot;: {
      &quot;optional&quot;: true
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-native는 optional이라 문제 될 건 없습니다. 자동으로 peer dependency를 설치했더라도 optional peer dependency는 제외하고 설치되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 react-native를 peer dependency로 가지는 다른 패키지가 있다는 것을 추론할 수 있습니다. 다시 한번 pnpm-lock.yml 파일을 확인하여 어디서 react-native를 peer dependency로 가지는지 보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2024.4.14 업데이트&lt;br /&gt;manual하게 찾는 것 대신에 아래 cli를 활용해서 react-native를 dependency로 가지는 패키지를 쉽게 찾을 수 있습니다.&lt;br /&gt;
&lt;pre id=&quot;code_1713063047082&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ pnpm list -r --depth Infinity | grep &quot;react-native&quot;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾았습니다! @react-spring/native 패키지에서 react-native를 peer dependency로 가지고 있더군요. 이 녀석 때문에 react-native를 optional로 가지는 다른 패키지에 영향을 주고 있던 것이었습니다. (앞서 @tanstack/react-query에 react-native가 peer dependency로 명시됐던 것)&lt;/p&gt;
&lt;pre id=&quot;code_1713001549868&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  /@react-spring/native@9.7.3(react-native@0.73.6)(react@18.2.0):
    peerDependencies:
      react: ^16.8.0  || &amp;gt;=17.0.0 || &amp;gt;=18.0.0
      react-native: '&amp;gt;=0.58'
    dependencies:
  // ...
      react-native: 0.73.6(@babel/core@7.24.4)(@babel/preset-env@7.24.4)(react@18.2.0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/pmndrs/react-spring?tab=readme-ov-file#%EF%B8%8F-jump-start&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;react-spring 문서&lt;/a&gt;를 확인해 보면 react-spring을 설치하는 경우 모든 종류의 패키지를 다운로드하기 때문에 그 안에 react-native에 대한 패키지도 같이 설치가 됩니다. 따라서 아래처럼 웹에 대한 패키지만 설치하는 것을 권장하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1713000125621&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Install the entire library
pnpm install react-spring

# or just install your specific target (recommended)
pnpm install @react-spring/web&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-spring을 제거하고 @react-spring/web을 설치하여 react-native와 관련된 모든 패키지가 제거됐습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생하는 문제는 여러 계층의 원인이 있습니다. 이를 타고 올라가 보면 근본적인 원인을 발견하게 되고 이를 해결하는 것이 바로 문제를 해결했다라고 말할 수 있을 것입니다. 이번에 공유드린 경험을 빗대어 보면 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;문제: @tanstack/react-query 에러가 발생했다.&lt;br /&gt;원인: peer dependency(react-native)가 다른 @tanstack/react-query가 2개가 존재한다.&lt;br /&gt;&lt;br /&gt;문제: 왜 react-native를 peer dependency로 가질까?&lt;br /&gt;원인: react-spring을 설치할 때 @react-spring/native가 같이 설치되는데 이 패키지에서 react-native를 peer dependency로 가지기 때문이다.&lt;br /&gt;&lt;br /&gt;해결: react-spring을 제거하고 @react-spring/web을 설치해 @react-spring/native가 같이 설치되지 않도록 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 @tanstack/react-query를 원인으로 해결하여&amp;nbsp;끝내려 했는데 몇 주가 지난 지금 찝찝함에 다시 디버깅해 보니 근본적인 원인을 파악할 수 있었습니다. 근본적인 원인을 찾아 해결하려고 하지 않았던&amp;nbsp; 것을 반성하고 이번 경험을 통해 문제해결력을 성장시킬 수 있어서 제겐 값진 경험이었던 것 같습니다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>디버깅</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/54</guid>
      <comments>https://mechaniccoder.tistory.com/54#entry54comment</comments>
      <pubDate>Sat, 13 Apr 2024 18:39:53 +0900</pubDate>
    </item>
    <item>
      <title>defer, async 스크립트 더 들여다보기</title>
      <link>https://mechaniccoder.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 HTML parsing 하는 도중에 script(synchronous script)를 만나면 parsing을 멈추고 script 처리하는 것을 기다리게 됩니다. DOM은 초기 페이지 렌더링에 필수적인 리소스(critical resource)이므로 parsing이 완료될 때까지 페이지는 렌더링 되지 않습니다. 유저는 늦게 페이지 콘텐츠를 확인하게 되겠죠.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biuTqW/btsF40EuAjE/XBQxqMkKqkaJdgJDyhEtK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biuTqW/btsF40EuAjE/XBQxqMkKqkaJdgJDyhEtK0/img.png&quot; data-alt=&quot;HTML parsing blocking&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biuTqW/btsF40EuAjE/XBQxqMkKqkaJdgJDyhEtK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiuTqW%2FbtsF40EuAjE%2FXBQxqMkKqkaJdgJDyhEtK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;803&quot; height=&quot;177&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTML parsing blocking&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://javascript.info/script-async-defer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;async, defer script&lt;/a&gt;를 활용하면 이 문제를 해결할 수 있다는 것을 잘 아실 겁니다. 그런데 defer script를 실행하기 위해 CSSOM이 생성될 때까지 기다리며 DOMContentLoaded 이벤트를 지연시키고 script 순서에 영향받는 것을 알고 계셨나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에선 async, defer script에 대해 좀 더 깊이 알아보는 시간을 갖고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;defer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML parsing하는 도중에 백그라운드에서 script를 다운로드하고 DOM이 생성된 후에 script가 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zSu5g/btsF3CcSjGE/ZyI6jK7GZZKu9HdapBZkT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zSu5g/btsF3CcSjGE/ZyI6jK7GZZKu9HdapBZkT1/img.png&quot; data-alt=&quot;defer script timeline&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zSu5g/btsF3CcSjGE/ZyI6jK7GZZKu9HdapBZkT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzSu5g%2FbtsF3CcSjGE%2FZyI6jK7GZZKu9HdapBZkT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;178&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;defer script timeline&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 DOMContentLoaded 이벤트는 어디서 호출이 되는 걸까요? 스크립트가 실행되기 전일까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CIhg8/btsF2MUzKR2/hCEKskkOEXw6Q7MSUTewT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CIhg8/btsF2MUzKR2/hCEKskkOEXw6Q7MSUTewT1/img.png&quot; data-alt=&quot;defer DOMContentLoaded&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CIhg8/btsF2MUzKR2/hCEKskkOEXw6Q7MSUTewT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCIhg8%2FbtsF2MUzKR2%2FhCEKskkOEXw6Q7MSUTewT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1014&quot; height=&quot;285&quot; data-origin-width=&quot;1014&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;defer DOMContentLoaded&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 질문에 답하기 위해 2가지를 더 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. DOMContentLoaded 이벤트는 defer script 실행이 완료될 때까지 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. defer script는 CSS가 다운로드되고 CSSOM 트리가 생성될 때까지 실행 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 내용을 종합해보면 결국 CSSOM 트리가 생성되고 script가 실행될때까지 DOMContentLoaded 이벤트는 발생하지 않게 됩니다. CSS 파일이 너무 커서 다운로드에 오래 걸리면 defer script 실행이 지연되고 DOMContentLoaded 이벤트에도 영향을 주게 되는 것이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2609&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JvG7s/btsF1JEjARY/n4kUDmskTl4U7bjfN5CJkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JvG7s/btsF1JEjARY/n4kUDmskTl4U7bjfN5CJkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JvG7s/btsF1JEjARY/n4kUDmskTl4U7bjfN5CJkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJvG7s%2FbtsF1JEjARY%2Fn4kUDmskTl4U7bjfN5CJkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2609&quot; height=&quot;591&quot; data-origin-width=&quot;2609&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer의 특징 한 가지를 더 알아보죠. defer는 script 태그 순서에 영향을 받습니다. HTML에 다음과 같은 순서로 정의돼 있고 small.js의 다운로드가 먼저 완료됐다고 하겠습니다. 그러나 defer script는 script 순서별로 실행되기 때문에 아무리 small.js가 먼저 다운로드됐더라도 big.js가 다운로드 되고 실행될 때까지 지연됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1711292842325&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    &amp;lt;script src=&quot;big.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt; // small.js보다 먼저 실행됨
    &amp;lt;script src=&quot;small.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;async&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer와 마찬가지로 HTML parsing과 동시에 백그라운드에서 다운로드받지만 실행 시점에 차이가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. HTML parsing 도중에 다운로드가 완료된 경우 HTML parsing을 blocking 하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nf9b3/btsF2yvtbuX/jcCGTw1opEUKzo67tMZht0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nf9b3/btsF2yvtbuX/jcCGTw1opEUKzo67tMZht0/img.png&quot; data-alt=&quot;async timeline1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nf9b3/btsF2yvtbuX/jcCGTw1opEUKzo67tMZht0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnf9b3%2FbtsF2yvtbuX%2FjcCGTw1opEUKzo67tMZht0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;884&quot; height=&quot;178&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;async timeline1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. HTML parsing이 완료되고 나서 다운로드가 완료된 경우 HTML parsing을 blocking 하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2mqZj/btsF16sqI2R/kKxzfOWYZ6TNmDvspSxqTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2mqZj/btsF16sqI2R/kKxzfOWYZ6TNmDvspSxqTK/img.png&quot; data-alt=&quot;async timeline2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2mqZj/btsF16sqI2R/kKxzfOWYZ6TNmDvspSxqTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2mqZj%2FbtsF16sqI2R%2FkKxzfOWYZ6TNmDvspSxqTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1020&quot; height=&quot;178&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;async timeline2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 DOMContentLoaded 이벤트는 어디서 발생할까요? defer와는 달리 async는 DOMContentLoaded 이벤트에 영향을 주지 않습니다. 따라서 둘은 관련이 없는 것으로 이해하면 됩니다. (실행 이전 또는 이후에 발생 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 async는 CSS의 다운로드와 parsing과는 별개로 처리됩니다. 앞서 defer의 경우, CSS가 다운로드 되고 parsing되는 것을 기다렸죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;defer는 CSS download, parsing을 기다리며, DOMContentLoaded 이벤트를 지연시킨다.&lt;/li&gt;
&lt;li&gt;defer는 script 순서별로 실행된다. 다운로드가 빨리 되더라도 순서가 늦으면 실행되지 않는다.&lt;/li&gt;
&lt;li&gt;async는 다운로드 시점에 따라 HTML parsing을 blocking할 수도 있다.&lt;/li&gt;
&lt;li&gt;async는 DOMContentLoaded, CSS download와 parsing과 관련이 없이 별개로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;defer, async는 잘 알고 있다고 생각했는데 자세히 들여다보니 모르는 것 투성이였습니다. 이번 기회를 통해 브라우저 동작을 더 깊이 알 수 있어 좋은 시간이었던 것 같네요. 내가 알고 있는 것을 진짜로 알고 있는지 항상 생각하고 정리하는 자세를 가져야겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/script-async-defer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://javascript.info/script-async-defer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/critical-rendering-path/analyzing-crp&quot;&gt;https://web.dev/articles/critical-rendering-path/analyzing-crp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/53</guid>
      <comments>https://mechaniccoder.tistory.com/53#entry53comment</comments>
      <pubDate>Mon, 25 Mar 2024 00:43:07 +0900</pubDate>
    </item>
    <item>
      <title>브라우저 Memory leak 디버깅 해보기</title>
      <link>https://mechaniccoder.tistory.com/52</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 서비스에서 메모리 누수가 발생하고 있는 것을 발견했습니다. 아무리 개발 환경이지만 JavaScript VM 힙 메모리에 1GB 가량 할당되어 있었죠. 그러다보니 시간이 지나면 앱이 너무 느려져 프로세스를 끄고 다시 실행하곤 했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;크롬 메모리 패널&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메모리 누수가 발생하는 곳을 찾기 위해선 먼저 메모리 패널을 어떻게 사용해야 하는지 알아야했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Heap snapshot&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 JavaScript VM에 할당된 객체와 DOM에 대한 정보를 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Allocation instrumentation on timeline&lt;span style=&quot;background-color: #ffffff; color: #1e1e21; text-align: left;&quot;&gt;.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1e1e21; text-align: left;&quot;&gt;시간에 따라 메모리 할당과 해제되는 것을 보고 싶으면 이 기능을 사용하면 됩니다. 메모리 누수를 찾을 때 이 기능을 활용하면 쉽게 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;메모리 누수 지점 파악하기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 어떤 객체들이 메모리 할당됐는지 보기 위해 Heap snapshot을 사용했습니다. 아래 이미지의 오른쪽 상단을 보면 shallow size와 retained size를 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;shallow size: 특정 생성자로부터 직접적으로 생성된 객체들의 메모리 사이즈입니다.&lt;/li&gt;
&lt;li&gt;retained size: 생성된 객체들을 삭제했을때 해제되는 메모리 사이즈입니다. 해당 객체에서 여러 객체를 참조할 수 있고 이를 해제했을때 더 이상 도달할 수 없는 객체들은 GC에 의해 메모리 수거되기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;shallow size가 11%이고 retained size가 72%임을 봤을때 참조하는 객체가 많으며 이 객체를 삭제했을때 같이 해제되는 메모리도 크다라는 의미입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이미지 하단의 박스는 이 객체를 어디서 참조하고 있는지를 보여줍니다. 예를 들어, Konva in Window는 Window 객체의 Konva 프로퍼티로 이 객체를 참조하고 있다는 의미입니다. 그 외에 여러 경로를 통해 이 객체를 참조하고 있어 도달 가능한 객체이므로 GC가 메모리를 수거하지 않을겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;1287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dORgJD/btsFEwFhaWF/idg3IXPPtqXkmnkpK8KLRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dORgJD/btsFEwFhaWF/idg3IXPPtqXkmnkpK8KLRk/img.png&quot; data-alt=&quot;Memory panel heap snapshot&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dORgJD/btsFEwFhaWF/idg3IXPPtqXkmnkpK8KLRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdORgJD%2FbtsFEwFhaWF%2Fidg3IXPPtqXkmnkpK8KLRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;1287&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;1287&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Memory panel heap snapshot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 서비스에서 canvas library인 konva를 사용중이었고 영상의 자막을 렌더링 하기 위해 Text 생성자를 사용해 자막의 너비를 계산하는 로직을 발견했습니다. 확실히 원인을 파악하기 위해 Allocation instrumentation on timeline 기능을 활용해 해당 로직을 실행하며 시간에 따른 메모리 할당과 해제를 확인해봤습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 이미지 상단의 박스를 확인해보면 파란색 막대가 보입니다. 메모리가 할당됐다는 의미입니다. GC에 의해 메모리가 수거되면 회색 막대로 변경됩니다. 시간이 지났음에도 파란색 막대가 남아있는걸 보니 메모리가 수거되지 않았고 어딘가에서 객체를 참조하고 있는 것입니다. 즉, 해당 로직이 실행될때마다 객체가 생성되고 참조를 들고 있어 메모리 누수가 발생하는 것을 파악했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;1243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUe4d5/btsFFY2njO5/F870LDfp4ph93TKztQ6svK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUe4d5/btsFFY2njO5/F870LDfp4ph93TKztQ6svK/img.png&quot; data-alt=&quot;Memory leak profiling&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUe4d5/btsFFY2njO5/F870LDfp4ph93TKztQ6svK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUe4d5%2FbtsFFY2njO5%2FF870LDfp4ph93TKztQ6svK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1754&quot; height=&quot;1243&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;1243&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Memory leak profiling&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 누수 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 누수를 발생시키는 코드를 확인해보니 Text 생성자로 textNode를 생성한 뒤, 텍스트 너비를 구해서 반환하고 있습니다. 여기서 의아했던 부분은 함수의 호출이 끝난 뒤 인스턴스를 가르키는 textNode 변수가 정리되기 때문에 더 이상 도달하지 못해서 GC가 수거할 것 같은데 라는 생각이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 메모리 패널로 확인한 결과를 보면 전역(Window) 및 다른 객체에서도 참조를 들고 있다는 것을 다시 한번 생각했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710004712870&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const measureTextWidth = () =&amp;gt; {
  const textNode = new Konva.Text();
  
  const textWidth = //...

  return textWidth;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 인스턴스는 필요한 결과를 다른 변수에 저장한 뒤 직접 삭제하여 메모리 누수를 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1710005031109&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const measureTextWidth = () =&amp;gt; {
  const textNode = new Konva.Text();
  
  const textWidth = //...
  
  textNode.destory() // 직접 인스턴스를 삭제

  return textWidth;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 이미지에서 파란색 막대가 남아있던 것과는 다르게 사라진 것을 확인할 수 있습니다. 즉 메모리가 잘 수거됐다는 것이죠.  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l9kYt/btsFEgpqe7k/KsDOSv49kEoKieZNsNtw01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l9kYt/btsFEgpqe7k/KsDOSv49kEoKieZNsNtw01/img.png&quot; data-alt=&quot;Resolve memory leak profiling&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l9kYt/btsFEgpqe7k/KsDOSv49kEoKieZNsNtw01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl9kYt%2FbtsFEgpqe7k%2FKsDOSv49kEoKieZNsNtw01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1821&quot; height=&quot;1244&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Resolve memory leak profiling&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 프로파일링을 사용하며 메모리 누수의 원인을 파악하고 실제 코드에서 해결하는 과정을 겪어보니 재밌었습니다. 직감에 의존해서 그냥 여기인 것 같은데? 가 아니라 몇 가지 단서로부터 범인을 찾아나가는 마치 탐정이 된 것 같은 기분이었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 문제가 발생했을때 활용해야할 도구들과 어떤 과정을 거쳐서 원인을 파악해야할 지 알게 됐습니다. 다음 번에 문제를 마주했을 땐 더 빠르게 파악하고 해결할 수가 있을 것 같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>debugging</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/52</guid>
      <comments>https://mechaniccoder.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 10 Mar 2024 02:35:19 +0900</pubDate>
    </item>
    <item>
      <title>V8 엔진에서 number, string은 어떻게 처리될까?</title>
      <link>https://mechaniccoder.tistory.com/51</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 number, string 타입을 생각 없이 사용하다 최근에 V8 엔진에서 어떻게 처리되고 있는지가 궁금해졌습니다. 이번 포스팅에서는 각각의 타입이 메모리에서 어떻게 할당되고 처리되는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Number&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;number 타입은 내부적으로 &lt;b&gt;SMI&lt;/b&gt;(Small Integer), &lt;b&gt;Heap number&lt;/b&gt; 2가지 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;SMI&lt;/b&gt;: 31 bits 범위에 속하는 정수를 의미합니다. 이 범위의 데이터는 Heap이 아닌 Stack 메모리에 저장됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Smi represents integer Numbers that can be stored in 31 bits. &lt;br /&gt;Smis are immediate which means they are NOT allocated in the heap.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/v8/v8/blob/c53c7952e1a4c69df7884250711c755e41d0d8a1/src/objects/smi.h#L23&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/v8/v8/blob/c53c7952e1a4c69df7884250711c755e41d0d8a1/src/objects/smi.h#L23&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;Heap number&lt;/b&gt;: SMI가 아닌 정수는 Heap number이며 64 bits의 floating point로 Heap 메모리에 저장됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The HeapNumber class describes heap allocated numbers that cannot be represented in a Smi (small integer).&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/v8/v8/blob/c53c7952e1a4c69df7884250711c755e41d0d8a1/src/objects/heap-number.h#L26&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/v8/v8/blob/c53c7952e1a4c69df7884250711c755e41d0d8a1/src/objects/heap-number.h#L26&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;String&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에선 UTF-16를 사용해 문자열을 인코딩합니다. 일반적인 라틴어 등은 2 bytes로 저장되고 이모지와 같은 문자는 4 bytes로 저장됩니다. apple이라는 문자열은 2 bytes * 4 = 8 bytes의 메모리 사이즈를 가지는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Constant pool&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;constant pool이라는 개념을 알고가면 좋을 것 같습 니다. V8 엔진은 메모리를 효율적으로 사용하기 위해 상수값을 constant pool에서 관리합니다. 아래 코드가 메모리에 어떻게 저장되는지를 그려보면 이해하기 쉬울 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;str1, str2는 constant pool에 동일한 hello를 바라보기 때문에 같은 메모리 주소를 참조하고 있는 것이죠.&lt;/p&gt;
&lt;pre id=&quot;code_1709971333049&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const str1 = &quot;Hello&quot;;
const str2 = &quot;Hello&quot;;
const str3 = &quot;World&quot;;
console.log(str1 == str2); // true
console.log(str1 == str3); // false&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;1478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beytIv/btsFFLBU88r/S29U2vd9ctce3HAf18nOtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beytIv/btsFFLBU88r/S29U2vd9ctce3HAf18nOtK/img.png&quot; data-alt=&quot;string constant pool&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beytIv/btsFFLBU88r/S29U2vd9ctce3HAf18nOtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeytIv%2FbtsFFLBU88r%2FS29U2vd9ctce3HAf18nOtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;620&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;1478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;string constant pool&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아래와 같이 String 생성자로 만든 문자열은 메모리에 어떻게 저장이 될까요? 아래 그림과 같이 생성자에 대한 인스턴스가 Heap 메모리에 저장되고 그 인스턴스에서 constant pool에 문자열을 참조하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1709971877394&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const str1 = &quot;Hello&quot;;
const str2 = &quot;Hello&quot;;
const str3 = new String(&quot;Hello&quot;)
console.log(str1 == str2); // true
console.log(str1 == str3); // false
console.log(str2 == str3); // false&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;1478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5vj4N/btsFFKprVwF/mewvey6fimKFvDFXeYIqmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5vj4N/btsFFKprVwF/mewvey6fimKFvDFXeYIqmk/img.png&quot; data-alt=&quot;string constant pool with String constructor&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5vj4N/btsFFKprVwF/mewvey6fimKFvDFXeYIqmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5vj4N%2FbtsFFKprVwF%2Fmewvey6fimKFvDFXeYIqmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;719&quot; height=&quot;658&quot; data-origin-width=&quot;1615&quot; data-origin-height=&quot;1478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;string constant pool with String constructor&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;V8 엔진에서 Number, String 타입이 어떻게 처리되는지 알아보았습니다. 자바스크립트를 그냥 사용하는 것이 아니라 내부적으로 동작하는 원리를 안다면 더 효율적인 코드를 작성할 수 있을 것이라 기대합니다. 앞으로 V8 엔진과 관련된 내용을 자주 올려 딥 다이브 해보도록 하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@nourhansaed6/what-is-string-constant-pool-in-js-1b1795f7624b&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@nourhansaed6/what-is-string-constant-pool-in-js-1b1795f7624b&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@seorim6417/%EC%9B%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-V8-JS-%EC%97%94%EC%A7%84-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C%EC%9A%94#2-string-type&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@seorim6417/%EC%9B%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-V8-JS-%EC%97%94%EC%A7%84-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C%EC%9A%94#2-string-type&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend</category>
      <category>JavaScript</category>
      <category>V8</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/51</guid>
      <comments>https://mechaniccoder.tistory.com/51#entry51comment</comments>
      <pubDate>Sat, 9 Mar 2024 17:23:01 +0900</pubDate>
    </item>
    <item>
      <title>2023년을 돌아보며</title>
      <link>https://mechaniccoder.tistory.com/50</link>
      <description>&lt;p&gt;2024년 새해가 밝았다. 2023년 개발자로서의 나는 얼마나 성장했고 어떤 것이 부족하며 앞으로 어떻게 할지 글을 쓰며 정리해보려 한다.&lt;/p&gt;
&lt;h2&gt;얼마나 성장했을까?&lt;/h2&gt;
&lt;p&gt;2023년 1월의 나와 지금의 나를 비교해봤을때 얼마나 성장했을지를 생각해봤다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;상태 관리와 컴포넌트를 설계하는 역량&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;컴포넌트 설계의 경우 함수형 프로그래밍을 학습한게 영향이 있었다. 순수한 부분과 순순하지 않은 부분을 구분 짓고 사고를 확장시켜 이를 컴포넌트 설계에도 적용하려고 했던 것이 도움이 됐다.&lt;/p&gt;
&lt;p&gt;상태 관리는 Redux, Tanstack Query와 같이 여러 상태 관리 라이브러리를 써보면서 공통되는 원칙을 기반으로 한게 도움이 됐다. 최소한의 데이터를 상태로 정의하며 이를 가져오도록 selector를 사용하되 컴포넌트에서는 최대한 작은 데이터를 가져오도록 하는 것이 공통된 부분이라고 생각한다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;테스트 코드를 작성하는 역량&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이펙티브 소프트웨어 테스팅이라는 책을 읽었던게 많은 도움이 되었다. 이 책을 읽고 테스트를 작성할때 명세 테스트를 기반으로 버그가 발생할 수 있는 범위와 경계를 생각하며 테스트하기 시작했다.&lt;/p&gt;
&lt;h2&gt;어떤게 부족할까?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;JavScript, React에 대한 깊은 지식&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;프런트엔드로서 사용하는 주요 도구라고 생각한다. 이제는 추상화된 인터페이스 및 동작원리를 아는 수준이 아니라 내부적으로 코드가 어떻게 동작하는지를 알아야 한다고 생각한다. React만 언급했지만 JavaScript도 마찬가지다. 좀 더 내려가 V8엔진 수준에서 어떻게 동작하는지 알아야 더 높은 수준에 도달할 수있다고 생각한다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;클라우드 지식과 이를 활용한 경험&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;프런트엔드 개발자도 클라우드를 잘 활용해야 한다고 생각한다. Serverless와 Edge computing의 활용도가 올라가고 있고 정적 리소스는 스토리지와 CDN을 사용한다. SSR이 여러 앱에서 활용되고 있는 만큼 서버에 관한 지식과 이를 배포하고 운영하는 능력도 갖춰야 한다.&lt;/p&gt;
&lt;h2&gt;무엇을 할까?&lt;/h2&gt;
&lt;p&gt;부족한 것을 보완하며 잘하고 있는 것은 더 잘 할 수 있도록 해야할 것이다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JavaScript, React등 사용하는 도구를 깊이 이해하자&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;V8엔진과 React 내부의 동작원리를 이해하며 어떻게 해야 이 도구들을 더 잘 활용할 수 있을지 고민하자&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;클라우드 지식 학습과 이를 활용한 경험&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AWS 클라우드를 학습하는 시간도 따로 챙기며 간단한 사이드 프로젝트에 이를 활용해보자. 추가적으로 회사에서 AWS가 어떻게 활용되고 있는지 내 것으로 만드는 것도 좋은 방법이라고 생각한다.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;웹에 대한 지식과 깊이&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;웹 기술에 대한 전문성을 쌓아서 실무에서 마주한 어려운 문제들을 해결하거나 앱을 고도화하는 것에 활용할줄 아는 개발자로 성장하고 싶다. 이를 위해선 결국 HTTP, 웹 브라우저를 깊이 있게 이해해야 한다. 현재 web.dev 사이트를 통해 학습중이고 2024년에는 이 사이트에 있는 내용들을 온전히 내 것으로 소화할 것이다.&lt;/p&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;확실히 코딩을 처음 시작했을때의 성장 속도보다는 더뎌진게 느껴진다. 그때는 성장을 했다기보다는 기본적인 지식도 모르는 상태이다 보니 조금만 알아도 성장했다고 느꼈기 때문일 것이다. 지금은 성장했다고 느끼기 위해선 많은 고민들과 값비싼 경험들을 지불해야한다고 생각한다. 우리가 RPG 게임 캐릭터를 키울때 레벨이 올라가면 더 많은 경험치가 필요한 것처럼 말이다. 가끔은 이렇게 느려진 성장속도로 인해 불안하고 초조해질때가 있다. 그럴때마다 나를 믿고 나아갈 수 밖에 없다고 생각한다. 물론 잘못된 방향으로 우직하게 나아가는 것 만큼 미련한 것은 없기 때문에 이에 대한 피드백은 해야할 것이다.&lt;/p&gt;</description>
      <category>성장</category>
      <category>회고</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/50</guid>
      <comments>https://mechaniccoder.tistory.com/50#entry50comment</comments>
      <pubDate>Tue, 2 Jan 2024 00:03:50 +0900</pubDate>
    </item>
    <item>
      <title>HTTP Cache 제대로 알기</title>
      <link>https://mechaniccoder.tistory.com/49</link>
      <description>&lt;p&gt;네트워크로 데이터를 가져오는건 느립니다. 데이터가 지구 반대편에 위치한다면 더욱 느려지겠죠. 우리는 데이터를 더 빠르게 가져올 방법이 필요합니다. 그 중에 하나가 HTTP cache를 활용하는 것입니다. 프런트엔드 개발자로서 어떤 사이트든 개발자 도구로 분석해보면 보이지 않는 곳에서 HTTP cache가 유용하게 사용되고 있는 것을 확인할 수 있습니다. HTML을 가져올때, 이미지나 비디오를 가져올때, Javascript,CSS 등 critical resources에도 HTTP cache는 사용됩니다. 그럼 왜 HTTP cache가 필요한 걸까요?&lt;/p&gt;
&lt;h2&gt;HTTP cache가 필요한 이유&lt;/h2&gt;
&lt;p&gt;HTTP cache를 사용하면 느린 네트워크를 통해 데이터를 가져오는 것이 아니라 사용자와 가까운 브라우저, 웹 서버 등으로부터 데이터를 가져올 수 있기 때문에 더 빠르게 데이터를 가져옵니다. 느린 네트워크는 다음과 같이 좋지 않은 사용자 경험의 원인이 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rendering blocking 리소스인 CSS, JavaScript를 느리게 가져올수록 사용자는 더 느리게 화면을 보게됩니다.&lt;/li&gt;
&lt;li&gt;네트워크 리소스를 더 사용하기 때문에 모바일 데이터가 낭비됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cache layers&lt;/h2&gt;
&lt;p&gt;사용자로부터 시작해 여러가지 cache layer에 대해 소개하겠습니다. 아래의 이미지는 사용자로부터 어떤 cache layer가 있는지 확인할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자 - service worker - browser - cdn(proxy) - origin server&lt;br&gt;이번 포스팅에선 HTTP cache를 다룹니다. Service worker에 대해서는 다음에 다뤄보도록 하겠습니다.&lt;br&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/03yYx/btsBaYGEI9L/OReKB09jqK9rzLo9nQ3rqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/03yYx/btsBaYGEI9L/OReKB09jqK9rzLo9nQ3rqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/03yYx/btsBaYGEI9L/OReKB09jqK9rzLo9nQ3rqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F03yYx%2FbtsBaYGEI9L%2FOReKB09jqK9rzLo9nQ3rqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;1170&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;1170&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fingerprint&lt;/h2&gt;
&lt;p&gt;Cache invalidation을 원활히 하기 위해서는 고유한 파일 이름을 사용하는 것이 좋은데요. 이를 fingerprint라고 합니다. 예를 들어보죠. 페이지가 로드되기 위해 &lt;code&gt;script.js&lt;/code&gt;라는 파일이 있고 이를 1년 동안 캐싱하도록 설정했다고 하겠습니다. 그런데 일주일 후 요구사항이 변경돼서 &lt;code&gt;script.js&lt;/code&gt; 파일이 변경됐습니다. 유저의 브라우저에는 이전에 캐싱한 파일이 남아있고 이를 1년동안 사용하도록 했기 때문에 변경된 코드를 새로 배포해도 변경사항이 적용되지 않을 것입니다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 고유한 파일 이름을 사용합니다. 처음 배포한 파일의 이름이 &lt;code&gt;script.102938.js&lt;/code&gt;이라고 해보죠. 파일의 contents로부터 고유한 값을 생성하기 때문에 만약 변경점이 있다고 하면 &lt;code&gt;script.919283.js&lt;/code&gt;와 같이 파일의 이름이 바뀌게 됩니다. &lt;code&gt;script.102938.js&lt;/code&gt;를 1년 동안 캐싱하더라도 새로 요청할 파일의 이름은 &lt;code&gt;script.919283.js&lt;/code&gt;이기 때문에 변경된 파일을 가져올 수 있게 됩니다. 그 과정은 아래와 같아요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;웹 서버로부터 html(&lt;code&gt;index.html&lt;/code&gt;)을 가져옵니다.&lt;/li&gt;
&lt;li&gt;브라우저는 html로부터 &lt;code&gt;script.102938.js&lt;/code&gt;를 가져옵니다. 캐시 설정이 1년이기 때문에 이를 브라우저에 캐싱합니다.&lt;/li&gt;
&lt;li&gt;변경 사항이 적용된 후 &lt;code&gt;script.919283.js&lt;/code&gt;를 바라보는 html 파일을 웹서버로 배포합니다.&lt;/li&gt;
&lt;li&gt;사용자의 브라우저로부터 html 파일을 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;script.919283.js&lt;/code&gt;를 요청합니다. 이 파일은 캐싱되지 않았기 때문에 변경점이 잘 반영됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;script 파일을 예로 들었는데 image, video, css등 여러 파일에도 똑같이 fingerprint를 적용할 수 있습니다. 실제로 &lt;a href=&quot;https://toss.tech/article/smart-web-service-cache&quot;&gt;토스&lt;/a&gt;에서 fingerprint를 어떻게 활용하고 있는지 확인할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;Cache-Control&lt;/h2&gt;
&lt;p&gt;기본적인 cache directive에 대해서는 여러 사이트에서 잘 설명하고 있습니다. 개인적으로 헷갈리는 것만 정리해보도록 하죠.&lt;/p&gt;
&lt;h3&gt;s-max-age&lt;/h3&gt;
&lt;p&gt;public에 대한 캐시 설정입니다.(CDN, proxy 등)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: s-max-age=3600&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;must-revalidate&lt;/h3&gt;
&lt;p&gt;cache를 사용하기 전에 서버에 검증을 먼저해야 합니다. 600초 안에 리소스를 요청하는 경우 캐시된 리소스를 사용하고 만료된 후에는 서버로 리소스가 변경됐는지 검증을 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: max-age=600, must-revalidate&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;stale-while-revalidate&lt;/h3&gt;
&lt;p&gt;지정된 시간내에 요청이 발생하는 경우 캐시된 리소스를 쓰는 동시에 비동기적으로 리소스에 대한 요청을 보냅니다. 리소스가 변경됐으면 캐시를 갈아끼우겠죠? 이 캐시 전략은 프런트엔드 개발자 분들이라면 친숙할겁니다. &lt;a href=&quot;https://tanstack.com/query/latest&quot;&gt;tanstack query&lt;/a&gt;와 &lt;a href=&quot;https://swr.vercel.app/ko/index.ko&quot;&gt;swr&lt;/a&gt;에서 사용되는 철학이기 때문이죠. stale-while-revalidate에 대한 더 자세한 내용은 &lt;a href=&quot;https://web.dev/articles/stale-while-revalidate&quot;&gt;web.dev 아티클&lt;/a&gt;에서 확인하세요.&lt;/p&gt;
&lt;h2&gt;실무에서 사용하기&lt;/h2&gt;
&lt;p&gt;그럼 지금까지 배운 캐시 전략을 실무에서는 어떻게 사용할 수 있을까요? 같이 한번 생각해보죠.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;html&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;먼저 html입니다. html은 모든 필요한 리소스의 시작점이기 때문에 사용자 브라우저에 잘못 캐싱되면 무효화를 할 수가 없습니다. 따라서 캐싱은 하지만 서버에 검증 요청을 하도록 설정하는게 좋을 것 같네요.&lt;/p&gt;
&lt;p&gt;아래와 같이 설정하면 매번 서버로 검증 요청을 보내지만 리소스가 변하지 않은 경우 304 Not Modified 응답을 받아서 캐싱된 리소스를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: max-age=0, must-revalidate&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;토스의 경우는 아래와 같이 public 캐시에 대한 설정을 추가했다고 합니다. 아래와 같이 브라우저에서는 항상 검증 요청을 보내고 CDN에서는 1년동안 캐시를 하도록 설정합니다. public 캐시를 1년으로 설정해도 문제가 안돼는 이유는 CDN은 manual하게 invalidation할 수가 있기 때문이죠. 배포를 하면 CDN invalidation 요청을 보내 캐싱된 리소스를 무효화하면 유저는 항상 최신 버전의 html파일을 바라볼 수가 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: max-age=0, s-max-age=31536000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만 제가 여기서 궁금했던 점은 html 파일에 개인정보가 있는 경우입니다. CDN의 경우 public하게 열려있을 수 있어서 만약 개인정보가 들어있다면 이 설정은 하지 않는 것이 좋다고 생각합니다.(물론 CDN을 private하게 사용하는 경우도 있을 수 있겠네요.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Media(image, video, audio...), javascript, css&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이미지, 비디오와 같은 미디어 파일들이나 javascript, css의 경우 보통 번들러(webpack, vite)에서 fingerprint를 적용하게 됩니다. 따라서 최대 설정인 1년으로 캐시하면 좋겠네요.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: max-age=31536000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그럼 위의 고민들을 가지고 제 회사의 랜딩페이지를 분석해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;index.html&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위에서 고민한대로 캐시 설정이 잘 되어있네요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;27&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LetJg/btsBc0jbxv8/kMqkifkGJF7VbVLVB1edv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LetJg/btsBc0jbxv8/kMqkifkGJF7VbVLVB1edv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LetJg/btsBc0jbxv8/kMqkifkGJF7VbVLVB1edv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLetJg%2FbtsBc0jbxv8%2FkMqkifkGJF7VbVLVB1edv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;464&quot; height=&quot;27&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;27&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;image&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;image의 경우 현재 fingerprint가 적용이 안되어 있어서 매번 검증 요청을 보내고 있네요. 이를 개선한다면 image에 fingerprint를 적용 후, 1년 캐시를 적용할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;22&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nHd17/btsBf9zEZl5/QKK31lTTN2iw4AV5LOBUrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nHd17/btsBf9zEZl5/QKK31lTTN2iw4AV5LOBUrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nHd17/btsBf9zEZl5/QKK31lTTN2iw4AV5LOBUrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnHd17%2FbtsBf9zEZl5%2FQKK31lTTN2iw4AV5LOBUrk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;22&quot; data-origin-width=&quot;456&quot; data-origin-height=&quot;22&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;js&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;js파일은 fingerprint가 적용되어 있어서 1년의 캐시기간을 설정했고 변하지 않을게 확실하기 때문에 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Cache-Control#immutable&quot;&gt;immutable&lt;/a&gt;속성까지 준 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;20&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPct0x/btsBeKgjdp3/DzA4PvPNq1EtbCsXbswmVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPct0x/btsBeKgjdp3/DzA4PvPNq1EtbCsXbswmVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPct0x/btsBeKgjdp3/DzA4PvPNq1EtbCsXbswmVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPct0x%2FbtsBeKgjdp3%2FDzA4PvPNq1EtbCsXbswmVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;470&quot; height=&quot;20&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;20&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;css&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;css도 마찬가지입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;23&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G57XW/btsBeU36AiZ/RA6kKigkQiy1F24sTSs58k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G57XW/btsBeU36AiZ/RA6kKigkQiy1F24sTSs58k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G57XW/btsBeU36AiZ/RA6kKigkQiy1F24sTSs58k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG57XW%2FbtsBeU36AiZ%2FRA6kKigkQiy1F24sTSs58k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;473&quot; height=&quot;23&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;23&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;HTTP 캐시를 잘 쓰면 트래픽에 따른 부하를 줄일 수 있고 더 빠른 리소스를 전달하기 때문에 사용자 경험을 향상시킬 수 있다고 생각합니다. 앞으로 HTTP cache뿐만 아니라 service worker를 통한 Cache API등 더 다양한 캐시에 대해 알아보도록 하겠습니다. 여기까지 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=HiBDZgTNpXY&amp;amp;t=1s&quot;&gt;https://www.youtube.com/watch?v=HiBDZgTNpXY&amp;amp;t=1s&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/http-cache&quot;&gt;https://web.dev/articles/http-cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/stale-while-revalidate&quot;&gt;https://web.dev/articles/stale-while-revalidate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/stale-while-revalidate&quot;&gt;https://web.dev/articles/stale-while-revalidate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Frontend</category>
      <category>HTTP Cache</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/49</guid>
      <comments>https://mechaniccoder.tistory.com/49#entry49comment</comments>
      <pubDate>Thu, 30 Nov 2023 22:57:30 +0900</pubDate>
    </item>
    <item>
      <title>웹 성능 세션을 준비하여 공유해보자</title>
      <link>https://mechaniccoder.tistory.com/48</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;웹 성능&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;서비스가 커지고 기능이 많아질수록 프런트엔드에서 많은 데이터를 관리하게 됩니다. 제가 현재 개발하는 AI SaaS에서는 one page에서 수백, 수천개 이상의 블럭 데이터, 미디어 파일들에 대한 상태관리를 하고 있습니다. 사용자 경험을 개선하여 KPI를 향상시키기 위해서 성능 최적화의 필요성을 많이 느꼈습니다. web.dev의 웹 성능에 관한 글이 있어서 이를 소화하고 팀원들에게 공유해보려 합니다.&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;웹 성능이 왜 문제일까?&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;시간이 지날수록 콘텐츠를 소비하기 위해 더 많은 유저들이 웹을 사용합니다. 유저들의 눈높이가 높아지면서 여러 서비스들을 비교해보며 느리거나 반응이 좋지 않은 서비스는 자연스레 사용하지 않게 됩니다. 저만 하더라도 어떤 서비스를 사용할때 유튜브나 인스타그램처럼 최적화가 잘 되어있는 서비스랑 비교하며 &quot;아 이 서비스 왜케 느려. 답답하네.&quot; 와 같은 생각을 했었습니다. 웹 성능이 좋으면 유저의 이탈을 예방하고 KPI에 도움이 됩니다. 이는 비지니스 성공과도 밀접한 연관이 있다는 것입니다. 따라서 프런트엔드 엔지니어로서 웹 성능 최적화는 필수적인 역량일 것입니다.&lt;/p&gt;&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;어떻게 학습할건데?&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그동안 저는 web.dev 사이트로 웹 성능 최적화에 대해 학습했습니다. 좋은 내용이 되게 많아서 이를 회사 팀원들에게 지식을 전파하고 싶었고 매주 이와 관련한 세션을 준비하여 공유한다면 저만 알고있는 지식을 팀 전체에 퍼뜨릴 수 있겠다는 생각을 하게 됐습니다. 모든 팀원들이 웹 성능에 대한 지식을 가지고 개발하는 product의 사용자 경험을 개선하고 개발자로서 성장하는 경험을 할 수 있으면 좋겠네요.&lt;br&gt;세션을 준비하면서 포스팅을 할건데 web.dev에 있는 지식을 중복으로 정리하는 것 보다는 이와 연관된 레퍼런스들을 정리하고 중요한 개념에 대해서는 짚고 넘어가보려 합니다.&lt;br&gt;메인 주제를 웹 성능으로 잡았는데 web.dev에는 전반적인 웹 기술을 학습할 수 있는 내용이 있습니다. 이와 관련한 것도 세션으로 같이 준비하려 합니다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/48</guid>
      <comments>https://mechaniccoder.tistory.com/48#entry48comment</comments>
      <pubDate>Sun, 26 Nov 2023 21:53:08 +0900</pubDate>
    </item>
    <item>
      <title>팀 기능 회고</title>
      <link>https://mechaniccoder.tistory.com/47</link>
      <description>&lt;h2&gt;팀 기능 (2023.10.15 ~ 2023.11.14)&lt;/h2&gt;
&lt;p&gt;한달동안 팀 기능 개발을 완료하여 운영환경에 배포했습니다. 기존의 스프린트 방식의 프로세스에서 &lt;a href=&quot;https://medium.com/@slow_scale/shape-up-%ED%95%9C%EA%B5%AD%EC%96%B4-%EC%9A%94%EC%95%BD-e6436f6eba8a&quot;&gt;shape up 방식의 프로세스&lt;/a&gt;를 처음 경험하다보니 시행착오를 겪었지만 운영환경에서 유저들이 잘 사용하고 있는 것을 보니 나름 잘 해낸 것 같네요.&lt;/p&gt;
&lt;h2&gt;잘한점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;할당된 작업과 추가적인 작업들을 빠른 속도로 개발 및 수정하여 다른 팀원들의 부담을 줄이고 더 빠르게 테스트를 하도록 했다.&lt;/li&gt;
&lt;li&gt;기존의 UI, 비지니스 로직을 따로 개발하는 것이 아닌 MSW를 사용해 함께 개발하도록 프로세스를 개선하여 연동할때 이슈가 발생하지 않도록 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;못한점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;디테일한 디자인을 놓치는 경우가 많았다.&lt;/li&gt;
&lt;li&gt;개발 상황에 대해 팀원들에게 자주 공유하지 않았다.&lt;/li&gt;
&lt;li&gt;개발하면서 테스트를 작성하지 않았다.&lt;/li&gt;
&lt;li&gt;꼼꼼하게 매뉴얼 테스트를 하지 않았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;개선할점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;빠른 속도로 개발하고 나서 자체적으로 테스트해야하는 항목들을 정해보자. 크로스 브라우징, 웹 접근성, 디자인, 테스트를 해보면서 더 완벽하게 개발하자.&lt;/li&gt;
&lt;li&gt;현재 진행 상황이나 팀 전체에 알려하는 이슈가 있다면 메신저로 공유하는 습관을 길러보자.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>성장</category>
      <category>회고</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/47</guid>
      <comments>https://mechaniccoder.tistory.com/47#entry47comment</comments>
      <pubDate>Sun, 19 Nov 2023 14:14:22 +0900</pubDate>
    </item>
    <item>
      <title>프로그래밍 패러다임</title>
      <link>https://mechaniccoder.tistory.com/46</link>
      <description>&lt;h2&gt;들어가면서&lt;/h2&gt;
&lt;p&gt;얼마전에 회사에서 클린 아키텍처 책을 바탕으로 스터디를 시작했다. 예전 코딩에 입문한지 얼마 되지 않았을 때 읽었던 기억이 있는데 그때는 배경 지식이 없다보니 대부분의 내용을 잘 이해하지 못했다. 이제는 책을 이해하기 위한 배경 지식이 어느 정도 쌓인 상태이고 이 책을 통해 더 나은 비지니스 모델의 변경에 유연하게 대처하는 아키텍처를 설계하는 역량을 쌓을 것이다.&lt;/p&gt;
&lt;p&gt;스터디를 진행하면서 중요한 내용은 나만의 관점으로 소화하여 블로그에 기록할 예정이다.&lt;/p&gt;
&lt;h2&gt;패러다임&lt;/h2&gt;
&lt;p&gt;프로그래밍 패러다임에 관한 내용이 나온다. 내가 알기로는 절차지향, 객체지향, 함수형 프로그래밍 이렇게 세가지가 존재하는 것으로 알고 있는데 책에서는 구조적, 객제지향, 함수형을 소개하고 있다. 절차지향과 구조적이 다른점인데 찾아보니 구조적 프로그래밍이 절차지향을 포함하는 패러다임이라고 한다. 자세한 내용은 아래에서 살펴보자.&lt;/p&gt;
&lt;p&gt;그리고 각 패러다임마다 개발자로부터 하나의 능력을 뺏어가는데 어떤 능력을 뺏어가는지 보는 것도 재밌을 것이다.&lt;/p&gt;
&lt;h2&gt;구조적 프로그래밍&lt;/h2&gt;
&lt;p&gt;구조적 프로그래밍은 개발자로부터 goto문을 뺏어갔다. 코드의 흐름을 뛰어넘어 다니는 goto문은 프로그램을 더 작은 단위의 모듈로 분해하는 것에 방해가 된다. 이를 위해서 순차, 분기, 반복만을 사용해 프로그램을 작성하도록 한다.&lt;/p&gt;
&lt;p&gt;나는 다음 문장을 읽고 영감을 받았다. &amp;quot;큰 시스템을 모듈, 컴포넌트로 나눌 수 있으며 각각의 모듈과 컴포넌트는 입증할 수 있는 작은 단위로 더 나눌 수 있다.&amp;quot; 즉 테스트 가능한 단위로 시스템을 나눌 수 있게 됐다는 의미라고 받아들였다. 하나의 큰 덩어리를 테스트하기는 정말 어렵다. 실무에서 많이 겪었던 부분이다. 테스트를 위해 중요한 코드를 다른 모듈로 분리해내고 단위테스트를 붙였던 경험들이 생각이 났다.&lt;/p&gt;
&lt;h2&gt;객체지향 프로그래밍&lt;/h2&gt;
&lt;p&gt;객체지향 프로그래밍은 간접적인 제어에 대한 규칙이 생긴다.&lt;/p&gt;
&lt;p&gt;이 책에서는 객제지향 개념중 다형성에 초점을 맞춰 설명하고 있다. 내가 이전에 공부했었던 객체지향은 객체들간의 협력과 객체들이 가지는 책임 그리고 역할을 강조하고 있었는데 다형성만 초점을 맞추고 있어서 의아하긴 했다.&lt;/p&gt;
&lt;p&gt;의존성 역전에 대해 책에서는 이렇게 표현하고 있다. &amp;quot;객체지향은 다형성을 활용해 소스코드의 제어 흐름에 절대적인 권한은 획득하도록 한다.&amp;quot; 의존성 역전 원칙을 생각해보면 구현체에 의존하는 것이 아닌 인터페이스에 의존하도록 설계한다. 그리고 런타임에 구현체를 주입함으로써 적절한 로직이 실행되도록 한다. 표현을 저렇게 하니 굉장히 멋진 능력을 손에 넣은 기분이 들었다.&lt;/p&gt;
&lt;h2&gt;함수형 프로그래밍&lt;/h2&gt;
&lt;p&gt;함수형 프로그래밍은 변수 할당에 대한 규칙을 부여한다.&lt;/p&gt;
&lt;p&gt;즉 불변성에 관한 얘기이다. 가변 변수는 버그을 발생시키는 주요 원인이 된다. 물론 프로그래밍은 무언가를 변화하기 위해 존재한다. 함수형 프로그래밍이 말하는 것은 불변한 것과 가변적인 것을 나눠서 가변적인 것을 고립시키는 것이다. 최대한 많은 것들을 불변하는 영역으에 두고 어쩔 수 없는 것들만 가변적인 영역에서 관리하면 버그를 격리시키고 관리하기가 용이해진다고 생각한다.&lt;/p&gt;
&lt;p&gt;내가 공부했던 함수형 프로그래밍에서는 가변적인 부분에 대해 액션이라는 용어를 사용했었다. 만약 어떤 함수가 액션이고 다른 함수가 이를 사용하게 되면 이 함수또한 액션이 전파된다. 이를 해결하기 위해 액션을 최대한 위로 꺼내서 격리시킨다. 클린 아키텍처, 어니언 아키텍처, 헥사고날 아키텍처를 생각해봤을때 이 개념이 녹아들어 있다고 생각했다.&lt;/p&gt;
&lt;p&gt;사실 내가 함수형 프로그래밍을 공부할때 불변, 가변이 전부는 아니었다. 추상화 계층을 어떤 식으로 설계할 것인지 (즉 이 레이어에서는 어떤 것을 모르게 할건지) 이를 위해 일급 함수를 사용했고 map, reduce 같은 파이프라인도 소개하고 있었다. 물론 불변 가변이 중요한 부분이니 다시 한번 머리에 정리하는 시간이 됐다.&lt;/p&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;프로그래밍 패러다임에 대한 지식을 가지고 있는 것은 좋다고 생각한다. 프런트엔드 혹은 백엔드 코드를 짤때 이 비지니스 로직은 어떤 식으로 접근할지 코드를 어떤 위치에 둘지 코딩을 접한지 얼마되지 않았을때 고민하는 것에 대한 기준이 생긴다. 이러한 기준이 생기면 좋은게 고민할 시간이 줄어드니 생산성이 올라가고 코드리뷰에서 다른 사람을 설득하기가 더 쉬워진다. &lt;/p&gt;
&lt;p&gt;다음 내용은 SOLID에 대한 개념이 나오기 시작한다. 굉장히 중요한 개념이고 나도 공부하면서 다시 한번 정리하는 시간이 될 것 같다.&lt;/p&gt;</description>
      <category>Clean Architecture</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/46</guid>
      <comments>https://mechaniccoder.tistory.com/46#entry46comment</comments>
      <pubDate>Sat, 21 Oct 2023 15:45:38 +0900</pubDate>
    </item>
    <item>
      <title>구독 취소 UX, Tree로 구현해보기</title>
      <link>https://mechaniccoder.tistory.com/45</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 회사에서 유저가 구독 취소 이유를 여러 단계에 걸쳐 form으로 입력받는 기능을 개발했습니다. churn rate을 줄이기 위해 비지니스적으로 변경이 많은 기능인데요. 이러한 변경에 유연하게 대응하기 위해 tree 데이터 구조를 활용하여 개발한 경험을 공유하려 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구독 취소 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현을 설명하기에 앞서 구독 취소 기능을 간략히 설명하겠습니다. 기존의 구독 취소는 한가지 이유를 선택하고 submit하는 간단한 스텝으로 이뤄져 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLxCSQ/btssfUewFs7/PgoMbsNkkoBYhLKiZlLQw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLxCSQ/btssfUewFs7/PgoMbsNkkoBYhLKiZlLQw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLxCSQ/btssfUewFs7/PgoMbsNkkoBYhLKiZlLQw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLxCSQ%2FbtssfUewFs7%2FPgoMbsNkkoBYhLKiZlLQw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;148&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 시간이 흐름에 따라서 churn rate이 높아지고 있어서 더 상세한 이유를 알아야만 했죠. 그래서 유저가 선택한 항목의 하위 항목들을 만들고, 선택한 하위 항목에 따라 여러 스텝들이 있도록 기획이 변경됐습니다. 말로만 설명하기는 어려우니 실제 사진으로 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 구독 취소 이유 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 구독 취소하려고 버튼을 클릭하면 왜 취소하는지 이유를 선택하는 화면입니다. 6가지 이유가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스에 문제가 있다.&lt;/li&gt;
&lt;li&gt;더 이상 서비스가 필요하지 않다.&lt;/li&gt;
&lt;li&gt;다른 서비스를 선호한다.&lt;/li&gt;
&lt;li&gt;서비스가 제공하는 목소리에 문제가 있다.&lt;/li&gt;
&lt;li&gt;너무 비싸다.&lt;/li&gt;
&lt;li&gt;기타&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GyLkb/btsshsnZ3i1/pLgaOreZTrctZRKu8JX6N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GyLkb/btsshsnZ3i1/pLgaOreZTrctZRKu8JX6N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GyLkb/btsshsnZ3i1/pLgaOreZTrctZRKu8JX6N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGyLkb%2FbtsshsnZ3i1%2FpLgaOreZTrctZRKu8JX6N1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;960&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 구독 취소 하위 이유 항목&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 &quot;서비스가 제공하는 목소리에 문제가 있다.&quot;를 선택했다고 하겠습니다. 그러면 하위 항목을 선택할 수 있는 화면으로 전환됩니다. 해당 예시에서는&lt;br /&gt;7가지 예시가 있네요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목소리가 자연스럽지 않다.&lt;/li&gt;
&lt;li&gt;목소리 발음이 별로다.&lt;/li&gt;
&lt;li&gt;목소리에 버그가 너무 많다.&lt;/li&gt;
&lt;li&gt;글로벌 목소리가 별로다.&lt;/li&gt;
&lt;li&gt;목소리 생성이 너무 느리다.&lt;/li&gt;
&lt;li&gt;마음에 드는 목소리가 없다.&lt;/li&gt;
&lt;li&gt;기타&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LlvzP/btssgDwzmg8/VkmyNPYnVZwylFS4H6BO8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LlvzP/btssgDwzmg8/VkmyNPYnVZwylFS4H6BO8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LlvzP/btssgDwzmg8/VkmyNPYnVZwylFS4H6BO8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLlvzP%2FbtssgDwzmg8%2FVkmyNPYnVZwylFS4H6BO8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;934&quot; height=&quot;964&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 구독 취소 하위 이유에 대한 추가 스텝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 &quot;목소리가 자연스럽지 않다.&quot;를 선택했다고 하겠습니다. 그러면 왜 그렇게 생각하는지 이유를 작성해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7qYeL/btssf3ibrdL/O7TV8BU1HSIgk9FY3ijcz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7qYeL/btssf3ibrdL/O7TV8BU1HSIgk9FY3ijcz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7qYeL/btssf3ibrdL/O7TV8BU1HSIgk9FY3ijcz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7qYeL%2Fbtssf3ibrdL%2FO7TV8BU1HSIgk9FY3ijcz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;918&quot; height=&quot;964&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 구독 취소 완료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 구독 취소를 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfJlKC/btssgG7UWcZ/Prw1ob5X4ZeA1uMfnli1jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfJlKC/btssgG7UWcZ/Prw1ob5X4ZeA1uMfnli1jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfJlKC/btssgG7UWcZ/Prw1ob5X4ZeA1uMfnli1jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfJlKC%2FbtssgG7UWcZ%2FPrw1ob5X4ZeA1uMfnli1jK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;934&quot; height=&quot;962&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구독 취소 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명드린 구독 취소 기능을 다이어그램으로 정리해보면 아래와 같습니다. 다이어그램을 이해하기는 어려우니 다음과 같은 조건들만 알아주시면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구독 취소 이유를 선택하면 그에 맞는 하위 이유를 선택해야 한다.&lt;/li&gt;
&lt;li&gt;하위 이유를 선택하면 그에 맞는 추가적인 스텝들으 진행해야 한다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2580&quot; data-origin-height=&quot;1410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bItRdH/btssaUT8G43/N6lHCN1r4aEiFjbgEbmhi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bItRdH/btssaUT8G43/N6lHCN1r4aEiFjbgEbmhi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bItRdH/btssaUT8G43/N6lHCN1r4aEiFjbgEbmhi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbItRdH%2FbtssaUT8G43%2FN6lHCN1r4aEiFjbgEbmhi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2580&quot; height=&quot;1410&quot; data-origin-width=&quot;2580&quot; data-origin-height=&quot;1410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반으로 생각하면 tree 구조가 적절해보입니다. 아래 이미지에서 구독취소는 여러 자식 노드들을 가지고 있고 목소리 이슈도 여러 자식 노드를 가지고 있습니다. &quot;부자연스럽다&quot; 노드는 하나의 자식을 가지죠. 이를 tree를 사용하여 구현하면 항목 추가 삭제 및 스텝 추가 삭제가 쉬워질거라 생각했고 이를 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rNYVc/btssbA8W9eq/9oArv4XS78KkgOdkbceDp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rNYVc/btssbA8W9eq/9oArv4XS78KkgOdkbceDp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rNYVc/btssbA8W9eq/9oArv4XS78KkgOdkbceDp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrNYVc%2FbtssbA8W9eq%2F9oArv4XS78KkgOdkbceDp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1910&quot; height=&quot;712&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드로 이를 구현해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;class CancelReasonTree {
  name: string;
  title: string;
  description: string;
  options: SelectOption[];
  children: Record&amp;lt;string, CancelReasonTree&amp;gt;;

  constructor(name: string, title: string, description: string, options: SelectOption[]) {
    this.name = name;
    this.title = title;
    this.description = description;
    this.options = options;
    this.children = {};
  }

  addChild(key: string, child: CancelReasonTree) {
    this.children[key] = child;
  }

  findNode(step: number, values: string[]): CancelReasonTree | null {
    if (values.length === 0) {
      return this;
    }

    let current: CancelReasonTree = this;

    for (let i = 0; i &amp;lt; step; i++) {
      const value = values[i];
      if (current.children[value]) {
        current = current.children[value];
      } else if (Object.keys(current.children).length &amp;gt; 1) {
        return null;
      } else if (Object.keys(current.children).length === 1) {
        current = Object.values(current.children)[0];
      } else {
        return null;
      }
    }

    return current;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 노드를 객체인 children 필드로 관리해줍니다. 여기서 알아야할건 현재 스텝이 여러 가지 항목 중 하나를 선택해야 한다면 선택 값을 객체의 키로 관리하고 이에 해당하는 노드를 값으로 돌려준다는 점입니다. 예를 들어 목소리 이슈의 경우 4가지 선택 항목이 있기 때문에 객체는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;{
    '부자연스럽다': 부자연스럽다 노드,
    '발음이 별로다': 발음이 별로다 노드,
    '버그가 많다': 버그가 많다 노드,
    '느리다': 느리다 노드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그가 많다를 선택한 경우 해당하는 노드를 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 스텝이 선택하는 것이 아니라 이전 스텝에 관련된 추가적인 스텝인 경우에는 하나의 property를 가지도록 설계했습니다. 예를 들어 앞서 선택한 버그가 많다에 대해 추가적인 스텝을 해야하는 경우 객체를 다음과 같이 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;{
  '버그가 많다 추가 스텝': 버그가 많다 추가 스텝 노드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findNode 메소드를 보면 알 수 있다시피 스텝에 매칭되는 키를 가지거나 하나의 property만을 가졌을때 해당하는 노드를 가져옵니다. 만약 null을 반환하는 경우 마지막 confirm 단계로 넘어가게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 문제를 풀때만 tree 구조를 사용했었는데 실제 실무에 적용해보니 색다른 느낌이었습니다. 이론으로만 알고 있던 지식이 실제 세상에도 도움이 되는구나! 이게 데이터 구조가 가지는 힘이구나 라는 생각이 들었던 것 같네요. 데이터 구조 알고리즘에 대한 이해도를 높여 실무에서 적절한 상황에 사용할 수 있도록 꾸준히 노력해야겠네요.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>tree</category>
      <author>mechaniccoder</author>
      <guid isPermaLink="true">https://mechaniccoder.tistory.com/45</guid>
      <comments>https://mechaniccoder.tistory.com/45#entry45comment</comments>
      <pubDate>Wed, 6 Sep 2023 23:57:54 +0900</pubDate>
    </item>
  </channel>
</rss>