<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Algoroot's space</title>
    <link>https://algoroot.tistory.com/</link>
    <description>안녕하세요. 프론트엔드 개발자 알고루트입니다. 좋은 개발자가 되기위한 여정을 공유하는 공간입니다. </description>
    <language>ko</language>
    <pubDate>Wed, 29 Jul 2026 17:15:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>AlgoRoot</managingEditor>
    <item>
      <title>메모장에서 꺼낸 타입스크립트 스킬 5가지</title>
      <link>https://algoroot.tistory.com/226</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;br /&gt;안녕하세요. 오늘의 주제는 타입스크립트입니다. 프론트엔드 개발을 하면서 타입스크립트의 &quot;어떤 특성&quot;까지 활용해 보셨나요?&lt;/span&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;일반적으로 자주 사용하는 Union, Generic, Tuple, index signature 등 타입을 표현하는 기법들은 많이 알려져 있지만, 이번 글에서는 이러한 문법들을 활용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;조금 더 깊이 있는 타입 구성 방법&lt;/b&gt;을 살펴보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;220&quot; data-end=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;평소 타입스크립트를 쓰면서 &amp;ldquo;이런 것도 되네?&amp;rdquo; 하고 신기하거나, 몰랐다가 알게 된 유용한 기법들을 개인적으로 정리해두고 있었는데요. 그중에서 인상에 더욱 남았던 특성들 위주로 직접 구현해보았던 예제 타입과 함께 정리해봤습니다.&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;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;예제에서는 재귀 타입, infer, 조건부 타입 분배 등 다소 복잡한 문법들이 사용됩니다.&lt;br /&gt;이 글에서는 해당 문법 자체에 대한 설명은 생략하고 있어, 어느 정도 익숙하신 분들이라면 더 수월하게 읽으실 수 있을 것 같습니다.&lt;/blockquote&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;타입스크립트 Skill List&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot; data-spread=&quot;false&quot; data-pm-slice=&quot;3 3 []&quot;&gt;
&lt;li&gt;&lt;span&gt;Union 타입을 조건부 타입으로 활용하는 기법 - Permutation&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;함수 시그니처를 활용한 양방향 타입 비교 - Equal&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;튜플을 이용해 함수 인자를 추가하는 방법 - AppendArgument (feat. Tuple)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;타입을 평탄화하는 스킬 - AppendToObject (feat. Omit)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;as로&amp;nbsp;Mapped&amp;nbsp;Type&amp;nbsp;활용하기&amp;nbsp;- MyOmit (feat. Homomoerphic)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;타입스크립트의 특성&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;타입스크립트는 &quot;범위&quot; 비교로 결과를 도출해야 합니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 타입스크립트를 깊이 파고들다 보면,&lt;br /&gt;&amp;ldquo;왜 이렇게 복잡하게 돌아가지?&amp;rdquo; 싶은 순간이 종종 찾아옵니다. (물론 그게 타입스크립트의 매력이기도 하지만,,)&lt;/p&gt;
&lt;p data-start=&quot;180&quot; data-end=&quot;209&quot; data-ke-size=&quot;size16&quot;&gt;저는 그런 상황을 마주할 때 이렇게 생각하며 그냥 타입스크립트 특성을 받아드립니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot; data-start=&quot;211&quot; data-end=&quot;256&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-start=&quot;213&quot; data-end=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;&quot;자바스크립트는 값을 비교하지만,&lt;b&gt; 타입스크립트는 '범위'를 비교한다.&lt;/b&gt;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-start=&quot;258&quot; data-end=&quot;332&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;258&quot; data-end=&quot;332&quot; data-ke-size=&quot;size16&quot;&gt;즉, 타입스크립트는 두 값이 같은지를 비교하는 게 아니라,&lt;br /&gt;&lt;b&gt;한 타입이 다른 타입에 포함되는지&lt;/b&gt;를 기준으로 조건을 판단합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752517023192&quot; class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const a = '1';
const b = 1;
console.log(a === b); // false

// 타입스크립트에서는:
type A = '1';
type B = 1;
type Result = A extends B ? true : false; // false&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;span&gt;그래서 때로는 우리가 예상한 것보다 복잡한 방식으로 타입을 구성해야만 원하는 결과를 얻을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 재귀적으로 타입을 구성하거나, 여러 속성이 얽힌 객체 타입을 처리할 때 이런 특성이 더 분명히 드러납니다.&lt;/span&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;앞으로 소개할 내용들이 조금 낯설게 느껴지더라도, 범위 비교를 위한 과정임을 생각해 주시고 봐주시면 좀 더 와닿을 부분들이라고 생각해 스킬을 소개하기 전 이 부분을 말씀드리고 넘어가면 좋겠다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span&gt;1. Union 타입을 조건부 타입으로 활용하는 기법 - Permutation&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;타입스크립트의 조건부 타입은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;제네릭 타입에 유니언 타입에 적용&lt;/b&gt;될 경우, 각 요소에 대해 자동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;분배(distribution)&lt;/b&gt;되는 특성을 갖고 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;이걸&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types&quot;&gt;분배 조건부 타입 Distributive Conditional Types&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이라고 하는데요.&amp;nbsp;&amp;nbsp;&lt;/span&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;&lt;span&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T extends T&lt;/span&gt;&lt;span&gt;와 같은 조건을 만들면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T = &quot;A&quot; | &quot;B&quot; | &quot;C&quot;&lt;/span&gt;&lt;span&gt;일 경우 다음과 같이 작동합니다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-spread=&quot;false&quot;&gt;
&lt;li&gt;&lt;span&gt;&quot;A&quot; extends &quot;A&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&quot;B&quot; extends &quot;B&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&quot;C&quot; extends &quot;C&quot;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 분배가 되는 게 일반적이지만, 분배를 막고 싶다면 아래와 같이 []기호로 감싸 선언하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023194&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;[T] extends [T]&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;span&gt;예를 들어 Union타입으로 순열을 만들고 싶다면 어떻게 해야 할까요?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023194&quot; class=&quot;ada&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Case1 = Permutation&amp;lt;'A' | 'B' | 'C'&amp;gt;
// expect:
| [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;]
| [&quot;A&quot;, &quot;C&quot;, &quot;B&quot;]
| [&quot;B&quot;, &quot;A&quot;, &quot;C&quot;]
| [&quot;B&quot;, &quot;C&quot;, &quot;A&quot;]
| [&quot;C&quot;, &quot;A&quot;, &quot;B&quot;]
| [&quot;C&quot;, &quot;B&quot;, &quot;A&quot;]


type Case2 = Permutation&amp;lt;boolean&amp;gt; // expect: [false, true] | [true, false]

type Case3 = Permutation&amp;lt;never&amp;gt;, // expect: []&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;Permutation Type&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞서 소개드린 분배 조건부 타입을 활용하면 아래와 같이 만들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;excel&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Permutation&amp;lt;T, Acc = T&amp;gt; = [T] extends [never]
  ? []
  : Acc extends Acc
    ? [Acc, ...Permutation&amp;lt;Exclude&amp;lt;T, Acc&amp;gt;&amp;gt;]
    : never;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Acc extends Acc에서&lt;/b&gt;&amp;nbsp;T가 유니언 타입('A' | 'B' | 'C') 일 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Acc extends Acc는 제네릭을 사용함으로써 분배 조건부 타입이기 때문에 Acc = 'A' | 'B' | 'C'가 'A', 'B', 'C' 각각으로 나뉘어서 재귀를 수행하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752517023196&quot; class=&quot;asciidoc&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;= ['A', ...Permutation&amp;lt;'B' | 'C'&amp;gt;]
= ['A', 'B', ...Permutation&amp;lt;'C'&amp;gt;]
= ['A', 'B', 'C']
...&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;b&gt;[T] extends [never]&lt;/b&gt;은 재귀를 종료시키기 위한 조건입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exclude는 아래처럼 구성되어 있어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Exclude &amp;lt;T, Acc&amp;gt;를 계속하다 보면 결국 T = never가 됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023197&quot; class=&quot;excel&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Exclude&amp;lt;T, U&amp;gt; = T extends U ? never : T;&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 []를 쓰면 분산을 막을 수 있다고 말씀드렸듯 , never가 오는 시점에서 조건부는 더 이상 필요 없기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;[T] extends [never]를 써서 재귀를 종료시켜 줍니다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span&gt;2. 함수 시그니처를 활용한 양방향 타입 비교 - Equal&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;A와, B가 정확히 동일한 타입인지, 그러니까 자바스크립트에서 1 === &quot;1&quot; 의 결과처럼 타입이 서로 같은지 도출하는 타입은 어떻게 구성할 수 있을까요?&amp;nbsp;&lt;/span&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;&lt;span&gt;타입스크립트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;A extends B&lt;/span&gt;&lt;span&gt;는 A가 B의 부분 집합인지, 즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;포함 여부만&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 판단합니다. 그런데 우리는 종종 A와 B가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;정확히 동일한 타입인지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;알고 싶을 때가 있습니다.&lt;/span&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;&lt;span&gt;마치 자바스크립트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1 === &quot;1&quot;&lt;/span&gt;&lt;span&gt;과 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;엄격한 동치 비교&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 타입 수준에서도 하고 싶은 거죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럴 때 사용하는 테크닉이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;함수 시그니처 기반의 비교&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Equal Type&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아래는 제가 잘 이용하는&lt;/span&gt;&lt;span&gt;&amp;nbsp;사이트인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;http:////%20https://github.com/type-challenges/type-challenges/blob/main/utils/index.d.ts&quot;&gt;type challenges 코드&lt;/a&gt;에서 테스팅 목적으로 항상 사용되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Equal이라는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Equal&amp;lt;A, B&amp;gt; = (&amp;lt;T&amp;gt;() =&amp;gt; T extends A ? 1 : 2) extends
                   (&amp;lt;T&amp;gt;() =&amp;gt; T extends B ? 1 : 2)
  ? true
  : false;
  
  
type A = Equal&amp;lt;&quot;1&quot;, 1&amp;gt;, true&amp;gt; // false
type B = Equal&amp;lt;&quot;1&quot;, 1&amp;gt;, false&amp;gt; // true&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;저는 처음에 이 타입을 보고 결국에는 1 extends 1 or 2 extends 2로 비교를 하는 건가?라고 헤맸던 기억이 나네요.&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 data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 숫자 1이나 2는 어떤 값이든 상관없습니다. 제네릭 T도 비교를 위한 도구일 뿐, 실제로 타입 시스템에 값을 넣어보는 테스트 케이스가 아닙니다.&lt;/span&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;&lt;span&gt;중요한 건 두 타입의 조건 평가 결과가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;완전히 일치하느냐&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&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;&lt;span&gt;좀 더 쉽게 설명하면, 아래 두 함수 타입은 겉보기에는 비슷해 보이지만, 타입스크립트는 함수 전체의 구조(시그니처)를 비교합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;(&amp;lt;T&amp;gt;() =&amp;gt; T extends &quot;1&quot; ? 1 : 2) extends
(&amp;lt;T&amp;gt;() =&amp;gt; T extends 1 ? 1 : 2)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 함수 전체가 같아야 한다는 건 단순히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;이냐&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;냐를 비교하는 것이 아니라, &quot;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;에 어떤 타입이 들어와도 두 함수가 항상 같은 결과를 낼 수 있느냐&quot;를 기준으로 삼는다는 뜻입니다.&lt;/span&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;&lt;span&gt;그래서 위처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;1&quot;&lt;/span&gt;&lt;span&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;은 타입이 다르기 때문에 결과적으로 두 함수 시그니처는 같지 않다고 판단되는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span&gt;3. 튜플을 이용해 특정 이름의 함수 인자를 추가하는 방법 - AppendArgument&lt;/span&gt;&lt;/h2&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;span&gt;타입스크립트에서 함수의 rest parameters를 표현할 때 아래처럼 표현할 수 있습니다. (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.typescriptlang.org/docs/handbook/2/functions.html#rest-parameters&quot;&gt;ts 공식문서 예제&lt;/a&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023200&quot; class=&quot;actionscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;function multiply(n: number, ...m: number[]) {
  return m.map((x) =&amp;gt; n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);&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;span&gt;이처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;함수의 매개변수 타입을 튜플 형태로 정의하고&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, 마지막에 스프레드(...)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;연산자를 써서 유연하게 인자를 받을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AppendArgument Type&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그렇다면, 이런 함수 타입에 새로운 인자를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;추가하고 싶을 때&lt;/b&gt;&lt;/span&gt;&lt;span&gt;는 어떻게 해야 할까요?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023201&quot; class=&quot;vim&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type A = AppendArgument&amp;lt;(a: number, b: string) =&amp;gt; number, boolean&amp;gt;
// expect :
// (a: number, b: string, x: boolean) =&amp;gt; number&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;튜플(Tuple)과 스프레드 문법(Spread)을 활용해 타입을 확장할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type AppendArgument&amp;lt;Fn extends (...args: any) =&amp;gt; any, A&amp;gt; =
  Fn extends (...args: infer Args) =&amp;gt; infer Return
    ? (...args: [...Args, A]) =&amp;gt; Return
    : never;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위처럼 선언하면 TS는 자동으로 해당 인자를 뒤에 추가해 줍니다. 타입에는 전혀 문제가 없지만,, 인자의 이름이 arg2로 되는 게 불편하지 않으신가요?&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;AppendArgument&amp;lt;(a: number, b: string) =&amp;gt; void, boolean&amp;gt;
// 결과:
// (a: number, b: string, arg2: boolean) =&amp;gt; void&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppendArgument type의 인자 타입 부분을 풀어보면 아래와 같이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752517023203&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type Args = [a: number, b: string];
type Extended = [...Args, arg2: boolean];

// 위는 아래와 동일하게 해석됩니다:
type ExtendedEquivalent = (a: number, b: string, arg2: boolean) =&amp;gt; void;&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;span&gt;실전에서는 타입 디버깅 시 arg2라고 되어있으면 가독성이 떨어질 것 같습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;좀 더 나은 타입을 만들기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;인자의 이름을 x처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;직접 명시&lt;/b&gt;하고 싶다면?&lt;/span&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;&lt;span&gt;arg2를 x로 치환해주기만 하면 되겠죠.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023204&quot; class=&quot;cos&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt; (...args: [...Args, x: A]) =&amp;gt; Return&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;위 타입은 x: A는 &amp;ldquo;이 인자의 이름은 x고 타입은 A입니다&amp;rdquo;라는 의미이고,&lt;span&gt;&amp;nbsp;이처럼&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;타입스크립트는 함수 시그니처 안에서도 이름을 타입 수준에서 표현할 수 있게 해 줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;4.&amp;nbsp; object 타입을 평탄화하는 스킬&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;- AppendToObject (feat. Omit)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;object로 구성된 Color type에 어떠한 key와 value를 추가하기 편한 타입을 만들고 싶으면 어떻게 해야 할까요?&lt;/p&gt;
&lt;pre id=&quot;code_1752517023204&quot; class=&quot;q&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Color = {
  key: string;
  value: &quot;green&quot; | &quot;red&quot;;
};

type ColorSystem = AppendToObject&amp;lt;Color, &quot;isPrimitive&quot;, boolean&amp;gt;;

const B: ColorSystem = {
  key: &quot;x-1&quot;,
  value: &quot;green&quot;,
  isPrimitive: true, // added!
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;AppendToObject Type&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 간단하게는 이렇게 만들 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752517023205&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type AppendToObject&amp;lt;T, U extends PropertyKey, V&amp;gt; = T &amp;amp; Record&amp;lt;U, V&amp;gt;;&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;하지만 위처럼 선언하면 타입은 아래와 같이 나옵니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023205&quot; class=&quot;ada&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type ColorSystem = Color &amp;amp; Record&amp;lt;&quot;isPrimitive&quot;, boolean&amp;gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &amp;amp; 연산자로 병합한 타입은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;타입스크립트가 타입을 내부적으로 병합하긴 하지만&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자동으로 평탄화(flatten)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하지는 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위의 타입은 Color&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;내부 타입을 다시 들여다봐야 하는 불편함이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 아래처럼 Flat 하게 평탄화된 타입을 추출하려면 어떻게 해야 할까요?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023206&quot; class=&quot;q&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type ColorSystem = {
    key: string;
    value: &quot;green&quot; | &quot;red&quot;;
    isPrimitive: boolean;
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이럴 때 타입을 평탄하게 만들고 싶다면, 다음과 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content&quot;&gt;Mapped Type를&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용하여 강제로 펼쳐 다시 구성해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023206&quot; class=&quot;elm&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type AppendToObject&amp;lt;T, U extends PropertyKey, V&amp;gt; = {
  [K in keyof (T &amp;amp; Record&amp;lt;U, V&amp;gt;)]: (T &amp;amp; Record&amp;lt;U, V&amp;gt;)[K];
};&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;이렇게 하면&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;둘 다 key를 다시 순회 하기 때문에, 타입스크립트가 평탄한 형태로 출력해 줍니다.&lt;/span&gt;&lt;/span&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;&lt;span&gt;또는 좀 더 간결하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Omit&lt;/span&gt;&lt;span&gt;을 활용해도 같은 결과를 얻을 수 있습니다. 아무 속성도 제거하지 않는다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Omit &amp;lt;T, never&amp;gt;&lt;/span&gt;&lt;span&gt;은 결국 T와 동일하니까요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023207&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type AppendToObject&amp;lt;T, U extends PropertyKey, V&amp;gt; = Omit&amp;lt;
  T &amp;amp; Record&amp;lt;U, V&amp;gt;,
  never
&amp;gt;;&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;span&gt;이게 가능한 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Omit&lt;/span&gt;&lt;span&gt;이 Pick과 Exclude를 사용해 구성되어 있고, Pick에서 이미 맵드 타입(Mapped Type)으로 구성되어 있기 때문입니다.&lt;/span&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;&lt;span&gt;Exclude &amp;lt;keyof T, never&amp;gt;&lt;/span&gt;&lt;span&gt;는 결국 아무 키도 제외하지 않기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;keyof T&lt;/span&gt;&lt;span&gt;와 동일하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 전체 키를 유지한 채&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Pick&lt;/span&gt;&lt;span&gt;으로 다시 타입을 재구성하는 효과를 얻게 되는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Omit&amp;lt;T, K extends keyof any&amp;gt; = Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;;

type Pick&amp;lt;T, K extends keyof T&amp;gt; = {
    [P in K]: T[P];
};

type Exclude&amp;lt;T, U&amp;gt; = T extends U ? never : T;&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span&gt;5. as로 Mapped Type 활용하기 - MyOmit&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서도 소개를 했지만 Omit은 타입스크립트에서 매우 자주 사용하는 유틸리티 타입입니다. 특정 키를 제거한 객체 타입을 만들 때 쓰이며, 다음과 같이 정의되어 있습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752517023208&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Omit&amp;lt;T, K extends keyof any&amp;gt; = Pick&amp;lt;T, Exclude&amp;lt;keyof T, K&amp;gt;&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;476&quot; data-end=&quot;530&quot;&gt;이 정의는 Pick과 Exclude를 조합하여 keyof T에서 지정한 키 K를 제거한 속성만 선택해 새로운 타입을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;382&quot; data-end=&quot;433&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;382&quot; data-end=&quot;433&quot;&gt;그렇다면 Pick 없이 Exclude만으로 Omit을 직접 구현해 보면 어떻게 될까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;382&quot; data-end=&quot;433&quot;&gt;갑자기 Pick을 없앤다니, 이상할 수 있지만(ㅎㅎ;) 제가 처음에 Omit을 직접 구현했을 때는 아래와 같이 구성했습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023208&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type MyOmit&amp;lt;T, K extends keyof T&amp;gt; = { [P in Exclude&amp;lt;keyof T, K&amp;gt;]: T[P] };&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;위 타입은 대부분의 오브젝트에서 잘 통과가 됐지만,&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;readonly나 optional과 같은 modifier 속성이 유지되지 않았습니다.&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752517023209&quot; class=&quot;crmsh&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type User = {
  readonly name: string;
  title?: string;
}

type A = MyOmit&amp;lt;User, 'title'&amp;gt;;
// expect: { readonly name: string }
// result: { name: string }&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;764&quot; data-end=&quot;785&quot;&gt;왜 modifier가 사라질까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pick과 Exclude로 구성된 TS의 Omit은 잘 구현이 되는데 왜 제가 한 방식은 속성들이 보존이 안될까요?&amp;nbsp;&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;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;제가 참고한 자료들에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Homomoerphic(동형), Non-Homomorphic(비동형)라는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;개념이 등장하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;우선 제가 MappedType을 구성할 때 사용한 Exclude &amp;lt;keyof T, K&amp;gt;가 단순히 유니언 타입으로 키를 반환하기 때문에, 타입스크립트는 이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Non-Homomorphic Mapped Type (비동형)으로&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인식하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 경우 원본 타입의 modifier는 보존되지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;그럼, 여기서 나오는 개념인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Homomoerphic(동형), Non-Homomorphic(비동형)&lt;/b&gt;&amp;nbsp;이란 뭘까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;용어가 조금 어려워서 제가 이해한 부분을 바탕으로 쉽게 말씀드리면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;&lt;b&gt;Homomoerphic(동형) 은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;맵드타입(Mapped type)으로 키를 정의할 때&amp;nbsp; 원본 객체의 키를 직접 참조하는 것이고 이 경우에는 readonly나 optional(?) 같은 modifier를 그대로 유지합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752517023210&quot; class=&quot;elm&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Hom&amp;lt;T&amp;gt; = { [K in keyof T]: T[K] };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;842&quot; data-end=&quot;979&quot;&gt;반대로 제가 처음 했던 방식처럼 keyof를 가공해서 사용할 경우 컴파일러는 T와의 관계를 추적하지 못해 modifier가 보존되지 않는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Non-Homomorphic(비동형)&lt;/b&gt;으로 분류됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752517023210&quot; class=&quot;elm&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type NonHom&amp;lt;T, K&amp;gt; = {
  [P in Exclude&amp;lt;keyof T, K&amp;gt;]: T[P];
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Homomoerphic(동형), Non-Homomorphic(비동형)&amp;nbsp;개념에&amp;nbsp;관련해서 반영된 PR, stack of flow 토론 등을 첨부할 테니 더 궁금하신 분은 링크를 참고해 주시면 감사하겠습니다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;979&quot; data-start=&quot;842&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/microsoft/TypeScript/pull/29787&quot;&gt;Infer&amp;nbsp;to&amp;nbsp;partially&amp;nbsp;homomorphic&amp;nbsp;types&amp;nbsp;(such&amp;nbsp;as&amp;nbsp;Pick &amp;lt;T,&amp;nbsp;K&amp;gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;979&quot; data-start=&quot;842&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://stackoverflow.com/questions/59790508/what-does-homomorphic-mapped-type-mean&quot;&gt;What&amp;nbsp;does&amp;nbsp;&quot;homomorphic&amp;nbsp;mapped&amp;nbsp;type&quot;&amp;nbsp;mean?&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;979&quot; data-start=&quot;842&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/%40settahkader/understanding-mapped-types-and-type-manipulation-in-typescript-0bf4ba67aab8&quot;&gt;4.&amp;nbsp;Homomorphic&amp;nbsp;vs.&amp;nbsp;Non-Homomorphic&amp;nbsp;Mapped&amp;nbsp;Types&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 문제는 keyof T를 그대로 쓰지 않고 Exclude &amp;lt;keyof T, K&amp;gt;로 바꾼 데 있었습니다. 그렇다면 어떻게 해결할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;as 사용해서 remapping 하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.typescriptlang.org/docs/handbook/2/mapped-types.html?utm_source=chatgpt.com#key-remapping-via-as&quot;&gt;as&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;키워드를 사용하는 방식입니다. 매핑 중인 키에 조건을 걸거나 이름을 바꿀 수 있게 해 줍니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752517023210&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type MyOmit&amp;lt;T, K extends keyof any&amp;gt; = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

// use Exclude!
type MyOmit&amp;lt;T, K extends keyof T&amp;gt; = { 
  [P in keyof T as Exclude&amp;lt;keyof T, K&amp;gt;]: T[P] 
 };&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;1692&quot; data-end=&quot;1783&quot;&gt;이 방식은 keyof T를 유지하면서 as를 통해 필터링을 조건부로 수행합니다. 따라서 homomorphic(동형) 특성을 유지하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;1692&quot; data-end=&quot;1783&quot;&gt;readonly, optional 같은 modifier도 정확히 보존이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;1692&quot; data-end=&quot;1783&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot; data-start=&quot;1707&quot; data-end=&quot;1831&quot;&gt;참고로 Mapped type에서 as를 사용한 방식은 조건부가 아닌 키 이름을 변형하는 데도 활용됩니다.&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.typescriptlang.org/docs/handbook/2/mapped-types.html?utm_source=chatgpt.com#key-remapping-via-as&quot;&gt;공식문서 예제:&lt;/a&gt;&amp;nbsp;&lt;/blockquote&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1752517023211&quot; class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;type Getters&amp;lt;Type&amp;gt; = {
    [Property in keyof Type as `get${Capitalize&amp;lt;string &amp;amp; Property&amp;gt;}`]: () =&amp;gt; Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters&amp;lt;Person&amp;gt;;
// result
type LazyPerson = {
    getName: () =&amp;gt; string;
    getAge: () =&amp;gt; number;
    getLocation: () =&amp;gt; string;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;2447&quot; data-end=&quot;2580&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;2582&quot; data-end=&quot;2707&quot; data-is-last-node=&quot;&quot; data-is-only-node=&quot;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트를 쓰다 보면, 처음엔 정말 편한데 어느 순간부터 &quot;이걸 이렇게까지 복잡하게 써야 하나?&quot; 싶은 지점들이 생깁니다.&lt;/p&gt;
&lt;p data-start=&quot;190&quot; data-end=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;190&quot; data-end=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;저도 처음 개발을 시작할 때는 단순한 props 정의 정도에서 시작했지만, 점점 더 복잡한 요구사항들을 마주하면서 &quot;타입으로도 이렇게까지 표현할 수 있구나&quot; 하는 놀라움을 많이 느꼈습니다. 물론 그만큼 어려움도 있었고, 도대체 왜 이렇게 동작하는지 이해 안 되어 애먹은 시간도 많았지만요. 물론 지금도 현재진행형입니다 :)&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;190&quot; data-end=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;340&quot; data-end=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;하지만 그 과정을 거치면서 몇몇의 특성과 테크닉들을 알게되었고, 또 그걸 활용해서 타입을 더 탄탄하게 만드는 능력치도 더 올라갔다고 생각합니다. 복잡한 타입으로 구성된 라이브러리의 타입을 살펴 볼 때 도움이 많이 되었고요.&lt;/p&gt;
&lt;p data-start=&quot;340&quot; data-end=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;340&quot; data-end=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;특히 저한테 많이 도움이 되었던 사이트인 type-challenges에서&amp;nbsp;요상한&amp;nbsp;문제들을 풀면서 단순한 문법을 넘어서 타입스크립트를 &quot;도구&quot;로 활용하는 감각을 조금씩 익히게 된 것 같습니다.&lt;/p&gt;
&lt;p data-start=&quot;340&quot; data-end=&quot;492&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;494&quot; data-end=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;이 글에 담은 내용들이 어떤 분에게는 너무 쉽고 당연한 내용, 또 어떤 분에게는 다소 낯선 내용일 수 있지만, 낯선 분들에게는 조금이라도 &quot;아 이런 것도 가능하구나&quot; 싶은 인사이트를 얻으셨다면 그걸로 충분하다고 생각합니다.&lt;/p&gt;
&lt;p data-start=&quot;494&quot; data-end=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-start=&quot;494&quot; data-end=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;다음에 또 찾아오겠습니다. 감사합니다.&lt;/p&gt;
&lt;p data-start=&quot;494&quot; data-end=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;Reference:&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://www.typescriptlang.org/docs/handbook/intro.html&quot;&gt;https://www.typescriptlang.org/docs/handbook/intro.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/59790508/what-does-homomorphic-mapped-type-mean&quot;&gt;https://stackoverflow.com/questions/59790508/what-does-homomorphic-mapped-type-mean&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://typehero.dev/&quot;&gt;https://typehero.dev/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/Typescript</category>
      <category>homomorphic mapped type</category>
      <category>homomorphic non-homomorphic</category>
      <category>typescript</category>
      <category>typescript advanced types</category>
      <category>typescript mapped type</category>
      <category>타입스크립트</category>
      <category>타입스크립트 고급스킬</category>
      <category>타입스크립트 스킬</category>
      <category>타입스크립트 팁</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/226</guid>
      <comments>https://algoroot.tistory.com/226#entry226comment</comments>
      <pubDate>Tue, 15 Jul 2025 03:18:33 +0900</pubDate>
    </item>
    <item>
      <title>내가 선언적인 코드를 좋아하는 이유 (Feat. FP로 만드는 애니메이션) - 2</title>
      <link>https://algoroot.tistory.com/224</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;안녕하세요. 지난 글&amp;nbsp;&lt;a href=&quot;https://algoroot.tistory.com/218&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;「내가 선언적인 코드를 좋아하는 이유 (Feat. React) &amp;ndash; 1」&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;에서 저는 선언적인 코드를 작성하는 방식이 어떻게 코드 품질의 방향키가 되어 주었는지, 그리고 React 렌더링 과정에서 그 철학이 어떤 식으로 구현되는지 살짝 살펴봤습니다. 혹시 1편을 아직 못 보셨다면, 먼저 읽고 오시면 흐름이 조금 더 자연스러울 거예요.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이번 글에서는 선언적 프로그래밍과 깊이 연결된 함수형 프로그래밍(FP, Functional Programming)에 대해 이야기하려 합니다. 1편에서 설명한 것처럼 저는 선언적 프로그래밍은 &quot;제가 알아야 하는 컨텍스트가 적어지는 코드&quot;라고 정의했는데요,&amp;nbsp;FP는 바로 이 개념을 한층 더 깊이 있게 다루는 패러다임입니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FP를 처음 접했던 순간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 FP를 처음 접한 건 작년 전 회사의 코드 리뷰 세션이었습니다. 당시 팀 리더님이 고차 함수(Higher Order Function)와 커링(Currying)을 설명해 주셨는데요, 커링에 대해서는 개념적으로는 명확히 이해됐지만 실제 코드에 적용하자니 손에 잘 익지 않아 필요할 때만 조금씩 사용하는 정도였습니다.&lt;br /&gt;시간이 흐르며 코드의 복잡도가 높아질 때마다 &quot;이거 커링을 사용하면 더 깔끔하지 않을까?&quot;라는 생각이 점점 자주 들었고, 자연스럽게 FP에 대한 관심이 다시 커지게 됐습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수형 프로그래밍(FP)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FP는 &quot;함수로 사고한다&quot;는 철학을 중심에 둔 프로그래밍 패러다임입니다. 특히 상태를 직접 변경하지 않고, 입력을 받아 출력을 반환하는 &amp;lsquo;순수 함수&amp;rsquo;를 조합해 프로그램 전체 흐름을 만들어가는 방식입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;쉽게 말해, 우리가 filter, map, reduce를 즐겨 쓰는 것도 이미 FP의 일부를 자연스럽게 활용하고 있는 셈입니다. 이 함수들은 원본 배열을 변경하지 않고 새로운 값을 반환하며, 동작 방식을 외부에서 주입받는 함수(콜백)를 통해 정의합니다. 이처럼&amp;nbsp;&lt;b&gt;&quot;무엇을 할 것인지&quot;를 선언&lt;/b&gt;하는 코드 구조는 명령형보다 훨씬 간결하고 의도를 잘 드러냅니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1750589487524&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const result = [1, 2, 3, 4]
  .filter(n =&amp;gt; n % 2 === 0) // 짝수만 남기고
  .map(n =&amp;gt; n * n)          // 각 값 제곱&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;1368&quot; data-end=&quot;1487&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-start=&quot;1368&quot; data-end=&quot;1487&quot;&gt;위 코드 한 줄로 &quot;짝수를 골라 제곱한다&quot;는 의도를 명확히 전달할 수 있습니다. 어떻게 동작하는지를 일일이 따라가지 않아도, &lt;b&gt;무엇을 하고자 하는지&lt;/b&gt;를 직관적으로 이해할 수 있죠. 이처럼 동작 방식보다 목적이 더 잘 드러난다는 점에서, 이 코드는 선언적이면서 함수형 프로그래밍 사고를 담은 코드이기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커링(Currying)은 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;FP를 실제로 적용해 보면,&amp;nbsp;&lt;b&gt;재사용성과 조합 가능성을 높이는 핵심 기법&lt;/b&gt;으로 커링이 자주 등장합니다.&lt;br /&gt;커링은 여러 인자를 한 번에 받는 함수 대신,&amp;nbsp;&lt;b&gt;하나의 인자만 받고 나머지를 기다리는 함수들을 단계적으로 반환&lt;/b&gt;하는 방식입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어 일반적인 add 함수는 이렇게 작성됩니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1750589487525&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const add = (a, b) =&amp;gt; a + b
add(1, 2) // &amp;rarr; 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;커링을 적용하면 아래와 같은 형태가 됩니다:&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1750589487525&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const add = a =&amp;gt; b =&amp;gt; a + b
add(1)(2) // &amp;rarr; 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이렇게 작성하면 첫 번째 인자를 고정한 상태로 새로운 함수를 만들 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1750589487526&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const increment = add(1)
increment(5) // &amp;rarr; 6

const addTen = add(10)
addTen(2) // 12&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커링(Curring)을 직접 써보며 느낀 장점 &amp;ndash; 흐름을 조립하는 힘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 단순히 &amp;ldquo;인자를 쪼개서 받는다&amp;rdquo; 정도로 이해했던 커링이었지만, 실제 프로젝트에서 반복되는 조건이나 동작을 함수로 추상화해 보니&amp;nbsp;&lt;b&gt;의미 있는 흐름을 조립하는 도구&lt;/b&gt;처럼 느껴졌습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어 다음과 같은 구조가 있다고 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const isLongerThan = min =&amp;gt; str =&amp;gt; str.length &amp;gt; min

const isLongerThan5 = isLongerThan(5)
isLongerThan5('hello') // false
isLongerThan5('functional') // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이제 이 함수를 다른 고차 함수들과 자연스럽게 조합할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const filter = pred =&amp;gt; arr =&amp;gt; arr.filter(pred)

const longWordsOnly = filter(isLongerThan5)
longWordsOnly(['hi', 'world', 'composition', 'js']) 
// &amp;rarr; ['composition']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;각 기능을 작은 함수로 쪼개 두고 조합하다 보니, 자연스럽게 의미 있는 동작들이 만들어졌습니다.&lt;br /&gt;&quot;이걸 어떻게 하지?&quot;가 아니라,&amp;nbsp;&lt;b&gt;&quot;이미 만든 함수들이 있으니까 그냥 붙이면 되겠다.&quot;&lt;/b&gt;라는 식으로 생각이 바뀌더라고요.&lt;br /&gt;커링은 생각보다 단순한 개념인데, 막상 써보면 흐름을 만들고 재사용하는 데 꽤 유용한 도구라는 걸 느꼈습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터는 나중에 Data Last!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커링을 설계할 때 제가 중요한 팁 중 하나는,&amp;nbsp;&lt;b&gt;중요한 설정값은 앞에 받고, 다루는 데이터는 마지막에 받도록&lt;/b&gt;&amp;nbsp;순서를 설계하는 것이었습니다.&lt;br /&gt;보통 인자는 data first방식으로 설계되지만, curring에서는 반대가 되어야지 curring에 유리한 함수가 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1750589487528&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const withPrefix = prefix =&amp;gt; str =&amp;gt; `${prefix}-${str}`

const labelWithDev = withPrefix('dev')
labelWithDev('server') // 'dev-server'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이렇게 하면 withPrefix('dev')는 &quot;dev 환경의 접두사를 붙이는 함수&quot;로 남겨두고, 이후 어떤 문자열이 오든 같은 규칙을 적용할 수 있습니다.&amp;nbsp;&lt;b&gt;무엇을&lt;/b&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;한다는 설정이 앞에 오고,&amp;nbsp;&lt;b&gt;무엇에&lt;/b&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;한다는 데이터는 가장 마지막에 받게 되면, 나중에 합성하거나 조합하기가 훨씬 수월해집니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://github.com/lodash/lodash/tree/4.17.15-npm/fp&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #e6f5ff;&quot;&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;loadsh/fp&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&amp;nbsp;소개&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 커링된 fp형태는 loadsh에서도 제공하는데요.&amp;nbsp;기존 lodash는 map(array, iteratee) 순서로 받지만, lodash/fp는 map(iteratee)(array)처럼 설계되어 있어&amp;nbsp;&lt;b&gt;flow와 함께 합성하기 쉽게 커링 구조로 구성되어 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://github.com/lodash/lodash/wiki/fp-guide&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;가이드 문서&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;도 있으니 궁금하신 분은 살펴보셔도 좋을 것 같습니다.&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1750589487529&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import { flow, filter, map } from 'lodash/fp'

const posts = [
	{ id: 1, title: 'Title_1', isPublished: true },
	{ id: 2, title: 'Title_2', isPublished: false },
	{ id: 3, title: 'Title_3', isPublished: true },
]

// 게시된 글의 제목만 소문자로 추출하자!

const getPublishedTitles = flow(
  filter((post) =&amp;gt; post.isPublished),
  map((post) =&amp;gt; post.title.toLowerCase())
)

getPublishedTitles(posts)
// &amp;rarr; ['title_1', 'title_3']&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;이 예시는 게시글 데이터 중에서 isPublished: true인 항목만 필터링한 뒤, 그 제목을 소문자로 변환해 리턴하는 흐름을 함수형 스타일로 표현한 것입니다. 이렇게 FP를 활용하면 불필요한 반복 없이 의도를 그대로 코드에 담을 수 있습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FP 스타일을 통해 얻는 장점 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커링과 합성을 바탕으로 흐름을 선언형으로 구성하는 함수형 프로그래밍(FP)의 매력을 조금이나마 느끼셨다면 좋겠습니다.&lt;br /&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;b&gt;컨텍스트 최소화&lt;/b&gt;&lt;br /&gt;각 함수가 외부 상태에 의존하지 않고 입력 &amp;rarr; 출력만 다루기 때문에, 전체 흐름을 이해하기가 한결 수월해졌습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예측 가능성&lt;/b&gt;&lt;br /&gt;동일한 입력에 대해 항상 같은 결과를 내는 순수 함수 덕분에, 디버깅이나 테스트도 훨씬 편해집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;합성 가능성&lt;/b&gt;&lt;br /&gt;작게 나눈 함수들을 조합해 흐름을 만들 수 있어, 코드 재사용성과 가독성이 자연스럽게 올라갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다시 FP를 꺼내 든 이유 &amp;ndash; GSAP를 사용하면서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년에 잠깐 GSAP을 꽤 활발히 썼는데, 시간이 지나 손에서 놓고 나니 금세 감이 흐려졌습니다.&lt;br /&gt;특히 작은 애니메이션 단위를 재사용 가능한 구조로 만들어보는 과정은, 좋은 코드를 설계하는 연습이 되기에도 꽤 괜찮은 경험이었습니다.&lt;br /&gt;그래서 최근 다시 GSAP + CSS 조합으로 애니메이션을 만들어보다가, 타임라인 코드가 길어질수록 명령형 스타일이 더 눈에 밟히기 시작했습니다.&lt;br /&gt;&lt;br /&gt;그 흐름을 더 명확하게 표현할 수 없을까 고민하던 와중, 다시 FP 방식을 꺼내 들게 되었습니다.&lt;br /&gt;이번에는 단순히 기능을 분리하는 수준을 넘어서,&amp;nbsp;&lt;b&gt;함수의 커링과 합성 구조를 바탕으로, 애니메이션 단계를 선언적으로 조립할 수 있는 구조&lt;/b&gt;를 만들어보자는 접근이었습니다.&lt;br /&gt;&lt;br /&gt;앞서 이야기했던 FP의 원칙들을 실제 코드에 녹여낸 과정이기도 했고요.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어 GSAP은 보통 아래처럼 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;gsap.to(box, { x: 100 })
gsap.to(box, { y: 50, delay: 0.3 })
...&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GSAP 특유의 명령형 호출 방식이 편리한 건 맞지만, 단계가 늘어나면 흐름을 눈으로 따라가기가 힘들다고 생각을 했습니다.&amp;nbsp;이런 코드를 보며&amp;nbsp;&lt;b&gt;&quot;FP의 파이프라인을 적용하면 흐름을 더 명확히 표현할 수 있지 않을까?&quot;&lt;/b&gt;라는 생각이 들었고, 실제로 적용해보기로 했습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 애니메이션 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하려는 동작은 토스 어플의 내부 동작 애니메이션 중 한 부분입니다.&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;정해져 있는 n 개의&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;카드 리스트에서&amp;nbsp;&lt;/span&gt;구현해야 하는 애니메이션은 총 두 가지 기능을 담고 있으며 GSAP를 통해 구현해보려고 했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;카드가 우측에서 좌측으로 순차적으로 나타나는(slide in)하는 효과&amp;nbsp;&lt;/li&gt;
&lt;li&gt;카드가 위쪽에서 아래쪽으로 뒤집어지는 (flip) 효과&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 애니메이션 출처:&amp;nbsp;&lt;a href=&quot;https://toss.im/career/article/ux-engineer-interview?utm_source=toss_careerpage&amp;amp;utm_medium=banner&amp;amp;utm_campaign=2309_UXE&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;토스 블로그&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아래 좌측은 토스의 애니메이션과 우측에는 제가 구현한 애니메이션입니다.&amp;nbsp;&lt;br /&gt;제 목적은 애니메이션 구현과, 코드를 잘 짜는 연습을 하는 것이었기 때문에 퍼블리싱에 신경을 쓰지는 않았음을 알려드립니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mep32/btsOLcStFfA/XV7jTvB1uZxCClt68kqAL1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mep32/btsOLcStFfA/XV7jTvB1uZxCClt68kqAL1/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;450&quot; data-filename=&quot;toss-animation.gif&quot; style=&quot;width: 49.9137%; margin-right: 10px;&quot; data-widthpercent=&quot;50.5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mep32/btsOLcStFfA/XV7jTvB1uZxCClt68kqAL1/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMep32%2FbtsOLcStFfA%2FXV7jTvB1uZxCClt68kqAL1%2Fimg.gif&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;348&quot; height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dp0B7/btsOLAS5sFZ/mwDWkxiaaakqNAK4Jr4GpK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dp0B7/btsOLAS5sFZ/mwDWkxiaaakqNAK4Jr4GpK/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;438&quot; data-filename=&quot;toss-animation-prac.gif&quot; data-widthpercent=&quot;49.5&quot; style=&quot;width: 48.9235%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dp0B7/btsOLAS5sFZ/mwDWkxiaaakqNAK4Jr4GpK/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDp0B7%2FbtsOLAS5sFZ%2FmwDWkxiaaakqNAK4Jr4GpK%2Fimg.gif&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;332&quot; height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌)토스 애니메이션 (우)구현&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 애니메이션을 봤을 때 컴포넌트와 기능을 어떻게 구현하면 좋을까요? 추상화 전 전체 동작과정을 먼저 소개드리면 좋을 것 같습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;import { useGSAP } from '@gsap/react'
import gsap from 'gsap'

const SlideAndFlipList = ({ children }) =&amp;gt; {
	const ref = useRef(null)

	useGSAP(() =&amp;gt; {
		const container = ref.current
		if (!container) return

		const targets = gsap.utils.toArray(container.children)
		const fronts = '.card-front'
		const backs = '.card-back'

		const tl = gsap.timeline()

		// 1. Slide 효과 (우측에서 좌측으로 들어옴)
		tl.fromTo(
			targets,
			{ x: '100%', opacity: 0 },
			{ x: '0%', opacity: 1, stagger: 0.1, ease: 'power3.out' },
		)

		// 2. Flip 효과 (카드를 뒤집음)
		tl.set(targets, { perspective: '1000px', transformStyle: 'preserve-3d' })
		tl.set(fronts, { backfaceVisibility: 'hidden' })
		tl.set(backs, { rotateX: 180, backfaceVisibility: 'hidden' })

		tl.to(targets, {
			rotateX: -180,
			stagger: 0.1,
			duration: 0.8,
			ease: 'power2.inOut',
		})
	}, [])

	return &amp;lt;div ref={ref}&amp;gt;{children}&amp;lt;/div&amp;gt;
}&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;SlideAndFlipList에서는 카드 요소에 SlideIn + Flip 애니메이션을 순차적으로 적용하기 위해 gsap.timeline()을 사용했습니다. timeline을 사용하면 복잡한 애니메이션을 순차적으로 제어할 수 있고, 각 단계의 흐름을 명확히 구조화할 수 있습니다.&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;selector element에 별로 css초기를 설정하지 않는다면 set()으로 초기화를 시켜줘야 하고, 이후 fromTo()나 to() 메서드를 통해 selector에 애니메이션 효과를 넣어줍니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 코드는 동작은 잘 하지만, 여러 개선사항이 보였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;slide와 flip 기능을 동시에 적용하는 경우에만 사용할 수 있어 재사용성이 낮습니다.&lt;/li&gt;
&lt;li&gt;내부에서 정적인 selector를 사용해 외부에서 주입할 children이 특정 구조를 의존하게 되었습니다.&lt;/li&gt;
&lt;li&gt;명령형 호출이 늘어나 흐름을 명확히 파악하기 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼, 어떻게 개선하면 좋을까요? 저는 다음 두 가지에 대해 큰 모듈과 작은 모듈로 나누어 설계했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;List를 받기만하고, animation이 실행되는 컴포넌트 별도 분리 (AnimationList)&lt;/li&gt;
&lt;li&gt;각 Item에 적용할 작은 단위의 애니메이션 함수 구성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 반복되는 구조 추상화하기 (AnimationList)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 차적으로 분리를 하고 싶었던 부분은, &lt;b&gt;애니메이션을 실행시키는 부분과, 리스트를 그리는 부분&lt;/b&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 &amp;nbsp;&quot;애니메이션을 List에 입히는&quot;AnimationList라는 컴포넌트를 제작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AnimationList 컴포넌트는 gsap의 timeline() 을 사용하여 순차적으로 진행할 수 있는 helper컴포넌트입니다.&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;해당 컴포넌트는 실행할 animation과 그려줄 children,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 애니메이션이 완료 후 실행 될 onCompleted callback함수,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;animation을 trigger 시킬 (의존성 배열에 들어가는) watch 배열을 받습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;interface AnimationListProps extends ComponentProps&amp;lt;'div'&amp;gt; {
  animate: (tl: gsap.core.Timeline, targets: HTMLElement[]) =&amp;gt; void;
  onComplete?: () =&amp;gt; void;
  watch?: any[];
}

const AnimationList = ({ animate, children, watch = [], onComplete, ...props }: AnimationListProps) =&amp;gt; {
  const containerRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null);

  useGSAP(() =&amp;gt; {
    const targets = gsap.utils.toArray(containerRef.current?.children);
    const tl = gsap.timeline({ onComplete });
    animate(tl, targets);
  }, watch);

  return &amp;lt;div ref={containerRef} {...props}&amp;gt;{children}&amp;lt;/div&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이 컴포넌트 덕분에, 이제 React 컴포넌트 안에서는 gsap 타임라인 설정과 DOM 선택 로직을 매번 작성할 필요가 없어졌습니다. 사용할 때는 이렇게만 써주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;&amp;lt;AnimationList
&amp;nbsp;&amp;nbsp;animate={(tl, targets) =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;tl.fromTo(targets, { x: 100 }, { x: 0, stagger: 0.1 })
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;tl.to(targets, { opacity: 1 })
&amp;nbsp;&amp;nbsp;}}
&amp;nbsp;&amp;nbsp;onComplete={() =&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;console.log('전체 플로우 끝')
&amp;nbsp;&amp;nbsp;}}
&amp;gt;
&amp;nbsp;&amp;nbsp;{items.map((_, idx) =&amp;gt; &amp;lt;Item key={idx} /&amp;gt;)}
&amp;lt;/AnimationList&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;덕분에 컴포넌트 바깥에서는 tl 생성이나 children 탐색을 신경 쓸 필요가 없어지게 되었습니다. 하지만 여기서도 여전히 animate 함수 내부는 명령형 호출로 채워져 있었고, 로직이 많아질수록 컨텍스트를 머릿속에 담아야 하는 부담은 여전했습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const animate = (tl, targets) =&amp;gt; {
	tl.fromTo(targets, ...)
	tl.set(targets, ...)
	tl.to(targets, ...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;명령형으로 흐름이 명확하게 드러나는 장점은 있지만, 조금 더 &amp;ldquo;&lt;b&gt;선언적인&amp;rdquo; 모습으로 코드를 다룰 방법&lt;/b&gt;은 없을지 고민이 시작됐습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: FP로 선언적 흐름 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 여기서 본격적으로 FP의 &quot;작은 함수의 합성&quot;이라는 철학을 도입해 보기로 했습니다. G&lt;b&gt;SAP의 애니메이션 단계를 각각의 독립된 함수로 쪼갠 뒤, 이를 자연스럽게 연결할 수 있는 FP 스타일의 유틸 함수&lt;/b&gt;를 만들었습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;흐름 제어 유틸: timeline&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 여러 애니메이션 단계를 순차적으로 실행할 수 있도록, GSAP의 timeline을 기반으로 한 작은 유틸을 만들었습니다.&lt;br /&gt;lodash/fp의 flow처럼 여러 함수를 순서대로 연결하는 함수형 스타일이지만, 내부적으로는 gsap.timeline()을 통해 애니메이션을 단계별로 쌓아가는 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export type TimelineFn = (
	tl: gsap.core.Timeline,
	targets: HTMLElement[],
) =&amp;gt; void

export const timeline =
	(...steps: TimelineFn[]) =&amp;gt;
	(...args: Parameters&amp;lt;TimelineFn&amp;gt;) =&amp;gt; {
		steps.forEach((step) =&amp;gt; step(...args))
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;562&quot; data-end=&quot;751&quot;&gt;
&lt;li data-start=&quot;562&quot; data-end=&quot;616&quot;&gt;TimelineFn은 timeline에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;한 단계의 애니메이션을 등록하는 함수&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li data-start=&quot;617&quot; data-end=&quot;672&quot;&gt;timeline(...)은 이런 단계들을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;차곡차곡 실행할 수 있는 흐름 제어자&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li data-start=&quot;673&quot; data-end=&quot;751&quot;&gt;결과적으로 timeline(a, b, c)는 a &amp;rarr; b &amp;rarr; c 순서로 타임라인에 애니메이션을 쌓아주는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;조립 함수&lt;/b&gt;가 됩니다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 제가 위에서 설명드렸던 tl, targets 같은&amp;nbsp;&lt;b&gt;공통으로 반복 전달되는 인자&lt;/b&gt;는 뒤로 밀고,&lt;br /&gt;각 애니메이션 단계 함수에서는 &lt;b&gt;실행할 애니메이션만 미리 받아서 재사용할 수 있게&lt;/b&gt;&amp;nbsp;구성한 점입니다.&lt;br /&gt;이런 식으로 만들면 slideIn, flip 같은 함수들을 아래처럼 &amp;ldquo;선언적으로&amp;rdquo; 조립할 수 있게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아래는 각 애니메이션 기능을 curring 된 형태로 제작한 함수입니다. 이 함수는 TimelineFn 타입을 리턴하는 커링된 함수입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const slideIn = (
	{ dir = 'right', stagger = 0.1, duration = 0.5, ...vars }: SlideOptions,
	position: gsap.Position = '&amp;gt;',
): TimelineFn =&amp;gt; {
	return (tl, targets) =&amp;gt; {
		const from: gsap.TweenVars = { opacity: 0 }
		const to: gsap.TweenVars = {
			...vars,
			opacity: 1,
			stagger,
			duration,
			ease: 'power3.out',
		}

		switch (dir) {
			case 'left':
				from.x = '-100%'
				to.x = 0
				break
			case 'right':
				from.x = '100%'
				to.x = 0
				break
			case 'top':
				from.y = '-100%'
				to.y = 0
				break
			case 'bottom':
				from.y = '100%'
				to.y = 0
				break
		}

		tl.fromTo(targets, from, to, position)
	}
}

const flip = (
	{
		front,
		back,
		axis = 'x',
		stagger = 0.1,
		duration = 0.8,
		invert = true,
		...restOptions
	}: FlipOptions,
	position: gsap.Position = '&amp;gt;',
): TimelineFn =&amp;gt; {
	return (tl, targets) =&amp;gt; {
		const rotateProp = axis === 'x' ? 'rotateX' : 'rotateY'
		const invertFlag = invert ? -1 : 1

		const parent = targets?.[0].parentElement

		// 카드 기본 설정
		tl.set(parent, {
			perspective: '1000px',
		})
		tl.set(targets, {
			width: '100%',
			position: 'relative',
			transformStyle: 'preserve-3d',
			transition: 'transform 0.6',
		})

		// front 초기 상태
		tl.set(front, {
			position: 'absolute',
			opacity: 1,
			backfaceVisibility: 'hidden',
		})

		// back 초기 상태
		tl.set(back, {
			position: 'absolute',
			opacity: 1,
			[rotateProp]: 180 * invertFlag,
			backfaceVisibility: 'hidden',
		})

		// 카드 회전
		tl.to(
			targets,
			{
				[rotateProp]: 180 * -invertFlag,
				duration,
				stagger,
				ease: 'power2.inOut',
				...restOptions,
			},
			position,
		)
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그리고 이 함수들을 timeline 유틸로 합성하면 다음처럼 매우 선언적으로 표현할 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;AnimationList 
    animate={
    	timeline(
          slideIn({ dir: 'right' }),
          flip({ axis: 'x', front: '.card-front', back: '.card-back' })
	)}
&amp;gt;
  {/* children */}
&amp;lt;/AnimationList&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: FP 스타일로 재구성한 최종 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 구성한 구조는 아래와 같습니다 (styling은 제외하였습니다.):&lt;/p&gt;
&lt;pre class=&quot;swift&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;&amp;lt;AnimationList
&amp;nbsp;&amp;nbsp;animate={timeline(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;slideIn({ dir: 'right' }),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;flip({ axis: 'x', front: '.card-front', back: '.card-back' })
&amp;nbsp;&amp;nbsp;)}
&amp;nbsp;&amp;nbsp;onComplete={()=&amp;gt;{
&amp;nbsp;&amp;nbsp;	console.log(&quot;전체 플로우 끝&quot;)
&amp;nbsp;&amp;nbsp;}
&amp;gt;
&amp;nbsp;&amp;nbsp;{items.map((_, idx) =&amp;gt; (
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;article key={idx}&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div className=&quot;card-front&quot;&amp;gt;Front-{idx}&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;div className=&quot;card-back&quot;&amp;gt;Back-{idx}&amp;lt;/div&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/article&amp;gt;
&amp;nbsp;&amp;nbsp;))}
&amp;lt;/AnimationList&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이처럼 timeline 유틸과 커링된 애니메이션 함수들을 조합하면, 각 단계가 명확히 분리되고 전체 흐름이 한눈에 들어옵니다.&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 블록이 독립적이라&amp;nbsp;&lt;b&gt;수정이나 순서 변경이 자유롭고&lt;/b&gt;,&lt;/li&gt;
&lt;li&gt;새 애니메이션도 기존 패턴에 맞춰 쉽게 확장할 수 있다는 점입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 slideIn, flip 정도만 있지만, 동일한 방식으로 fadeIn, scaleUp, shake 등도 같은 구조 안에 자연스럽게 녹일 수 있습니다.&amp;nbsp;&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;&amp;nbsp;&lt;br /&gt;1편에서는 선언적인 코드를 작성한다는 게 어떤 의미인지, 그리고 그 철학이 리액트 렌더링 구조와 어떻게 연결되는지를 살펴봤습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이번 글에서는 그 연장선에서,&amp;nbsp;&lt;b&gt;실제 코드의 흐름과 구조를 어떻게 선언적으로 바꿔볼 수 있을까&lt;/b&gt;라는 고민을 함수형 프로그래밍(FP)을 통해 풀어봤습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;물론 모든 상황에서 FP가 정답은 아니지만, 애니메이션처럼 흐름이 길고 재사용이 많은 구조에선 훨씬 깔끔하고 명확하게 정리할 수 있었고,&lt;br /&gt;&amp;ldquo;코드가 그대로 설명이 되는 구조&amp;rdquo;가 주는 이점을 체감할 수 있었습니다. 앞으로도 복잡한 흐름을 설계할 때는&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot; data-start=&quot;1231&quot; data-end=&quot;1281&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;1233&quot; data-end=&quot;1281&quot;&gt;&amp;ldquo;이걸 함수 단위로 쪼갤 수 있을까?&amp;rdquo;&lt;br /&gt;&amp;ldquo;조립 가능한 구조로 만들 수 있을까?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;같은 질문을 계속 던져보게 될 것 같습니다.&lt;br /&gt;1편이 선언형 사고의 방향을 정리한 글이었다면, 이번 2편은 그 방향을&amp;nbsp;&lt;b&gt;실제 코드 구조로 구현해 본 기록&lt;/b&gt;이었습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;읽어주셔서 감사합니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>curring</category>
      <category>FP</category>
      <category>고차함수</category>
      <category>선언형 프로그래밍</category>
      <category>커링</category>
      <category>코드 추상화</category>
      <category>함수형프로그래밍</category>
      <category>합성함수</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/224</guid>
      <comments>https://algoroot.tistory.com/224#entry224comment</comments>
      <pubDate>Sun, 22 Jun 2025 19:53:45 +0900</pubDate>
    </item>
    <item>
      <title>내가 선언적인 코드를 좋아하는 이유 (Feat. React) - 1</title>
      <link>https://algoroot.tistory.com/218</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;안녕하세요. 오늘은 제가 코드를 작성할 때 추구하는 프로그래밍인 선언적 프로그래밍에 대해 기술하고자 합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;1년 차에는 개발을 할 때 어떻게 구현할지를 고민했다면, 경험이 쌓이면서는 어떻게 짜야할지 고민을 더 많이 하게 되는 것 같습니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하나의 알고리즘 문제를 풀 때도 풀이 법이 수십 가지이듯, 하나의 기능 구현에도 개발자마다 중요도와 스타일에 맞게 다르게 작성됩니다. 그렇기에 &quot;유지보수가 쉽고, 보기 좋고, 디버깅이 편한 코드&quot;를 작성하는 건 더 이상 단순한 선택이 아닌 좋은 개발 습관이자 필요조건이라 생각합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;저 역시 처음엔 좋은 코드를 위해 &quot;추상화&quot;라는 개념부터 차근차근 익혀나갔습니다. 하지만 경험이 쌓이면서 점점 더 넓은 컨텍스트에서 코드를 바라보게 되었고, 단순한 추상화를 넘어서 코드를 읽는 방식 자체를 바꾸는 사고로 이어지게 되었습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그 무렵부터 제게 &quot;선언적으로 짜라&quot;는 리액트의 철학이 다르게 들리기 시작했습니다. 처음에는 단순히 문법적인 이야기로만 받아들였지만, 코드 리뷰를 통해 함수형 프로그래밍(FP)이라는 개념을 접하게 되었고, 현재도&amp;nbsp;&lt;a href=&quot;https://search.shopping.naver.com/book/catalog/32445790635?cat_id=50010881&amp;amp;frm=PBOKPRO&amp;amp;query=%ED%95%A8%EC%88%98%ED%98%95+%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8&amp;amp;NaPm=ct%3Dmbymttt4%7Cci%3D72f291dacf6a02d59c2f9a916ce2532be1399258%7Ctr%3Dboknx%7Csn%3D95694%7Chk%3D12b54f0352857566472acafb34668121d2c016c1&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #e6f5ff;&quot;&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;함수형 자바스크립트&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;책을 읽으며&lt;/span&gt;&amp;nbsp;조금씩 관심을 갖고 익혀나가고 있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;지금부터 제가 그동안 익혀왔던 &quot;선언적인 프로그래밍&quot;에 대해&amp;nbsp; 1편에서는 제가 이해했던 방식으로 아주 이해하기 쉽게 설명드린 후 React를 통해 자연스럽게 그 체감한 과정을, 2편에는 함수형 프로그래밍(FP) 소개와 제가 실제로 활용한 예제도 담아보려고 합니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 생각하는 &quot;선언적&quot; 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가 저에게 &quot;&lt;b&gt;선언적인 코드가 뭔가요?&lt;/b&gt;&quot;라고 묻는다면, 저는 이렇게 답할 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;제가 알아야하는 컨텍스트가 적어지는 코드입니다.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이 말은 곧, 코드를 이해하기 위해 머릿속에 담아야 할 정보(=컨텍스트)가 줄어든다는 뜻입니다.&lt;br /&gt;그리고 이 문장 안에는&amp;nbsp;&lt;b&gt;불변성을 유지&lt;/b&gt;한다는 선언적 프로그래밍의 핵심 개념도 자연스럽게 포함되어 있다고 생각합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그렇다면 컨텍스트가 적어진다는 말은 어떤 말일까요?&lt;br /&gt;이 부분은 &quot;명령적&quot;인 코드와 비교를 하면 좋을 것 같네요.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선언형 vs 명령형&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 선언적인 코드를 좋아하는 이유는 앞서 말했듯이 &lt;b&gt;제가 알아야하는 컨텍스트가 줄어들기 때문&lt;/b&gt;입니다. 반면 명령형 코드는 제가 따라가야 할 흐름이 많아지고, 알아야 할 상태가 점점 많아지게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어 자바스크립트에서 배열을 다룰 때를 생각해 볼까요?&lt;br /&gt;&amp;nbsp;&lt;br /&gt;push, pop 메서드는 원본 배열을 직접 수정합니다. 코드가 길어지고 상태가 여러 곳에서 변경되기 시작하면, 이 원본 배열이 어디서 어떻게 바뀌었는지 추적하는 데 많은 에너지가 들게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;반면 concat, filter, slice는 원본 배열을 수정하지 않고 새로운 배열을 반환합니다. 이전 상태를 신경 쓰지 않아도 되니, 코드를 읽는 입장에서 부담이 훨씬 줄어듭니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아래 조건을 통해 선언형과 명령형 코드 예시를 보겠습니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열 [1, 2, 3, 4]에서 짝수만 남기고&lt;/li&gt;
&lt;li&gt;각 값에 제곱을 한 후&amp;nbsp;&lt;/li&gt;
&lt;li&gt;마지막 값 제거&lt;/li&gt;
&lt;li&gt;결과: [4]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;명령형 코드:&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 4];
const result = [];

for (let i = 0; i &amp;lt; arr.length; i++) {
&amp;nbsp;&amp;nbsp;if (arr[i] % 2 === 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result.push(arr[i] * arr[i]);
&amp;nbsp;&amp;nbsp;}
}

result.pop(); // 마지막 제거

console.log('최종 result:', result); // [4]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선언형 코드 :&amp;nbsp;&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 4];

const filtered = arr.filter(n =&amp;gt; n % 2 === 0);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // [2, 4]
const squared = filtered.map(n =&amp;gt; n * n);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// [4, 16]
const trimmed = squared.slice(0, -1);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// [4]

console.log('최종 result:', trimmed); // [4]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;두 코드 모두 [4]를 출력하지만, 읽는 사람이 머릿속으로 따라가야 하는 상태의 복잡도는 다릅니다.&amp;nbsp;&lt;br /&gt;&lt;b&gt;명령형 코드는 arr이라는 하나의 객체가 계속 바뀌는 것에 집중&lt;/b&gt;해야 하고,&amp;nbsp;&lt;b&gt;선언형은 각 단계를 새로운 상태로 나눠볼 수 있어&lt;/b&gt;&amp;nbsp;흐름을 예측하기 더 쉽습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;지금 비교가 와닿지 않는다면 이슈가 났을 때를 생각하면 이해가 좀 더 쉬울 수 있습니다. 내가 생각한 배열 [4]가 나오지 않았을 때 어떻게 디버깅을 해봐야 할까요?&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예기치 못한 결과가 나왔을 때&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에 의도적으로 실수를 넣어서 결괏값이 [1]로 나오는 상황입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;명령형 코드 - 디버깅 예시:&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 결과 [1]만 보고는 push 조건이 잘못된 건지, 제곱이 잘 안 된 건지,&amp;nbsp;pop이 잘못된 건지&amp;nbsp;알기 어렵습니다.&lt;br /&gt;조건을 확인하더라도, 위에서부터 코드 흐름을 따라가야 합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;간단한 예제라 흐름을 따라가서 조건문이 홀수로 잘 못 되어있다는 것은 디버깅을 하지 않아도 찾을 수도 있지만,&lt;br /&gt;코드의 복잡도가 높아질수록 전체 루프 흐름을 모두 따라가며&amp;nbsp;&lt;b&gt;각 단계의 값&lt;/b&gt;을 직접 출력해 봐야 원인을 찾을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 4];
const result = [];

for (let i = 0; i &amp;lt; arr.length; i++) {
&amp;nbsp;&amp;nbsp;console.log('현재 값:', arr[i]);

&amp;nbsp;&amp;nbsp;// ❌ 실수: 홀수를 필터링
&amp;nbsp;&amp;nbsp;if (arr[i] % 2 !== 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const squared = arr[i] * arr[i];
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;console.log('조건 통과 &amp;rarr; push:', squared);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;result.push(squared);
&amp;nbsp;&amp;nbsp;}
}

result.pop();
console.log('최종 result:', result);

// 출력
현재 값: 1
조건 통과 &amp;rarr; push: 1 // 여기서 잘 못 됐구나!
현재 값: 2
현재 값: 3
조건 통과 &amp;rarr; push: 9
현재 값: 4
최종 result: [1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선언형 코드- 디버깅 예시:&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언적인 코드는 각 단계가 filter &amp;rarr; map &amp;rarr; slice처럼 명확히 나뉘어 있습니다. 이 덕분에&amp;nbsp;&lt;b&gt;각 단계의 출력만으로 어디서 문제가 발생했는지 쉽게 파악&lt;/b&gt;할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 4];

// ❌ 실수: 홀수 필터링
const filtered = arr.filter(n =&amp;gt; n % 2 !== 0);&amp;nbsp;&amp;nbsp; // [1, 3]
console.log('filtered:', filtered);

const squared = filtered.map(n =&amp;gt; n * n);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// [1, 9]
console.log('squared:', squared);

const trimmed = squared.slice(0, -1);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// [1]
console.log('trimmed:', trimmed);

// 출력
filtered: [1, 3] // 여기서 잘 못 됐구나!
squared: [1, 9]
trimmed: [1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;앞서 저는 선언형 코드를 &amp;ldquo;내가 알아야 하는 컨텍스트가 적어지는 코드&amp;rdquo;이며 이는 불변성과도 관련이 있다고 설명드렸습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;filtered는 주어진 배열에서 조건에 맞는 요소만 골라&amp;nbsp;&lt;b&gt;새로운 배열&lt;/b&gt;을 만듭니다.&lt;/li&gt;
&lt;li&gt;squared는 각 요소를 제곱해서 또 다른&amp;nbsp;&lt;b&gt;새 배열&lt;/b&gt;을 생성하고,&lt;/li&gt;
&lt;li&gt;trimmed는 마지막 요소를 제거한&amp;nbsp;&lt;b&gt;또 하나의 새 배열&lt;/b&gt;을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 각 함수는&amp;nbsp;&lt;b&gt;원본을 직접 수정하지 않고, 자신만의 역할을 수행하는 순수 함수&lt;/b&gt;입니다.&lt;br /&gt;그래서 중간 어디서 잘못되었는지 로그 한 줄만 봐도 바로 찾을 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;결국, 선언적인 코드에서는&amp;nbsp;&lt;b&gt;알아야 할 상태가 적고&lt;/b&gt;,&amp;nbsp;&lt;b&gt;단계별로 원인을 좁히는 것이 쉬워&lt;/b&gt;&amp;nbsp;디버깅 부담이 줄어듭니다.&lt;br /&gt;이 점이 제가 선언적인 코드를 좋아하게 된 가장 큰 이유 중 하나입니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래도 이해가 어렵다면? -&amp;gt; 사람 느낌 나는 해석..&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시로 선언형과 명령형 코드의 차이가 어느 정도 감이 오셨을까요?&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;제가 이때까지 느낀 바로 좀 더 쉽게 사람 말처럼 표현하자면,&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;명령형은 &quot;이걸 이렇게 바꿔!&quot;라고&lt;/b&gt; 명확하게 지시하는 스타일이고,&lt;br /&gt;&lt;b&gt;선언형은 &quot;이럴 땐 이거야&quot;라고 규칙이나 조건을 서술하는&lt;/b&gt;&amp;nbsp;느낌에 더 가깝습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 명령형은 실행 흐름이 눈에 보이기 때문에&amp;nbsp;&lt;b&gt;초반에는 이해하기 쉬울 수 있지만&lt;/b&gt;, 상태가 많아질수록 따라가야 할 컨텍스트가 점점 늘어나게 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;반면 선언형은 마치&amp;nbsp;&lt;b&gt;수학 공식을 읽듯&lt;/b&gt;, 한눈에 &quot;무슨 처리를 하는지&quot;를 파악하기 쉬운 구조입니다.&lt;br /&gt;그 대신 처음엔 조금 추상적으로 느껴질 수 있고, 익숙하지 않으면 해석에 시간이 걸릴 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잠깐 보자 리액트의 선언적 프로그래밍!&amp;nbsp;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 서두에 말씀드렸던 리액트의 선언적 프로그래밍도 잠깐 보고 가면 좋을 것 같습니다.&lt;a href=&quot;https://opensource.fb.com/projects/react/?utm_source=chatgpt.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;a href=&quot;https://opensource.fb.com/projects/react/?utm_source=chatgpt.com&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Meta Opensource&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;에서는 리액트를 이렇게 소개합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;React is:&lt;br /&gt;Declarative: React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes. Declarative views make your code more predictable, simpler to understand, and easier to debug.&lt;br /&gt;&lt;br /&gt;선언적: React는 인터랙티브 UI를 손쉽게 만들 수 있도록 해줍니다. 애플리케이션의 각 상태에 맞는 간단한 뷰를 디자인하면 React가 데이터 변경 시 필요한 컴포넌트만 효율적으로 업데이트하고 렌더링 합니다. 선언적 뷰는 코드를 더 예측 가능하고, 이해하기 쉽고, 디버깅하기 쉽게 만들어줍니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이 문장을 저는 이렇게 해석했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;상태에 따라 어떻게 보여야 할지를 선언하세요. 나머지는 React가 알아서 처리합니다.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;내부 로직은 모른 채로도 개발을 할 수 있다는 뜻입니다. 추상화 개념을 이해할 때 많이 쓰는 개념이죠? 선언적인 코드도 추상적인 범주에 들어가기 때문에 그렇게 이해해도 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 React로 개발할 때 개발자가 직접 DOM을 조작하거나, 언제 어떤 요소를 업데이트할지 고민하지 않아도 할 수 있는 거죠.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;내부 로직까지 알 필요 없어 &amp;mdash; &quot;이 상태일 땐 이렇게 보여줄 거야&quot;만 말하면 돼!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;여기서 &quot;내부 복잡한 로직을 숨겨 둘 수도 있다!&quot;라는 선언적인 코드의 장점이 하나 더 추가되네요.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데, 내부를 몰라도 되는 걸까요?&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 해서 &amp;ldquo;프런트엔드개발자는 리액트 사용할 때 내부 동작을 몰라도 된다&amp;rdquo;는 뜻은 아닙니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실제로 저도 처음에는 선언적인 사용법만 익혀서 리액트로 개발을 했고, JSX와 리액트에서 제공하는 hooks 정도만 알아도 화면을 구현하는 데 큰 어려움은 없었습니다. 어떻게 보면 이건 리액트가 정말 잘 추상화된 프레임워크라는 증거이기도 하죠. 내부 동작을 몰라도 개발이 가능하다는 건, 엄청난 장점이라고 생각합니다.&lt;br /&gt;&lt;br /&gt;하지만 선언형에 관심을 갖다 보니 리액트라는 프레임워크에 내부동작이 궁금해지게 되었고, 리액트의 렌더링 과정(Reconciliation, Commit Phase, Diffing 등)을 조금씩 이해하면서 왜 리액트가 선언적 프로그래밍 철학을 갖고 있는 프레임워크인지 더 명확히 알 수 있었습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;선언적인 코드도 결국&amp;nbsp;&lt;b&gt;어디까지가 추상화되고, 어디부터 명시해야 하는지&lt;/b&gt;를 이해하려면, 그 안에서 돌아가는 흐름을 파악하는 것도 중요하다고 생각합니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리액트 렌더링 과정의 핵심: Diffing&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로는&amp;nbsp;선언적인 부분을 React 로서 설명할 때의 &lt;b&gt;핵심은 리액트의 랜더링 과정 중 Diffing 이라고&lt;/b&gt; 생각합니다. 왜냐하면 이 비교 개념에서 불변성 유지가 명확하게 드러나는 부분이라고 생각하기 때문입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;비교라는 건 항상 두 개의 대상이 필요합니다. 리액트에서는 그 대상이 이전 가상 돔(Virtual DOM)과, 새로운 가상 돔(Virtual DOM)이 됩니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;간단하게 리액트 렌더링 과정을 설명하면 다음과 같습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우리가 선언한 JSX는 렌더링이 발생할 때마다 새로운 Virtual DOM으로 변환됩니다.&lt;/li&gt;
&lt;li&gt;리액트는 이전 Virtual DOM과 새 Virtual DOM을 비교(Diffing) 합니다.&lt;/li&gt;
&lt;li&gt;그 결과 바뀐 부분만 실제 DOM에 반영(Commit)됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 선언형과 명령형으로 좀 더 설명드리자면,&lt;br /&gt;&lt;br /&gt;개발할 때 &lt;b&gt;우리는 선언적으로 코드를 작성&lt;/b&gt;하고, &lt;b&gt;리액트 내부에서&lt;/b&gt; 비교를 통해 최종적으로 리액트에서 실제 Dom에 &lt;b&gt;명령적으로 수정&lt;/b&gt;을 하게 되는 흐름입니다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이 구조를 이해하셨다면 우리가 항상 사용하는 useState 훅이 동작하는 방식도 다시 생각해 보게 됩니다. 저는 실제로 이 과정을 처음 공부했을 때, 문득 이런 생각이 들었습니다:&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot; data-start=&quot;690&quot; data-end=&quot;756&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;692&quot; data-end=&quot;756&quot;&gt;&amp;ldquo;setState는 기존 값을 바꾸는 게 아니라, 내부 어딘가에 새로운 값을 저장해 두는 거겠구나!&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;지금 생각하면 당연한 이야기 같기도 하지만, 내부 동작을 이해하기 전에 &amp;ldquo;불변성이 왜 중요한데?&amp;rdquo;라는 질문을 받았다면 제대로 답변을 못 했을 것 같아요.&lt;br /&gt;&lt;br /&gt;Diffing의 전제가 &amp;lsquo;이전과 새로운 값을 비교하는 것&amp;rsquo; 이니, 불변성을 유지해야 하는 이유가 자연스럽게 연결됐습니다.&lt;br /&gt;&lt;br /&gt;기존 값을 직접 바꿔버리면 비교 자체가 안 되니까요.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 리액트는 상태의 불변성을 유지합니다.&amp;nbsp;&lt;b&gt;이전 상태와 새로운 상태를 명확히 나눠야만&lt;/b&gt;,&amp;nbsp;&lt;b&gt;바뀐 부분만 빠르게 찾고, 효율적으로 렌더링 할 수 있기 때문입니다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;o3&quot; data-message-id=&quot;566aef3b-3fbe-4627-9fb6-984140238ceb&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;h3 data-end=&quot;67&quot; data-start=&quot;0&quot; data-ke-size=&quot;size23&quot;&gt;선언형 사고가 남긴 것&lt;/h3&gt;
&lt;p data-end=&quot;67&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 선언적인 프로그래밍은 &amp;lsquo;코드를 어떻게 읽히게 할까?&amp;rsquo;를 고민하는 과정에서 저에게 좋은 방향키가 되어 주었습니다.&lt;/p&gt;
&lt;p data-end=&quot;67&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;아직 3년 차 개발자라 배울 것이 산더미지만, 오늘은 그동안 제가 코드를 작성하며 중요하게 여겨 온 관점을 나누고자 했습니다.&lt;/p&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;237&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;추상적 용어를 겉핥기로 넘기기보다 그 배경과 의도를 깊이 파고들었던 경험을 담았으니, 처음 개념을 접하는 분께도 작은 길잡이가 되길 바랍니다.&lt;/p&gt;
&lt;p data-end=&quot;237&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;416&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;리액트의 &amp;lsquo;선언적 프로그래밍&amp;rsquo;을 탐구하다 보니 관심은 자연스레 &lt;b&gt;함수형 프로그래밍(FP)으로&lt;/b&gt; 확장되었습니다. 아직 FP를 자유자재로 다루진 못하지만, &amp;ldquo;이 기능은 FP로 풀어야 더 깔끔하겠다!&amp;rdquo; 싶을 때마다 과감히 적용해 보고 있습니다. 처음엔 다소 낯설지만, 한 번 맛보면 제법 중독적인 매력이 있더라고요.&lt;/p&gt;
&lt;p data-end=&quot;416&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;496&quot; data-start=&quot;418&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 &lt;b&gt;함수형 프로그래밍(FP)&lt;/b&gt; 이 선언형 사고와 어떻게 닿아 있는지 살펴보고, 핵심 개념을 간단히 짚은 뒤, 실제 프로젝트에 적용해 본 예제를 함께 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;496&quot; data-start=&quot;418&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;496&quot; data-start=&quot;418&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2편 바로보기&lt;/b&gt;&lt;/p&gt;
&lt;a href=&quot;https://algoroot.tistory.com/224&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://algoroot.tistory.com/224&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;figure id=&quot;og_1761833425134&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;내가 선언적인 코드를 좋아하는 이유 (Feat. FP로 만드는 애니메이션) - 2&quot; data-og-description=&quot;안녕하세요. 지난 글 「내가 선언적인 코드를 좋아하는 이유 (Feat. React) &amp;ndash; 1」에서 저는 선언적인 코드를 작성하는 방식이 어떻게 코드 품질의 방향키가 되어 주었는지, 그리고 React 렌더링 과정&quot; data-og-host=&quot;algoroot.tistory.com&quot; data-og-source-url=&quot;https://algoroot.tistory.com/224&quot; data-og-url=&quot;https://algoroot.tistory.com/224&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bk8fHo/hyZMWW5MdU/SE0KbPNcWI54rto5bja641/img.gif?width=332&amp;amp;height=438&amp;amp;face=0_0_332_438,https://scrap.kakaocdn.net/dn/cqm8RI/hyZMjsNYs0/vsmWekcl7rd72WB518FSs0/img.gif?width=332&amp;amp;height=438&amp;amp;face=0_0_332_438&quot;&gt;&lt;a href=&quot;https://algoroot.tistory.com/224&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://algoroot.tistory.com/224&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bk8fHo/hyZMWW5MdU/SE0KbPNcWI54rto5bja641/img.gif?width=332&amp;amp;height=438&amp;amp;face=0_0_332_438,https://scrap.kakaocdn.net/dn/cqm8RI/hyZMjsNYs0/vsmWekcl7rd72WB518FSs0/img.gif?width=332&amp;amp;height=438&amp;amp;face=0_0_332_438');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;내가 선언적인 코드를 좋아하는 이유 (Feat. FP로 만드는 애니메이션) - 2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. 지난 글 「내가 선언적인 코드를 좋아하는 이유 (Feat. React) &amp;ndash; 1」에서 저는 선언적인 코드를 작성하는 방식이 어떻게 코드 품질의 방향키가 되어 주었는지, 그리고 React 렌더링 과정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;algoroot.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>리액트 선언적 프로그래밍</category>
      <category>선언적 프로그래밍</category>
      <category>선언적인 코드</category>
      <category>선언형 명령형</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/218</guid>
      <comments>https://algoroot.tistory.com/218#entry218comment</comments>
      <pubDate>Sun, 25 May 2025 16:14:32 +0900</pubDate>
    </item>
    <item>
      <title>토스 프론트엔드 UX Engineer 직무 면접 회고</title>
      <link>https://algoroot.tistory.com/216</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 최근 토스에서 대규모 채용을 진행했었죠. 관심 있게 지켜보신 분들도 많으셨을 텐데요.&lt;br&gt;&amp;nbsp;&lt;br&gt;저 역시 그중 UX Engineer 포지션에 지원했고, 최종적으로 직무 면접까지 진행한 후 탈락하게 되었습니다.&lt;br&gt;아쉬운 결과였지만, 그만큼 얻은 인사이트와 배움도 많았던 시간이었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 글에서는 UX Engineer 직무에 지원하게 된 계기, 직무 면접을 준비하고 겪으며 느꼈던 점, 그리고 돌이켜본 아쉬움과 배움을 정리해보려 합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;지원한 계기&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;저는 아직 많은 회사에 지원하진 않았습니다. 지금까지 토스를 포함해 총 세 곳에만 지원했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그 이유는 단순히 취업 자체보다는, 해당 직무를 통해 제가 진심으로 성장할 수 있을지, 기여할 수 있을지를 더 중요하게 생각하기 때문입니다. 지원 여부를 결정할 때는 &lt;b&gt;“이 직무가 재미있을까?”, “일이 많아도 가치 있다고 느낄 수 있을까?”, “내가 이 회사에 의미 있는 기여를 할 수 있을까?”&lt;/b&gt;와 같은 질문을 스스로에게 던져보며 신중하게 판단하고 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그런 의미에서 토스의 UX Engineer 직무는 저에게 매우 큰 울림을 주었습니다.&lt;br&gt;생산성을 중요하게 생각하며 개발하는 제 스타일과 잘 맞았고, 공고를 읽는 순간부터 ‘이 팀의 일원으로 함께 일해보고 싶다’는 생각이 들었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;평소에도 토스의 기술 문서, Slash 코드, 영상 등을 자주 참고해 왔고, 아직도 침대에 누워 &lt;a href=&quot;https://www.youtube.com/watch?v=5WBlhIl8KkY&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #e6f5ff;&quot;&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;토스ㅣSLASH 22 - UX와 DX, 그 모든 경험을 위한 디자인 시스템&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;영상을 보고 가슴이 뛰던 기억이 생생합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그들의 공유 기술을 봤을 때 UX/DX 향상에 대한 깊은 고민과 실행력이 돋보였고, 저 역시 전 직장에서 디자인 시스템을 구축하며 사용성과 일관성, 생산성의 균형을 고민해 왔기에 더욱 공감할 수 있었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;토스의 실무자 분들이 내가 만든 모듈을 사용한다니, 그 얼마나 책임감이 높고 가치있는 일인가 ? 라는 생각도 들었고요 ㅎㅎ&lt;br&gt;&amp;nbsp;&lt;br&gt;이런 점에서 토스의 UX Engineer 팀은 제가 지향하는 방향과 완전히 맞닿아 있었고, 그래서 못 먹어도 고!라는 마음을 가지고 지원하게 되었습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;서류 전형 (합격)&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이력서는 자율 형식으로 작성했고, 기존 제 이력서에 토스 UX Engineer 디자인 시스템 팀의 직무 요구사항에 맞게 조금 수정했습니다.&lt;br&gt;기존 이력서에 덧붙인 내용은 없었고, 했던 프로젝트들의 순서만 바꿨습니다. 보통 최신순으로 나열하지만, 저는 디자인 시스템 관련 진행했던 프로젝트 순으로 나열했고, 관련된 기술은 볼드체로 강조했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;제출 후 2-3일 만에 문자로 합격 연락이 왔고, 과제 전형 일정은 2주가 벗어나지 않는 선에서 제가 선택할 수 있었지만,&lt;br&gt;저는 빨리 보고 싶어서 4일 뒤 일정으로 잡았었습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;과제 전형 (합격)&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;과제를 보기 전 혼자 예상문제를 생각하며 구현해 보았습니다. 결론적으로 과제에서는 제가 예상한 문제들이 나오지 않았지만, 그 과제들이 훨씬 더 재밌었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;기술에 대해서는 말씀드릴 수는 없는 부분이지만 과제를 하면서 느낀 점은, 실제 해당 직무에 소속이 되면 내가 이런 일을 하는 거구나 라는 생각이 들게 되는 과제들이었습니다. 분명한 건 매우 재밌었고, 출제자분의 의도를 파악하면서 구현해 나가는 재미가 있었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;반면에 개인적으로 아쉬웠던 부분들도 있었습니다. 이 부분에 대해서는 스스로 복기를 하며 채워 나가는 식으로 진행했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;과제 전형 또한 제출 후 3일 정도 안에 전화로 합격 연락이 왔었던 것 같고, 직무 면접 일정도 4일 뒤에 잡았습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;직무 면접 준비&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;4일이라는 제한된 시간이었지만, 그래도 준비를 하기에는 충분했던 시간이었던 것 같습니다. 우선 과제에 대해서 제가 놓친 부분은 없나, 처음부터 다시 과제를 진행해 보았습니다. 기존 코드 베이스 위에서 개선하는 방식이 효율적이었지만, 제가 혹시 놓치고 있었던 부분이 있을까 하는 생각에서였습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;또 이력서 기반으로는 전체적으로 검토하고, 직무와 연관된 프로젝트에 대해 더 심층 높게 보았던 것 같습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;직무 면접 (탈락)&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;토스 UX Engineer 포지션의 직무 면접은 약 1시간 30분 동안 진행되었습니다. 이 번 역시 예상했던 질문은 &quot;이력서와 과제에 대해 질문을 한다&quot;라는 것뿐 그 질문에 디테일에 대해서는 예상대로 진행된 부분이 단 하나도 없었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;다만 면접을 본 당시에는 (엄청난) 착각을 하게 되는데요. 면접 당시 분위기가 굉장히 편안하게 해 주셔서 그런지, 당시 질문에 대해 70%는 답변했다라고 생각했습니다. 이 외에도 과제 구현사항에 대해 일부 “너무 마음에 든다”는 긍정적인 피드백도 들었기 때문에 전체적으로 아쉬웠던 부분이 있었지만, 나쁘지는 않았다고 생각했습니다. (긍정회로를 돌렸습니다...ㅠ)&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 결과는 3일 뒤 이메일로 도착한 불합격 통보였습니다.&lt;br&gt;&lt;br&gt;‘토스는 메일이 오면 탈락’이라는 얘기를 들은 터라, 메일이 오지 않기를 바랐지만… 아침에 도착한 메일을 보고 아쉬움이 밀려왔습니다.&lt;br&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;br&gt;이번 면접은 제가 약 3년 만에 본 직무 면접이었습니다. 그동안 다양한 피드백을 참고하며 ‘잘 보이기 위한 답변’보다는 ‘진심을 담은 답변’을 준비하려 노력했습니다. 하지만 몇 가지 조언을 상황에 맞게 적용하지 못한 점이 오히려 마이너스 요소였다는 생각이 들었습니다.&lt;br&gt;예를 들면:&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;“모르면 모른다고 하자&quot;는 조언은 정말 모를 때에만 해당되는 말이었지만, 저는 충분히 본질적으로 접근하면 답할 수 있었던 질문도 “잘 모른다”라고 넘겨버렸습니다. (이 부분이 가장 후회됩니다.) 조금 생각할 시간을 달라고 한 후 차분히 대답하는 것도 방법이었을 것 같습니다&lt;/li&gt;&lt;li&gt;반대로, “아는 부분은 최대한 설명하라”는 말을 지나치게 적용해 핵심을 벗어난 장황한 답변을 한 적도 있었습니다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다 중요한 건, &lt;b&gt;질문의 의도를 명확히 파악하고 그에 맞게 깊이와 방향을 맞추는 것&lt;/b&gt;이었습니다. 당시에는 파악했다고 생각했지만, 지금 돌이켜보면 그렇지 않았습니다.&lt;br&gt;저는 기술적인 깊이에 더 초점을 맞췄던 반면, 면접관분들의 질문은 행동에 대한 더 근본적인 부분을 궁금해하셨습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또 기술적인 깊이라 하면 해당 직무에 대한 기술적인 깊이도 부족했던 부분이 있었던 것 같습니다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이 경험을 통해 확실히 배운 점은 지식의 양보다 중요한 건 핵심을 짚어내는 사고력과 의도를 파악하는 커뮤니케이션 능력이라는 것입니다.&lt;br&gt;면접 당시에는 몰랐지만, 지금은 왜 그렇게 대답했는지, 왜 그게 부족했는지가 명확히 보입니다.&lt;br&gt;이게 면접 당시에 의도를 파악하는 능력이 긴장하는 와중에서도 잘 파악해야 되는 부분이라는 것입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;면접관님께서 &quot;이런 거 이런거 우리 중요하게 생각하는데&quot;를 알려주시면서 질의응답과정이 있는 것은 아니니까요.&lt;br&gt;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;비록 직무 면접에서 탈락이라는 결과는 아쉬웠지만, 저에게는 매우 큰 배움이 있는 시간이었습니다.&lt;br&gt;보안상 면접 내용은 공유할 수 없지만, 마지막으로 한 가지 확실히 느낀 점은 다음과 같습니다:&lt;/p&gt;&lt;blockquote data-end=&quot;1425&quot; data-start=&quot;1354&quot; data-ke-style=&quot;style2&quot;&gt; 
 &lt;p data-end=&quot;1425&quot; data-start=&quot;1356&quot; data-ke-size=&quot;size16&quot;&gt;‘내가 이 직무를 왜 하고 싶은가?’에 대해 더 깊이 생각하고, 질문에 담긴 의도를 정확히 이해하는 훈련이 필요하다.&lt;/p&gt; 
&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;다음 면접에서는 더 단단한 태도로, 더 본질적인 답변을 할 수 있을 거라는 자신이 생겼습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;* 그리고 면접 보신 분들 합불합 관계없이 꼭 회고과정은 거치시길 바랍니다.. 회고 하면서 얻는게 너무나도 많습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>토스 과제 후기</category>
      <category>토스 직무면접 후기</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/216</guid>
      <comments>https://algoroot.tistory.com/216#entry216comment</comments>
      <pubDate>Fri, 2 May 2025 18:33:30 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 개발자의 AI 챗봇 개발기</title>
      <link>https://algoroot.tistory.com/214</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;안녕하세요! 지난 2월 27일,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://algoroot.tistory.com/208&quot;&gt;&amp;ldquo;AI로 뭘 해볼까?&amp;rdquo;&lt;/a&gt;라는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;고민에서 출발해 자기소개 챗봇을 만들어보자! 는 다짐으로 첫 글을 남겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;302&quot; data-start=&quot;161&quot;&gt;서비스는 한 달 후 3월 말에 완성이 되었지만, 글을 이제서야 쓰게 되었습니다. 이 글에서는 그동안 챗봇을 개발하며 겪은 기술적 고민과 구현 과정, 그리고 구조 개선의 흐름까지 정리해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;302&quot; data-start=&quot;161&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-end=&quot;302&quot; data-start=&quot;161&quot; data-ke-style=&quot;style2&quot;&gt;해당 서비스는 GPT-4o-mini 모델의 API 호출 비용 문제로 인해 이름과 주소를 공개하지 않고 있습니다. 혹시 저와 같이 AI Chatbot을 개발해보고 싶은 분들은 이메일(algoroot524@gmail.com)로 보내주시거나 댓글을 남겨주시면, 제가 아는 선에서 최대한 도움을 드리겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;1 3 []&quot;&gt;&lt;span&gt;개발 기간 및 기술 스택&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;개발 기간:&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;약 1개월&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;AI:&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;LangGraph.js, Supabase Vector DB, OpenAI, AI SDK&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;웹 프론트엔드:&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Next.js (App Router), Tailwind CSS, Shadcn UI&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;API 및 백엔드:&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;tRPC, Supabase, Zod&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;서비스 이용 안내 및 모델 정보&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;260&quot; data-start=&quot;83&quot;&gt;이 서비스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GPT-4o-mini 모델&lt;/b&gt;을 기반으로 응답을 생성합니다.&lt;br /&gt;비용 최적화를 고려해 해당 모델을 선택했으며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로그인 없이도 누구나 사용할 수 있도록 설계&lt;/b&gt;했습니다.&lt;br /&gt;주 이용 대상이 채용 담당자님인 만큼, 로그인 절차는 오히려&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서비스 접근성을 떨어뜨리는 요인&lt;/b&gt;이라고 판단했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;308&quot; data-start=&quot;262&quot;&gt;또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;과도한 API 호출을 방지하기 위해&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아래와 같은 제약을 설정했습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;416&quot; data-start=&quot;310&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;356&quot; data-start=&quot;310&quot;&gt;클라이언트의 request header에서 고유 식별자(IP 등)를 추출&lt;/li&gt;
&lt;li data-end=&quot;381&quot; data-start=&quot;357&quot;&gt;해당 값을 기준으로 요청 기록을 저장&lt;/li&gt;
&lt;li data-end=&quot;416&quot; data-start=&quot;382&quot;&gt;&lt;b&gt;IP당 하루 최대 50회&lt;/b&gt;의 대화로 이용 횟수를 제한
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;416&quot; data-start=&quot;382&quot;&gt;IP는&amp;nbsp;User-Agent와&amp;nbsp;함께&amp;nbsp;해싱된&amp;nbsp;값으로&amp;nbsp;저장되며,&amp;nbsp;주&amp;nbsp;사용자층인&amp;nbsp;채용&amp;nbsp;담당자의&amp;nbsp;접근&amp;nbsp;허들을&amp;nbsp;낮추기&amp;nbsp;위해&amp;nbsp;로그인&amp;nbsp;과정을&amp;nbsp;생략한&amp;nbsp;구조이며,&amp;nbsp;과도한&amp;nbsp;사용을&amp;nbsp;방지하고&amp;nbsp;기본적인&amp;nbsp;디버깅&amp;nbsp;및&amp;nbsp;통계&amp;nbsp;수집&amp;nbsp;용도로&amp;nbsp;충분한&amp;nbsp;수준으로&amp;nbsp;설계되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;617&quot; data-start=&quot;566&quot;&gt;&lt;b&gt;최근 대화 흐름 유지를 위해&lt;/b&gt;, 최대 10개의 메시지를 기억하여 문맥을 반영합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;695&quot; data-start=&quot;619&quot;&gt;이러한 설정은 개인 프로젝트이면서도 실제 사용자 경험을 고려해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비용과 접근성, 사용성을 균형 있게 조율&lt;/b&gt;하려는 고민의 결과였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-end=&quot;55&quot; data-start=&quot;43&quot;&gt;핵심 개발 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;90&quot; data-start=&quot;57&quot;&gt;AI 기반 데이터 벡터화 및 검색 최적화 (RAG)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;173&quot; data-start=&quot;91&quot;&gt;포트폴리오(Web Crawling), 이력서(Markdown), 자기소개(JSON) 데이터를 Supabase Vector DB에 벡터화하여 저장&lt;/li&gt;
&lt;li data-end=&quot;260&quot; data-start=&quot;174&quot;&gt;LangGraph의 StateGraph를 활용해&lt;br /&gt;&amp;rarr; 질문 정제&lt;br /&gt;&amp;rarr; 검색 필요 판단&lt;br /&gt;&amp;rarr; 조건 분기&lt;br /&gt;&amp;rarr; 응답 생성 흐름을 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;300&quot; data-start=&quot;262&quot;&gt;지속적인 대화 흐름 유지 (Memory Management)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;334&quot; data-start=&quot;301&quot;&gt;MemorySaver로 채팅 내역 저장 및 문맥 유지&lt;/li&gt;
&lt;li data-end=&quot;382&quot; data-start=&quot;335&quot;&gt;Message Trimmer로 최대 10 Token 유지하여 메모리 사용량 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;407&quot; data-start=&quot;384&quot;&gt;과도한 사용을 방지하기 위한 제한
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;435&quot; data-start=&quot;408&quot;&gt;IP 기준 하루 최대 50회의 대화로 제한&lt;/li&gt;
&lt;li data-end=&quot;573&quot; data-start=&quot;436&quot;&gt;IP는 User-Agent와 함께 해싱된 값으로 저장되며,&lt;br /&gt;주 사용자층인 채용 담당자의 접근 허들을 낮추기 위해 로그인 과정을 생략한 구조이며,&lt;br /&gt;과도한 사용을 방지하고 기본적인 디버깅 및 통계 수집 용도로 충분한 수준으로 설계됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;622&quot; data-start=&quot;575&quot;&gt;실시간 스트리밍 응답 처리 (LangGraph stream &amp;amp; AI SDK)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;669&quot; data-start=&quot;623&quot;&gt;LangGraph와 AI SDK를 활용해 AI 응답을 토큰 단위로 스트리밍 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;688&quot; data-start=&quot;671&quot;&gt;편안한 UI/UX 고려
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;713&quot; data-start=&quot;689&quot;&gt;Chat GPT의 UX 참고해 UX 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;775&quot; data-start=&quot;715&quot;&gt;서버 컴포넌트 환경에서 tRPC&amp;middot;React Query 기반 API Prefetch 및 SSR 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;프런트엔드 개발자가 AI 챗봇을 만들기까지&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;482&quot; data-start=&quot;338&quot;&gt;AI 기술은 그동안 접해보지 못했던 분야였지만, ChatGPT가 등장하고 생성형 AI가 일상이 되면서 &quot;AI를 활용한 개발은 어떻게 이루어질까?&quot;에 대한 궁금증이 생기기 시작했습니다. 당시엔 LangChain이나 RAG 같은 키워드를 간략히 접해본 정도였죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;560&quot; data-start=&quot;484&quot;&gt;이번 프로젝트는 그런 저에게 처음으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AI와 직접 맞닿는 개발 경험&lt;/b&gt;이었고, 아래에 소개할 레퍼런스 문서들이 큰 도움이 되었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-end=&quot;604&quot; data-start=&quot;567&quot;&gt;참고한 문서들 (Reference Documentation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;694&quot; data-start=&quot;606&quot;&gt;AI 챗봇을 처음 개발하며 실제로 참고했던 문서들을 아래에 정리했습니다. 각 문서는 특정 문제를 해결하거나, 구조를 이해하는 데에 실질적인 도움을 주었습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1375&quot; data-start=&quot;696&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;772&quot; data-start=&quot;696&quot;&gt;&lt;b&gt;ChatBot 전체 플로우 이해에 도움 된&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://js.langchain.com/docs/tutorials/chatbot/&quot;&gt;https://js.langchain.com/docs/tutorials/chatbot/&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;868&quot; data-start=&quot;774&quot;&gt;&lt;b&gt;RAG VectorStore 구성&lt;b&gt;에 도움 된&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://js.langchain.com/docs/integrations/vectorstores/supabase/&quot;&gt;https://js.langchain.com/docs/integrations/vectorstores/supabase/&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;985&quot; data-start=&quot;870&quot;&gt;&lt;b&gt;LangGraph + Stream 연동&lt;b&gt;에 도움 된&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://langchain-ai.github.io/langgraphjs/how-tos/stream-tokens/#the-stream-method&quot;&gt;https://langchain-ai.github.io/langgraphjs/how-tos/stream-tokens/#the-stream-method&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1083&quot; data-start=&quot;987&quot;&gt;&lt;b&gt;RSC 환경에서의 Streaming 응답 처리&lt;b&gt;에 도움 된&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://sdk.vercel.ai/docs/reference/ai-sdk-core/stream-text&quot;&gt;https://sdk.vercel.ai/docs/reference/ai-sdk-core/stream-text&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1138&quot; data-start=&quot;1085&quot;&gt;&lt;b&gt;채팅 로그 관리에&amp;nbsp;&lt;b&gt;도움 된&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://smith.langchain.com/&quot;&gt;https://smith.langchain.com/&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1264&quot; data-start=&quot;1140&quot;&gt;&lt;b&gt;tRPC 설정 및 React Query 연결&lt;b&gt;에 도움 된&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://trpc.io/docs/client/tanstack-react-query/setup#3a-setup-the-trpc-context-provider&quot;&gt;https://trpc.io/docs/client/tanstack-react-query/setup#3a-setup-the-trpc-context-provider&lt;/a&gt;&lt;/li&gt;
&lt;li data-end=&quot;1375&quot; data-start=&quot;1266&quot;&gt;&lt;b&gt;Next.js App Router 환경에서의 tRPC 적용&lt;b&gt;에 도움 된&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;a href=&quot;https://trpc.io/docs/client/tanstack-react-query/server-components&quot;&gt;https://trpc.io/docs/client/tanstack-react-query/server-components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span&gt;Chatbot 기능 개선 과정&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터는 Chatbot을 만든 과정을 단계별로 설명합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1단계: MVP로 빠르게 시작하기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는&amp;nbsp;구조를&amp;nbsp;단순하게&amp;nbsp;잡았습니다.&amp;nbsp;Next.js&amp;nbsp;기반&amp;nbsp;모노레포&amp;nbsp;구조를&amp;nbsp;설정하고,&amp;nbsp;Tailwind와&amp;nbsp;Shadcn으로&amp;nbsp;UI&amp;nbsp;틀을&amp;nbsp;빠르게&amp;nbsp;잡은&amp;nbsp;뒤,&amp;nbsp;OpenAI&amp;nbsp;API에&amp;nbsp;사용자의&amp;nbsp;질문을&amp;nbsp;바로&amp;nbsp;전달하고&amp;nbsp;응답을&amp;nbsp;보여주는&amp;nbsp;형태로만&amp;nbsp;동작하도록&amp;nbsp;했습니다.&amp;nbsp;이때는&amp;nbsp;JSON&amp;nbsp;형식으로&amp;nbsp;정리한&amp;nbsp;자기소개&amp;nbsp;데이터를&amp;nbsp;프롬프트에&amp;nbsp;그대로&amp;nbsp;끼워&amp;nbsp;넣는&amp;nbsp;수준이었고,&amp;nbsp;대화형&amp;nbsp;인터페이스보다는&amp;nbsp;질문&amp;nbsp;&amp;rarr;&amp;nbsp;응답&amp;nbsp;형태의&amp;nbsp;단방향&amp;nbsp;흐름에&amp;nbsp;가까웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;OpenAI API에 사용자 질문을 바로 전달하는 단방향 구조&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;JSON 형식 자기소개 데이터를 그대로 프롬프트에 삽입&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;대화 흐름은 없고, 응답은 항상 동일한 패턴으로 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;예: &quot;너 누구야?&quot; &amp;rarr; &quot;답변을 찾을 수 없습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2단계: 벡터 DB + RAG 구조 도입&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다&amp;nbsp;정확하고&amp;nbsp;풍부한&amp;nbsp;문맥&amp;nbsp;기반&amp;nbsp;응답을&amp;nbsp;위해,&amp;nbsp;RAG(Retrieval-Augmented&amp;nbsp;Generation)&amp;nbsp;구조를&amp;nbsp;도입했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;포트폴리오, 이력서(. md), 자기소개(JSON) &amp;rarr; 벡터화&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Supabase Vector DB + LangGraph로 검색&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;검색 결과 기반으로 프롬프트 구성 &amp;rarr; 더 자연스러운 응답&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1745486819854&quot; class=&quot;pf&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const searchVectorStore = async (state: GraphAnnotationState) =&amp;gt; {
	const { refinedQuestion } = state
	const input = refinedQuestion || ''
	const searchResults = await search(input).then(formatSearchResults)
	return {
		...state,
		searchResults,
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export const search = async (
	input: string,
	options?: SearchOptions,
): Promise&amp;lt;SearchResult[]&amp;gt; =&amp;gt; {
	const { count = 3, minScore = 0 } = options ?? {}

	/**
	 * 필요하면 filter 함수로 metadata 넣어 필터링 시킬 수 있음
	 * @see https://js.langchain.com/docs/integrations/vectorstores/supabase/#metadata-query-builder-filtering
	 */
	const results = await vectorStore.similaritySearchWithScore(input, count)

	const answered = results.filter(([doc, score]) =&amp;gt; {
		return score &amp;gt;= minScore &amp;amp;&amp;amp; doc.metadata?.answer
	})

	const unanswered = results.filter(([doc, score]) =&amp;gt; {
		return score &amp;gt;= minScore &amp;amp;&amp;amp; !doc.metadata?.answer
	})

	const bestMatch = [...answered, ...unanswered]

	const data = bestMatch.map(([d, s]) =&amp;gt; {
		return { data: d, similarity: s, answer: d.metadata?.answer || '정보 없음' }
	})

	return data
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3단계: 대화 흐름 유지 &amp;ndash; Memory 관리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자와의 대화들을 AI가 기억하는 것처럼 보이려면, 채팅 히스토리를 같이 invoke 시켜야 했습니다. 이를 위해 LangGraph의 Memory 시스템을 도입해 다음과 같은 흐름으로 개선했습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;MemorySaver&lt;/span&gt;&lt;span&gt;로 이전 메시지 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;MessageTrimmer&lt;/span&gt;&lt;span&gt;로 오래된 메시지 제거 (최대 10 토큰 유지)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;refineQuestion&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;node를&amp;nbsp;추가해&amp;nbsp;사용자와의&amp;nbsp;대화&amp;nbsp;내역을&amp;nbsp;기반으로&amp;nbsp;한&amp;nbsp;정제된&amp;nbsp;질문으로&amp;nbsp;검색&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;refineQuestion&amp;nbsp;과정은 아래 섹션의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;LangGraph를 통한 챗봇 흐름 최적화 과정&lt;/b&gt;에서 더 자세히 보실 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 아래 함수를 사용하여 prompt에는 maxTokens만큼 잘라진 메세지가 보내지게 됩니다.
export const getTrimMessages = async (messages: BaseMessage[]) =&amp;gt; {
	const trimer = trimMessages({
		maxTokens: 10,
		strategy: 'last',
		tokenCounter: (msgs) =&amp;gt; msgs.length,
		includeSystem: true,
		allowPartial: false,
		startOn: 'human',
	})

	const res = await trimer.invoke(messages)
	return res
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;AI&amp;nbsp;응답&amp;nbsp;흐름&amp;nbsp;시각화&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아래 이미지는 사용자의 채팅 내용을 바탕으로, AI가 관련 정보를 검색해 적절한 답변을 생성한 예시입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4단계: 실시간 스트리밍 응답 처리 &amp;ndash;&amp;nbsp;대기&amp;nbsp;시간&amp;nbsp;최소화&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존&amp;nbsp;방식은&amp;nbsp;사용자가&amp;nbsp;질문을&amp;nbsp;입력하면&amp;nbsp;AI가&amp;nbsp;전체&amp;nbsp;답변을&amp;nbsp;모두&amp;nbsp;생성한&amp;nbsp;뒤&amp;nbsp;한&amp;nbsp;번에&amp;nbsp;응답을&amp;nbsp;반환하는&amp;nbsp;구조였기&amp;nbsp;때문에,&amp;nbsp;실제&amp;nbsp;답변을&amp;nbsp;보기까지&amp;nbsp;약간의&amp;nbsp;체감&amp;nbsp;지연이&amp;nbsp;있었습니다.&amp;nbsp;이를&amp;nbsp;개선하기&amp;nbsp;위해,&amp;nbsp;AI&amp;nbsp;SDK의&amp;nbsp;스트리밍&amp;nbsp;기능을&amp;nbsp;도입했고&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;흐름으로&amp;nbsp;구성했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;LangGraph의 stream 모드 + AI SDK를 조합하여, 마치 ChatGPT처럼 &amp;lsquo;타자 치듯&amp;rsquo; 응답&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;체감 대기 시간 60% 단축 (5초 &amp;rarr; 2초)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;export async function chat(history: Message[], userIp: string) {
	const stream = createStreamableValue('')
    // ... inputData 만드는 과정 생략
	;(async () =&amp;gt; {
		const messageStream = await app.stream(inputData, {
			configurable: { thread_id: options.thread_id },
			streamMode: 'messages',
			tags: ['user-chat', `ip:${options.userIp}`],
			callbacks: [
				{
					handleLLMEnd(output, runId, parentRunId, tags) {
						console.log('handleLLMEnd', { output, runId, parentRunId, tags })
					},
					handleLLMError(err, runId) {
						console.log('handleLLMError', { err, runId })
						stream.done()
					},
				},
			],
		})

		for await (const content of messageStream) {
			const [chunk, info] = content
			if (info.langgraph_node === NODES.GENERATE_RESPONSE) {
				stream.update(chunk.content)
			}
		}
		stream.done()
	})()

	return { history, newMessage: stream.value }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot; data-end=&quot;136&quot; data-start=&quot;96&quot;&gt;배포 이후 LangSmith를 통해 발견한 실사용 이슈와 개선 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;290&quot; data-start=&quot;138&quot;&gt;서비스를 배포하고 한 기업에 지원서를 제출한 직후, 아쉽게도 서류 탈락을 경험했습니다.&lt;br /&gt;그러나 감사하게도 해당 회사에서 제 챗봇 서비스를 실제로 사용해 본 흔적이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;LangSmith 로그를 통해 확인&lt;/b&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;&lt;span&gt;1. 응답 끊김 이슈&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&gt;로컬환경에서는 응답이 끊기지는 않지만, 버셀로 배포한 환경에서 간헐적으로 응답이 생성되지 않는 이슈였습니다.&amp;nbsp;&lt;/span&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;&lt;span&gt;2. 생각보다 사용자는 단순하게 질문을 한다.&amp;nbsp;&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&gt;예 : 서버도 하나요?&lt;/span&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;1-2. 응답 끊김 이슈 해결 : maxDuration 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배포 환경에서 테스트를 진행하던 중, 응답이 간헐적으로 누락되는 것이 아니라, 요청 후 정확히 10초가 지난 시점에 실패하는 패턴을 발견했습니다. 이 현상은 로컬 개발 환경에서는 재현되지 않고 Vercel 배포 환경에서만 발생했기 때문에, Vercel Functions와 관련된 제한 사항일 가능성이 높다고 판단했습니다.&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 data-ke-size=&quot;size16&quot; data-end=&quot;454&quot; data-start=&quot;287&quot;&gt;이에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://vercel.com/docs/functions/limitations#max-duration&quot;&gt;Vercel 공식 문서&lt;/a&gt;를 확인한 결과, Hobby 플랜에서 제공되는 Vercel Functions는 기본적으로 maxDuration이 10초로 설정되어 있으며, 이 시간을 초과하면 504 FUNCTION_INVOCATION_TIMEOUT 오류가 발생한다는 점을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;454&quot; data-start=&quot;287&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;603&quot; data-start=&quot;456&quot;&gt;제가 구현한 chat 함수는 서버 액션(Server Actions)을 통해 호출되며, 이는 Next.js App Router 구조상 내부적으로 Vercel의 Node.js Function으로 처리되기 때문에 해당 제한의 영향을 그대로 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;603&quot; data-start=&quot;456&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;603&quot; data-start=&quot;456&quot;&gt;문제 해결을 위해 maxDuration을 요청하는 /chat페이지에 추가하여 이슈를 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1745486819860&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * vercel에서 최대 함수 지속시간 설정
 * @see https://sdk.vercel.ai/docs/troubleshooting/timeout-on-vercel
 */
export const maxDuration = 60

export default async function ChatPage() {
	await prefetch(trpc.getSuggestQuestions.queryOptions())

	return (
		&amp;lt;ChatInterface&amp;gt;
			&amp;lt;HydrateClient&amp;gt;
				&amp;lt;ChatQuestionSheet /&amp;gt;
			&amp;lt;/HydrateClient&amp;gt;
		&amp;lt;/ChatInterface&amp;gt;
	)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;1-2. 응답 끊김 이슈 해결 : 응답&amp;nbsp;실패&amp;nbsp;시&amp;nbsp;&quot;다시&amp;nbsp;시도하기&quot;&amp;nbsp;버튼&amp;nbsp;추가&lt;/span&gt;&lt;/h4&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;span&gt;이와 더불어, 혹시 또 응답이 생성되지 않아 에러처리가 될 경우&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아래와 같이 &quot;다시 시도하기&quot; UI를 추가했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745486819861&quot; class=&quot;xml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Show when={type === 'error'}&amp;gt;
	&amp;lt;div className=&quot;bg-muted prose-p:m-0 flex flex-col items-center justify-center gap-2 p-0 px-2 py-2 md:flex-row md:px-2&quot;&amp;gt;
		&amp;lt;p className=&quot;text-muted-foreground text-sm&quot;&amp;gt;
			답변에 문제가 있습니다.
		&amp;lt;/p&amp;gt;
		&amp;lt;Button
			variant=&quot;outline&quot;
			size=&quot;sm&quot;
			className=&quot;w-full md:w-fit&quot;
			onClick={onRetry}
		&amp;gt;
			다시 시도하기
		&amp;lt;/Button&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/Show&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. 사용자는 생각보다 단순하게 질문한다:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;LangGraph로 흐름 최적화하기&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 구조에서는 LangGraph를 도입하긴 했지만, 단일 노드만으로 구성된 매우 단순한 상태(StateGraph)였습니다.&lt;br /&gt;서비스 특성상 외부 검색이나 복잡한 분기 처리가 필요하지 않다고 판단했기 때문에, 추가적인 노드 구성 없이 배포를 진행했습니다. 당시에는 어디에 어떤 노드를 추가해야 할지도 명확하지 않았고, 실제 사용자 사용 패턴을 보기 전까지는 큰 필요성을 느끼지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;436&quot; data-start=&quot;342&quot;&gt;하지만 LangSmith 로그를 통해 실사용자의 질문을 확인해 보니, 제가 사전 테스트에서 사용한 복잡한 질문들과 달리 매우 짧고 단순한 질문들이 대부분이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;436&quot; data-start=&quot;342&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;444&quot; data-start=&quot;438&quot;&gt;예를 들어:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;484&quot; data-start=&quot;446&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;458&quot; data-start=&quot;446&quot;&gt;&quot;서버도 하나요?&quot;&lt;/li&gt;
&lt;li data-end=&quot;484&quot; data-start=&quot;471&quot;&gt;&quot;어디서 일했어요?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;711&quot; data-start=&quot;545&quot;&gt;이처럼 명확한 맥락 없이 질문하는 경우, 기존 구조에서는 LLM이 정확한 응답을 생성하기 어려웠습니다. 당시에는 MemorySaver를 통해 직전 메시지 10개를 유지하는 방식이 문맥 전달에 도움이 될 것이라 생각했지만, LangGraph의 실제 흐름을 되짚어보니, 사용자의 질문은 먼저 벡터 DB 검색에 사용된 후, 그 결과와 함께 히스토리가 LLM에 전달되는 구조였기 때문에,&lt;br /&gt;검색 단계에서는 히스토리가 전혀 반영되지 않고 있는 상태라는 점을 뒤늦게 인지하게 되었습니다. 결국, LLM에 문맥을 전달하는 방식은 검색 이후에야 작동하고 있었고, 질문이 짧거나 모호할 경우 검색 자체가 부정확해지는 구조적 한계가 있었던 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;711&quot; data-start=&quot;545&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-end=&quot;747&quot; data-start=&quot;718&quot;&gt;흐름 개선의 방향: 질문 정제 노드 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;785&quot; data-start=&quot;749&quot;&gt;이 경험을 통해, 저는 다음과 같은 문제 인식을 하게 되었습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1028&quot; data-start=&quot;856&quot;&gt;&quot;사용자의&amp;nbsp;질문은&amp;nbsp;충분히&amp;nbsp;구체적이지&amp;nbsp;않기&amp;nbsp;때문에,&amp;nbsp;질문을&amp;nbsp;정제하거나&amp;nbsp;보완하는&amp;nbsp;전처리&amp;nbsp;단계가&amp;nbsp;반드시&amp;nbsp;필요하다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1028&quot; data-start=&quot;856&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1028&quot; data-start=&quot;856&quot;&gt;이에 따라, LangGraph의 StateGraph 구조에서 질문 정제용 노드를 새롭게 추가하기로 결정했습니다.&lt;br /&gt;이 노드는 메시지 히스토리를 참고하여 현재 입력된 질문을 더 명확하고 LLM이 이해하기 쉬운 형태로 변환하며,&lt;br /&gt;다음 단계로 전달되는 전반적인 응답 품질을 높이기 위한 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1028&quot; data-start=&quot;856&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1028&quot; data-start=&quot;856&quot;&gt;아래에서는 제가 어떤 기준으로 노드를 설계했고, 실제로 어떻게 LLM 응답 품질이 개선되었는지를 구체적으로 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;기존 문제&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;기존 흐름 사용자&amp;nbsp;질문&amp;nbsp;&amp;rarr;&amp;nbsp;벡터&amp;nbsp;DB&amp;nbsp;검색&amp;nbsp;&amp;rarr;&amp;nbsp;메세지&amp;nbsp;히스토리&amp;nbsp;추가&amp;nbsp;&amp;rarr;&amp;nbsp;LLM&amp;nbsp;호출&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;백터 DB 검색이 사용자의 질문 원문에 그대로 의존함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;모든 질문에 대해 벡터 검색 수행 &amp;rarr; 불필요한 비용 발생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;StateGraph의 조건 분기 기능을 활용하지 못함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;개선 방향: 검색&amp;nbsp;흐름을&amp;nbsp;조건&amp;nbsp;분기로&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에&amp;nbsp;따라&amp;nbsp;아래와&amp;nbsp;같은&amp;nbsp;흐름으로&amp;nbsp;검색&amp;nbsp;여부를&amp;nbsp;판단하고,&amp;nbsp;질문이&amp;nbsp;애매한&amp;nbsp;경우에는&amp;nbsp;먼저&amp;nbsp;질문을&amp;nbsp;정제하는&amp;nbsp;노드를&amp;nbsp;추가하는&amp;nbsp;방식으로&amp;nbsp;개선했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;refineQuestion&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;shouldSearch&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;노드 도입&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;검색 필요 여부 판단 &amp;rarr; 필요할 때만 검색&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;노드&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;refineQuestion&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;사용자의&amp;nbsp;마지막&amp;nbsp;입력이&amp;nbsp;짧거나&amp;nbsp;모호할&amp;nbsp;경우,&amp;nbsp;대화&amp;nbsp;흐름을&amp;nbsp;기반으로&amp;nbsp;검색에&amp;nbsp;적합한&amp;nbsp;질문으로&amp;nbsp;정제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shouldSearch&lt;/td&gt;
&lt;td&gt;정제된&amp;nbsp;질문과&amp;nbsp;대화&amp;nbsp;내역을&amp;nbsp;기반으로&amp;nbsp;벡터&amp;nbsp;검색이&amp;nbsp;필요한지&amp;nbsp;판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;searchVectorStore&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;Supabase&amp;nbsp;기반의&amp;nbsp;벡터&amp;nbsp;DB에서&amp;nbsp;관련&amp;nbsp;내용을&amp;nbsp;검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;generateResponse&lt;/td&gt;
&lt;td&gt;검색&amp;nbsp;결과&amp;nbsp;및&amp;nbsp;대화&amp;nbsp;맥락을&amp;nbsp;바탕으로&amp;nbsp;최종&amp;nbsp;응답&amp;nbsp;생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;__start__
   &amp;darr;
refineQuestion
   &amp;darr;
shouldSearch
   &amp;darr;
  ┌───────────────┐
  │ needSearch ?  │
  └──────┬────────┘
         &amp;darr;
      true &amp;rarr; searchVectorStore &amp;rarr; generateResponse
      false ───────────────────&amp;rarr; generateResponse&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;덕분에&amp;nbsp;응답 정확성과&amp;nbsp;비용 효율성을 동시에 개선할 수 있었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;UI/UX 개선 및 인터페이스 고도화&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;제 서비스는 초기 단계에서 Tailwind와 Shadcn 기반의 모노레포 설정을 완료하고, AI 모델을 활용한 간단한 채팅 기능만 우선 퍼블리싱한 상태로 출발했습니다. 이후 챗봇의 기본 기능을 모두 구현한 뒤, 사용성과 디자인을 중심으로 전체적인 UI/UX를 개선해 나가며, 서비스의 완성도를 높이는 데 집중했습니다. 아래는 그 과정에서 중점적으로 개선한 요소들과 고민했던 지점들입니다.&lt;/span&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;채팅 서비스는 Vercel의 v0와 OpenAI의 ChatGPT를 참고했습니다. 두 UI와 UX를 비교 후 ChatGPT가 더 유저 접근성이 좋다고 판단되어 ChatGPT의 구조를 보면서 서비스를 개선했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;최신 메시지 상단 고정&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가&amp;nbsp;질문을&amp;nbsp;입력할&amp;nbsp;때,&amp;nbsp;Vercel의&amp;nbsp;v0는&amp;nbsp;단순히&amp;nbsp;메시지를&amp;nbsp;입력&amp;nbsp;순서대로&amp;nbsp;쌓는&amp;nbsp;구조를&amp;nbsp;사용합니다.&amp;nbsp;반면&amp;nbsp;ChatGPT는&amp;nbsp;사용자의&amp;nbsp;최신&amp;nbsp;질문을&amp;nbsp;스크린의&amp;nbsp;상단에&amp;nbsp;위치시킴으로써,&amp;nbsp;질문과&amp;nbsp;그에&amp;nbsp;대한&amp;nbsp;응답을&amp;nbsp;한눈에&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;설계되어&amp;nbsp;있습니다.&amp;nbsp;이러한&amp;nbsp;구조는&amp;nbsp;특히&amp;nbsp;스크린이&amp;nbsp;작은&amp;nbsp;모바일&amp;nbsp;환경에서&amp;nbsp;더욱&amp;nbsp;유용하게&amp;nbsp;작용하며,&amp;nbsp;사용자가&amp;nbsp;자신의&amp;nbsp;질문과&amp;nbsp;이에&amp;nbsp;대한&amp;nbsp;응답을&amp;nbsp;한&amp;nbsp;화면에서&amp;nbsp;동시에&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;되어&amp;nbsp;전반적인&amp;nbsp;사용자&amp;nbsp;경험이&amp;nbsp;향상된다고&amp;nbsp;느꼈습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;ChatGPT UX를 참고하여 최신 메시지를 상단에 배치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;모바일에서도 사용자가 질문-응답을 한 화면에서 확인 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;스크롤 동작 세분화&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 동작을 서비스에 적용하기 위해 사용자의 가장 최신 메시지 Element에 scrollIntoView를 사용하여 사용자가 질문을 할 때 해당 메서드를 실행시켰습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;채팅 중엔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;scrollIntoView&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 부드러운 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Home &amp;rarr; Chat 이동 시엔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;behavior: 'instant'&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;메세지&amp;nbsp;height&amp;nbsp;조정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때&amp;nbsp;모든&amp;nbsp;메시지의&amp;nbsp;Element의&amp;nbsp;Height&amp;nbsp;이&amp;nbsp;auto로&amp;nbsp;되어있어&amp;nbsp;스크롤&amp;nbsp;이동을&amp;nbsp;하더라도&amp;nbsp;사용자의&amp;nbsp;메시지가&amp;nbsp;최상단에&amp;nbsp;올라오지는&amp;nbsp;않습니다.&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;span&gt;ChatGPT의&amp;nbsp;적용&amp;nbsp;방식을&amp;nbsp;보니&amp;nbsp;최신&amp;nbsp;AI&amp;nbsp;답변의&amp;nbsp;min-height를&amp;nbsp;조정하여&amp;nbsp;최신&amp;nbsp;사용자&amp;nbsp;질문&amp;nbsp;Element가&amp;nbsp;스크린의&amp;nbsp;상단에&amp;nbsp;오도록&amp;nbsp;조정하고&amp;nbsp;있었습니다.&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT의&amp;nbsp;방식처럼,&amp;nbsp;최신&amp;nbsp;AI&amp;nbsp;응답&amp;nbsp;메시지의&amp;nbsp;min-height를&amp;nbsp;조절하여&amp;nbsp;사용자의&amp;nbsp;질문&amp;nbsp;메시지가&amp;nbsp;항상&amp;nbsp;상단에&amp;nbsp;오도록&amp;nbsp;처리하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;사용자&amp;nbsp;접근성을&amp;nbsp;고려한&amp;nbsp;질문&amp;nbsp;리스트&amp;nbsp;UI&amp;nbsp;제안&amp;nbsp;기능&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;&quot;과연 사용자들이 저에게 하고 싶은 질문이 바로 떠오를까?&quot; 라는 고민이 생겼습니다.&amp;nbsp;&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;결론은&amp;nbsp;그렇지&amp;nbsp;않다는&amp;nbsp;것이었고,&amp;nbsp;이로&amp;nbsp;인해&amp;nbsp;질문&amp;nbsp;리스트를&amp;nbsp;제안하는&amp;nbsp;UI를&amp;nbsp;추가하게&amp;nbsp;되었습니다.&amp;nbsp;해당&amp;nbsp;기능은&amp;nbsp;사용자&amp;nbsp;경험&amp;nbsp;흐름에&amp;nbsp;맞춰&amp;nbsp;다르게&amp;nbsp;노출되도록&amp;nbsp;구성했습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈&amp;nbsp;화면에서는&amp;nbsp;사용자가&amp;nbsp;서비스를&amp;nbsp;처음&amp;nbsp;접했을&amp;nbsp;때&amp;nbsp;자연스럽게&amp;nbsp;질문을&amp;nbsp;유도할&amp;nbsp;수&amp;nbsp;있도록,&amp;nbsp;리스트를&amp;nbsp;바로&amp;nbsp;노출하고,&amp;nbsp;&lt;/li&gt;
&lt;li&gt;채팅 화면에서는 대화 흐름을 방해하지 않도록, 아이콘 버튼을 통해 액션시트 형태로 리스트를 확인할 수 있게 했습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한&amp;nbsp;구조를&amp;nbsp;통해&amp;nbsp;사용자는&amp;nbsp;막연한&amp;nbsp;상태에서도&amp;nbsp;클릭&amp;nbsp;한&amp;nbsp;번으로&amp;nbsp;대화를&amp;nbsp;시작할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;구현했습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;API 설계 - tRPC + Zod&lt;/span&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;왜 tRPC를 선택했나요?&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 작업을 했을 때 저는 백엔드에서 데이터를 직접 가공하진 않았지만 저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;프런트엔드 주도의 BFF(Backend for Frontend)스러운 개발 방식을 추구해 왔다고 생각합니다. 복잡한 백엔드 데이터를 클라이언트에서 한 번 가공하는 유틸함수를 만들었던 것처럼요.&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;div style=&quot;color: #bdc0c6;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;굳이 요청/응답 스키마를 따로 정의하지 않아도 된다면?&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div style=&quot;color: #bdc0c6;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버와 클라이언트가&amp;nbsp;&lt;b&gt;동일한 타입을 공유&lt;/b&gt;한다면?&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div style=&quot;color: #bdc0c6;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;프런트엔드 코드 기반으로 백엔드 서비스까지 빠르게 구축&lt;/b&gt;할 수 있다면?&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 고민 끝에 tRPC를 집중적으로 살펴보았고, 특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #346ddb;&quot; href=&quot;https://trpc.io/blog/introducing-tanstack-react-query-client&quot; data-state=&quot;closed&quot;&gt;&lt;b&gt;2025년에 v11로 업그레이드되면서 React Query 통합&lt;/b&gt;&lt;/a&gt;이 공식적으로 정비되었다는 점이 눈에 띄었습니다. 기존에 React Query를 사용하던 저에게도&lt;span&gt;&amp;nbsp;&lt;/span&gt;거의 학습 곡선 없이 바로 적용 가능한 구조였고, 실제로 공식 문서에서 queryOptions, useSuspenseQuery 등을 활용한 예시가 명확하게 제공되어 있어 도입을 결정하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;325&quot; data-start=&quot;162&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-end=&quot;353&quot; data-start=&quot;332&quot;&gt;1. tRPC와 Zod의 결합&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RPC와 Zod는 이번 프로젝트에서 타입 안전성과 유연성을 동시에 확보하기 위해 선택한 핵심 도구였습니다. 서버와 클라이언트가 모두 TypeScript로 작성되는 환경에서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;타입 중복 없이 런타임 유효성 검증과 정적 타입 추론을 동시에 만족시킬 수 있다는 점이 특히 매력적이었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;609&quot; data-start=&quot;427&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;491&quot; data-start=&quot;427&quot;&gt;&lt;b&gt;런타임 유효성 검사:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;실제 API 요청에서 유효하지 않은 값이 들어오는 경우, 서버에서 사전에 방어 가능&lt;/li&gt;
&lt;li data-end=&quot;546&quot; data-start=&quot;492&quot;&gt;&lt;b&gt;자동 타입 추론:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클라이언트에서 쿼리를 작성할 때 자동으로 타입이 추론되어 생산성 향상&lt;/li&gt;
&lt;li data-end=&quot;609&quot; data-start=&quot;547&quot;&gt;&lt;b&gt;서버-클라이언트 간 타입 동기화:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;별도의 DTO나 타입 정의 없이, 한 번의 정의로 타입 일관성 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1745486819869&quot; class=&quot;cs&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	/**
	 * @mutation ip count 증가 (max limit 5)
	 * @param ip: 조회 할 ip
	 * @optional day: 조회할 날짜 (선택값) default 요청 시점 한국 날짜
	 */
	addIpCount: procedures.public
		.input(z.object({ ip: z.string(), day: z.string().optional() }))
		.mutation(async ({ input }) =&amp;gt; {
			const { ip, day = getKSTDay() } = input

			const { data } = await supabaseClient
				.from('ip_question_count')
				.select('count')
				.eq('ip', ip)
				.eq('date', day)
				.maybeSingle()

			const curCount = data?.count || 0
			if (curCount &amp;gt;= DAILY_LIMIT &amp;amp;&amp;amp; ip !== ME) {
				throw new TRPCError({
					code: 'BAD_REQUEST',
					message: `하루 최대 ${DAILY_LIMIT}개의 질문만 가능합니다.`,
				})
			}

			const updatedCount = curCount + 1

			await supabaseClient
				.from('ip_question_count')
				.upsert(
					{ ip, date: day, count: updatedCount },
					{ onConflict: 'ip,date' },
				)
			return { count: updatedCount }
		}),&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-end=&quot;1044&quot; data-start=&quot;1019&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-end=&quot;1044&quot; data-start=&quot;1019&quot;&gt;2. Supabase 타입 자동 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1158&quot; data-start=&quot;1046&quot;&gt;Supabase 쿼리 결과의 타입을 신뢰성 있게 사용하기 위해, CLI 기반으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB 스키마를 TypeScript 타입으로 자동 생성하였습니다. 이&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입은 tRPC 내부에서 Supabase 쿼리 응답에 대한 보장으로 활용되며, 추후 리팩토링이나 타입 확장 시에도 안정적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1158&quot; data-start=&quot;1046&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot; data-end=&quot;1363&quot; data-start=&quot;1313&quot;&gt;3. React Query + Server Component에서의 prefetch&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1475&quot; data-start=&quot;1365&quot;&gt;tRPC v11에서는 공식적으로 React Query와의 연동이 강화되어, queryOptions() 메서드를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;클라이언트/서버 어디서든 동일한 방식으로 API를 사용할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745486819870&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서버 컴포넌트에서 prefetch
await prefetch(trpc.getSuggestQuestions.queryOptions());

// 클라이언트에서는 Suspense 기반으로 사용
const { data } = useSuspenseQuery(trpc.getSuggestQuestions.queryOptions());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;1801&quot; data-start=&quot;1673&quot;&gt;&lt;br /&gt;&lt;b&gt;SSR과 CSR 간의 데이터 흐름을 타입 안전하게 일관된 방식으로 처리할 수 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;회고&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;2088&quot; data-start=&quot;1926&quot;&gt;&amp;ldquo;AI로 뭘 해볼까?&amp;rdquo;라는 막연한 물음에서 시작된 프로젝트였습니다. 처음에는 AI가 너무 빠르게 발전하는 걸 보며, 프론트엔드 개발자의 역할도 언젠가는 대체될 수 있겠다는 두려움이 들었고, 그래서 오히려 직접 부딪혀보자는 마음으로 시작했습니다. 단순히 기술을 소비하는 입장이 아니라, 어떻게 서비스 안에 AI를 자연스럽게 녹일 수 있을지를 고민해보고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;2088&quot; data-start=&quot;1926&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;479&quot; data-start=&quot;276&quot;&gt;혼자서 프론트부터 백엔드, 흐름 설계와 UI까지 모두 책임지며 진행한 프로젝트였기에, 처음엔 기능 구현 위주로 빠르게 MVP를 만들었지만, 시간이 갈수록 더 많이 고민한 건 사용자 경험이었습니다. 모바일 환경에서 스크롤 위치나 메시지 정렬 같은 디테일을 조정하는 데 더 많은 시간을 썼고, 그 과정에서 예전 실무에서 겪었던 문제들이 떠오르며 큰 도움이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;479&quot; data-start=&quot;276&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;621&quot; data-start=&quot;481&quot;&gt;AI 기술을 도입하는 것도 흥미로웠지만, 결국 가장 느낀 건 &amp;lsquo;기술은 사람을 향해야 한다&amp;rsquo;는 사실이었습니다. 사용자 흐름에 맞게 질문을 보완하고, 응답 속도와 대화 흐름을 개선하는 일들이 단순한 기술 적용보다 훨씬 중요한 문제라는 걸 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;621&quot; data-start=&quot;481&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-end=&quot;785&quot; data-start=&quot;623&quot;&gt;Chatbot 서비스를 개발하는 건 저에게도 처음이라 아직 부족한 점도 많고, 고민도 끊이지 않습니다. 당장 Chatbot 서비스 하나에만 몰두할 수 없는 상황이라 지금은 완벽하게 만들 수 없더라도, 사용자 로그를 기반으로 개선했던 지난 날 처럼, 앞으로도 실제 사용 흐름을 살펴보며 조금씩 더 나은 방향으로 다듬어갈 수 있다고 생각합니다. 지금 할 수 있는 만큼 천천히, 하지만 꾸준히 좋은 서비스를 만들어가고 싶습니다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>ai sdk</category>
      <category>Chatbot</category>
      <category>langchain</category>
      <category>stream chat</category>
      <category>챗봇</category>
      <category>챗봇개발</category>
      <category>프론트엔드 챗봇</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/214</guid>
      <comments>https://algoroot.tistory.com/214#entry214comment</comments>
      <pubDate>Thu, 24 Apr 2025 18:27:52 +0900</pubDate>
    </item>
    <item>
      <title>AI로 뭘 해볼까?</title>
      <link>https://algoroot.tistory.com/208</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트를 해보고 싶어 모노레포는 미리 구축해두었습니다. 꼭 모노레포일 필요는 없었지만, 몇 개의 서비스를 만들더라도 UI와 컴포넌트는 통일감 있게 유지하고 싶었고, 여러 프로젝트를 효율적으로 관리할 수 있을 거라 생각해 자연스럽게 그런 구조로 출발했습니다.&lt;/p&gt;
&lt;p data-end=&quot;610&quot; data-start=&quot;426&quot; data-ke-size=&quot;size16&quot;&gt;하지만 막상 무얼 만들지에 대해서는 쉽게 결정하지 못했습니다. 처음부터 AI를 염두에 두고 있었던 건 아니었고, 오히려 개인 패키지나 문서 자동화처럼 이미 손에 익은 것들부터 다뤄보고 싶다는 생각이 더 컸습니다. docs 구조도 어느 정도 정리되어 있었고, 웹 프로젝트도 하나쯤 만들어보고 싶었지만, 딱 꽂히는 주제가 없었습니다.&lt;/p&gt;
&lt;p data-end=&quot;610&quot; data-start=&quot;426&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;859&quot; data-start=&quot;612&quot; data-ke-size=&quot;size16&quot;&gt;그러다 문득 예전에 만들었던 포트폴리오를 열어보게 되었습니다. 당시에는 회사에 재직 중이었고 시간 여유가 없어서 GitBook을 활용해 빠르게 정리했었는데요. 지금 보니 업데이트할 내용도 많았고, 단순히 에디터에 텍스트를 채워 넣는 일 자체가 생각보다 지루하게 느껴졌습니다. 정적인 문서만으로는 저를 충분히 보여주기 어렵다는 생각도 들었습니다. 그래서 &amp;lsquo;조금 더 재미있는 방식으로 나를 소개할 수는 없을까?&amp;rsquo;라는 방향으로 관심이 옮겨갔습니다.&lt;/p&gt;
&lt;p data-end=&quot;859&quot; data-start=&quot;612&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1086&quot; data-start=&quot;861&quot; data-ke-size=&quot;size16&quot;&gt;퇴사 후 이력서를 본격적으로 제출해보진 않았지만, 어느 순간부터는 자연스럽게 &amp;lsquo;이걸 보는 사람&amp;rsquo;의 입장에서 생각하게 되었습니다. 요즘처럼 채용 시장이 치열한 시기엔 공고 하나에 수십, 수백 개의 이력서가 몰릴 텐데, 그 속에서 한 명의 지원자로서 어떻게 기억될 수 있을지 고민이 되었습니다. 단순히 정리된 이력보다, 조금 더 유쾌하고, 조금 더 사람 냄새 나는 방식이 있다면 좋겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-end=&quot;1086&quot; data-start=&quot;861&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1366&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 떠오른 아이디어가 바로, &lt;b&gt;면접처럼 대화하듯 저를 소개하는 경험&lt;/b&gt;이었습니다. 누군가 저에 대해 궁금한 점을 자유롭게 물어볼 수 있고, 거기에 맞는 이야기로 응답하는 형태라면 정적인 문서보다 훨씬 더 입체적으로 다가갈 수 있을 것 같았습니다.&lt;/p&gt;
&lt;p data-end=&quot;1366&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1366&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;이왕이면, 그 대화를 &lt;b&gt;AI가 자연스럽게 이어갈 수 있다면&lt;/b&gt; 어떨까? 이력서를 넘기는 대신, &amp;lsquo;나&amp;rsquo;라는 사람과 직접 대화하는 듯한 경험을 제공할 수 있지 않을까? 라는 생각에서 비로소 개인 프로젝트에 대한 답이 생겼습니다.&lt;/p&gt;
&lt;p data-end=&quot;1366&quot; data-start=&quot;1088&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1582&quot; data-start=&quot;1368&quot; data-ke-size=&quot;size16&quot;&gt;그래서 만들어보기로 했습니다. 단순한 이력서 한 장이 아니라, 저를 소개하는 이야기를 대화 형식으로 풀어내는 자기소개 챗봇입니다. 보는 분이 궁금한 점을 직접 묻고, 그에 맞는 맞춤형 응답을 통해 저에 대해 조금 더 흥미롭게 알아가실 수 있기를 기대하며 만들고 있습니다. 아직 이름은 정하지 않았습니다. 일단 만들어보고, 어울리는 이름이 떠오르면 자연스럽게 붙이면 된다고 생각합니다.&lt;/p&gt;
&lt;p data-end=&quot;1582&quot; data-start=&quot;1368&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1714&quot; data-start=&quot;1584&quot; data-ke-size=&quot;size16&quot;&gt;지금은 AI에 대한 경험도 지식도 없고, 어디까지 확장될 수 있을지도 모르지만, 일단 만들어보고 나서야 얻을 수 있는 인사이트가 있다고 믿습니다.&lt;/p&gt;
&lt;p data-end=&quot;1714&quot; data-start=&quot;1584&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;조만간 더 구체적이고 완성도 있는 형태로 소개드릴 수 있기를 기대합니다. 감사합니다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>AI</category>
      <category>Chatbot</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/208</guid>
      <comments>https://algoroot.tistory.com/208#entry208comment</comments>
      <pubDate>Thu, 27 Feb 2025 14:20:43 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 개발자 3년차 퇴사 회고록 &amp;amp; 퇴사 후 한 버킷리스트</title>
      <link>https://algoroot.tistory.com/204</link>
      <description>&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 지난 2년 6개월 동안 똑똑한개발자라는 외주 개발사에서 프론트엔드 개발자로 근무했습니다. 처음에는 외주 프로젝트를 담당하며 다양한 프로젝트의 프론트엔드 개발을 수행했고, 이후에는 사내 시스템 팀에 합류하여 약 20명의 프론트엔드 개발자들의 작업 효율성을 높이는 &lt;b&gt;사내 시스템 툴과 보일러플레이트를 개발&amp;middot;개선하는 일&lt;/b&gt;을 했습니다.&amp;nbsp;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;퇴사를 결심한 이유&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑똑한개발자는 신입이었던 저에게 좋은 기회를 주었고, 훌륭한 동료들과 협업하며 값진 경험을 쌓을 수 있었던 고마운 회사입니다. 그렇다면, 저는 왜 퇴사를 결심했을까요?&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;시스템 팀은 프론트엔드 리더 한 분과 저, 두 명으로 구성된 팀이었습니다. 원래 리더분께서 혼자 담당하시던 부분을 제가 합류하면서 확장할 기회가 생겼고,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;214&quot; data-start=&quot;127&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;146&quot; data-start=&quot;127&quot;&gt;보일러플레이트 모노레포 전환&lt;/li&gt;
&lt;li data-end=&quot;157&quot; data-start=&quot;147&quot;&gt;패키지 분리&lt;/li&gt;
&lt;li data-end=&quot;170&quot; data-start=&quot;158&quot;&gt;CI/CD 구축&lt;/li&gt;
&lt;li data-end=&quot;184&quot; data-start=&quot;171&quot;&gt;GitHub 연동&lt;/li&gt;
&lt;li data-end=&quot;203&quot; data-start=&quot;185&quot;&gt;피그마 토큰 플러그인 개발&lt;/li&gt;
&lt;li data-end=&quot;214&quot; data-start=&quot;204&quot;&gt;문서화 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;등을 진행하며 내부 개발 환경을 개선했습니다. 초반 상반기에는 회사에서도 시스템 팀의 방향성을 존중해 주어 자유롭게 의견을 내고 프로젝트를 추진할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;216&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;시스템 팀의 목표는 팀원들의 생산성을 높이는 것이었지만, 저희가 진행한 일들이 팀원들의 생산성을 올렸나?라고 판단했을 때 잘 접목하면 좋지만, 오히려 어려워졌다는 피드백을 받기도 했습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;시스템을 확산하려면 일정 수준의 학습이 필요했지만, 외주 일정이 빠듯한 상황에서 학습 부담이 가중될 수 있다는 점도 고려해야 했습니다. 그래서 모든 팀원 분들이 항목별로 학습할 수 있게 온보딩 테이블를 구성하여 어느정도 개선이 됐다고는 생각하지만, 계속되는 업데이트와 팀원분들의 프로젝트 속도를 맞추기는 쉽지 않았습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;686&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;686&quot; data-ke-size=&quot;size16&quot;&gt;또 하반기에는 회사의 주요 사업이 외주 프로젝트였던 만큼, 상황에 따라 시스템 작업보다 외주 업무나, 마케팅적인 부분에서 우선시되는 흐름이 있었습니다. 그렇다 보니 시스템 개선 작업에 집중할 수 있는 환경을 계속 유지하는 것이 어려워졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;686&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-last-node=&quot;&quot; data-end=&quot;951&quot; data-start=&quot;859&quot; data-ke-size=&quot;size16&quot;&gt;아쉬웠던 부분이 있기는 했지만, 제가 바꿀 수 있는 환경이 아니었기에 이직에 대한 고민이 들기 시작했습니다. 그럼에도 불구하고 분명한건 시스템 팀에서 다양한 기술적 시도를 해볼 수 있었고, &lt;b&gt;개발 생산성과 기술력을 높이는 다양한 방안을 고민하고 적용해 본 경험&lt;/b&gt;은 매우 의미 있었고, 이러한 기회를 주셨던 것에 감사한 마음을 가지고 있었기에 더더욱 고민을 신중히 했었습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1213&quot; data-start=&quot;1186&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1213&quot; data-start=&quot;1186&quot; data-ke-size=&quot;size23&quot;&gt;2. 내가 보지 못한 우물 밖은 어떨까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1345&quot; data-start=&quot;1214&quot; data-ke-size=&quot;size16&quot;&gt;저는 &lt;b&gt;개발자로서 첫 회사&lt;/b&gt;를 똑똑한개발자에서 시작했고, 정말 좋은 동료들과 함께 즐겁게 일했습니다. 특히 시스템 고도화를 하며 &lt;b&gt;다양한 레퍼런스를 참고하고, 타 회사의 코드를 분석하며 배워가는 과정&lt;/b&gt;이 무척 흥미로웠습니다.&lt;/p&gt;
&lt;p data-end=&quot;1478&quot; data-start=&quot;1347&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1478&quot; data-start=&quot;1347&quot; data-ke-size=&quot;size16&quot;&gt;그렇게 점점 &lt;b&gt;기능 구현을 넘어 시스템을 이해하고, 운영 효율과 유지보수성까지 고민하는 개발&lt;/b&gt;에 매력을 느끼게 되었고, 동시에 문득 이런 생각이 들었습니다.&lt;br /&gt;&lt;b&gt;&amp;ldquo;오랜 시간 다듬어진 시스템은 어떤 기준과 방향으로 운영되고 있을까?&amp;rdquo;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;ldquo;개발 생산성을 극대화하기 위해, 더 큰 조직에서는 어떤 도구와 설계를 선택할까?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;738&quot; data-start=&quot;555&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;738&quot; data-start=&quot;555&quot; data-ke-size=&quot;size16&quot;&gt;이런 궁금증은 제가 경험하지 못했던 새로운 환경, 특히 &lt;b&gt;더 많은 사용자와 복잡한 요구사항이 얽힌 구조 속에서&lt;/b&gt; 답을 찾아보고 싶다는 동기로 이어졌습니다.&lt;br /&gt;사용자에게 직접 닿는 인터페이스, 효율적인 구조와 협업 문화, 그리고 깊이 있는 기술 설계가 공존하는 곳에서, &lt;b&gt;저만의 답을 찾아가고 싶다&lt;/b&gt;는 마음이 들었습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1504&quot; data-start=&quot;1480&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1504&quot; data-start=&quot;1480&quot; data-ke-size=&quot;size23&quot;&gt;3. 이직 결심, 그리고 퇴사 결정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1641&quot; data-start=&quot;1505&quot; data-ke-size=&quot;size16&quot;&gt;이직을 결심했지만, &lt;b&gt;업무와 병행하며 준비하는 것이 쉽지 않았습니다.&lt;/b&gt; 회사에서 야근을 강요하는 분위기는 아니었지만, 저는 시스템 개발이 재미있고 의미 있다고 생각했기 때문에 &lt;b&gt;퇴근 후에도 자연스럽게 관련 작업을 이어가곤 했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1750&quot; data-start=&quot;1643&quot; data-ke-size=&quot;size16&quot;&gt;이슈가 발생하거나 개선해야 할 점이 보이면 해결할 때까지 잠들지 않는 성격이라, 결국 이직 준비와 병행하기 어려웠고, &lt;b&gt;온전히 새로운 도전에 집중하기 위해 퇴사를 결심하게 되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1750&quot; data-start=&quot;1643&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-end=&quot;1750&quot; data-start=&quot;1643&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1750&quot; data-start=&quot;1643&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1월의 난 뭘 하며 쉬었나?&lt;/b&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;
&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 강습시간 외 수영 티칭 받기, 영상 촬영&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 주 3회 수영을 하고 있습니다. 중간중간 헬스나 복싱 등 다른 운동을 하기도 했지만, 수영 경력은 햇수로 9년째  입니다. 퇴사 후 평소에 너무나 받고 싶었던 인플루언서 강사님께 접영 1회, IAM 턴 1회 강습을 받았습니다. 수영을 한지는 꽤 오래 되었지만, 제 자세를 볼 기회가 거의 없었는데 해당 강습에서 영상을 촬영해주셔서 수영에 대한 의지가 더더 오르게 되었습니다. 주 수입이 없어 더 강습 받기는 힘들지만, 아주 의미있었던 시간이었습니다. 영상을 통해 제가 출수할 때 머리를 든다는 부분도 알게 되었고요.&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 유기견 산책 봉사 활동&amp;nbsp;&lt;/h3&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;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IQLJf/btsMjGtzTSz/2WCTg61ENKYACdoV8REkKK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IQLJf/btsMjGtzTSz/2WCTg61ENKYACdoV8REkKK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-14-14-22-33 001.jpeg&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IQLJf/btsMjGtzTSz/2WCTg61ENKYACdoV8REkKK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIQLJf%2FbtsMjGtzTSz%2F2WCTg61ENKYACdoV8REkKK%2Fimg.jpg&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;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/betfzR/btsMipM54am/QU7KgITa4YvgHENbysgsHK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/betfzR/btsMipM54am/QU7KgITa4YvgHENbysgsHK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-14-14-22-35 002.jpeg&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/betfzR/btsMipM54am/QU7KgITa4YvgHENbysgsHK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbetfzR%2FbtsMipM54am%2FQU7KgITa4YvgHENbysgsHK%2Fimg.jpg&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;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;귀여운 수일이&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1750&quot; data-start=&quot;1643&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1750&quot; data-start=&quot;1643&quot; data-ke-size=&quot;size23&quot;&gt;3. 드럼 배우기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 드럼을 멋있게 치는 분들을 너무 좋아합니다. 7년 전 위플래시 라는 영화를 보고 2시간 동안 내내 울리는 드럼 소리에 감명을 받고, 지금도 드럼 영상은 제 알고리즘의 30%는 차지합니다.&amp;nbsp;&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회 드럼 강습도 진행하고 있어서 작년에 배웠다가 일이 너무 바빠져 그만뒀는데, 퇴사 후 다시 드럼 강습을 등록했습니다. 지금 배우는 곡은 우즈님의 Drowning 이라는 곡인데, 제가 한 때 빠져서 들었던 곡 악보여서 너무 재밌게 하고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 치앙마이 일주일 여행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 치앙마이에 일주일동안 여행을 다녀 왔습니다. 일주일 꽉 채워서 7박 9일로 다녀왔는데요. 보통 힐링하러 한달 살기로 많이 다녀온다고도 하고, 추천 여행지로 많이 받아서 계획표 알차게 짜서 다녀왔습니다. 수영 영상도 물론 찍고, 야외 수영장도 찾아가구요. 수영은 7일 중 몬쨈에 갔던 이틀 빼고 5일 내내 했던 것 같네요. 정말 즐거웠던 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YoqQD/btsMj41Pnoh/KuYj6VOD1CbvNSq2pLLZYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YoqQD/btsMj41Pnoh/KuYj6VOD1CbvNSq2pLLZYk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1876&quot; data-filename=&quot;Screenshot 2025-02-14 at 2.39.05 PM.png&quot; style=&quot;width: 73.9663%; margin-right: 10px;&quot; data-widthpercent=&quot;74.84&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YoqQD/btsMj41Pnoh/KuYj6VOD1CbvNSq2pLLZYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYoqQD%2FbtsMj41Pnoh%2FKuYj6VOD1CbvNSq2pLLZYk%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;2162&quot; height=&quot;1876&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wAKpF/btsMjQ3XnzV/3gRqXNW69lHd6XNYhBMZY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wAKpF/btsMjQ3XnzV/3gRqXNW69lHd6XNYhBMZY0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;1489&quot; data-filename=&quot;edited_Screenshot 2025-02-14 at 2.38.24 PM.png&quot; data-widthpercent=&quot;25.16&quot; style=&quot;width: 24.8709%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wAKpF/btsMjQ3XnzV/3gRqXNW69lHd6XNYhBMZY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwAKpF%2FbtsMjQ3XnzV%2F3gRqXNW69lHd6XNYhBMZY0%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;577&quot; height=&quot;1489&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;치앙마이 여행 계획&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYLaLx/btsMj9vgxoJ/BkLPhnvl3ZP72ZbCazUj1k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYLaLx/btsMj9vgxoJ/BkLPhnvl3ZP72ZbCazUj1k/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-14-14-46-23.jpeg&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYLaLx/btsMj9vgxoJ/BkLPhnvl3ZP72ZbCazUj1k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYLaLx%2FbtsMj9vgxoJ%2FBkLPhnvl3ZP72ZbCazUj1k%2Fimg.jpg&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;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjVKPe/btsMjBswBlO/Ms2Dqq5ujBbQV8LmINdZIK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjVKPe/btsMjBswBlO/Ms2Dqq5ujBbQV8LmINdZIK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-14-14-46-27.jpeg&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjVKPe/btsMjBswBlO/Ms2Dqq5ujBbQV8LmINdZIK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjVKPe%2FbtsMjBswBlO%2FMs2Dqq5ujBbQV8LmINdZIK%2Fimg.jpg&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;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prJjP/btsMjANWv0Q/oBftx1OGzrTVwkiyqUFWSK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prJjP/btsMjANWv0Q/oBftx1OGzrTVwkiyqUFWSK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-14-14-46-31.jpeg&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prJjP/btsMjANWv0Q/oBftx1OGzrTVwkiyqUFWSK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprJjP%2FbtsMjANWv0Q%2FoBftx1OGzrTVwkiyqUFWSK%2Fimg.jpg&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;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;치앙마이 수영장, 코코넛마켓&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;앞으로의 나는 ?&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;672&quot; data-start=&quot;570&quot; data-ke-size=&quot;size16&quot;&gt;퇴사 후 한 달, 하고 싶었던 것들을 알차게 해왔고 후회는 없습니다.&lt;br /&gt;이직 시장이 녹록지 않다는 사실도 알고 있었지만, 저는 이번 기회에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;충분한 시간과 집중을 가지고&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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-start=&quot;270&quot; data-end=&quot;413&quot; data-ke-size=&quot;size16&quot;&gt;이제 다시 새로운 도전을 준비하려 합니다.&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단순히 일자리를 찾는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내가 어떤 팀과 잘 맞을지&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어떤 환경에서 더 성장할 수 있을지&lt;/b&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;어떤 기술을 더 깊이 탐구하고 싶은지&lt;/b&gt;를 진지하게 고민하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;270&quot; data-end=&quot;413&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;415&quot; data-end=&quot;568&quot; data-ke-size=&quot;size16&quot;&gt;그리고 그 고민은 단지 기술적인 부분에만 머물지 않습니다. 사용자에게 더 좋은 경험을 줄 수 있는 인터페이스, 협업 효율을 높이는 시스템 구조, 유지보수성과 성능을 함께 고려한 설계 등, 더 나은 제품과 더 나은 팀을 만들기 위한 방향성을 기준으로 삼고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;415&quot; data-end=&quot;568&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;570&quot; data-end=&quot;672&quot; 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>회고</category>
      <category>개발자 퇴사</category>
      <category>개발자 퇴사 회고록</category>
      <category>이직준비</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/204</guid>
      <comments>https://algoroot.tistory.com/204#entry204comment</comments>
      <pubDate>Thu, 13 Feb 2025 15:09:14 +0900</pubDate>
    </item>
    <item>
      <title>[귀찮은건 질색이야-2] 나는 코드를 작성했는데 문서가 생성된다고?: api-extractor (feat. Docusaurus, Github A</title>
      <link>https://algoroot.tistory.com/202</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 오늘은 코드와 주석을 기반으로 문서 자동화한 경험을 공유하고자 합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;보일러플레이트에서 응집되어 있던 utils, hooks을 용도에 맞게 패키지로 분리하면서, 관리적과 성능적으로는 이점이 있었지만, 실무진 분들이 어떠한 함수를 가져오기 위해 참고할 수 있는 문서가 필요했습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;21개가 넘는 패키지들, 총 100개는 넘을 듯한 컴포넌트나 함수들을 개발하는 것을 떠나서 또 문서작업을 해야 한다는 게 굉장히 부담으로 다가왔습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;따로 md파일을 생성하여 관리하는 것이 아닌, 명령어만으로 문서가 자동생성 되게 하면 얼마나 편할까요?&amp;nbsp;&lt;br&gt;다행히도, 아주 다행이도 이미 잘 개발된 라이브러리를 발견했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;바로 MicroSoft 에서 개발한 오픈소스도구인&amp;nbsp;&lt;a href=&quot;https://api-extractor.com/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;api-extractor인데요.&lt;/span&gt;&lt;/a&gt; 해당 라이브러리는 다룬 글이 없고, 이슈 대응에 있어 고민이 되는 부분이 있었지만, 최근까지도 활발하게 이슈가 픽스되고 있고, 해외 레퍼런스도 충분히 있는 것 같아 해당 툴을 사용해 문서 자동화를 진행하게 되었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;TSDoc&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;설명하기 앞서 코드기반으로 문서를 잘 생성하려면, TSDoc 형식으로 작성이 되어야 합니다. API Extractor는 typescript 코드 구문을 분석하고, TSDoc 형식을 읽어 분석 후 Docmodel를 생성해 주기 때문입니다.&amp;nbsp;아직 잘 모르신다면,&amp;nbsp;&lt;a href=&quot;https://microsoft.github.io/tsdoc/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;TSDoc Playground&lt;/span&gt;&lt;/a&gt;를 실행해서 TSDoc 구문이 어떻게 동작하는지 보시면 좋을 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;API Extractor 공식문서에서도 주석구문에 대한 정보를 제공하고 있습니다.&amp;nbsp; 어떤 주석을 분석하는지는 아래 경로에서 확인해보시면 됩니다.&amp;nbsp;&lt;/p&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://api-extractor.com/pages/tsdoc/doc_comment_syntax/#which-comments-are-parsed-by-api-extractor&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;API&amp;nbsp;Extractor는&amp;nbsp;어떤&amp;nbsp;주석을&amp;nbsp;분석합니까&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;미리보기&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;TS Doc 규칙에 맞게 잘 작성된 주석과, 올바르게 타입 된 함수만 있다면 명령어 하나만으로 우측의 md파일이 생성됩니다.&amp;nbsp;&amp;nbsp;&lt;br&gt;물론 예쁘게 보이기 위해 커스텀 스크립트를 만든 부분이 있지만, 기본적으로는 명령어 한 줄만으로도 가능합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;div data-snappify=&quot;JTdCJTIydHlwZSUyMiUzQSUyMmFyZWFzJTIyJTJDJTIyc25hcFZlcnNpb24lMjIlM0EzJTJDJTIyZGF0YSUyMiUzQSU3QiUyMnNuYXBJZCUyMiUzQSUyMjdhMmFmYTJmLWJiNTItNGViYS1hNTE3LTU4ZGI2OTMzZTZmMiUyMiUyQyUyMmZyYW1lSWQlMjIlM0ElMjJkZGViMzZlYS1kYjRkLTRiODEtOTIxYy02ZDY4Y2JiOGQyOTIlMjIlMkMlMjJhcmVhcyUyMiUzQSU1QiU3QiUyMmlkJTIyJTNBJTIyZGY5NzNlMDItOWY3Ny00ZTEwLWFiZWMtNDAwN2JiYjA2ZjM0JTIyJTJDJTIyYW5pbWF0aW9uSWQlMjIlM0ElMjI2NTAyMDk3Ni01MTMwLTQ2M2YtYTRkOC1lZTYyZmVmYTZkYjMlMjIlMkMlMjJ0eXBlJTIyJTNBMSUyQyUyMnBvc2l0aW9uU2l6ZSUyMiUzQSU3QiUyMnglMjIlM0E4MCUyQyUyMnklMjIlM0ExMDAlMkMlMjJ3JTIyJTNBJTIyYXV0byUyMiUyQyUyMmglMjIlM0ElMjJhdXRvJTIyJTJDJTIya2VlcFJhdGlvJTIyJTNBZmFsc2UlN0QlMkMlMjJwb3NpdGlvbkNvbnN0cmFpbnQlMjIlM0ElN0IlMjJ4JTIyJTNBJTIybGVmdCUyMiUyQyUyMnklMjIlM0ElMjJ0b3AlMjIlMkMlMjJsZWZ0JTIyJTNBMSUyQyUyMnRvcCUyMiUzQTElMkMlMjJyaWdodCUyMiUzQW51bGwlMkMlMjJib3R0b20lMjIlM0FudWxsJTdEJTJDJTIyc3RhdGUlMjIlM0ElN0IlMjJjb2RlJTIyJTNBJTIyaW1wb3J0JTIwJTdCJTIwY3VycnklMkMlMjBmbG93JTJDJTIwcmVkdWNlJTIwJTdEJTIwZnJvbSUyMCdsb2Rhc2glMkZmcCclNUNuJTVDbmltcG9ydCUyMCU3QiUyMHBhc3MlMjAlN0QlMjBmcm9tJTIwJy4uJTJGLi4lMkZmbiclNUNuJTVDbiUyRioqJTVDbiUyMColMjAlNDBjYXRlZ29yeSUyMFV0aWxzJTJGQXJyYXklNUNuJTIwKiU1Q24lMjAqJTIwJUVCJUIwJUIwJUVDJTk3JUI0JUVDJTlEJTg0JTIwJUVEJThBJUI5JUVDJUEwJTk1JTIwJUVBJUIwJUFGJUVDJTg4JTk4JUVCJUExJTlDJTIwJUVCJTgyJTk4JUVCJTg4JTg0JUVDJTk2JUI0JUVDJUEzJUJDJUVCJThBJTk0JTIwJUVEJTk1JUE4JUVDJTg4JTk4JUVDJTlFJTg1JUVCJThCJTg4JUVCJThCJUE0LiU1Q24lMjAqJTVDbiUyMColMjAlNDBwYXJhbSUyMGxpbWl0JTIwLSUyMCVFQiVCMCVCMCVFQyU5NyVCNCVFQyU5RCU4NCUyMCVFQiU4MiU5OCVFQiU4OCU4QyUyMCVFQSVCMCVBRiVFQyU4OCU5OCVFQyU5RSU4NSVFQiU4QiU4OCVFQiU4QiVBNC4lNUNuJTIwKiUyMCU0MHBhcmFtJTIwYXJyJTIwLSUyMCVFQiU4MiU5OCVFQiU4OCU4QyUyMCVFQiVCMCVCMCVFQyU5NyVCNCVFQyU5RSU4NSVFQiU4QiU4OCVFQiU4QiVBNC4lNUNuJTIwKiU1Q24lMjAqJTIwJTQwcmV0dXJucyUyMCVFQiU4MiU5OCVFQiU4OCU4NCVFQyU5NiVCNCVFQyVBNyU4NCUyMCVFQiVCMCVCMCVFQyU5NyVCNCVFQyU5RCU4NCUyMCVFQiVCMCU5OCVFRCU5OSU5OCVFRCU5NSVBOSVFQiU4QiU4OCVFQiU4QiVBNC4lNUNuJTIwKiU1Q24lMjAqJTIwJTQwZXhhbXBsZSU1Q24lMjAqJTVDbiUyMColMjAlNjAlNjAlNjB0cyU1Q24lMjAqJTIwY29uc3QlMjBhcnIlMjAlM0QlMjAlNUIxJTJDJTIwMiUyQyUyMDMlMkMlMjA0JTJDJTIwNSUyQyUyMDYlMkMlMjA3JTJDJTIwOCUyQyUyMDklNUQlNUNuJTIwKiUyMGNvbnN0JTIwcmVzdWx0JTIwJTNEJTIwcGFnaW5hdGUoMyUyQyUyMGFyciklNUNuJTIwKiU1Q24lMjAqJTIwY29uc29sZS5sb2cocmVzdWx0KSU1Q24lMjAqJTVDbiUyMColMjAlMkYlMkYlMjAlNUIlNUNuJTIwKiUyMCUyRiUyRiUyMCUyMCUyMCU1QjElMkMlMjAyJTJDJTIwMyU1RCUyQyU1Q24lMjAqJTIwJTJGJTJGJTIwJTIwJTIwJTVCNCUyQyUyMDUlMkMlMjA2JTVEJTJDJTVDbiUyMColMjAlMkYlMkYlMjAlMjAlMjAlNUI3JTJDJTIwOCUyQyUyMDklNUQlMkMlNUNuJTIwKiUyMCUyRiUyRiUyMCU1RCU1Q24lMjAqJTIwJTYwJTYwJTYwJTVDbiUyMColMkYlNUNuZXhwb3J0JTIwY29uc3QlMjBwYWdpbmF0ZSUzQSUyMCU3QiU1Q24lMjAlMjAlM0NUJTNFKGxpbWl0JTNBJTIwbnVtYmVyJTJDJTIwYXJyJTNBJTIwVCU1QiU1RCklM0ElMjBUJTVCJTVEJTVCJTVEJTVDbiUyMCUyMCUzQ1QlM0UobGltaXQlM0ElMjBudW1iZXIpJTNBJTIwKGFyciUzQSUyMFQlNUIlNUQpJTIwJTNEJTNFJTIwVCU1QiU1RCU1QiU1RCU1Q24lN0QlMjAlM0QlMjBjdXJyeSglM0NUJTNFKGxpbWl0JTNBJTIwbnVtYmVyJTJDJTIwYXJyJTNBJTIwVCU1QiU1RCklM0ElMjBUJTVCJTVEJTVCJTVEJTIwJTNEJTNFJTVDbiUyMCUyMGZsb3coJTVDbiUyMCUyMCUyMCUyMHBhc3MoYXJyKSUyQyU1Q24lMjAlMjAlMjAlMjByZWR1Y2UoKGFjYyUzQSUyMFQlNUIlNUQlNUIlNUQlMkMlMjBjdXIlM0ElMjBUKSUyMCUzRCUzRSUyMCU3QiU1Q24lMjAlMjAlMjAlMjAlMjAlMjBpZiUyMChhY2MubGVuZ3RoJTIwJTNEJTNEJTNEJTIwMCklMjAlN0IlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwcmV0dXJuJTIwYWNjLmNvbmNhdCglNUIlNUJjdXIlNUQlNUQpJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCU3RCU1Q24lMjAlMjAlMjAlMjAlMjAlMjBpZiUyMChhY2MlNUJhY2MubGVuZ3RoJTIwLSUyMDElNUQubGVuZ3RoJTIwJTNEJTNEJTNEJTIwbGltaXQpJTIwJTdCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHJldHVybiUyMGFjYy5jb25jYXQoJTVCJTVCY3VyJTVEJTVEKSU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlN0QlNUNuJTVDbiUyMCUyMCUyMCUyMCUyMCUyMHJldHVybiUyMGFjYy5zbGljZSgwJTJDJTIwLTEpLmNvbmNhdCglNUJhY2MlNUJhY2MubGVuZ3RoJTIwLSUyMDElNUQuY29uY2F0KCU1QmN1ciU1RCklNUQpJTVDbiUyMCUyMCUyMCUyMCU3RCUyQyUyMCU1QiU1RCklMkMlNUNuJTIwJTIwKSgpJTJDJTVDbiklMjIlMkMlMjJmaWxlTmFtZSUyMiUzQSUyMnBhZ2luYXRlLnRzJTIyJTJDJTIydGhlbWUlMjIlM0ElMjJnaXRodWItZGFyay1kaW1tZWQlMjIlMkMlMjJiZ0NvbG9yJTIyJTNBJTdCJTIydHlwZSUyMiUzQSUyMnNvbGlkJTIyJTJDJTIydmFsdWUlMjIlM0ElMjIlMjMyMjI3MmUlMjIlN0QlMkMlMjJsYW5ndWFnZSUyMiUzQSUyMlR5cGVTY3JpcHQlMjIlMkMlMjJmb250RmFtaWx5JTIyJTNBJTIyTW9ub0xpc2ElMjIlMkMlMjJmb250TGlnYXR1cmVzJTIyJTNBdHJ1ZSUyQyUyMnRhYkFjY2VudCUyMiUzQXRydWUlMkMlMjJwYWRkaW5nJTIyJTNBJTdCJTIydG9wJTIyJTNBMjElMkMlMjJib3R0b20lMjIlM0EyMSUyQyUyMmxlZnQlMjIlM0EyMSUyQyUyMnJpZ2h0JTIyJTNBMjElN0QlMkMlMjJ0YWJTaXplJTIyJTNBMiUyQyUyMm1pblclMjIlM0EyMCU3RCUyQyUyMmFuaW1hdGlvblN0YXRlJTIyJTNBJTdCJTIyY29kZUNoYW5nZVRyYW5zaXRpb24lMjIlM0ElN0IlMjJ0eXBlJTIyJTNBJTIyRmFkZUluJTIyJTJDJTIyYXV0b0R1cmF0aW9uJTIyJTNBdHJ1ZSU3RCU3RCUyQyUyMnN0eWxlUHJvcHMlMjIlM0ElN0IlMjJyb3RhdGUlMjIlM0EwJTJDJTIyc2NhbGUlMjIlM0ExJTJDJTIyb3BhY2l0eSUyMiUzQTElMkMlMjJib3hTaGFkb3clMjIlM0ElMjJyZ2IoMzglMkMlMjA1NyUyQyUyMDc3KSUyMDBweCUyMDIwcHglMjAzMHB4JTIwLTEwcHglMjIlMkMlMjJib3JkZXJSYWRpdXMlMjIlM0ExMCU3RCU3RCU1RCUyQyUyMm1pblglMjIlM0E4MCUyQyUyMm1pblklMjIlM0ExMDAlMkMlMjJ3aWR0aCUyMiUzQTY5Ni4xMjUlMkMlMjJoZWlnaHQlMjIlM0ExMDc0JTdEJTdE&quot;&gt;
  &amp;nbsp; 
&lt;/div&gt;&lt;div data-snappify=&quot;JTdCJTIydHlwZSUyMiUzQSUyMmFyZWFzJTIyJTJDJTIyc25hcFZlcnNpb24lMjIlM0EzJTJDJTIyZGF0YSUyMiUzQSU3QiUyMnNuYXBJZCUyMiUzQSUyMjdhMmFmYTJmLWJiNTItNGViYS1hNTE3LTU4ZGI2OTMzZTZmMiUyMiUyQyUyMmZyYW1lSWQlMjIlM0ElMjJkZGViMzZlYS1kYjRkLTRiODEtOTIxYy02ZDY4Y2JiOGQyOTIlMjIlMkMlMjJhcmVhcyUyMiUzQSU1QiU3QiUyMmlkJTIyJTNBJTIyZGY5NzNlMDItOWY3Ny00ZTEwLWFiZWMtNDAwN2JiYjA2ZjM0JTIyJTJDJTIyYW5pbWF0aW9uSWQlMjIlM0ElMjI2NTAyMDk3Ni01MTMwLTQ2M2YtYTRkOC1lZTYyZmVmYTZkYjMlMjIlMkMlMjJ0eXBlJTIyJTNBMSUyQyUyMnBvc2l0aW9uU2l6ZSUyMiUzQSU3QiUyMnglMjIlM0E4MCUyQyUyMnklMjIlM0ExMDAlMkMlMjJ3JTIyJTNBJTIyYXV0byUyMiUyQyUyMmglMjIlM0ElMjJhdXRvJTIyJTJDJTIya2VlcFJhdGlvJTIyJTNBZmFsc2UlN0QlMkMlMjJwb3NpdGlvbkNvbnN0cmFpbnQlMjIlM0ElN0IlMjJ4JTIyJTNBJTIybGVmdCUyMiUyQyUyMnklMjIlM0ElMjJ0b3AlMjIlMkMlMjJsZWZ0JTIyJTNBMSUyQyUyMnRvcCUyMiUzQTElMkMlMjJyaWdodCUyMiUzQW51bGwlMkMlMjJib3R0b20lMjIlM0FudWxsJTdEJTJDJTIyc3RhdGUlMjIlM0ElN0IlMjJjb2RlJTIyJTNBJTIyaW1wb3J0JTIwJTdCJTIwY3VycnklMkMlMjBmbG93JTJDJTIwcmVkdWNlJTIwJTdEJTIwZnJvbSUyMCdsb2Rhc2glMkZmcCclNUNuJTVDbmltcG9ydCUyMCU3QiUyMHBhc3MlMjAlN0QlMjBmcm9tJTIwJy4uJTJGLi4lMkZmbiclNUNuJTVDbiUyRioqJTVDbiUyMColMjAlNDBjYXRlZ29yeSUyMFV0aWxzJTJGQXJyYXklNUNuJTIwKiU1Q24lMjAqJTIwJUVCJUIwJUIwJUVDJTk3JUI0JUVDJTlEJTg0JTIwJUVEJThBJUI5JUVDJUEwJTk1JTIwJUVBJUIwJUFGJUVDJTg4JTk4JUVCJUExJTlDJTIwJUVCJTgyJTk4JUVCJTg4JTg0JUVDJTk2JUI0JUVDJUEzJUJDJUVCJThBJTk0JTIwJUVEJTk1JUE4JUVDJTg4JTk4JUVDJTlFJTg1JUVCJThCJTg4JUVCJThCJUE0LiU1Q24lMjAqJTVDbiUyMColMjAlNDBwYXJhbSUyMGxpbWl0JTIwLSUyMCVFQiVCMCVCMCVFQyU5NyVCNCVFQyU5RCU4NCUyMCVFQiU4MiU5OCVFQiU4OCU4QyUyMCVFQSVCMCVBRiVFQyU4OCU5OCVFQyU5RSU4NSVFQiU4QiU4OCVFQiU4QiVBNC4lNUNuJTIwKiUyMCU0MHBhcmFtJTIwYXJyJTIwLSUyMCVFQiU4MiU5OCVFQiU4OCU4QyUyMCVFQiVCMCVCMCVFQyU5NyVCNCVFQyU5RSU4NSVFQiU4QiU4OCVFQiU4QiVBNC4lNUNuJTIwKiU1Q24lMjAqJTIwJTQwcmV0dXJucyUyMCVFQiU4MiU5OCVFQiU4OCU4NCVFQyU5NiVCNCVFQyVBNyU4NCUyMCVFQiVCMCVCMCVFQyU5NyVCNCVFQyU5RCU4NCUyMCVFQiVCMCU5OCVFRCU5OSU5OCVFRCU5NSVBOSVFQiU4QiU4OCVFQiU4QiVBNC4lNUNuJTIwKiU1Q24lMjAqJTIwJTQwZXhhbXBsZSU1Q24lMjAqJTVDbiUyMColMjAlNjAlNjAlNjB0cyU1Q24lMjAqJTIwY29uc3QlMjBhcnIlMjAlM0QlMjAlNUIxJTJDJTIwMiUyQyUyMDMlMkMlMjA0JTJDJTIwNSUyQyUyMDYlMkMlMjA3JTJDJTIwOCUyQyUyMDklNUQlNUNuJTIwKiUyMGNvbnN0JTIwcmVzdWx0JTIwJTNEJTIwcGFnaW5hdGUoMyUyQyUyMGFyciklNUNuJTIwKiU1Q24lMjAqJTIwY29uc29sZS5sb2cocmVzdWx0KSU1Q24lMjAqJTVDbiUyMColMjAlMkYlMkYlMjAlNUIlNUNuJTIwKiUyMCUyRiUyRiUyMCUyMCUyMCU1QjElMkMlMjAyJTJDJTIwMyU1RCUyQyU1Q24lMjAqJTIwJTJGJTJGJTIwJTIwJTIwJTVCNCUyQyUyMDUlMkMlMjA2JTVEJTJDJTVDbiUyMColMjAlMkYlMkYlMjAlMjAlMjAlNUI3JTJDJTIwOCUyQyUyMDklNUQlMkMlNUNuJTIwKiUyMCUyRiUyRiUyMCU1RCU1Q24lMjAqJTIwJTYwJTYwJTYwJTVDbiUyMColMkYlNUNuZXhwb3J0JTIwY29uc3QlMjBwYWdpbmF0ZSUzQSUyMCU3QiU1Q24lMjAlMjAlM0NUJTNFKGxpbWl0JTNBJTIwbnVtYmVyJTJDJTIwYXJyJTNBJTIwVCU1QiU1RCklM0ElMjBUJTVCJTVEJTVCJTVEJTVDbiUyMCUyMCUzQ1QlM0UobGltaXQlM0ElMjBudW1iZXIpJTNBJTIwKGFyciUzQSUyMFQlNUIlNUQpJTIwJTNEJTNFJTIwVCU1QiU1RCU1QiU1RCU1Q24lN0QlMjAlM0QlMjBjdXJyeSglM0NUJTNFKGxpbWl0JTNBJTIwbnVtYmVyJTJDJTIwYXJyJTNBJTIwVCU1QiU1RCklM0ElMjBUJTVCJTVEJTVCJTVEJTIwJTNEJTNFJTVDbiUyMCUyMGZsb3coJTVDbiUyMCUyMCUyMCUyMHBhc3MoYXJyKSUyQyU1Q24lMjAlMjAlMjAlMjByZWR1Y2UoKGFjYyUzQSUyMFQlNUIlNUQlNUIlNUQlMkMlMjBjdXIlM0ElMjBUKSUyMCUzRCUzRSUyMCU3QiU1Q24lMjAlMjAlMjAlMjAlMjAlMjBpZiUyMChhY2MubGVuZ3RoJTIwJTNEJTNEJTNEJTIwMCklMjAlN0IlNUNuJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwcmV0dXJuJTIwYWNjLmNvbmNhdCglNUIlNUJjdXIlNUQlNUQpJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCU3RCU1Q24lMjAlMjAlMjAlMjAlMjAlMjBpZiUyMChhY2MlNUJhY2MubGVuZ3RoJTIwLSUyMDElNUQubGVuZ3RoJTIwJTNEJTNEJTNEJTIwbGltaXQpJTIwJTdCJTVDbiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHJldHVybiUyMGFjYy5jb25jYXQoJTVCJTVCY3VyJTVEJTVEKSU1Q24lMjAlMjAlMjAlMjAlMjAlMjAlN0QlNUNuJTVDbiUyMCUyMCUyMCUyMCUyMCUyMHJldHVybiUyMGFjYy5zbGljZSgwJTJDJTIwLTEpLmNvbmNhdCglNUJhY2MlNUJhY2MubGVuZ3RoJTIwLSUyMDElNUQuY29uY2F0KCU1QmN1ciU1RCklNUQpJTVDbiUyMCUyMCUyMCUyMCU3RCUyQyUyMCU1QiU1RCklMkMlNUNuJTIwJTIwKSgpJTJDJTVDbiklMjIlMkMlMjJmaWxlTmFtZSUyMiUzQSUyMnBhZ2luYXRlLnRzJTIyJTJDJTIydGhlbWUlMjIlM0ElMjJnaXRodWItZGFyay1kaW1tZWQlMjIlMkMlMjJiZ0NvbG9yJTIyJTNBJTdCJTIydHlwZSUyMiUzQSUyMnNvbGlkJTIyJTJDJTIydmFsdWUlMjIlM0ElMjIlMjMyMjI3MmUlMjIlN0QlMkMlMjJsYW5ndWFnZSUyMiUzQSUyMlR5cGVTY3JpcHQlMjIlMkMlMjJmb250RmFtaWx5JTIyJTNBJTIyTW9ub0xpc2ElMjIlMkMlMjJmb250TGlnYXR1cmVzJTIyJTNBdHJ1ZSUyQyUyMnRhYkFjY2VudCUyMiUzQXRydWUlMkMlMjJwYWRkaW5nJTIyJTNBJTdCJTIydG9wJTIyJTNBMjElMkMlMjJib3R0b20lMjIlM0EyMSUyQyUyMmxlZnQlMjIlM0EyMSUyQyUyMnJpZ2h0JTIyJTNBMjElN0QlMkMlMjJ0YWJTaXplJTIyJTNBMiUyQyUyMm1pblclMjIlM0EyMCU3RCUyQyUyMmFuaW1hdGlvblN0YXRlJTIyJTNBJTdCJTIyY29kZUNoYW5nZVRyYW5zaXRpb24lMjIlM0ElN0IlMjJ0eXBlJTIyJTNBJTIyRmFkZUluJTIyJTJDJTIyYXV0b0R1cmF0aW9uJTIyJTNBdHJ1ZSU3RCU3RCUyQyUyMnN0eWxlUHJvcHMlMjIlM0ElN0IlMjJyb3RhdGUlMjIlM0EwJTJDJTIyc2NhbGUlMjIlM0ExJTJDJTIyb3BhY2l0eSUyMiUzQTElMkMlMjJib3hTaGFkb3clMjIlM0ElMjJyZ2IoMzglMkMlMjA1NyUyQyUyMDc3KSUyMDBweCUyMDIwcHglMjAzMHB4JTIwLTEwcHglMjIlMkMlMjJib3JkZXJSYWRpdXMlMjIlM0ExMCU3RCU3RCU1RCUyQyUyMm1pblglMjIlM0E4MCUyQyUyMm1pblklMjIlM0ExMDAlMkMlMjJ3aWR0aCUyMiUzQTY5Ni4xMjUlMkMlMjJoZWlnaHQlMjIlM0ExMDc0JTdEJTdE&quot;&gt; 
 &lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PSDRT/btsMi497mhL/X7JwUjIouFJKBf9LtiP3o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PSDRT/btsMi497mhL/X7JwUjIouFJKBf9LtiP3o0/img.png&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;2548&quot; data-is-animation=&quot;false&quot; style=&quot;width: 39.7205%; margin-right: 10px;&quot; data-widthpercent=&quot;40.19&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PSDRT/btsMi497mhL/X7JwUjIouFJKBf9LtiP3o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPSDRT%2FbtsMi497mhL%2FX7JwUjIouFJKBf9LtiP3o0%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;1712&quot; height=&quot;2548&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqLdSQ/btsMjnnZi6k/2Og4pVGRu1KTpcFGhFblt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqLdSQ/btsMjnnZi6k/2Og4pVGRu1KTpcFGhFblt0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1786&quot; data-filename=&quot;Screenshot 2025-02-14 at 5.58.51 PM.png&quot; style=&quot;width: 59.1167%;&quot; data-widthpercent=&quot;59.81&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqLdSQ/btsMjnnZi6k/2Og4pVGRu1KTpcFGhFblt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqLdSQ%2FbtsMjnnZi6k%2F2Og4pVGRu1KTpcFGhFblt0%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;1786&quot; height=&quot;1786&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;문서 생성 결과 미리보기&lt;/figcaption&gt;
&lt;/figure&gt;
 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 도구 : api-extractor &amp;amp; Docusaurus&lt;/h2&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-style=&quot;style8&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;사용 도구&lt;/td&gt;&lt;td&gt;사용 목적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ms&amp;nbsp;&lt;a href=&quot;https://api-extractor.com/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;api-extractor&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;각 패키지들에 선언된 코드 주석을 기반으로 지정한 경로에 doc model를(.api.json 파일) 생성해줍니다. 주 목적은 패키지 api 변경 사항을 추적하는 report 파일을 생성해주지만, 저는 doc model 생성을 목적으로 사용하였습니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ms&amp;nbsp;&lt;a href=&quot;https://api-extractor.com/pages/overview/demo_docs/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;api-documentor&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;.api.json 파일들을 읽어 md파일로 생성해줍니다.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;meta&amp;nbsp;&lt;a href=&quot;https://docusaurus.io/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;docusaurus&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;추출한 md파일을 문서에 최적화된 문서 생성 툴 docusaurus를 사용하여 웹사이트를 생성하였습니다.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;api-extractor는 TypeScript 라이브러리에서 API를 추출하고 문서화하는 데 사용됩니다. 라이브러리의 공개 API를 분석하고, 변경을 추적하기 용이한 기능도 있지만, 저희는 &lt;b&gt;추출된 API 파일을 이용하여 md파일을 생성해 주는 기능만을 사용하고 있습니다.&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;각 패키지의 빌드된 index.d.ts파일을 읽어 docModel이라는 JSON 파일로 출력하고, api-documentor를 사용하여 이를 md 파일로 변환하였습니다. 이렇게 생성된 md 파일을 스크립트를 통해 구조화하고, Docusaurus를 사용하여 웹사이트를 생성하였습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;초반에는 추출된 API를 @next/mdx를 통해 웹사이트를 구축하였지만, 검색 기능, 소개 페이지, 스타일링 등 많은 기능이 필요하게 되었습니다. 이러한 기능들은 Docusaurus에서 이미 제공하고 있어, Docusaurus를 사용하는 것이 더 효율적이라는 판단 하에 선택하게 되었습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;API Extractor&amp;nbsp;(@microsoft/api-extractor)&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 설치 후 루트경로에 &lt;a href=&quot;https://api-extractor.com/pages/configs/api-extractor_json/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;api-extractor.json&lt;/span&gt;&lt;/a&gt; 파일이 있어야 합니다. 앞서 말했듯 문서 생성 기초 데이터인 docModel 생성만이 목적이기 때문에 apiReport, dtsRollup은 false로 설정하고, docModel을 설정해 줍니다.&lt;br&gt;&lt;br&gt;&lt;span style=&quot;background-color: #f6f7f8;&quot;&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;mainEntryPointFilePath는 분석의 시작점으로 사용되는 파일 경로입니다. 확장자는 d.ts여야 합니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;apiJsonFilePath 은 docModel이 생성될 경로로 따로 지정해주지 않으면&amp;nbsp; &lt;span style=&quot;background-color: #f6f7f8;&quot;&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;&amp;lt;projectFolder&amp;gt;/temp/&amp;lt;unscopedPackageName&amp;gt;. api.json 경로에 생성됩니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&quot;$schema&quot;: &quot;https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json&quot;,
&amp;nbsp;&amp;nbsp;&quot;mainEntryPointFilePath&quot;: &quot;&amp;lt;projectFolder&amp;gt;/dist/index.doc.d.ts&quot;,
&amp;nbsp;&amp;nbsp;&quot;compiler&quot;: {},
&amp;nbsp;&amp;nbsp;&quot;apiReport&quot;: { &quot;enabled&quot;: false },
&amp;nbsp;&amp;nbsp;&quot;dtsRollup&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;enabled&quot;: false
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&quot;tsdocMetadata&quot;: { &quot;enabled&quot;: false },
&amp;nbsp;&amp;nbsp;&quot;docModel&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;enabled&quot;: true,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;apiJsonFilePath&quot;: &quot;./apps/docs/api-extractor/&amp;lt;unscopedPackageName&amp;gt;.api.json&quot;
&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&quot;messages&quot;: {
&amp;nbsp;&amp;nbsp; ...
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;모노레포를 위한 API Extractor&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 모노레포로 여러 패키지를 관리하고 있어서 각 패키지에 script를 등록하고, turbo와 같은 관리 도구를 같이 사용하여 동시 스크립트를 돌려 실행합니다. 또 루트경로에 설정한 api-extractor.json 파일을 단일 패키지 루트경로에도 생성 후 extends 해주시면 추후 유지보수가 편합니다.&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;[root] api-extractor-base.json&lt;/li&gt;&lt;li&gt;[package] packages/[example-package]/api-extractor.json&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// [package] packages/[example-package]/api-extractor.json 
{
&amp;nbsp;&amp;nbsp;&quot;extends&quot;: &quot;../../api-extractor-base.json&quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;API Documenter (@microsoft/api-documenter)&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 docModel 파일들을 읽어 md 파일을 생성해 줍니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2034&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgsmGU/btsMjB75elq/zyN2gYGE7IuWwaLRTkIkX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgsmGU/btsMjB75elq/zyN2gYGE7IuWwaLRTkIkX1/img.png&quot; data-alt=&quot;초기 docModel로 생성 된 md파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgsmGU/btsMjB75elq/zyN2gYGE7IuWwaLRTkIkX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgsmGU%2FbtsMjB75elq%2FzyN2gYGE7IuWwaLRTkIkX1%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;2034&quot; height=&quot;1186&quot; data-origin-width=&quot;2034&quot; data-origin-height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초기 docModel로 생성 된 md파일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;문제는 위 생성된 md파일에서 코드블록 밖에서 특정 문자가 들어갈 경우 ( &amp;lt;!-- --&amp;gt;, * 등) docusaurus &amp;nbsp;실행시 깨지거나 가독성이 좋지 않아 보였습니다.&amp;nbsp; &amp;nbsp;또 docusaurus에는 브레드 크럼블을 메타태그, 타이틀 정보로 생성이 되어 상단 md구문도 불필요해 보였습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;해당 이슈를 한 번에 관리하기 위해 찾아보던 중&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;api-documenter에서 plugin을 등록할&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;수 있는 점을 알아냈고,&amp;nbsp;&lt;/span&gt;&lt;br&gt;제공해 주는 함수 중&amp;nbsp;MarkdownDocumenterFeature의&amp;nbsp; onBeforeWritePage를 사용하면 md파일이 생성되기 전에 작성된 로직을 실행하게 할 수 있었습니다. (&lt;a href=&quot;https://api.rushstack.io/pages/api-documenter.iapidocumenterpluginmanifest/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;문서 참조&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;)&lt;br&gt;&amp;nbsp;&lt;br&gt;플러그인에 실행할 부분은 자유롭게 할 수 있지만, 이슈 방지를 위해 아래 문자들은 코드블록 밖에서 제거해 주시면 됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;const REPLACERS = new Map&amp;lt;RegExp, string&amp;gt;([
&amp;nbsp;&amp;nbsp;[/\\\*/g, '*'],
&amp;nbsp;&amp;nbsp;[/\\_/g, '_'],
&amp;nbsp;&amp;nbsp;[/\\`/g, '`'],
&amp;nbsp;&amp;nbsp;[/\\\[/g, '['],
&amp;nbsp;&amp;nbsp;[/\\\]/g, ']'],
&amp;nbsp;&amp;nbsp;[/&amp;lt;!-- --&amp;gt;/g, ''],
&amp;nbsp;&amp;nbsp;[/&amp;lt;!--[\s\S]*?--&amp;gt;/g, ''],
&amp;nbsp;&amp;nbsp;[/\{([^}]+)\}/g, '\\{$1\\}'],
])&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;좌측에 우측이 플러그인 실행 후 변환 된 md파일입니다. 해당 파일을 docusaurus에 실행하면 배포했을 때도 예쁘게 보입니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/16RhS/btsMiY3MeVv/CSr9FnS49wVk4YW3HG0TK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/16RhS/btsMiY3MeVv/CSr9FnS49wVk4YW3HG0TK1/img.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;1450&quot; style=&quot;width: 69.9393%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/16RhS/btsMiY3MeVv/CSr9FnS49wVk4YW3HG0TK1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F16RhS%2FbtsMiY3MeVv%2FCSr9FnS49wVk4YW3HG0TK1%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;2066&quot; height=&quot;1450&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rilKg/btsMllWMrJu/C0jaAonaJwTyc2VVQZsXtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rilKg/btsMllWMrJu/C0jaAonaJwTyc2VVQZsXtK/img.png&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;1702&quot; style=&quot;width: 28.8979%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rilKg/btsMllWMrJu/C0jaAonaJwTyc2VVQZsXtK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrilKg%2FbtsMllWMrJu%2FC0jaAonaJwTyc2VVQZsXtK%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;1002&quot; height=&quot;1702&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;docusaurus 배포&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;카테고라이징을 위한 커스텀 스크립트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 api-documentor를 사용하면 md파일이 잘 생성이 되지만, 파일의 폴더 뎁스를 좀 더 분류하고 싶었습니다. 때문에 카테고라이징 하는 부분에 있어서 패키지별, 용도별로 분류를 하기 위해 @category 태그를 추가하였습니다.&lt;br&gt;주석에만 @category태그를 통해 어떤 폴더에 들어갈 건지 명시를 해주면 추후 스크립트에 해서 해당 폴더명으로 분류가 됩니다.&lt;br&gt;&lt;br&gt;예를 들어 paginate.ts 파일의 tsdoc 주석 안에서&lt;br&gt;@category Utils/Array라고 적어주면 스크립트가 돈 후 아래 이미지처럼 분류가 됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;/**
 * @category Utils/Array
*/&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qjqqZ/btsMiHN7X6d/uGR6FclX5gN40djxktAbCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qjqqZ/btsMiHN7X6d/uGR6FclX5gN40djxktAbCK/img.png&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;354&quot; style=&quot;width: 34.2439%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qjqqZ/btsMiHN7X6d/uGR6FclX5gN40djxktAbCK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqjqqZ%2FbtsMiHN7X6d%2FuGR6FclX5gN40djxktAbCK%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;494&quot; height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBojjm/btsMiXpP5gO/sTIUnlY1xOo9lHF98NAIF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBojjm/btsMiXpP5gO/sTIUnlY1xOo9lHF98NAIF0/img.png&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;310&quot; style=&quot;width: 64.5933%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBojjm/btsMiXpP5gO/sTIUnlY1xOo9lHF98NAIF0/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBojjm%2FbtsMiXpP5gO%2FsTIUnlY1xOo9lHF98NAIF0%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;816&quot; height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;최종적으로 스크립트까지 실행하고 나면 아래와 같이 폴더 분류가 깔끔하게 됩니다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4nr3g/btsMjSaqHvT/suzrZiqyKKAAzIeJV6U0Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4nr3g/btsMjSaqHvT/suzrZiqyKKAAzIeJV6U0Q1/img.png&quot; data-alt=&quot;카테고라이징 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4nr3g/btsMjSaqHvT/suzrZiqyKKAAzIeJV6U0Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4nr3g%2FbtsMjSaqHvT%2FsuzrZiqyKKAAzIeJV6U0Q1%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;1532&quot; height=&quot;1802&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;카테고라이징 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 스크립트 : gen:doc&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;gen:doc이라는 스크립트 명령어를 치면 아래 파일이 실행이 되는데요. 수정을 하면서 추가되는 스크립트가 많아지고, 자동화와 유지보수를 위해 flow를 통해 흐름을 한눈에 파악할 수 있도록 구분했습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// apps/docs/scripts/generate-api-docs/index.ts
flow(
&amp;nbsp;&amp;nbsp;// 패키지들을 설치 후 빌드합니다.
&amp;nbsp;&amp;nbsp;flow(installPackages, awaited(successLog('install packages'))),
&amp;nbsp;&amp;nbsp;awaited(flow(buildPackages, awaited(successLog('build packages')))),
&amp;nbsp;&amp;nbsp;// curring 함수에서 타입 정의가 여러 개로 나와서 하나로만 나오게 하는 과정 
&amp;nbsp;&amp;nbsp;// 카테고리 분석을 위한 카테고리를 메모리에 저장 
&amp;nbsp;&amp;nbsp;awaited(flow(handleDts, successLog('handle dts'))),
&amp;nbsp;&amp;nbsp;// api-extractor 실행 &amp;gt; json 파일 생성 
&amp;nbsp;&amp;nbsp;awaited(flow(buildApiExtractorJson, successLog('build api-extractor.json'))),
&amp;nbsp;&amp;nbsp;// 생성된 api-extractor json 파일중 parameterName에 개행문자가 포함된 경우 제거
&amp;nbsp;&amp;nbsp;awaited(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;flow(handleApiExtractorJson, successLog('handle api-extractor.json')),
&amp;nbsp;&amp;nbsp;),
&amp;nbsp;&amp;nbsp;// api-documenter 실행 &amp;gt; md 파일 생성 
&amp;nbsp;&amp;nbsp;awaited(flow(buildApiDocs, awaited(successLog('build api docs markdown')))),
&amp;nbsp;&amp;nbsp;// 테이블 그룹화 및 메모리에 저장 된 카테고리 리턴
&amp;nbsp;&amp;nbsp;awaited(flow(handleIndexMarkdown, successLog('handle index markdown'))),
&amp;nbsp;&amp;nbsp;// 카테고리를 통한 폴더 분류 작업
&amp;nbsp;&amp;nbsp;awaited(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;flow(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;handleApiDocFolderStructure,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;successLog('handle api-doc folder structure'),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;),
&amp;nbsp;&amp;nbsp;),
&amp;nbsp;&amp;nbsp;awaited(flow(cleanDts, successLog(`index.doc.d.ts file has been cleaned.`))),
)()&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docusaurus + Algoria&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Docusaurus에서 검색엔진은 문서에서 추천하는 Algoria를 사용해 무료로 사용하고 있습니다. 배포를 시키면 인덱싱을 해두어 검색리스트에 반영이 되는 구조입니다. 자세한 과정은 &lt;a href=&quot;https://docusaurus.io/docs/search&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;공식문서(Search)를&lt;/span&gt;&lt;/a&gt; 참조해 주세요.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;1136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM32f4/btsMlmBn4vR/4AsFq4qckOY8PM8UPAxanK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM32f4/btsMlmBn4vR/4AsFq4qckOY8PM8UPAxanK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM32f4/btsMlmBn4vR/4AsFq4qckOY8PM8UPAxanK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM32f4%2FbtsMlmBn4vR%2F4AsFq4qckOY8PM8UPAxanK%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;1718&quot; height=&quot;1136&quot; data-origin-width=&quot;1718&quot; data-origin-height=&quot;1136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github Action + Changesets + Vercel 배포&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 패키지 배포 관리를 Changesets으로 하고 있습니다. 배포 후 Changesets에서 제공해 주는 outputs 상태를 통해 npm 패키지에 성공적으로 퍼블리시가 되면 그때 composite action으로 분리한 generate documentation action을 실행해 주어 불필요한 액션을 방지했습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;Generate Documentation 액션 과정이 길어지면서 action을 분리할 방법을 찾던 중 composite action에 대해 알게 되었는데 굉장히 유용하게 썼습니다. 관심 있으신 분들을 위해 공식문서를 첨부합니다.&lt;/p&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://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;Creating&amp;nbsp;a&amp;nbsp;composite&amp;nbsp;action&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// .github/workflows/release.yml
 ....
 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: Generate documentation
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if: steps.changesets.outputs.published == 'true'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;uses: ./.github/composite-actions/gen-docs&lt;/code&gt;&lt;/pre&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;1016&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYl0QO/btsMkvFRkHE/THwV6ipTiU0TQSAueyEbW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYl0QO/btsMkvFRkHE/THwV6ipTiU0TQSAueyEbW0/img.png&quot; data-alt=&quot;composite action&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYl0QO/btsMkvFRkHE/THwV6ipTiU0TQSAueyEbW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYl0QO%2FbtsMkvFRkHE%2FTHwV6ipTiU0TQSAueyEbW0%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;1364&quot; height=&quot;1016&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;1016&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;composite action&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Algolia&amp;nbsp;Crawler&amp;nbsp;Github&amp;nbsp;Action&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Algolia Search도 크롤러 action이 있어서 해당 부분도 적용해 배포 시에 크롤러가 실행되게끔 하였습니다.&amp;nbsp; 설정법은 &lt;a href=&quot;https://github.com/algolia/algoliasearch-crawler-github-actions&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;문서&lt;/span&gt;&lt;/a&gt;를 참조해 주세요.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;/p&gt;&lt;h2 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;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서 자동화 플로우를 완성한 이후로는 이슈나 업데이트 등으로 패키지 버전을 올릴 때 GitHub Action을 통해 gen:doc 명령어 하나로 최신 문서가 자동으로 생성되고, Docusaurus를 통해 웹사이트가 빌드되며, Vercel을 통해 배포까지 자동화되었습니다. 이제는 새 기능 추가나 수정 사항 반영 후 번거로운 문서 작업 없이도 최신 상태의 문서가 항상 유지가 되어 유지보수가 매우 편안해진 경험이었습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;물론 레퍼런스가 많지는 않고, Docusaurus와 같이 빌드하며 이슈도 많이 발생했고, 또 커스텀이 생각보다 많이 되었고 쓰여진 글을 보니 꽤 복잡해보이지만, 커스텀은 대부분 깔끔하게 보이기 위한 저희의 욕심이었기 때문에 해당 부분은 너무 큰 목적이 아니라면 간단한 공식문서만큼 러닝커브는 그리 크지 않았다고 생각합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;저는 문서 자동화를 진행하면서 새롭게 알게된 것들이 너무 많고, 로직이 복잡해지면서 적용했던 함수형 프로그래밍에 대해서도 좀 더 익숙해지는 시간이었습니다. 다음에는 이 부분에 대해 다뤄봐도 재밌을 것 같아요.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;긴 글 읽어주셔서 감사합니다. &lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;Reference&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&amp;nbsp;&lt;a href=&quot;https://api-extractor.com/pages/setup/invoking/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;API Extractor 공식문서&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>api documenter</category>
      <category>api extractor</category>
      <category>docmodel</category>
      <category>tsdoc 문서생성</category>
      <category>문서자동생성</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/202</guid>
      <comments>https://algoroot.tistory.com/202#entry202comment</comments>
      <pubDate>Fri, 20 Dec 2024 00:07:17 +0900</pubDate>
    </item>
    <item>
      <title>[귀찮은건 질색이야-1] 24개 패키지 버전 관리 자동화 시키기: Changesets</title>
      <link>https://algoroot.tistory.com/196</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;br /&gt;안녕하세요. 오늘은 제가 현재 현업에 종사하고 있는 똑똑한개발자의 시스템 팀으로서 모노레포(Monorepo) 환경에서 효율적인 배포 플로우를 구축한 경험들을 공유하고자 합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/span&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;제목이 조금 심심해서 고민하던 중, 어릴 적 즐겨보던 [무서운게 딱 좋아] 시리즈가 생각이 났어요.. 스물아홉의 전 [귀찮은게 질색이야] 편으로 써볼까 합니다.&amp;nbsp;&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;조금 개인적인 이야기를 쓰자면 어릴 적 제 별명이 &amp;radic;2(루트2, 2의 제곱근) 였어요. 친구들과 코너 길에서 길을 걸을 때 &quot;대각선으로 걸어다니면&amp;radic;2를 절약할 수 있어&quot; 라고 했더니 이후로 친구들이 그렇게 부르고는 했었습니다. 학교가는 길도 신호등 시간까지 기억하고 시간 맞춰 등교하기도 하고요. 유난이었죠.. 지금은 윤리적 인 사고가 쌓여서 그렇게까지 하지는 않지만 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(그 때는 잔디를 밟고 대각선으로 가고 그랬어요)&lt;/span&gt;, 아무튼 여전히 효율적인 작업을 추구하는 편입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-09-07 at 12.41.50 AM.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfj6Ke/btsJuE6NxRI/r895eu3DcQVK7a6bHZIx2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfj6Ke/btsJuE6NxRI/r895eu3DcQVK7a6bHZIx2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfj6Ke/btsJuE6NxRI/r895eu3DcQVK7a6bHZIx2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfj6Ke%2FbtsJuE6NxRI%2Fr895eu3DcQVK7a6bHZIx2K%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;300&quot; height=&quot;100&quot; data-filename=&quot;Screenshot 2024-09-07 at 12.41.50 AM.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;318&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;당연히 일을 할 때도 그렇고요. 일을 할 때는 저 뿐만 아니라 아마 모두가 좋아하시는 단어이지 않을까 싶습니다. 그래서 저는 개발자로서 귀찮음을 도와주었던 작업, 도구, 방법 등 종류를 가리지 않고 [귀찮은건 질색이야] 시리즈를 올려볼 생각입니다. 꼭 기술적인 부분이 아니더라도요.&amp;nbsp;&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;서론이 조금 길었습니다. 거두절미하고 오늘 이&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;글에서는 모노레포를 관리하면서, Changesets을 통해 패키지 배포 자동화를 어떻게 구현했는지 소개해드리겠습니다. (기본적인 배포 플로우 설명은 덤!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;모노레포란 여러 패키지, 애플리케이션, 또는 모듈을 하나의 대규모 저장소에서 관리할 수 있도록 구성한 환경을 말합니다. 똑똑한개발자의 모노레포에는 npm에 배포된 다양한 패키지들, 보일러플레이트, 그리고 패키지 문서를 위한 앱 등이 포함되어 있습니다. 여기서 패키지들은 26개를 관리하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;틈새 어필:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.npmjs.com/settings/toktokhan-dev/packages?page=2&amp;amp;perPage=10&quot;&gt;@toktokhan-dev 패키지들을 확인해 보세요!&lt;/a&gt;&lt;/blockquote&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 많은 패키지를 각기 버전 관리하고 배포하는 것은 굉장히 번거로운 작업입니다. 일일이 수동으로 처리해야 할까요? 한두 개 정도는 금방 하겠지만 무려 26개나 되는걸요.. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-08-29 at 6.12.19 PM.png&quot; data-origin-width=&quot;2362&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btIQaN/btsJnXTsS5P/8eoHhvtJEp32tEL5JHyACk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btIQaN/btsJnXTsS5P/8eoHhvtJEp32tEL5JHyACk/img.png&quot; data-alt=&quot;toktokhan-dev package list&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btIQaN/btsJnXTsS5P/8eoHhvtJEp32tEL5JHyACk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtIQaN%2FbtsJnXTsS5P%2F8eoHhvtJEp32tEL5JHyACk%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;2362&quot; height=&quot;1334&quot; data-filename=&quot;Screenshot 2024-08-29 at 6.12.19 PM.png&quot; data-origin-width=&quot;2362&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;toktokhan-dev package list&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;사실 이 부분에 있어서 저희는 처음부터 자동화 작업을 고려하고 있었습니다. 이미 모노레포를 사용하기 이전부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/semantic-release/semantic-release&quot;&gt;Semantic Release&lt;/a&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;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;패키지 수정부터 배포까지&lt;/h2&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 패키지 수정&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@toktokhan-dev/cli-plugin-gen-route-pages라는 패키지가 있습니다. 해당 패키지는 @toktokhan-dev/cli 에 등록할 수 있는 플러그인으로, nextjs 프레임워크의 page 라우터 버전에서 사용할 수 있어요. 이 패키지를 사용하면 &amp;nbsp;pages 폴더를 조회하여 route 경로를 포함한 객체를 생성해 줍니다. 타이핑으로 경로를 가져오는 것이 아닌, 객체의 key 값을 가져옴으로써 편의성과 안정성을 제공하고 있는 패키지입니다.&amp;nbsp;&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;아래의 경로를&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725281286480&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;pages
├── index.tsx
├── login
│   └── index.tsx
└── post
    └── [id].tsx&lt;/code&gt;&lt;/pre&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;이렇게 객체 상수로 변환을 해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725281286480&quot; class=&quot;dts&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;export const ROUTES = {
  MAIN: &quot;/&quot;,
  LOGIN_MAIN: &quot;/login&quot;,
  SOCIAL_CALLBACK: &quot;/social/callback&quot;,
  POST: {
   dynamic_id: {
     MAIN: &quot;/post/[id]&quot;,
   }
  }
}
};&lt;/code&gt;&lt;/pre&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;위의 변환된 객체 상수에서 이상한 점이 보이시나요? dynamic id에 해당하는 경로의 key값이 소문자로 변환이 되었습니다. 이는 일관되지 않은 변환 결과로 이슈로 판단이 되네요. 대문자로 수정을 해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-08-29 at 7.27.14 PM.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u0GWe/btsJoApZjnE/hTHKKGlxmi43a4kyw562Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u0GWe/btsJoApZjnE/hTHKKGlxmi43a4kyw562Mk/img.png&quot; data-alt=&quot;issue fix&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u0GWe/btsJoApZjnE/hTHKKGlxmi43a4kyw562Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu0GWe%2FbtsJoApZjnE%2FhTHKKGlxmi43a4kyw562Mk%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;2100&quot; height=&quot;448&quot; data-filename=&quot;Screenshot 2024-08-29 at 7.27.14 PM.png&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;issue fix&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 버전 업데이트&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;수정한&amp;nbsp;내용을&amp;nbsp;반영하여&amp;nbsp;패키지&amp;nbsp;버전을&amp;nbsp;업데이트할&amp;nbsp;차례입니다.&amp;nbsp;Node.js&amp;nbsp;생태계에서는&amp;nbsp;npm을&amp;nbsp;통해&amp;nbsp;패키지를&amp;nbsp;관리하며,&amp;nbsp;이는&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://semver.org/&quot;&gt;Semantic&amp;nbsp;Versioning&lt;/a&gt;을&amp;nbsp;따릅니다.&amp;nbsp;따라서,&amp;nbsp;이번&amp;nbsp;수정&amp;nbsp;사항에&amp;nbsp;맞게&amp;nbsp;적절한&amp;nbsp;버전으로&amp;nbsp;업데이트를&amp;nbsp;진행해야&amp;nbsp;합니다.&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;이번 수정은 간단한 버그 수정이며, 새로운 기능을 추가한 것은 아니기 때문에 PATCH 버전에 해당합니다. 현재 해당 패키지의 버전이 v0.0.10이라면,&amp;nbsp; v0.0.11로 업데이트하는 것이 적절합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/61lWZ/btsJm9AiukU/karluKhl9qXWTXURoaFHk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/61lWZ/btsJm9AiukU/karluKhl9qXWTXURoaFHk0/img.png&quot; data-alt=&quot;package.json version&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/61lWZ/btsJm9AiukU/karluKhl9qXWTXURoaFHk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F61lWZ%2FbtsJm9AiukU%2FkarluKhl9qXWTXURoaFHk0%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;1010&quot; height=&quot;121&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;121&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;package.json version&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. Changelog 작성 및 Release 및 Tag 등록&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후에는 어떤 변경사항이 발생했는지, Release Note와 ChangeLog를 를 적어주면 되는데요.&lt;/p&gt;
&lt;p id=&quot;ea3d&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;수동 작업 기준으로 설명을 드리면, Release를 만들기 위해서는 tag등록도 필요하고, Changelog를 만들기 위해서는 하나의 CHANGELOG.md에 버전과 변경 내용을 써줘야 합니다. 아래 이미지처럼요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biZMO0/btsJnuEhQu6/eB2xL7k47eLZMNuA9bBrA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biZMO0/btsJnuEhQu6/eB2xL7k47eLZMNuA9bBrA0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;1604&quot; data-filename=&quot;Screenshot 2024-09-03 at 12.24.52 AM.png&quot; style=&quot;width: 48.6459%; margin-right: 10px;&quot; data-widthpercent=&quot;49.22&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biZMO0/btsJnuEhQu6/eB2xL7k47eLZMNuA9bBrA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiZMO0%2FbtsJnuEhQu6%2FeB2xL7k47eLZMNuA9bBrA0%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;1884&quot; height=&quot;1604&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SAubK/btsJptjn7hB/578wAHbKKcUkjRJBdbqDIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SAubK/btsJptjn7hB/578wAHbKKcUkjRJBdbqDIk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;623&quot; data-filename=&quot;Screenshot 2024-09-03 at 12.21.18 AM.png&quot; style=&quot;width: 50.1913%;&quot; data-widthpercent=&quot;50.78&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SAubK/btsJptjn7hB/578wAHbKKcUkjRJBdbqDIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSAubK%2FbtsJptjn7hB%2F578wAHbKKcUkjRJBdbqDIk%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;755&quot; height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) Release (우) Changelog&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;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 준비가 완료되었으니, 비로소 배포를 할 수 있게 되었습니다. npm publish 명령어를 사용하면 package.json버전을 읽어 배포가 됩니다.&amp;nbsp;&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;자동화의 필요성: Changesets&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 하나의 패키지를 배포하는 과정을 설명드렸습니다. Changelog나 Release 같은 작업이 배포 과정에서 꼭 필요한 요소는 아닙니다. 하지만 누구나 사용할 수 있는 공개된 패키지라면, 사용자에게 버전 업데이트 내용을 문서로 전달하는 작업은 필수적이라고 생각합니다. 게다가 로그가 쌓이면 개발자 입장에서 관리도 훨씬 편리해지죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;7721&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;만약 이 과정이 자동화될 수 있다면 얼마나 좋을까요? 이제 Changesets를 소개할 때가 된 것 같네요 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Changesets&amp;nbsp;소개&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;Changesets는 프로젝트에서 버전 관리와 변경 로그 생성을 도와주는 도구입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;앞서 설명드린 배포 과정의 2~4단계를 자동으로 처리해 줍니다.. &lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;물론, Changesets의 장점은 이 뿐만이 아닙니다. 다른 장점들은 이전에 사용했던 Semantic-release와 비교해 가며 설명드릴게요.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;6132&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;semantic-release&lt;/b&gt;는 커밋 메시지 기반으로 버전을 자동으로 결정하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Changesets&lt;/b&gt;은 개발자가 직접 변경 사항에 대한 설명과 버전 업데이트의 종류(major, minor, patch)를 결정할 수 있습니다.&lt;/li&gt;
&lt;li id=&quot;371c&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;semantic-release&lt;/b&gt;는 별도의 플러그인을 통해 모노레포를 지원하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Changesets&lt;/b&gt;은 기본적으로 모노레포를 지원합니다.&lt;/li&gt;
&lt;li id=&quot;2e78&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;semantic-release&lt;/b&gt;는 CI 환경에서 main브랜치에 병합될 때마다 자동으로 버전을 업데이트하지만 Changesets은./changesets폴더의 임시 md파일을 읽어 해당 md파일이 포함된 커밋이 병합이 되면, [Version Packages]라는 PR생성을 통해 배포여부를 결정할 수 있습니다.&lt;br /&gt;note. 이 기능은 Changesets GitHub App &amp;amp; Changesets GitHub Action의 조합으로 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;http://%20https://algoroot.tistory.com/184&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;보일러플레이트 모노레포 적용기&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;직접 느낀 Changesets 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 Semantic-release에서 changesets으로 변경하게 된 이유, 제가 만든 툴은 아니지만 어필하고 싶은 changesets의 장점은&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;개발자가 직접 변경 사항에 대한 설명과 버전 업데이트의 종류(major, minor, patch)를 결정할 수 있으며, 커밋 기반으로 md파일을 쌓을 수 있고(중요) 이 커밋들이 main(기준 브랜치)에 병합이 되면, [Version Packages] PR이 생성되고, 그 PR을 머지하기 전까지는 계속 해당 PR에 커밋기반 md정보가 쌓이며, 개발자가 merge를 하면 비로소 배포가 된다는 점...!입니다. (장점이&amp;nbsp;많아&amp;nbsp;문장&amp;nbsp;호흡이&amp;nbsp;기네요 )&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;단점이란 게 있다면 공식 문서가 너무나 간단합니다. 문서조차 md파일입니다. 그렇다고 구현하기에 부족한 문서는 아닙니다. 커뮤니티도 활성화되어있고요.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/changesets/changesets&quot;&gt;Changesets 공식문서&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Semantic-release은 slack action bot을 제공하는데, changesets은 따로 구현해야 한다는 점도 조금 아쉽긴 한 부분이네요.&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;그리고 Changesets은 단독으로 사용하기보다는 &lt;a href=&quot;https://github.com/apps/changeset-bot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Changesets Github App&lt;/a&gt;,&amp;nbsp; &lt;a href=&quot;https://github.com/changesets/action&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Changesets Github Action과&lt;/a&gt; 통합하여 사용해야만 제가 말씀드렸던 장점들에 공감하실 수 있을 것 같습니다.&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Changesets 배포 과정&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;준비. changesets 설치 및 config 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;자세한 설치와 설정 과정은 생략되었습니다. 실 사용하실 분들께서는 &lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/changesets/changesets&quot;&gt;Changesets 공식문서&lt;/a&gt;를 참고해 주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1725294788059&quot; class=&quot;json&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://unpkg.com/@changesets/config@3.0.0/schema.json&quot;,
  &quot;changelog&quot;: [
    &quot;@changesets/changelog-github&quot;, 
    { &quot;repo&quot;: &quot;TOKTOKHAN-DEV/toktokhan-dev&quot; }
  ],
  &quot;commit&quot;: false,
  &quot;fixed&quot;: [],

  &quot;linked&quot;: [],
  &quot;access&quot;: &quot;public&quot;,
  &quot;baseBranch&quot;: &quot;main&quot;,
  &quot;updateInternalDependencies&quot;: &quot;patch&quot;,
  &quot;ignore&quot;: [&quot;Toktoken&quot;, &quot;@toktokhan-dev/docs&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;config.json 에서 changelog에 대해&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 변경사항 문서 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 pnpm changeset 명령어를 입력하면, Changesets가 지정된 패키지 스코프 안에서 변경사항을 감지하고 대화형 프롬프트를 실행합니다. 변경사항이 있는 패키지가 우선으로 뜨기 때문에 선택도 편하고, 다음 프롬프트로 넘어가게 되면 버전 종류(major, minor, patch)도 지정할 수 있습니다. 심지어 간략하게 description을 작성할 수도 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YkapI/btsJpq1fHEG/eo34NaCAOJBTkDJD0DYMK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YkapI/btsJpq1fHEG/eo34NaCAOJBTkDJD0DYMK0/img.png&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.09.49 AM.png&quot; data-origin-height=&quot;225&quot; data-origin-width=&quot;826&quot; data-is-animation=&quot;false&quot; style=&quot;width: 43.0208%; margin-right: 10px;&quot; data-widthpercent=&quot;43.53&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YkapI/btsJpq1fHEG/eo34NaCAOJBTkDJD0DYMK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYkapI%2FbtsJpq1fHEG%2Feo34NaCAOJBTkDJD0DYMK0%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;826&quot; height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CA8C2/btsJntSTCNU/RaIF2UMgBwT2rTfwYmoMkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CA8C2/btsJntSTCNU/RaIF2UMgBwT2rTfwYmoMkk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;173&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.13.25 AM.png&quot; style=&quot;width: 55.8164%;&quot; data-widthpercent=&quot;56.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CA8C2/btsJntSTCNU/RaIF2UMgBwT2rTfwYmoMkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCA8C2%2FbtsJntSTCNU%2FRaIF2UMgBwT2rTfwYmoMkk%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;824&quot; height=&quot;173&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Changeset 변경사항 문서 작성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 변경사항 커밋&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 사항을 문서화하면. changeset 경로에 임의의 md 파일이 생성됩니다. 이 파일을 커밋에 포함시키면 되며, 커밋 메시지는 변경 사항을 잘 설명할 수 있도록 작성하는 것이 좋습니다. 저희는 사내 CLI 플러그인을 통해 이 md 파일의 내용을 읽어, 커밋 시 자동으로 메시지 입력을 제안하는 기능을 개발했습니다. (&lt;a href=&quot;https://www.google.com/search?q=%40toktokhan-dev%2Fcli-plugin-commit&amp;amp;rlz=1C5CHFA_enKR1013KR1013&amp;amp;oq=%40toktokhan-dev%2Fcli-plugin-commit&amp;amp;gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIGCAEQRRg8MgYIAhBFGDrSAQcxMzdqMGo5qAIAsAIB&amp;amp;sourceid=chrome&amp;amp;ie=UTF-8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;패키지 보러 가기)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnwbwk/btsJpIHmY4u/GjmV89amBN8Qk5ofsZOIh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnwbwk/btsJpIHmY4u/GjmV89amBN8Qk5ofsZOIh0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;152&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.15.50 AM.png&quot; style=&quot;width: 44.9804%; margin-right: 10px;&quot; data-widthpercent=&quot;45.51&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnwbwk/btsJpIHmY4u/GjmV89amBN8Qk5ofsZOIh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnwbwk%2FbtsJpIHmY4u%2FGjmV89amBN8Qk5ofsZOIh0%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;832&quot; height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/luhOr/btsJprFQKbu/ke6BYgOf3u3O2T1McDj7ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/luhOr/btsJprFQKbu/ke6BYgOf3u3O2T1McDj7ek/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;65&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.19.19 AM.png&quot; style=&quot;width: 53.8568%;&quot; data-widthpercent=&quot;54.49&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/luhOr/btsJprFQKbu/ke6BYgOf3u3O2T1McDj7ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FluhOr%2FbtsJprFQKbu%2Fke6BYgOf3u3O2T1McDj7ek%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;426&quot; height=&quot;65&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) 생성된 md (우) @toktokhan-dev/cli-plugin-commit&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Pull Request 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경사항을 커밋한 후, 메인 브랜치로의 변경 사항을 병합하기 위해 Pull Request를 생성합니다. 이 PR에서 Changesets가 생성한 문서가 자동으로 포함되어 배포될 준비가 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.24.46 AM.png&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;1592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwMwT4/btsJpLKRhS8/VKrAoqOLQ8YzMmKxj9sCC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwMwT4/btsJpLKRhS8/VKrAoqOLQ8YzMmKxj9sCC1/img.png&quot; data-alt=&quot;Version Packages PR&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwMwT4/btsJpLKRhS8/VKrAoqOLQ8YzMmKxj9sCC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwMwT4%2FbtsJpLKRhS8%2FVKrAoqOLQ8YzMmKxj9sCC1%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;1948&quot; height=&quot;1592&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.24.46 AM.png&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;1592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Version Packages PR&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. PR 머지 및 자동 배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 머지하면 Changesets는 main 브랜치에 병합된 문서들을 바탕으로 자동으로 버전을 업데이트하고, Changelog 및 Release 노트를 생성해 줍니다. 이때 1번의 과정에서 생성하고 커밋한 md파일은 삭제됩니다. (해당 md파일은 단순 문서 작성 용도로만 쓰인 임시 파일이기 때문이에요.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.29.09 AM.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1BLcb/btsJnZ4RhIB/OkmpkMzsOZRuFgDEY7Il2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1BLcb/btsJnZ4RhIB/OkmpkMzsOZRuFgDEY7Il2k/img.png&quot; data-alt=&quot;PR merge&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1BLcb/btsJnZ4RhIB/OkmpkMzsOZRuFgDEY7Il2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1BLcb%2FbtsJnZ4RhIB%2FOkmpkMzsOZRuFgDEY7Il2k%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;685&quot; height=&quot;274&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.29.09 AM.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PR merge&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 새롭게 업데이트된 패키지가 NPM에 배포되게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.30.56 AM.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOe7q2/btsJn7Pozi0/8ikIgkE00N3yuXzqwKr2n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOe7q2/btsJn7Pozi0/8ikIgkE00N3yuXzqwKr2n0/img.png&quot; data-alt=&quot;Publish npm Package&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOe7q2/btsJn7Pozi0/8ikIgkE00N3yuXzqwKr2n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOe7q2%2FbtsJn7Pozi0%2F8ikIgkE00N3yuXzqwKr2n0%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;1920&quot; height=&quot;930&quot; data-filename=&quot;Screenshot 2024-09-03 at 1.30.56 AM.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Publish npm Package&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Tips&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Tip #1 : 기여자에게 감사 태그 및 관련 PR 링크를 자동으로 추가할 수 있어요.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;@changesets/changelog-github&quot; 모듈을 사용하면, CHANGELOG.md와 Release note에 기여자에게 감사태그와, 관련 PR링크까지 걸어줍니다. 다만 '@changesets/cli'를 설치하면 제공되는 '@changesets/chagelog-git'모듈과는 역할이 다르니 &lt;b&gt;별도 패키지 설치&lt;/b&gt;가 필요합니다.&amp;nbsp; (&lt;a href=&quot;https://github.com/changesets/changesets/blob/main/docs/config-file-options.md#changelog-false-or-a-path&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Docs-changesets/changelog&lt;/a&gt; )&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WC5iQ/btsJqppwPSc/3oKxj0Uw2Un71schkTeyI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WC5iQ/btsJqppwPSc/3oKxj0Uw2Un71schkTeyI0/img.png&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;349&quot; data-is-animation=&quot;false&quot; style=&quot;width: 58.0478%; margin-right: 10px;&quot; data-widthpercent=&quot;58.73&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WC5iQ/btsJqppwPSc/3oKxj0Uw2Un71schkTeyI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWC5iQ%2FbtsJqppwPSc%2F3oKxj0Uw2Un71schkTeyI0%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;832&quot; height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSZNwx/btsJrpoXzCd/cIOxN7p2czkBkgoPKU5DkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSZNwx/btsJrpoXzCd/cIOxN7p2czkBkgoPKU5DkK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;431&quot; data-filename=&quot;Screenshot 2024-09-05 at 1.43.08 PM.png&quot; style=&quot;width: 40.7894%;&quot; data-widthpercent=&quot;41.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSZNwx/btsJrpoXzCd/cIOxN7p2czkBkgoPKU5DkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSZNwx%2FbtsJrpoXzCd%2FcIOxN7p2czkBkgoPKU5DkK%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;722&quot; height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725511618510&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ./changeset/config.json
{
 ...
 &quot;changelog&quot;: [
    &quot;@changesets/changelog-github&quot;,
    { &quot;repo&quot;: &quot;TOKTOKHAN-DEV/toktokhan-dev&quot; }
  ],
 }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Tip #2: npm에 배포하고 싶지는 않은데 버전관리는 하고 싶어요.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Changesets의 tag 명령어를 사용하면 npm에 패키지를 게시하지 않고도 버전 관리를 할 수 있습니다. 저희 사내 프런트엔드 템플릿은 Git 태그를 통해 버전 관리를 하고 있으며, 이를 활용해 각 버전별로 템플릿을 관리하고 다운로드할 수 있는 Tokit도 개발했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[틈새 어필] Tokit 은 똑똑한개발자에서 관리하는 프런트엔드 보일러 템플릿을 관리하고, 로컬 환경에 설치하기 위한 CLI tool입니다. create-react-app처럼 커멘드라인 한 줄로 쉽게 설치가 가능하며 원격 레포지토리에 생성하고, 연동시킬 수 있는 옵션이 있습니다. [&lt;a href=&quot;https://toktokhan-dev-docs.vercel.app/docs/docs/tokit/Overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tokit 가이드 보러 가기]&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCWeET/btsJqQtRQ1l/OFd0dys5elJAG4qWBFtgmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCWeET/btsJqQtRQ1l/OFd0dys5elJAG4qWBFtgmK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;374&quot; data-filename=&quot;Screenshot 2024-09-05 at 2.26.33 PM.png&quot; style=&quot;width: 49.9152%; margin-right: 10px;&quot; data-widthpercent=&quot;50.5&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCWeET/btsJqQtRQ1l/OFd0dys5elJAG4qWBFtgmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCWeET%2FbtsJqQtRQ1l%2FOFd0dys5elJAG4qWBFtgmK%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;450&quot; height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UauVf/btsJrTpEYMS/g3uRh4KVs3WyPCb9OZ0HbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UauVf/btsJrTpEYMS/g3uRh4KVs3WyPCb9OZ0HbK/img.png&quot; style=&quot;width: 48.922%;&quot; data-widthpercent=&quot;49.5&quot; data-filename=&quot;Screenshot 2024-09-05 at 2.22.48 PM.png&quot; data-origin-height=&quot;463&quot; data-origin-width=&quot;546&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UauVf/btsJrTpEYMS/g3uRh4KVs3WyPCb9OZ0HbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUauVf%2FbtsJrTpEYMS%2Fg3uRh4KVs3WyPCb9OZ0HbK%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;546&quot; height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1725514131685&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .github/workflows/release.yml
name: Release
...
jobs:
  release:
	...
    steps:
      ...
      - name: Create Release Pull Request
        id: changesets
        uses: changesets/action@v1
        with:
          publish: pnpm changeset tag
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      ...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Tip #3: 배포가 되면 슬랙 방에 알림을 보내고 싶어요.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Changesets는 슬랙 액션 기능을 제공하지 않습니다  하지만 Git Action Context를 통해 배포 성공 여부, pr number, 패키지 정보를 제공해 줍니다. 이를 활용하여 슬랙 알림도 자동화했습니다. (&lt;a href=&quot;https://github.com/changesets/action?tab=readme-ov-file#outputs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Docs-Changesets actions-outputs&lt;/a&gt;)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;slack 알림 템플릿은 &lt;a href=&quot;https://api.slack.com/messaging/webhooks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;webhook&lt;/a&gt;을 통해 구현하였습니다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcB1dQ/btsJs0gYlHA/3QUKvVGKYQowJm0NimN4a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcB1dQ/btsJs0gYlHA/3QUKvVGKYQowJm0NimN4a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcB1dQ/btsJs0gYlHA/3QUKvVGKYQowJm0NimN4a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcB1dQ%2FbtsJs0gYlHA%2F3QUKvVGKYQowJm0NimN4a0%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;1280&quot; height=&quot;545&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1725512120519&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .github/workflows/release.yml
name: Release
...
jobs:
  release:
	...
    steps:
      ...
      - name: Slack notification
        if: steps.changesets.outputs.published == 'true'
        run: pnpm slack ${{ steps.pr.outputs.pull_request_number }} '${{steps.changesets.outputs.publishedPackages}}'
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        continue-on-error: true

      ...&lt;/code&gt;&lt;/pre&gt;
&lt;p 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 data-ke-size=&quot;size16&quot;&gt;어떠한 일을 진행하는 데에 있어서 자동화는 생산성과 정확성을 위해 중요한 부분이라고 생각합니다. 그만큼 좋은 도구들이 너무 많아, 자동화 이전의 개발자들분들은 얼마나 반복적이고 힘든 작업을 했을지 새삼 대단하게 느껴집니다.&lt;/p&gt;
&lt;p id=&quot;9c62&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;자동화를 통해 많은 작업이 편리해지지만, 때때로 예기치 못한 상황에서는 수동적인 해결이 필요할 때가 있습니다. 이럴 때 근본적인 이해가 뒷받침되어야만 유연하게 대처할 수 있습니다.&lt;/p&gt;
&lt;p id=&quot;f935&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;저 또한 앞으로도 유용한 도구들을 효과적으로 활용하되, 기본 원칙에 충실하며 안정적이고 효율적인 개발 환경을 구축해 나가야겠다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;References&lt;/h3&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://github.com/changesets/changesets&quot;&gt;Changesets 공식문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apps/changeset-bot&quot;&gt;Changesets Github App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/changesets/action&quot;&gt;Changesets Github Action&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;&amp;nbsp;&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;&amp;nbsp;&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;</description>
      <category>git/monorepo</category>
      <category>changesets</category>
      <category>changesets action</category>
      <category>Git Action</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/196</guid>
      <comments>https://algoroot.tistory.com/196#entry196comment</comments>
      <pubDate>Mon, 2 Sep 2024 21:48:16 +0900</pubDate>
    </item>
    <item>
      <title>Proxy 사용하여 벨리데이션 체크하기</title>
      <link>https://algoroot.tistory.com/191</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 오늘은 JavaScript의 Proxy 객체를 활용하여 벨리데이션 체크를 하는 방법에 대해 소개해드리려고 합니다. 조금 지난 얘기이지만 지난 3월 보일러플레이트의 의 Analytics(GA, 카카오 픽셀 등 분석도구 모음) class로 리팩토링한 후, 테스트를 하지 않아서 문제가 발생했습니다. 그래서 이슈 픽스 할 겸, 리팩토링 과정을 거쳤는데 그 과정에 Proxy를 사용한 경험을 소개하고자 합니다.&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;Proxy란 무엇일까요??&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy는 JavaScript에서 객체에 대한 동작을 가로채거나 수정할 수 있는 강력한 도구입니다. 객체의 속성에 접근하거나 값을 설정할 때, 이를 중간에서 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Proxy 기본 사용법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Proxy 객체 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Proxy 객체를 생성해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725287844493&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const targetObject = {
  name: 'John',
  age: 30,
};

const handler = {
  get: (target, propKey)  =&amp;gt; {
    console.log(`Getting property &quot;${propKey}&quot;`);
    return target[propKey];
  },
  set: (target, propKey, value) =&amp;gt; {
    console.log(`Setting property &quot;${propKey}&quot; to ${value}`);
    target[propKey] = value;
  },
};

const proxy = new Proxy(targetObject, handler);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;targetObject는 원본 객체입니다.&lt;/li&gt;
&lt;li&gt;handler는 Proxy가 객체의 속성에 접근하거나 수정할 때 호출되는 함수들을 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Proxy를 이용한 객체 접근&lt;/h4&gt;
&lt;pre id=&quot;code_1725287865475&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(proxy.name); // Getting property &quot;name&quot; / &quot;John&quot;
proxy.age = 31; // Setting property &quot;age&quot; to 31&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 메서드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;get(target, propKey, receiver)&lt;/b&gt;: 속성에 접근할 때 호출됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;target: Proxy의 대상이 되는 객체&lt;/li&gt;
&lt;li&gt;propKey: 접근하려는 속성의 키&lt;/li&gt;
&lt;li&gt;receiver: getter가 호출될 때 this. 상속받은 객체일 경우에는 상속한 객체가 this가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;set(target, propKey, value, receiver)&lt;/b&gt;: 속성에 값을 할당할 때 호출됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;target: Proxy의 대상이 되는 객체&lt;/li&gt;
&lt;li&gt;propKey: 할당하려는 속성의 키&lt;/li&gt;
&lt;li&gt;value: 할당하려는 값&lt;/li&gt;
&lt;li&gt;receiver: Proxy 객체 자체 또는 상속 체인상의 다음 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활용 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. get 활용 예제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;get 메서드는 객체의 속성 값을 읽을 때 호출됩니다. 이 예제에서는 속성에 접근할 때마다 메시지를 출력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725288086989&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const target = {};
const proxy = new Proxy(target, {
  get(target, prop) {
    return `${prop}을 조회했더니 ${target[prop]}가 나왔당~!`;
  }
});

proxy.test = 5;
console.log(proxy.test); // test을 조회했더니 5가 나왔당~!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. set 활용 예제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set 메서드는 속성 값이 설정될 때 호출됩니다. 여기서는 문자열 길이를 검사하여 유효성 검사를 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725288107689&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const target = {};
const proxy = new Proxy(target, {
  set(target, prop, value) {
    if (typeof value !== 'string') {
      console.log('ERROR: 문자열을 입력해주세요');
      return false;
    }
    if (value.length &amp;lt; 4) {
      console.log('ERROR: 4자 이상의 문자열을 입력해주세요');
      return false;
    }
    console.log(`${prop}에 '${value}' 저장`);
    target[prop] = value;
    return true;
  }
});

proxy.test = 5; // ERROR: 문자열을 입력해주세요
proxy.test = 'abc'; // ERROR: 4자 이상의 문자열을 입력해주세요
proxy.test = '안녕하세요'; // test에 '안녕하세요' 저장&lt;/code&gt;&lt;/pre&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;이제&amp;nbsp; 실제 적용했던 사례를 소개하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 분석도구 클래스가 있었고, 분석 도구 메서드를 설정할 때 window 객체를 직접 참조하는 부분이 있었습니다. 하지만 클라이언트 사이드가 아닌 환경에서는 window 객체가 정의되지 않아서 문제가 발생했습니다. 이 문제를 해결하기 위해 매 메서드마다 window가 유효한지 확인해야 했는데요, 이를 Proxy를 사용해 간편하게 해결했습니다.&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;예시를&amp;nbsp; 같이 볼까요? &amp;nbsp;analyticsValidCheckHandler는 각 메서드가 호출되기 전에 window 객체와 해당 메서드의 유효성을 체크합니다. window 객체가 없거나 메서드가 올바르게 초기화되지 않은 경우 경고를 출력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725288168840&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type GaHandler = {
  get: (
    target: { [key: string]: (...args: any[]) =&amp;gt; any },
    propKey: string,
  ) =&amp;gt; (...args: any[]) =&amp;gt; any;
};

const ANALYTICS_NAME = {
  gtag: 'Google Analytics',
  fbq: 'Meta(Facebook)',
  kakao: 'Kakao',
};

export const analyticsValidCheckHandler: GaHandler = {
  get: (target, propKey) =&amp;gt; {
    const origMethod = target[propKey];
    return (...args) =&amp;gt; {
      let isValid = false;
      if (typeof window !== 'undefined') {
        switch (propKey) {
          case 'gtag':
            isValid = typeof window.gtag === 'function';
            break;
          case 'fbq':
            isValid = typeof window.fbq === 'function';
            break;
          case 'kakao':
            isValid = typeof kakaoPixel === 'function';
            break;
          default:
            break;
        }
      }

      if (!isValid) {
        return console.warn(
          `${ANALYTICS_NAME[propKey as keyof typeof ANALYTICS_NAME]} Analytics is not initialized.`,
        );
      }
      return origMethod.apply(target, args);
    };
  },
};&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;그 다음, GoogleAnalytics 클래스를 살펴보겠습니다. 이 클래스에서는 Proxy를 사용하여 gtag 메서드 호출 전 검증을 수행합니다. window 객체가 정의되지 않은 환경에서는 빈 함수로 대체하여 문제가 발생하지 않도록 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1725288783468&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Analytics } from './types';
import { analyticsValidCheckHandler } from './utils/valid-check-with-proxy';

export class GoogleAnalytics {
  private ga: Gtag.Gtag = () =&amp;gt; {};

  constructor(private key?: string) {
    if (!key) {
      console.warn('GA 키 설정이 필요합니다.');
      return;
    }
    this.key = key;
    // Proxy를 사용하여 gtag메서드 호출 전 검증 수행 
    this.ga = new Proxy(
      { gtag: typeof window !== 'undefined' ? window.gtag : () =&amp;gt; {} },
      analyticsValidCheckHandler,
    ).gtag;
  }

  ...
}&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;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy를 제대로 사용해 보기 전에는 그저 객체의 동작을 가로챈다는 의미로만 알고 있었는데요. 실제로 사용해보니, 예상보다 훨씬 유용하더라고요. 만약 Proxy를 사용하지 않았다면, 각 메서드에서 window의 정의 여부를 일일이 체크해야 했을 겁니다. 물론 이슈가 발생한 점은 반성해야겠지만 이번 일을 계기로 Proxy를 사용함으로서 오히려 코드가 훨씬 간결해졌고, 유지보수도 용이해졌습니다. 이런 경험을 통해 Proxy의 강력한 기능을 직접 체감할 수 있었던 좋은 기회였다고 생각합니다.&lt;/p&gt;</description>
      <category>Frontend/JavaScript</category>
      <category>new proxy</category>
      <category>proxy</category>
      <author>AlgoRoot</author>
      <guid isPermaLink="true">https://algoroot.tistory.com/191</guid>
      <comments>https://algoroot.tistory.com/191#entry191comment</comments>
      <pubDate>Mon, 10 Jun 2024 10:09:17 +0900</pubDate>
    </item>
  </channel>
</rss>