<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 메모장</title>
    <link>https://kir93.tistory.com/</link>
    <description>개발을 하며 새로 배운 지식과 에러 등에 대해 공유하는 블로그입니다.
문의사항은 kir931028@gmail.com로 연락바랍니다.</description>
    <language>ko</language>
    <pubDate>Mon, 22 Jun 2026 22:49:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Kir93</managingEditor>
    <image>
      <title>개발 메모장</title>
      <url>https://tistory1.daumcdn.net/tistory/4845731/attach/e53bf0e1bc8049a48445f74bd3397b43</url>
      <link>https://kir93.tistory.com</link>
    </image>
    <item>
      <title>squash merge가 release마다 cherry-pick이 충돌하는 이유</title>
      <link>https://kir93.tistory.com/entry/squash-merge%EA%B0%80-release%EB%A7%88%EB%8B%A4-cherry-pick%EC%9D%B4-%EC%B6%A9%EB%8F%8C%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 (Why This Matters)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴리즈 브랜치를 만들 때마다 같은 일이 반복된다. 분명 일상 통합 브랜치에서는 깨끗하게 머지됐던 변경인데, 릴리즈 브랜치에 모으려고 cherry-pick을 돌리면 매번 1~3건씩 충돌이 난다. &lt;code&gt;-x&lt;/code&gt;, &lt;code&gt;-m&lt;/code&gt;, &lt;code&gt;-X theirs&lt;/code&gt; 같은 옵션을 바꿔봐도, 전략(&lt;code&gt;recursive&lt;/code&gt;/&lt;code&gt;ort&lt;/code&gt;)을 바꿔봐도 결과는 비슷하다. 손으로 풀고, 다음 릴리즈에서 또 푼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 결론을 먼저 말하면 이렇다. &lt;b&gt;cherry-pick을 튜닝하는 건 헛수고다.&lt;/b&gt; 충돌의 범인은 cherry-pick이 아니라, 그 한참 앞에서 일어난 &lt;b&gt;squash 머지&lt;/b&gt;다. squash가 git이 3-way 머지의 기준점으로 쓰는 &lt;b&gt;공통 조상(merge base)을 지워버리기&lt;/b&gt; 때문에, cherry-pick은 &quot;이미 적용된 변경&quot;을 알아보지 못하고 매번 충돌을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 (1) squash가 공통 조상을 어떻게 파괴하는지 git 동작 수준에서 풀고, (2) &lt;code&gt;merge --no-ff&lt;/code&gt; 직접 머지로 전환해 릴리즈 생성 충돌을 0으로 만든 방법과 검증 결과를 다룬다. 읽는 데 약 8분.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git의 머지&amp;middot;cherry-pick 동작은 안정적이지만, 본문 설명&amp;middot;수치는 2026년 6월 기준이다. 동작이 헷갈리면 &lt;code&gt;git merge-base&lt;/code&gt;, &lt;code&gt;git patch-id&lt;/code&gt; 문서로 직접 확인하길 권한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 (What &amp;amp; Why)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git의 머지는 &lt;b&gt;3-way merge&lt;/b&gt;다. 비유하자면, 두 사람이 같은 문서를 각자 고쳐 왔을 때 이를 합치는 일이다. 이때 &lt;b&gt;원본&lt;/b&gt;이 있으면 &quot;A는 3 문단을, B는 7 문단을 고쳤구나&quot; 하고 각자의 변경을 분리해 합칠 수 있다. 원본이 없으면 두 결과물을 글자 단위로 비교하는 수밖에 없고, 조금만 겹쳐도 &quot;둘 중 뭐가 맞아?&quot;라며 충돌이 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;원본&lt;/b&gt;에 해당하는 것이 &lt;b&gt;merge base(공통 조상)&lt;/b&gt; 다. 두 브랜치가 갈라지기 직전의 공통 커밋이며, &lt;code&gt;git merge&lt;/code&gt;와 &lt;code&gt;git cherry-pick&lt;/code&gt; 모두 이 기준점을 잡아 &quot;양쪽이 각각 무엇을 바꿨는가&quot;를 계산한다. 좋은 공통 조상이 있으면 겹치는 변경도 자동으로 풀리고, 공통 조상이 어긋나면 충돌이 난다.&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;b&gt;&lt;code&gt;merge --no-ff&lt;/code&gt;&lt;/b&gt;: 머지 커밋을 새로 만들어 &lt;b&gt;양쪽 부모를 모두 기록&lt;/b&gt;한다. 히스토리 그래프(조상 관계)가 그대로 보존된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;squash merge&lt;/b&gt;: feature의 커밋 여러 개를 &lt;b&gt;단일 커밋으로 압축&lt;/b&gt;해 대상 브랜치에 얹는다. 이 커밋은 원본 커밋들과의 &lt;b&gt;조상 링크가 없다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cherry-pick&lt;/b&gt;: 특정 커밋의 &quot;부모와의 차이(diff)&quot;를 현재 브랜치에 패치처럼 다시 적용한다. 실제로는 &lt;b&gt;그 커밋의 부모를 base로 삼는 3-way 머지&lt;/b&gt;다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은, 이 세 가지가 merge base를 &lt;b&gt;각각 다르게 다룬다&lt;/b&gt;는 점이다. 그리고 그 차이가 릴리즈 충돌의 발생 여부를 가른다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 (How It Works)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. squash가 공통 조상을 지운다&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP7xMA/dJMcabEBmx6/RqFN9xlanIvYYVg6ovXx41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP7xMA/dJMcabEBmx6/RqFN9xlanIvYYVg6ovXx41/img.png&quot; data-alt=&quot;스쿼시 + cherry-pick 릴리즈: 조상 링크가 끊겨 공통 조상을 못 찾고 충돌하는 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP7xMA/dJMcabEBmx6/RqFN9xlanIvYYVg6ovXx41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP7xMA%2FdJMcabEBmx6%2FRqFN9xlanIvYYVg6ovXx41%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;1639&quot; height=&quot;940&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스쿼시 + cherry-pick 릴리즈: 조상 링크가 끊겨 공통 조상을 못 찾고 충돌하는 구조&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;&lt;i&gt;그림 1. feature를 squash로 통합하면 단일 커밋 S가 생기는데, S는 f1&amp;middot;f2&amp;middot;f3와의 조상 관계가 끊겨 있다. 릴리즈 브랜치에 그 변경을 넣으려면 S를 cherry-pick 할 수밖에 없고, 이때 공통 조상이 어긋나 충돌이 난다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feature 브랜치의 커밋 &lt;code&gt;f1&amp;middot;f2&amp;middot;f3&lt;/code&gt;를 squash로 통합 브랜치에 머지하면 단일 커밋 &lt;code&gt;S&lt;/code&gt;가 생긴다. 그런데 &lt;code&gt;S&lt;/code&gt;의 부모는 통합 브랜치의 직전 커밋 하나뿐이다. &lt;code&gt;f1&amp;middot;f2&amp;middot;f3&lt;/code&gt;와 &lt;code&gt;S&lt;/code&gt; 사이에는 git이 추적할 수 있는 &lt;b&gt;조상 관계가 존재하지 않는다.&lt;/b&gt; 원본 feature 브랜치의 커밋들은 영구 히스토리 그래프에서 사라지고, 압축된 &lt;code&gt;S&lt;/code&gt; 하나만 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 릴리즈 브랜치를 &lt;code&gt;main&lt;/code&gt;에서 잘라낸 뒤 그 변경을 넣는다고 해보자. feature 브랜치를 직접 머지하려 해도, 릴리즈와 feature의 공통 조상은 &lt;code&gt;main&lt;/code&gt;인데 정작 그 변경은 &lt;code&gt;S&lt;/code&gt;에만 있다. 결국 &lt;b&gt;&lt;code&gt;S&lt;/code&gt;를 cherry-pick 하는 길밖에 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;cherry-pick S&lt;/code&gt;는 &lt;b&gt;&lt;code&gt;S&lt;/code&gt;의 부모를 base로 삼는 3-way 머지&lt;/b&gt;다. 문제는 &lt;code&gt;S&lt;/code&gt;의 부모(통합 브랜치 tip)에는 &lt;b&gt;릴리즈 브랜치에는 없는 다른 squash 변경들이 섞여 있다&lt;/b&gt;는 것이다. base와 릴리즈의 맥락이 어긋나 있으니, 논리적으로는 &quot;그 feature 변경만&quot;인데도 주변 콘텍스트 불일치로 충돌이 난다. 게다가 git이 &quot;이 변경은 이미 적용됐다&quot;를 인식하는 수단(조상 추적, &lt;code&gt;patch-id&lt;/code&gt;)이 squash로 깨져 있어, 같은 변경이 다른 경로로 들어와도 중복으로 보지 못하고 &lt;b&gt;다시 적용하려다 또 충돌&lt;/b&gt;한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. &lt;code&gt;merge --no-ff&lt;/code&gt;는 공통 조상을 살린다&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvLCr3/dJMcajvToQE/imOpcFz4uSypJ5yYKmQkW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvLCr3/dJMcajvToQE/imOpcFz4uSypJ5yYKmQkW1/img.png&quot; data-alt=&quot;merge --no-ff 직접 머지: 공통 조상 main이 살아 있어 자동 3-way로 해소되는 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvLCr3/dJMcajvToQE/imOpcFz4uSypJ5yYKmQkW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvLCr3%2FdJMcajvToQE%2FimOpcFz4uSypJ5yYKmQkW1%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;1639&quot; height=&quot;940&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;merge --no-ff 직접 머지: 공통 조상 main이 살아 있어 자동 3-way로 해소되는 구조&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;&lt;i&gt;그림 2. feature와 릴리즈 모두 main에서 갈라져 공통 조상이 main으로 살아 있다. merge --no-ff는 두 부모를 가진 머지 커밋 M을 만들고, git은 main을 merge base로 잡아 겹치는 변경까지 자동으로 풀어낸다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해법은 squash와 cherry-pick을 둘 다 버리는 것이다. 모든 feature 브랜치를 &lt;code&gt;main&lt;/code&gt;에서 자르고, 릴리즈 브랜치도 &lt;code&gt;main&lt;/code&gt;에서 자른다. 그러면 둘의 &lt;b&gt;공통 조상은 항상 &lt;code&gt;main&lt;/code&gt;&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴리즈 브랜치에 feature를 &lt;code&gt;merge --no-ff&lt;/code&gt;로 직접 머지하면, 두 부모(릴리즈 tip + &lt;code&gt;f3&lt;/code&gt;)를 가진 머지 커밋 &lt;code&gt;M&lt;/code&gt;이 생긴다. 이제 git은 &lt;code&gt;main&lt;/code&gt;을 merge base로 잡아 정상 3-way 머지를 수행한다. 양쪽이 같은 곳을 건드렸더라도 &quot;둘 다 공통 조상 대비 같은 변경을 했네&quot;를 인식하므로 &lt;b&gt;겹치는 변경까지 자동으로 병합&lt;/b&gt;된다. 덤으로 커밋이 압축되지 않아 &lt;b&gt;히스토리가 100% 보존&lt;/b&gt;된다(추적성).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면, cherry-pick이 아니라 &lt;b&gt;&quot;원본 feature 브랜치를 그대로 머지&quot;&lt;/b&gt; 하는 것이 핵심이다. 원본을 머지해야 공통 조상이 살고, 공통 조상이 살아야 3-way가 충돌을 자동으로 해소한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 (Practical Examples)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 안티패턴 (Anti-Pattern): squash + cherry-pick 릴리즈&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 통합 브랜치: PR을 squash로 머지 &amp;rarr; 커밋 압축, 조상 링크 끊김
git checkout develop
git merge --squash feature/awesome
git commit -m &quot;feat: awesome&quot;

# 릴리즈: main에서 자른 뒤 squash 커밋을 cherry-pick
git switch -c release/x main
git cherry-pick &amp;lt;squash커밋&amp;gt;     # ⚡ 매 릴리즈 1~3건 충돌

# 안 통하는 처방들 &amp;mdash; 근본 원인(끊긴 조상)을 못 고친다
git cherry-pick -X theirs &amp;lt;squash커밋&amp;gt;
git cherry-pick -m 1 &amp;lt;squash머지커밋&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 문제인가:&lt;/b&gt; cherry-pick은 squash 커밋의 부모를 base로 삼는데, 그 base에는 릴리즈에 없는 변경이 섞여 있어 3-way 기준점이 어긋난다. &lt;code&gt;-m&lt;/code&gt;, &lt;code&gt;-x&lt;/code&gt;, &lt;code&gt;-X theirs&lt;/code&gt; 같은 옵션은 충돌을 &lt;b&gt;회피&lt;/b&gt;할 뿐 원인인 &quot;끊긴 조상&quot;을 복원하지 못한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 권장 패턴 (Good Practice): main 기반 + &lt;code&gt;merge --no-ff&lt;/code&gt; 직접 머지&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 모든 feature는 main에서 자른다 (공통 조상 = main)
git switch -c feature/awesome main
# ... 작업, 일상 통합용 PR은 develop으로 ...

# 릴리즈: main에서 자른 뒤, 포함할 feature 브랜치를 직접 --no-ff 머지
git switch -c release/x main
git merge --no-ff feature/awesome     # 공통 조상 main &amp;rarr; 자동 3-way
git merge --no-ff feature/another&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 되는가:&lt;/b&gt; 릴리즈와 각 feature의 공통 조상이 &lt;code&gt;main&lt;/code&gt;으로 살아 있어, git이 정상 3-way 머지로 겹침을 자동 해소한다. squash 커밋을 패치로 재적용하는 게 아니라 &lt;b&gt;원본 브랜치를 그대로 머지&lt;/b&gt;하는 것이 차이의 전부다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 결과 (한 레포의 측정 사례)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 프로덕션 프론트엔드 레포에서 전략을 전환한 뒤, 릴리즈 한 건(약 9개 PR 규모)을 생성해 측정한 결과다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;포함 대상 9건 &lt;b&gt;전부 자동 머지 성공&lt;/b&gt;, 수동 개입 0.&lt;/li&gt;
&lt;li&gt;자동 충돌 해소 2건(&lt;code&gt;package.json&lt;/code&gt;, 자동 생성된 타입 선언 &lt;code&gt;.d.ts&lt;/code&gt; 파일) &amp;mdash; git 3-way가 스스로 처리.&lt;/li&gt;
&lt;li&gt;커밋 약 100개 &lt;b&gt;전부 보존&lt;/b&gt;(squash였다면 9개로 압축됐을 분량).&lt;/li&gt;
&lt;li&gt;릴리즈 생성 충돌 &lt;b&gt;1~3건 &amp;rarr; 0건&lt;/b&gt;, 생성 체감 시간 &lt;b&gt;약 5분 &amp;rarr; 약 30초&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 수치는 특정 레포&amp;middot;릴리즈 1건 기준의 측정치이며, 코드베이스 규모와 변경 분포에 따라 달라질 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&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;✓ 커밋 히스토리&amp;middot;추적성 100% 보존&lt;/td&gt;
&lt;td&gt;✗ feature 브랜치를 릴리즈 시점까지 보존해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ cherry-pick 옵션 튜닝이 필요 없음&lt;/td&gt;
&lt;td&gt;✗ &quot;main은 모든 분기의 깨끗한 베이스&quot; 불변식을 유지해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ PR&amp;rarr;develop, feature&amp;rarr;release, release&amp;rarr;main에 동일 전략 일관 적용&lt;/td&gt;
&lt;td&gt;✗ 단일 trunk + squash가 더 잘 맞는 팀에는 과할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도입 체크리스트:&lt;/b&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;code&gt;main&lt;/code&gt;을 모든 feature/release 분기의 공통 베이스로 고정한다.&lt;/li&gt;
&lt;li&gt;모든 머지를 &lt;code&gt;--no-ff&lt;/code&gt; 일반 머지로 통일한다(squash&amp;middot;rebase&amp;middot;cherry-pick 배제).&lt;/li&gt;
&lt;li&gt;feature 브랜치는 릴리즈에 포함될 때까지 살려두고, 릴리즈 완료 자동화에서 일괄 삭제한다.&lt;/li&gt;
&lt;li&gt;릴리즈 완료 후 &lt;code&gt;main&lt;/code&gt;을 통합 브랜치로 동기화해 &quot;통합 브랜치 = main + 진행 중 PR&quot; 상태를 유지한다.&lt;/li&gt;
&lt;li&gt;히스토리 가독성은 &lt;code&gt;git log --first-parent&lt;/code&gt;로 보완한다(머지 커밋만 따라 1차 흐름만 보기).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;흔한 함정:&lt;/b&gt; &quot;squash가 히스토리를 깔끔하게 해 준다&quot;는 통념이다. 단일 trunk 모델에서는 맞는 말일 수 있지만, &lt;b&gt;릴리즈를 cherry-pick으로 조립하는 구조라면 그 깔끔함의 청구서가 매 릴리즈 충돌로 돌아온다.&lt;/b&gt; 전략은 팀의 릴리즈 방식과 함께 골라야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론 및 다음 단계 (Conclusion)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 3줄 요약:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;릴리즈 cherry-pick 충돌의 근본 원인은 cherry-pick이 아니라, 그 앞단의 squash가 &lt;b&gt;공통 조상(merge base)을 지운 것&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 기반 + &lt;code&gt;merge --no-ff&lt;/code&gt; 직접 머지로 공통 조상을 살리면, 3-way 머지가 겹치는 변경까지 &lt;b&gt;자동으로 해소&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;머지 전략 자체를 바꾸지 않으면 cherry-pick 옵션 튜닝은 부질없다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Git</category>
      <category>3-way-merge</category>
      <category>cherry-pick</category>
      <category>Git</category>
      <category>git-flow</category>
      <category>merge-base</category>
      <category>squash-merge</category>
      <category>릴리즈자동화</category>
      <category>머지전략</category>
      <category>브랜치전략</category>
      <category>형상관리</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/191</guid>
      <comments>https://kir93.tistory.com/entry/squash-merge%EA%B0%80-release%EB%A7%88%EB%8B%A4-cherry-pick%EC%9D%B4-%EC%B6%A9%EB%8F%8C%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0#entry191comment</comments>
      <pubDate>Mon, 22 Jun 2026 18:40:20 +0900</pubDate>
    </item>
    <item>
      <title>프런트엔드 AX 설계기 0편 &amp;mdash; 레거시 3개와 차세대 FE를 위한 워크플로우 설계기</title>
      <link>https://kir93.tistory.com/entry/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-AX-%EC%84%A4%EA%B3%84%EA%B8%B0-0%ED%8E%B8-%E2%80%94-%EB%A0%88%EA%B1%B0%EC%8B%9C-3%EA%B0%9C%EC%99%80-%EC%B0%A8%EC%84%B8%EB%8C%80-FE%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%84%A4%EA%B3%84%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 (Why This Matters)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 15에 묶인 레거시 프로젝트 3개와, Next.js 기반 차세대 프로젝트들을 한 사람이 동시에 굴리는 상황을 떠올려 보자. 프로젝트마다 컨벤션이 다르고, 빌드 함정이 다르고, &quot;여기서는 이렇게 하면 안 된다&quot;는 암묵지가 다르다. 컨텍스트 스위칭 비용만으로도 하루가 샌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 AI를 얹으면 처음엔 빨라진 것 같다. 그런데 곧 다른 종류의 비용이 보인다. 채팅창에 그때그때 프롬프트를 복붙 하니 &lt;b&gt;결과가 매번 다르고&lt;/b&gt;, AI가 건드리면 안 되는 곳을 건드려도 &lt;b&gt;막을 경계가 없고&lt;/b&gt;, 잘 된 흐름을 &lt;b&gt;재현할 수도 팀에 넘길 수도 없다.&lt;/b&gt; 산발적 사용의 한계는 속도가 아니라 &lt;i&gt;운영 가능성&lt;/i&gt;에서 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 그 한계를 마주한 FE 한 명이 4개월에 걸쳐 AI 워크플로를 &quot;설계&quot;로 끌어올린 기록의 입구다. 이 글을 읽고 나면 (1) 왜 팁 모음이 아니라 &lt;i&gt;설계&lt;/i&gt;가 필요한지, (2) 그 설계가 어떤 4개의 축으로 묶이는지, (3) 11편 중 어디부터 읽으면 되는지를 얻는다. 예상 소요: 약 8분.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 (What &amp;amp; Why)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 신생 용어 두 개를 한 줄로 정의하고 넘어가자. 둘 다 아직 자리 잡는 중인 말이라 용어 자체에 무게를 싣지는 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;바이브 코딩(vibe coding)&lt;/b&gt;: AI 에이전트와 &lt;i&gt;함께&lt;/i&gt; 코드를 만드는 개발 방식. 즉 &lt;i&gt;개발 과정&lt;/i&gt;에 AI가 들어온 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AX(Agent Experience)&lt;/b&gt;: 에이전트를 &lt;i&gt;위해&lt;/i&gt;, 에이전트와 &lt;i&gt;함께&lt;/i&gt; 쓰도록 인터페이스를 설계하는 일. 즉 &lt;i&gt;제품 표면&lt;/i&gt;에 AI가 들어온 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하자면 AI에게 일을 맡기는 것은 &lt;b&gt;유능하지만 처음 온 신입에게 일을 맡기는 것&lt;/b&gt;과 같다. 신입에게 우리는 무엇을 하는가? 건드려도 되는 것과 안 되는 것을 정하고(권한), 결과를 검수하고(게이트), 신뢰가 쌓이면 재량을 늘리고(자율성), 온보딩 문서를 정비한다(운영). AI도 정확히 같은 네 가지가 필요하다. 산발적 프롬프트에는 이 넷이 전부 빠져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 연결고리 하나. &lt;b&gt;이건 FE의 본업에서 벗어난 일이 아니다.&lt;/b&gt; FE의 본질은 &quot;사람과 시스템 사이의 인터페이스를 설계&amp;middot;구현하는 craft&quot;다. 그 인터페이스의 양 끝에 이제 사람만이 아니라 AI가 들어온다 &amp;mdash; 우리가 AI와 &lt;i&gt;함께&lt;/i&gt; 만들고(바이브 코딩), AI를 &lt;i&gt;위해&lt;/i&gt; 만들기(AX) 때문이다. AI 워크플로를 설계한다는 것은 &lt;b&gt;개발자와 에이전트 사이의 인터페이스를 설계&lt;/b&gt;한다는 뜻이고, 그건 영역이 &lt;i&gt;넓어진&lt;/i&gt; FE의 일이다. 표면이 사람이든 에이전트든, 인터페이스를 잘 만드는 craft는 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 시리즈는 &quot;Claude Code 팁 모음&quot;이 아니다. 팁은 개별 기교지만, 여기서 다루는 건 &lt;i&gt;팀 단위 운영을 전제로 한&lt;/i&gt; 권한&amp;middot;검증&amp;middot;자율성&amp;middot;운영의 설계다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 (How It Works)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계는 크게 &lt;b&gt;4-layer 책임 분리&lt;/b&gt;라는 뼈대 위에서, &lt;b&gt;4개의 축&lt;/b&gt;으로 펼쳐진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1875&quot; data-origin-height=&quot;1141&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DByCW/dJMcaiqcAYS/mwAbNzDx8ETEd33oORTJP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DByCW/dJMcaiqcAYS/mwAbNzDx8ETEd33oORTJP0/img.png&quot; data-alt=&quot;프론트엔드 AX 설계기 &amp;amp;mdash; 시리즈 지도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DByCW/dJMcaiqcAYS/mwAbNzDx8ETEd33oORTJP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDByCW%2FdJMcaiqcAYS%2FmwAbNzDx8ETEd33oORTJP0%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;1875&quot; height=&quot;1141&quot; data-origin-width=&quot;1875&quot; data-origin-height=&quot;1141&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프론트엔드 AX 설계기 &amp;mdash; 시리즈 지도&lt;/figcaption&gt;
&lt;/figure&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;뼈대: 4-layer 책임 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;산발적 사용이 위험한 핵심 이유는 &quot;권한&quot;과 &quot;재사용&quot;과 &quot;실행&quot;이 한 덩어리로 뭉쳐 있다는 데 있다. 이걸 네 겹으로 가른다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Public command&lt;/b&gt; &amp;mdash; 사람이 부르는 진입점. &lt;i&gt;이름&amp;middot;인자&amp;middot;게이트&lt;/i&gt;만 책임진다. 권한 게이트가 여기 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Internal skill&lt;/b&gt; &amp;mdash; 여러 커맨드가 공유하는 &lt;i&gt;재사용 계약&lt;/i&gt;. 단, skill 자체는 &lt;b&gt;권한 경계가 아니다.&lt;/b&gt; (이 명제가 1편의 주제다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Shared docs&lt;/b&gt; &amp;mdash; 권한 경계 매트릭스 같은 &lt;i&gt;공유 가이드&lt;/i&gt;. 결정의 근거를 한 곳에 모은다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Agent&lt;/b&gt; &amp;mdash; &lt;i&gt;권한 분리 실행&lt;/i&gt;. tool scope로 &quot;이 작업은 읽기 전용 도구만&quot; 같은 식으로 권한을 분배한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한은 skill에 숨기는 게 아니라 &lt;b&gt;public command의 게이트 &amp;middot; 명시적 사용자 확인 &amp;middot; agent의 tool scope&lt;/b&gt; 세 곳으로 분배된다. 이 원칙 하나가 나머지 설계 전체의 토대다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4개의 축&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;권한 경계&lt;/b&gt; &amp;mdash; AI가 무엇을 건드릴 수 있나. (1편 skill&amp;ne;경계, 2편 agent 기본 OFF, 8편 자율 모드 경계, 9편 외부 도구(MCP) 경계)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 게이트&lt;/b&gt; &amp;mdash; 결과를 어떻게 믿나. (4편 Claude 분석&amp;middot;Codex 비평의 교차 검증, 5편 writer/evaluator 분리, 6편 게이트 보정&amp;middot;철회&amp;middot;earn-back, 7편 AI 변조&amp;middot;CI-gaming 탐지)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라이프사이클&lt;/b&gt; &amp;mdash; 계획과 구현을 어떻게 잇나. (3편 양방향 spec lifecycle)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 결정&lt;/b&gt; &amp;mdash; 무엇을 계속 유지&amp;middot;폐기하나. (10편 모델 선택 운영, 11편 운영 루프 회고)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 편은 독립된 &quot;결정 + 측정&quot; 아크라서, 어느 편부터 읽어도 성립한다. 0편(이 글)은 그 전체 지도와 &quot;왜 설계가 필요했나&quot;를 담는 입구다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 (Practical Examples)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축적된 규모를 먼저 정직하게 밝히면, 약 4개월간 200+ 커밋, 공개 커맨드 13개&amp;middot;내부 skill family 10개&amp;middot;에이전트 6개로 수렴했고, 정식 릴리스 흐름(CHANGELOG&amp;middot;semver&amp;middot;릴리스 노트&amp;middot;공지)으로 v0.1.0부터 v0.6.1까지 운영했다. 이 숫자는 &lt;i&gt;생산성 배수&lt;/i&gt;가 아니라 &lt;i&gt;들인 노력과 범위&lt;/i&gt;를 가리킨다. &quot;AI로 N배 빨라졌다&quot; 류 주장은 측정 조건 없이는 하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 권장 패턴 (Good Practice)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책임을 4-layer로 가르고, 진입점은 게이트를 명시한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&amp;lt;!-- commands/ship.md &amp;mdash; public command: 이름&amp;middot;인자&amp;middot;게이트만 책임진다 --&amp;gt;
---
description: 변경분을 검증하고 릴리스한다
argument-hint: [patch|minor|major]
---
1. 검증 게이트: 테스트&amp;middot;린트&amp;middot;타입체크 통과 확인 (실패 시 즉시 중단)
2. 사용자 확인: 버전 범프 내용을 명시하고 명시적 승인을 대기
3. 실행: 승인 시에만 release 에이전트에 위임 (해당 agent는 제한된 tool scope)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 디렉터리도 책임별로 갈라 둔다
commands/   # public  : 사람이 부르는 진입점 + 게이트
skills/     # internal: 재사용 계약 (권한 경계 아님)
agents/     # 실행    : tool scope로 권한 분리
docs/guides/# shared  : 권한 경계 매트릭스 등 결정의 근거&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 세 가지다. &lt;b&gt;게이트가 진입점에 보이게 있고&lt;/b&gt;(2단계 전 1단계가 막는다), &lt;b&gt;위험한 실행은 명시적 승인 뒤로 미루며&lt;/b&gt;, &lt;b&gt;실행 권한은 agent의 tool scope로 좁힌다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 안티패턴 (Anti-Pattern)&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 모든 규칙&amp;middot;권한&amp;middot;예외가 한 파일에 뒤섞임
~/.claude/CLAUDE.md   # 수천 줄, 무엇이 무엇을 막는지 추적 불가
# 실행: 매번 채팅창에 긴 프롬프트를 복붙, 사람마다 표현이 다름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이 문제인가? 권한 경계가 &lt;i&gt;어디에도 명시되지 않아&lt;/i&gt; AI가 무엇이든 건드릴 수 있고, 같은 작업도 사람&amp;middot;시점마다 결과가 달라 &lt;b&gt;재현이 안 되며&lt;/b&gt;, 잘 된 흐름을 &lt;b&gt;팀에 넘기거나 회귀를 추적할 수 없다.&lt;/b&gt; 빨라 보이지만 운영이 불가능하다.&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;code&gt;/ship minor&lt;/code&gt;를 부르면, 테스트가 깨진 순간 1단계에서 멈춘다. 통과하면 &quot;minor 범프: 0.6.1 &amp;rarr; 0.7.0&quot; 같은 내용을 보여주고 사람의 승인을 기다린다. 승인 후에야 tool scope가 제한된 에이전트가 릴리스를 수행한다. 같은 명령은 누가 부르든 같은 게이트를 통과하므로 &lt;b&gt;재현 가능하고, 그대로 팀에 공유된다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ 권한 사고를 게이트&amp;middot;승인&amp;middot;scope로 구조적으로 차단&lt;/td&gt;
&lt;td&gt;✗ 초기 설계&amp;middot;정비 비용이 든다 (산발적 사용보다 느린 출발)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 재현 가능 &amp;mdash; 같은 명령은 같은 결과 게이트를 통과&lt;/td&gt;
&lt;td&gt;✗ 도구 스펙 변화에 맞춰 지속 유지보수 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 팀 공유&amp;middot;온보딩이 명령 한 줄로 가능&lt;/td&gt;
&lt;td&gt;✗ 과설계 위험 &amp;mdash; 1인&amp;middot;소규모엔 무거울 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 회귀&amp;middot;변조를 검증 게이트로 조기 포착&lt;/td&gt;
&lt;td&gt;✗ 측정 없이 &quot;효과&quot;를 주장하면 hype가 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;가장 위험한 작업 하나(예: 릴리스&amp;middot;마이그레이션)부터 &lt;b&gt;게이트 + 명시 승인&lt;/b&gt;으로 감싼다.&lt;/li&gt;
&lt;li&gt;권한이 &quot;어디서&quot; 분배되는지 한 장의 표(권한 경계 매트릭스)로 적는다.&lt;/li&gt;
&lt;li&gt;자주 쓰는 흐름 1~2개만 public command로 고정해 재현성을 확보한다.&lt;/li&gt;
&lt;li&gt;효과는 &lt;b&gt;측정 조건과 함께만&lt;/b&gt; 기록한다(예: &quot;이 코퍼스 기준 토큰 N% 절감(추정)&quot;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔한 함정: skill에 권한을 숨겨두고 &quot;경계를 세웠다&quot;라고 착각하는 것. skill은 재사용 계약일뿐 권한 경계가 아니다(&amp;rarr; 1편).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론 및 다음 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 3줄 요약:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;산발적 AI 사용의 한계는 속도가 아니라 &lt;b&gt;운영 가능성&lt;/b&gt;(권한&amp;middot;재현&amp;middot;공유)에서 드러난다.&lt;/li&gt;
&lt;li&gt;해법은 팁이 아니라 &lt;b&gt;설계&lt;/b&gt; &amp;mdash; 4-layer 책임 분리 위에 권한&amp;middot;검증&amp;middot;라이프사이클&amp;middot;운영 4개 축을 얹는다.&lt;/li&gt;
&lt;li&gt;이건 곁다리가 아니라 &lt;i&gt;개발자와 에이전트 사이 인터페이스&lt;/i&gt;를 만드는, 넓어진 &lt;b&gt;FE의 본업&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI 엔지니어링</category>
      <category>Agent-Experience</category>
      <category>AI보조개발</category>
      <category>ax</category>
      <category>claude-code</category>
      <category>개발자경험</category>
      <category>검증게이트</category>
      <category>권한경계</category>
      <category>바이브코딩</category>
      <category>워크플로설계</category>
      <category>프론트엔드</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/190</guid>
      <comments>https://kir93.tistory.com/entry/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-AX-%EC%84%A4%EA%B3%84%EA%B8%B0-0%ED%8E%B8-%E2%80%94-%EB%A0%88%EA%B1%B0%EC%8B%9C-3%EA%B0%9C%EC%99%80-%EC%B0%A8%EC%84%B8%EB%8C%80-FE%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%84%A4%EA%B3%84%EA%B8%B0#entry190comment</comments>
      <pubDate>Fri, 19 Jun 2026 19:26:40 +0900</pubDate>
    </item>
    <item>
      <title>안 만든 것이 규율이다 &amp;mdash; 절제로 끌고 간 토이 프로젝트 회고 (Scrooge 5편)</title>
      <link>https://kir93.tistory.com/entry/%EC%95%88-%EB%A7%8C%EB%93%A0-%EA%B2%83%EC%9D%B4-%EA%B7%9C%EC%9C%A8%EC%9D%B4%EB%8B%A4-%E2%80%94-%EC%A0%88%EC%A0%9C%EB%A1%9C-%EB%81%8C%EA%B3%A0-%EA%B0%84-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-Scrooge-5%ED%8E%B8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;  scrooge&lt;/a&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 &amp;mdash; 토이 프로젝트가 망하는 진짜 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트는 보통 기능 부족으로 망하지 않는다. &lt;b&gt;하고 싶은 게 너무 많아서&lt;/b&gt; 망한다. &quot;이왕 만드는 김에 이것도&quot;, &quot;나중에 필요할 테니 미리&quot;, &quot;더 일반적으로 추상화해두면&quot; &amp;mdash; 이런 충동이 쌓이면, 정작 핵심은 미완인 채로 곁가지에 짓눌려 프로젝트가 무거워진다. 혼자 쓰는 도구일수록 이 제동장치가 없다. 리뷰어도, 데드라인도, 말려줄 동료도 없으니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrooge는 약 2주, 57개 커밋으로 v0.6.1까지 굴러간 작은 도구다(2026-05-25 첫 커밋 ~ 06-02). 이 회고에서 말하고 싶은 건 그동안 &lt;i&gt;만든 기능 목록&lt;/i&gt;이 아니다 &amp;mdash; 그건 0~4화에서 충분히 다뤘다. 대신 &lt;b&gt;&quot;안 만든 것&quot;이 어떻게 이 도구를 가볍게 유지했는가&lt;/b&gt;를 말하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 당신이 얻어갈 것은, 토이 프로젝트에 적용할 수 있는 &lt;b&gt;&quot;절제의 규율(discipline of restraint)&quot;&lt;/b&gt; 몇 가지와, 그 규율이 &lt;i&gt;실제로 무엇을 막았는지&lt;/i&gt;에 대한 구체적 사례다. 추상적 다짐이 아니라, &quot;이 규칙이 없었다면 만들었을 것&quot;의 목록이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; 규율은 &quot;막은 것&quot;으로 측정된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 working rule의 효과는 눈에 잘 안 띈다. &lt;b&gt;막은 것은 존재하지 않으므로 보이지 않기 때문이다.&lt;/b&gt; 쓰지 않을 추상을 안 만들었다면, 그 안 만든 추상은 코드베이스 어디에도 없어서 &quot;내가 뭘 아꼈는지&quot;조차 잊는다. 그래서 절제의 규율은 의식적으로 &lt;i&gt;기록&lt;/i&gt;해야 그 가치를 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 척추는 하나의 원칙이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;speculative 금지 &amp;mdash; 미래의 Task는 스캐폴딩일 뿐, 미리 만들지 않는다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나중에 필요할 것 같아서&quot; 미리 짓는 코드를 금지하는 규칙이다. 그리고 이 원칙은 혼자 굴러가지 않는다. simplicity first(가장 단순한 해법부터), surgical changes(필요한 곳만 최소 변경), verification gate(완료 전 검증 통과 강제)가 함께 절제의 망을 짠다. 이들은 CLAUDE.md의 Working rules에 명문화돼 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 &amp;mdash; 각 규칙이 막은 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙을 &quot;지킨 미덕&quot;이 아니라 &quot;막은 사고&quot;로 뒤집어 보면 그 값어치가 드러난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;restraint-rules.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V6Pex/dJMcadI1Z7a/SSNZcXRKxbkeVdbXpxJnSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V6Pex/dJMcadI1Z7a/SSNZcXRKxbkeVdbXpxJnSK/img.png&quot; data-alt=&quot;working rule이 실제로 막은 것: speculative 금지&amp;amp;middot;simplicity first&amp;amp;middot;surgical changes&amp;amp;middot;verification gate가 각각 차단한 부채&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V6Pex/dJMcadI1Z7a/SSNZcXRKxbkeVdbXpxJnSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV6Pex%2FdJMcadI1Z7a%2FSSNZcXRKxbkeVdbXpxJnSK%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;760&quot; height=&quot;420&quot; data-filename=&quot;restraint-rules.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;working rule이 실제로 막은 것: speculative 금지&amp;middot;simplicity first&amp;middot;surgical changes&amp;middot;verification gate가 각각 차단한 부채&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;&lt;b&gt;speculative 금지 &amp;rarr; 안 쓸 추상&amp;middot;죽은 코드를 막았다.&lt;/b&gt;&lt;br /&gt;scrooge를 만들다 보면 &quot;압축 규칙을 플러그인 시스템으로 일반화하면 어떨까&quot;, &quot;dial을 임의 단계로 확장 가능하게 미리 설계하면&quot; 같은 유혹이 온다. 전부 &lt;i&gt;지금은 안 쓰는&lt;/i&gt; 기능이다. speculative 금지는 이걸 &quot;필요해지면 그때&quot;로 미뤘다. 결과적으로 코드베이스에 쓰이지 않는 추상 레이어와 죽은 코드가 쌓이지 않았다. 미래를 위한 스캐폴딩은 대부분 &lt;i&gt;그 미래가 안 와서&lt;/i&gt; 부채로만 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;simplicity first &amp;rarr; 과설계를 막았다.&lt;/b&gt;&lt;br /&gt;4화에서 본 단일 전역 상태 파일(&lt;code&gt;~/.claude/.scrooge-active&lt;/code&gt;)이 좋은 예다. 프로젝트별&amp;middot;세션별 상태 키를 두는 &quot;더 일반적인&quot; 설계가 가능했지만, simplicity first는 &quot;지금 필요한 가장 단순한 것&quot;을 골랐다. 전역이라는 한계는 4화에서 솔직히 적었지만, 그 단순함이 상태 관리 코드를 한 군데로 모아줬다. &lt;i&gt;필요해지기 전에 일반화하지 않는다&lt;/i&gt;가 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;surgical changes &amp;rarr; 곁다리 리팩터의 회귀 위험을 막았다.&lt;/b&gt;&lt;br /&gt;기능 하나를 고치다 보면 &quot;이 김에 주변도 정리하자&quot;는 충동이 온다. surgical changes는 변경을 필요한 곳에 국한했다. 곁다리 리팩터는 리뷰하기 어려운 큰 diff를 만들고, 본 작업과 무관한 회귀를 끌어들인다. 작은 변경은 되돌리기도 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;verification gate &amp;rarr; 불일치와 &quot;됐겠지&quot; 머지를 막았다.&lt;/b&gt;&lt;br /&gt;scrooge의 registry는 &lt;code&gt;language &amp;times; dial &amp;rarr; rule path&lt;/code&gt; 1:1 계약이다(0화). 규칙 파일을 옮기거나 이름을 바꾸면 &lt;i&gt;같은 변경에서&lt;/i&gt; registry도 동기화해야 한다. verification gate는 이 동기화를 사람의 기억이 아니라 &lt;b&gt;완료 전 검증&lt;/b&gt;에 맡겼다. &quot;아마 맞겠지&quot; 하고 머지했다가 registry와 실제 파일이 어긋나는 사고를 구조적으로 막은 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 &amp;mdash; 절제를 규칙으로 박기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서 &quot;코드 예제&quot; 자리는 working rule 문서가 채운다. 토이 프로젝트에도 그대로 옮길 수 있는 형태다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Good Practice &amp;mdash; working rule을 명문화하고 &quot;안 만들 것&quot;을 적기&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;# CLAUDE.md &amp;mdash; Working rules (발췌, 개념적 표현)

## 절제 원칙
- speculative 금지: &quot;나중에 필요할 것 같아서&quot;는 만들 이유가 아니다.
  미래 Task는 스캐폴딩으로만 남기고 코드로 짓지 않는다.
- simplicity first: 지금 필요한 가장 단순한 해법부터. 일반화는 두 번째
  사용 사례가 실제로 등장하면 그때.
- surgical changes: 변경은 필요한 곳에 국한. &quot;이 김에&quot;를 금지.
- verification gate: 완료 전 검증을 통과해야 머지. registry-rule 1:1
  동기화는 검증 항목에 포함.

## (선택) &quot;안 만들기로 한 것&quot; 로그
- 압축 규칙의 범용 플러그인화 &amp;rarr; 보류 (사용 사례 1개뿐)
- dial 임의 단계 확장 &amp;rarr; 보류 (lite/full로 충분)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 마지막 섹션이다. &lt;b&gt;&quot;안 만들기로 한 것&quot;을 명시적으로 적으면&lt;/b&gt;, 막은 것이 비로소 보인다. 같은 충동이 또 올 때 &quot;이미 보류하기로 했지&quot;로 빠르게 처리된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ Anti-Pattern &amp;mdash; &quot;이왕 만드는 김에&quot;의 누적&lt;/h3&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;# 규칙 없는 토이 프로젝트의 전형
- &quot;이왕이면 플러그인 시스템으로&quot; &amp;rarr; 안 쓰는 추상
- &quot;나중을 위해 설정 레이어 미리&quot; &amp;rarr; 죽은 코드
- &quot;이 김에 주변 리팩터&quot; &amp;rarr; 큰 diff, 회귀 위험
- &quot;혼자 쓰는데 검증은 무슨&quot; &amp;rarr; registry-rule 불일치 방치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제인가. 각각은 &quot;더 좋게 만들려는&quot; 선의지만, 누적되면 &lt;b&gt;핵심을 곁가지가 짓누른다.&lt;/b&gt; 혼자 쓰는 도구라 제동장치가 없으니 더 빨리 무거워진다. 그리고 이 부채는 &quot;기능&quot;처럼 보여서 줄이기도 어렵다 &amp;mdash; 만든 걸 지우는 건 안 만드는 것보다 늘 힘들다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 결과 &amp;mdash; 절제가 남긴 궤적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 2주&amp;middot;57커밋&amp;middot;v0.6.1이라는 작은 궤적은 &lt;i&gt;규모로 인상적인&lt;/i&gt; 숫자가 아니다. 이 숫자의 의미는 반대다 &amp;mdash; 이 기간 동안 &lt;b&gt;쌓이지 않은 부채&lt;/b&gt;가 이 도구를 계속 손볼 만하게(maintainable) 유지했다는 것이다. dogfooding도 같은 결이다. 압축 도구를 만들며 그 도구로 압축한 문서를 쓰는(자기 도구를 자기가 먹는) 습관은, &quot;안 쓰는 기능&quot;을 일찍 발견하게 해줘 speculative를 자연히 억제했다. (registry 확장성과 dogfooding의 자세한 이야기는 0화에서 다뤘으니 여기선 콜백만.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&amp;middot;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ 부채가 안 쌓여 도구가 계속 손볼 만함&lt;/td&gt;
&lt;td&gt;✗ &quot;나중에 필요했던&quot; 걸 그때 만드는 비용 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 작은 변경이라 리뷰&amp;middot;롤백이 쉬움&lt;/td&gt;
&lt;td&gt;✗ 큰 그림 리팩터를 미루다 한꺼번에 몰릴 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ &quot;안 만들 것&quot; 로그가 같은 고민을 재활용&lt;/td&gt;
&lt;td&gt;✗ 규율을 기록&amp;middot;유지하는 약간의 오버헤드&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;실무 팁 &amp;mdash; 토이 프로젝트 절제 체크리스트&lt;/b&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;&quot;나중에 필요할 것 같아서&quot;는 만들 이유가 아니다.&lt;/b&gt; 두 번째 사용 사례가 &lt;i&gt;실제로&lt;/i&gt; 올 때까지 미룬다(speculative 금지).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가장 단순한 해법부터 시작&lt;/b&gt;한다. 일반화는 단순함이 한계에 부딪힌 뒤에.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경을 필요한 곳에 국한&lt;/b&gt;한다. &quot;이 김에&quot;가 떠오르면 별도 작업으로 적어두고 지금은 안 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;혼자 써도 검증 게이트를 둔다.&lt;/b&gt; 계약(registry 같은)이 있으면 그 동기화를 사람 기억이 아니라 검증에 맡긴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;안 만들기로 한 것&quot;을 기록&lt;/b&gt;한다. 막은 것은 보이지 않으므로, 적어야 그 가치를 안다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 3줄 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;토이 프로젝트는 기능 부족이 아니라 &lt;b&gt;&quot;이왕이면&quot;의 누적&lt;/b&gt;으로 무거워진다. 품질은 무엇을 만들었나가 아니라 &lt;b&gt;무엇을 안 만들었나&lt;/b&gt;로 갈린다.&lt;/li&gt;
&lt;li&gt;speculative 금지&amp;middot;simplicity first&amp;middot;surgical changes&amp;middot;verification gate는 각각 죽은 코드&amp;middot;과설계&amp;middot;회귀 위험&amp;middot;계약 불일치를 막았다 &amp;mdash; 규율은 &lt;b&gt;막은 것&lt;/b&gt;으로 측정된다.&lt;/li&gt;
&lt;li&gt;막은 것은 보이지 않으므로 &lt;b&gt;&quot;안 만들기로 한 것&quot;을 기록&lt;/b&gt;해야 그 가치가 드러난다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시리즈를 마치며&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 &quot;출력을 다루는 일도 인터페이스 craft&quot;라는 한 줄을 따라왔다. scrooge는 LLM 출력의 표현&amp;middot;register&amp;middot;포맷을 설계하는 도구였고, 그건 FE가 오래 잘해온 presentation layer가 &lt;b&gt;에이전트 표면으로 넓어진&lt;/b&gt; 것일 뿐이다. 접근성으로서의 압축(0화), 측정 우선(1화), 안전성 계약(2화), 비자명한 디버깅(3화), 멀티 호스트 이식(4화), 그리고 절제(5화) &amp;mdash; 표면이 사람이든 에이전트든, 결과물이든 그걸 만드는 과정이든, 잘 만드는 craft는 결국 하나다.&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;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;에 공개돼 있고, 이슈&amp;middot;PR&amp;middot;새 언어 rule 기여 모두 환영한다.&lt;/p&gt;</description>
      <category>AI 엔지니어링</category>
      <category>dogfooding</category>
      <category>scrooge</category>
      <category>YAGNI</category>
      <category>검증게이트</category>
      <category>기술부채</category>
      <category>단순함</category>
      <category>소프트웨어설계</category>
      <category>엔지니어링규율</category>
      <category>토이프로젝트</category>
      <category>회고</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/189</guid>
      <comments>https://kir93.tistory.com/entry/%EC%95%88-%EB%A7%8C%EB%93%A0-%EA%B2%83%EC%9D%B4-%EA%B7%9C%EC%9C%A8%EC%9D%B4%EB%8B%A4-%E2%80%94-%EC%A0%88%EC%A0%9C%EB%A1%9C-%EB%81%8C%EA%B3%A0-%EA%B0%84-%ED%86%A0%EC%9D%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-Scrooge-5%ED%8E%B8#entry189comment</comments>
      <pubDate>Tue, 16 Jun 2026 17:53:09 +0900</pubDate>
    </item>
    <item>
      <title>켰는데 왜 안 먹지 &amp;mdash; 하나의 모드를 Claude Code와 Codex 두 호스트에 (Scrooge 4편)</title>
      <link>https://kir93.tistory.com/entry/%EC%BC%B0%EB%8A%94%EB%8D%B0-%EC%99%9C-%EC%95%88-%EB%A8%B9%EC%A7%80-%E2%80%94-%ED%95%98%EB%82%98%EC%9D%98-%EB%AA%A8%EB%93%9C%EB%A5%BC-Claude-Code%EC%99%80-Codex-%EB%91%90-%ED%98%B8%EC%8A%A4%ED%8A%B8%EC%97%90-Scrooge-4%ED%8E%B8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;  scrooge&lt;/a&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 &amp;mdash; &quot;같은 기능&quot;이 호스트마다 다른 표면을 갖는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 모드 하나를 Claude Code에서 잘 돌렸다고 하자. 이걸 Codex에도 태우려 한다. 기능은 &quot;똑같다&quot; &amp;mdash; 사용자 입력에 압축 지시를 끼워 LLM이 짧게 답하게 만든다. 그런데 막상 옮기려 보면, &lt;b&gt;두 호스트가 개입하는 메커니즘 자체가 다르다.&lt;/b&gt; 같은 기능인데 &lt;i&gt;표면&lt;/i&gt;이 갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 더 음험한 문제가 겹친다. &quot;scrooge가 켜졌다&quot;는 말이 실은 &lt;b&gt;세 가지 다른 의미&lt;/b&gt;로 쓰이고 있었다는 점이다. 설치됐다는 뜻인지, 지금 활성이라는 뜻인지, 이 세션에만 거는 지시라는 뜻인지. 이 셋을 섞으면 &quot;분명 켰는데 왜 안 먹지&quot; 같은 혼란이 반드시 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 당신이 얻어갈 것은 두 가지다. 하나는 &lt;b&gt;같은 기능을 다른 hook 메커니즘에 이식하는 패턴&lt;/b&gt;, 다른 하나는 &lt;b&gt;&quot;설치 범위 &amp;ne; 활성 상태 &amp;ne; 세션 지시&quot;라는 세 개념을 분리하는 멘탈 모델&lt;/b&gt;과, 그걸 어겼을 때 생기는 구체적 함정이다. 워크플로 글인 만큼 멱등성&amp;middot;가드레일&amp;middot;검증을 특히 강조한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; 섞이기 쉬운 세 가지&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;용어 1줄 정의 &amp;mdash; hook:&lt;/b&gt; 에이전트의 동작 흐름 중간에 끼어들어 입력&amp;middot;출력을 가로채거나 변형하는 확장 지점. scrooge는 hook으로 사용자 입력에 압축 지시를 주입한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrooge를 멀티 호스트로 옮기며 가장 중요했던 통찰은, &quot;scrooge 켜짐&quot;을 &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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;multihost-concepts.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WHzW1/dJMcah5R6ET/nvwnMFndylHiKZMJSWt6z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WHzW1/dJMcah5R6ET/nvwnMFndylHiKZMJSWt6z1/img.png&quot; data-alt=&quot;설치 범위&amp;amp;middot;활성 상태&amp;amp;middot;세션 지시 3개념 분리와, 단일 전역 상태 파일이 만드는 함정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WHzW1/dJMcah5R6ET/nvwnMFndylHiKZMJSWt6z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWHzW1%2FdJMcah5R6ET%2FnvwnMFndylHiKZMJSWt6z1%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;760&quot; height=&quot;450&quot; data-filename=&quot;multihost-concepts.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설치 범위&amp;middot;활성 상태&amp;middot;세션 지시 3개념 분리와, 단일 전역 상태 파일이 만드는 함정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;&gt;
&lt;li&gt;&lt;b&gt;설치 범위(install scope):&lt;/b&gt; hook이 &lt;i&gt;어디에&lt;/i&gt; 설치됐는가. user-level(전역)에 깔렸는가, 특정 위치에만 깔렸는가. &amp;rarr; 핵심: &lt;b&gt;설치됐다고 켜진 게 아니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활성 상태(active state):&lt;/b&gt; &lt;i&gt;지금&lt;/i&gt; 켜져 있는가. scrooge는 이걸 단일 전역 파일 &lt;code&gt;~/.claude/.scrooge-active&lt;/code&gt; 하나로 표현한다. 파일이 있으면 켜짐, 없으면 꺼짐.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 지시(session instruction):&lt;/b&gt; &lt;i&gt;이 세션에만&lt;/i&gt; 거는 일회성 지시. 전역 상태를 건드리지 않고 현재 대화에만 압축을 적용하는 경로.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋은 서로 독립이다. 설치돼 있어도 비활성일 수 있고, 전역은 꺼져 있는데 한 세션만 켤 수도 있다. 이 독립성을 인지하지 못하면 디버깅이 미궁에 빠진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 &amp;mdash; 다른 메커니즘, 같은 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;호스트별로 갈리는 hook 표면&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-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Claude Code:&lt;/b&gt; &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook으로 사용자 입력 제출 시점에 개입하고, &lt;b&gt;가시 출력&lt;/b&gt;으로 압축 지시를 드러낸다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Codex:&lt;/b&gt; &lt;b&gt;hook intercept&lt;/b&gt;로 흐름을 가로채는 방식. 같은 목적이지만 경로가 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;&quot;기능 명세는 하나, 호스트 어댑터는 둘&quot;&lt;/b&gt;로 본 것이다. 압축 모드라는 추상은 공유하되, 각 호스트의 hook 메커니즘에 맞는 어댑터를 따로 둔다. FE에서 같은 컴포넌트를 다른 렌더 타깃에 태우는 것과 같은 구조다 &amp;mdash; 로직은 공유, 어댑터는 분리.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활성 상태는 왜 &quot;전역 파일 하나&quot;인가, 그리고 그 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활성 상태를 단일 전역 파일(&lt;code&gt;~/.claude/.scrooge-active&lt;/code&gt;)로 둔 건 의도적 단순화다. 상태 관리 코드는 이 파일을 가리키는 &lt;code&gt;getStatePath()&lt;/code&gt; 한 군데로 모인다(&lt;code&gt;hooks/scrooge-config.js&lt;/code&gt;의 해당 로직). 프로젝트별&amp;middot;세션별 키가 없으니 구현이 단순하고, &quot;지금 켜졌나?&quot;의 답이 항상 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대가는 &lt;b&gt;켜고 끄기가 전역&lt;/b&gt;이라는 점이다. 그리고 여기서 이 글의 핵심 함정이 나온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 명령 &lt;code&gt;/scrooge&lt;/code&gt;는 &lt;b&gt;전역 상태 파일&lt;/b&gt;을 켜고 끈다 &amp;rarr; 모든 세션에 영향.&lt;br /&gt;반면 skill 단독 호출은 &lt;b&gt;그 세션에만&lt;/b&gt; 적용되고 전역 상태는 건드리지 않는다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;scrooge를 켜는&quot; 두 경로가 &lt;b&gt;서로 다른 층을 건드린다.&lt;/b&gt; 사용자가 한 세션에서 skill로 켜고 &quot;켰다&quot;고 믿은 뒤 다른 세션을 열면, 전역 상태는 여전히 꺼져 있어 &quot;어, 껐는데 왜 안 먹지(혹은 켰는데 왜 안 먹지)&quot;가 된다. 이건 버그가 아니라 &lt;b&gt;세 개념(②와 ③)을 섞었을 때 필연적으로 생기는 혼란&lt;/b&gt;이다. 그래서 이 함정을 &lt;i&gt;코드 주석이나 버그가 아니라 설계 문서/멘탈 모델 레벨에서&lt;/i&gt; 명시하는 게 중요하다(이 동작이 왜 전역인지 코드로 추적한 작업이 한 세션에 기록돼 있다).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멱등한 설치기 &amp;mdash; 재설치해도 같은 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 호스트 + 전역 상태는 설치/재설치를 까다롭게 만든다. 이전 버전의 hook 블록이 설정 파일에 남아 중첩되거나(중첩 레거시 hook), 더는 안 쓰는 상태가 남는(stale state) 문제다. scrooge 설치기는 이걸 &lt;b&gt;멱등(idempotent)하게&lt;/b&gt; 처리한다 &amp;mdash; 몇 번을 재설치해도 결과가 같도록, 설치 전에 중첩 레거시 hook 블록과 stale state를 청소한다. 설치가 멱등하지 않으면, 재설치할 때마다 hook이 쌓여 같은 입력에 압축 지시가 두 번 끼는 식의 부작용이 난다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 &amp;mdash; 멀티 호스트 어댑터 + 멱등 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서 &quot;코드 예제&quot; 자리는 hook 설정&amp;middot;설치기 구조가 채운다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Good Practice &amp;mdash; 어댑터 분리 + 멱등 설치 + 상태 단일화&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 1) 활성 상태는 단일 전역 파일 한 군데로 (getStatePath 하나)
function getStatePath() {
  return path.join(os.homedir(), &quot;.claude&quot;, &quot;.scrooge-active&quot;);
}
const isActive = () =&amp;gt; fs.existsSync(getStatePath());

// 2) 호스트별 어댑터 분리 &amp;mdash; 기능 명세는 공유, 메커니즘만 다름
const adapters = {
  &quot;claude-code&quot;: injectViaUserPromptSubmitHook,  // 가시 출력
  &quot;codex&quot;:       injectViaHookIntercept,         // intercept
};

// 3) 멱등 설치: 항상 &quot;청소 &amp;rarr; 설치&quot; 순서
function install(host) {
  removeNestedLegacyHooks(host);   // 중첩 레거시 hook 제거
  clearStaleState(host);           // stale state 청소
  adapters[host].register();       // 그다음 설치 &amp;rarr; 몇 번 돌려도 같은 결과
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 (a) 상태가 한 군데(&lt;code&gt;getStatePath&lt;/code&gt;)로 모이고, (b) 호스트 차이는 어댑터로 격리되며, (c) 설치가 항상 &quot;청소 먼저&quot;라 멱등하다는 점이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ Anti-Pattern &amp;mdash; 세 개념을 섞고, 설치가 비멱등&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 잘못된 설계
function enableScrooge() {
  // 설치하면서 동시에 켜고, 세션 지시랑도 안 구분
  appendHookBlock(config);           // &amp;larr; 재설치마다 블록이 쌓임 (비멱등)
  // 활성 상태를 세션마다 따로? 전역으로? 기준 없음
  // skill 호출과 /scrooge가 같은 걸 켠다고 가정 &amp;larr; 실제론 다른 층
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제인가. &lt;code&gt;appendHookBlock&lt;/code&gt;은 재설치할 때마다 hook을 누적시켜 압축 지시가 중복 주입된다(비멱등). 그리고 &quot;설치=활성=세션&quot;을 한 함수에 뭉치면, skill로 켠 것과 &lt;code&gt;/scrooge&lt;/code&gt;로 켠 것이 같다고 착각하게 만들어 3화에서 본 종류의 &quot;재현 안 되는&quot; 혼란을 낳는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 결과 &amp;mdash; 분리가 사주는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 개념을 분리하고 설치를 멱등하게 만들면, &quot;왜 안 먹지&quot;의 답이 명확해진다. &lt;i&gt;설치는 됐는데 전역이 꺼졌나(②)? 이 세션만 skill로 켠 거였나(③)?&lt;/i&gt; 를 각각 확인하면 된다. 그리고 재설치를 몇 번 하든 hook이 한 벌만 남으므로, 압축 지시 중복 같은 부작용이 구조적으로 사라진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&amp;middot;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&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;✓ 단일 전역 상태 파일로 &quot;켜졌나?&quot;가 항상 명확&lt;/td&gt;
&lt;td&gt;✗ 켜고 끄기가 전역 &amp;mdash; 프로젝트별 분리 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 멱등 설치로 재설치 부작용(중복 hook) 차단&lt;/td&gt;
&lt;td&gt;✗ &quot;청소 먼저&quot; 로직을 호스트별로 유지해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;실무 팁 &amp;mdash; 멀티 호스트 툴링 체크리스트&lt;/b&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;&quot;켜짐&quot;을 세 층으로 분리&lt;/b&gt;한다: 설치 범위 / 활성 상태 / 세션 지시. 한 단어로 뭉치지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기능 명세와 호스트 어댑터를 분리&lt;/b&gt;한다. 호스트가 늘 때 건드리는 건 어댑터뿐이어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활성 상태의 단위를 명시적으로 정한다.&lt;/b&gt; 전역이면 &quot;전역임&quot;을 문서에 박고, &lt;code&gt;/scrooge&lt;/code&gt;와 skill 단독 호출의 차이를 사용자에게 분명히 알린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설치를 멱등하게&lt;/b&gt;: 항상 &quot;청소(중첩 레거시 hook&amp;middot;stale state) &amp;rarr; 설치&quot; 순서. 재설치가 결과를 바꾸면 안 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 게이트를 둔다.&lt;/b&gt; 설치 후 &quot;hook이 한 벌만 등록됐는가 / 상태 파일 경로가 단일한가&quot;를 자동 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 3줄 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;같은 압축 모드라도 호스트마다 hook 메커니즘이 다르다(Claude=&lt;code&gt;UserPromptSubmit&lt;/code&gt;+가시 출력, Codex=hook intercept). &lt;b&gt;기능은 공유, 어댑터는 분리&lt;/b&gt;로 푼다.&lt;/li&gt;
&lt;li&gt;&quot;scrooge 켜짐&quot;은 &lt;b&gt;설치 범위 &amp;ne; 활성 상태 &amp;ne; 세션 지시&lt;/b&gt; 세 개념이며, 섞으면 &quot;켰는데 왜 안 먹지&quot; 혼란이 난다. 특히 &lt;code&gt;/scrooge&lt;/code&gt;(전역)와 skill 단독 호출(세션 한정)이 다른 층을 건드린다.&lt;/li&gt;
&lt;li&gt;멀티 호스트 + 전역 상태에서는 &lt;b&gt;멱등 설치&lt;/b&gt;(청소 먼저)가 필수다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 &lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;에 공개돼 있고, 이슈&amp;middot;PR&amp;middot;새 언어 rule 기여 모두 환영한다.&lt;/p&gt;</description>
      <category>AI 엔지니어링</category>
      <category>claude code</category>
      <category>codex</category>
      <category>Hook</category>
      <category>scrooge</category>
      <category>개발자UX</category>
      <category>멀티호스트</category>
      <category>멱등성</category>
      <category>상태관리</category>
      <category>에이전트툴링</category>
      <category>플러그인</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/188</guid>
      <comments>https://kir93.tistory.com/entry/%EC%BC%B0%EB%8A%94%EB%8D%B0-%EC%99%9C-%EC%95%88-%EB%A8%B9%EC%A7%80-%E2%80%94-%ED%95%98%EB%82%98%EC%9D%98-%EB%AA%A8%EB%93%9C%EB%A5%BC-Claude-Code%EC%99%80-Codex-%EB%91%90-%ED%98%B8%EC%8A%A4%ED%8A%B8%EC%97%90-Scrooge-4%ED%8E%B8#entry188comment</comments>
      <pubDate>Mon, 15 Jun 2026 17:50:00 +0900</pubDate>
    </item>
    <item>
      <title>재현 안 되는 버그의 정답은 버그가 아니었다 &amp;mdash; 비자명한 디버깅 기록 (Scrooge 3편)</title>
      <link>https://kir93.tistory.com/entry/%EC%9E%AC%ED%98%84-%EC%95%88-%EB%90%98%EB%8A%94-%EB%B2%84%EA%B7%B8%EC%9D%98-%EC%A0%95%EB%8B%B5%EC%9D%80-%EB%B2%84%EA%B7%B8%EA%B0%80-%EC%95%84%EB%8B%88%EC%97%88%EB%8B%A4-%E2%80%94-%EB%B9%84%EC%9E%90%EB%AA%85%ED%95%9C-%EB%94%94%EB%B2%84%EA%B9%85-%EA%B8%B0%EB%A1%9D-Scrooge-3%ED%8E%B8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;  scrooge&lt;/a&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 &amp;mdash; 가장 비싼 버그는 &quot;버그가 아닌 버그&quot;다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅의 교과서적 함정은 &lt;b&gt;존재하지 않는 버그를 쫓는 것&lt;/b&gt;이다. 코드 어딘가가 틀렸다고 확신한 채로 가설을 세우고 검증하고 또 세우는데, 알고 보니 코드는 멀쩡했고 내 &lt;i&gt;전제&lt;/i&gt;가 틀렸던 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrooge를 굴리며 만난 두 버그가 정확히 이 결을 가졌다. 하나는 &quot;무응답&quot;으로 보였지만 사실 정상 동작이었고, 다른 하나는 &quot;압축 때문&quot;으로 보였지만 압축은 원인이 아니었다. 둘 다 &lt;b&gt;표면 증상이 엉뚱한 범인을 가리키고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 당신이 얻어갈 것은 두 가지다. 하나는 &lt;b&gt;가설을 체계적으로 배제해 &quot;버그 아님&quot;에 도달하는 절차&lt;/b&gt;, 다른 하나는 &lt;b&gt;여러 원인이 겹친 현상에서 &quot;진짜 원인&quot;과 &quot;가중 요인&quot;을 분리하는 분석법&lt;/b&gt;이다. 둘 다 에이전트 툴링처럼 비결정적이고 여러 레이어가 얽힌 시스템에서 특히 쓸모 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; 표면 증상과 root cause의 거리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트 플러그인은 여러 레이어가 포개진 시스템이다. 마켓플레이스/배포 레이어, 호스트(Claude Code 등)의 hook 레이어, 플러그인의 command&amp;middot;skill 레이어, 그 아래 모델의 생성 레이어. &lt;b&gt;증상은 맨 위에서 보이지만 원인은 어느 층에든 있을 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 시스템의 디버깅은 &quot;코드를 읽어 버그를 찾는다&quot;보다 &lt;b&gt;&quot;가능한 원인 레이어를 하나씩 배제해 범위를 좁힌다&quot;&lt;/b&gt;에 가깝다. 의학의 감별 진단(differential diagnosis)과 같은 구조다. 그리고 그 배제 과정의 끝에서 가끔, &lt;b&gt;&quot;모든 레이어가 정상이다 = 버그가 아니다&quot;&lt;/b&gt;라는 결론을 만난다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;용어 1줄 정의 &amp;mdash; drift:&lt;/b&gt; 의도한 출력 양식에서 모델 출력이 조금씩 벗어나는 현상. 여기서는 한국어 출력에 한자(漢字)가 새어 나오는 &quot;한자 drift&quot;를 가리킨다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 ① &amp;mdash; &quot;무응답&quot;의 4겹 오진&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/scrooge-stats&lt;/code&gt;(절감 통계를 보여주는 명령)가 아무 응답도 내지 않는 증상이 보고됐다. 분명 버그처럼 보였다. 하지만 결정적 단서가 하나 있었다 &amp;mdash; &lt;b&gt;재현이 일정하지 않았다.&lt;/b&gt; 이 &quot;재현 안 됨&quot;이야말로 첫 번째 힌트였다. 확정적 버그라면 같은 조건에서 항상 재현돼야 하니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가설을 세우고 하나씩 배제해 나갔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;misdiagnosis-funnel.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkgghK/dJMcacXJ8Co/r2jUM5KDJ131qxhFVzsepk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkgghK/dJMcacXJ8Co/r2jUM5KDJ131qxhFVzsepk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkgghK/dJMcacXJ8Co/r2jUM5KDJ131qxhFVzsepk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkgghK%2FdJMcacXJ8Co%2Fr2jUM5KDJ131qxhFVzsepk%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;760&quot; height=&quot;470&quot; data-filename=&quot;misdiagnosis-funnel.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;470&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;&lt;b&gt;가설 ① 플러그인 마켓플레이스 캐시 vs 로컬 repo 버전 차이.&lt;/b&gt;&lt;br /&gt;가장 흔한 용의자. 마켓플레이스에 캐시 된 플러그인 버전과 로컬에서 개발 중인 repo 버전이 달라, 옛 코드가 도는 상황. &amp;rarr; &lt;b&gt;배제.&lt;/b&gt; 버전을 맞춰 확인했지만 증상이 그대로였고, 무엇보다 &quot;재현 안 됨&quot;이 버전 불일치로는 설명되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가설 ② hook intercept가 입력을 가로채 먹어버림.&lt;/b&gt;&lt;br /&gt;scrooge는 hook으로 동작에 개입한다. hook이 stats 호출을 가로채 응답을 삼켰을 수 있다. &amp;rarr; &lt;b&gt;배제.&lt;/b&gt; hook intercept 경로를 시뮬레이션해 입력이 어디로 흐르는지 추적했지만, 여기서 응답이 사라지는 게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가설 ③ command와 skill의 이름 충돌(&lt;code&gt;disable-model-invocation&lt;/code&gt;).&lt;/b&gt;&lt;br /&gt;같은 이름의 command와 skill이 서로를 가리는(shadow) 관계가 있었다(이 그림자 관계는 메모리에도 기록돼 있다). 이게 stats 호출을 잘못된 대상으로 보냈을 수 있다. &amp;rarr; &lt;b&gt;배제.&lt;/b&gt; shadow 관계는 실재했지만, 무응답의 직접 원인은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;root cause ④ 측정할 대화가 없는 &quot;0 턴 새 세션&quot;.&lt;/b&gt;&lt;br /&gt;세 가설을 배제하고 나서야 전제를 의심했다. stats는 &lt;i&gt;대화의 토큰을 집계해 절감률을 보여주는&lt;/i&gt; 명령이다. 그런데 호출된 시점이 &lt;b&gt;막 시작한 0 턴짜리 새 세션&lt;/b&gt; &amp;mdash; 즉 &lt;i&gt;집계할 대화 자체가 없는&lt;/i&gt; 상황이었다. stats는 정확히 동작하고 있었다. 셀 게 없으니 보여줄 것도 없었던 것이다. &lt;b&gt;무응답은 버그가 아니라, &quot;측정 대상 없음&quot;이 무응답처럼 보인 UX 문제였다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 교훈은 그림 맨 아래에 있다 &amp;mdash; &lt;b&gt;&quot;재현 안 되는 버그&quot;의 정답이 &quot;버그 아님&quot;일 수 있다.&lt;/b&gt; 그리고 가설을 하나씩 배제하는 과정 자체가, 결국 틀린 건 코드가 아니라 내 전제였음을 드러내 준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 동작 원리 ② &amp;mdash; 한자 drift: 원인과 가중 요인 분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 버그는 분류가 까다로웠다. 한국어 출력에 한자가 섞여 나오는 &lt;b&gt;한자 drift&lt;/b&gt;다(2화의 정합성 가드가 막으려는 바로 그 현상). 가장 쉬운 결론은 &quot;압축이 한국어를 한자로 치환해서&quot;였다. 압축 강도를 올리면 더 자주 보였으니까. 하지만 이 결론은 틀렸다 &amp;mdash; 정확히는, &lt;b&gt;인과의 방향을 착각한&lt;/b&gt; 결론이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hanja-drift.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQaTSP/dJMb99Uffnr/5cfy6hHjleH1xiLWKfFFNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQaTSP/dJMb99Uffnr/5cfy6hHjleH1xiLWKfFFNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQaTSP/dJMb99Uffnr/5cfy6hHjleH1xiLWKfFFNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQaTSP%2FdJMb99Uffnr%2F5cfy6hHjleH1xiLWKfFFNK%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;760&quot; height=&quot;380&quot; data-filename=&quot;hanja-drift.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;380&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;현상을 세 원인으로 분해했다.&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;① CJK 토크나이저 공유:&lt;/b&gt; 많은 모델이 한&amp;middot;중&amp;middot;일 문자를 같은 토큰 공간에서 다룬다. 한국어 한자어와 그에 대응하는 한자(漢字) 토큰이 토큰 공간상 가깝다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;② 의미벡터 근접:&lt;/b&gt; 한자어(예: 한글로 쓴 단어)와 그 한자 표기는 의미 공간에서도 가깝다. 모델 입장에서 둘은 &quot;거의 같은 뜻&quot;이라, 치환이 일어나기 쉬운 토양이 깔려 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;③ 샘플링 무작위성:&lt;/b&gt; 디코딩은 확률 분포에서 토큰을 뽑는 과정이라, 가끔 한자 토큰이 뽑힌다. 비결정성 그 자체다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋이 겹쳐 한자 drift가 생긴다. 그렇다면 압축은? &lt;b&gt;압축은 원인이 아니라 가중 요인(aggravator)이다.&lt;/b&gt; 압축 압력이 출력을 짧고 밀도 높게 몰면 한자 치환이 &lt;i&gt;더 자주&lt;/i&gt; 유발되는 건 맞다. 하지만 압축을 꺼도 위 세 원인은 그대로 남으므로 drift가 0이 되지 않는다. 만약 &quot;압축이 원인&quot;이라 결론 냈다면, 압축을 약화하는 헛된 처방으로 갔을 것이다 &amp;mdash; 토큰만 손해 보고 drift는 안 사라지는.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리가 실무적으로 중요한 이유는 &lt;b&gt;처방이 달라지기 때문&lt;/b&gt;이다. 원인이 모델 레벨(토크나이저&amp;middot;샘플링)에 있으므로, 올바른 대응은 압축을 줄이는 게 아니라 &lt;b&gt;출력 레벨의 정합성 가드&lt;/b&gt;(2화의 한글 전용 제약)로 drift를 잡는 것이다. 원인과 가중 요인을 섞었다면 이 처방에 도달하지 못한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항 &amp;mdash; 이런 디버깅에서 배운 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;잘한 점 (Good)&lt;/th&gt;
&lt;th&gt;빠지기 쉬운 함정 (Anti-Pattern)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ &quot;재현 안 됨&quot;을 단서로 읽고 전제를 의심함&lt;/td&gt;
&lt;td&gt;✗ &quot;분명 버그다&quot;라는 전제를 끝까지 안 놓음&lt;/td&gt;
&lt;/tr&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;✓ 원인 vs 가중 요인을 분리 (압축은 가중 요인)&lt;/td&gt;
&lt;td&gt;✗ 상관(압축&amp;uarr;&amp;rarr;drift&amp;uarr;)을 인과로 단정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 원인 레이어에 맞는 처방(정합성 가드)&lt;/td&gt;
&lt;td&gt;✗ 가중 요인을 줄이는 헛된 처방(압축 약화)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;실무 팁 &amp;mdash; 비 자명한 버그 체크리스트&lt;/b&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;&quot;재현이 일정한가?&quot;를 가장 먼저 묻는다.&lt;/b&gt; 비일관적 재현은 종종 &quot;버그 아님&quot; 또는 &quot;전제 오류&quot;의 신호다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가능한 원인을 레이어로 나열하고 하나씩 배제한다.&lt;/b&gt; 배제 과정 자체가 기록이 되고, 나중에 같은 증상이 오면 재사용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세 가설을 배제했다면 전제를 의심한다.&lt;/b&gt; &quot;셀 게 있긴 한가?&quot;처럼, 코드가 아니라 &lt;i&gt;입력 조건&lt;/i&gt;을 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상관과 인과를 구분한다.&lt;/b&gt; &quot;X를 올리면 Y가 는다&quot;는 X가 Y의 원인이라는 증거가 아니다. X를 꺼도 Y가 남는지 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인 레이어에 처방을 건다.&lt;/b&gt; 모델 레벨 원인은 출력 가드로, 설정 레벨 원인은 설정으로. 레이어를 잘못짚으면 처방이 헛돈다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 3줄 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;재현 안 되는 버그&quot;의 정답은 &lt;b&gt;&quot;버그 아님&quot;&lt;/b&gt;일 수 있다. &lt;code&gt;/scrooge-stats&lt;/code&gt; 무응답은 세 가설을 배제한 끝에 &quot;측정할 대화가 없는 0 턴 새 세션&quot;이라는 전제 오류로 판명됐다.&lt;/li&gt;
&lt;li&gt;한자 drift의 진짜 원인은 &lt;b&gt;CJK 토크나이저 공유&amp;middot;의미벡터 근접&amp;middot;샘플링 무작위성&lt;/b&gt;이며, 압축은 원인이 아니라 &lt;b&gt;가중 요인&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;디버깅의 핵심은 &lt;b&gt;상관을 인과로 착각하지 않고&lt;/b&gt;, 원인이 사는 레이어에 처방을 거는 것이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 &lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;에 공개돼 있고, 이슈&amp;middot;PR&amp;middot;새 언어 rule 기여 모두 환영한다.&lt;/p&gt;</description>
      <category>AI 엔지니어링</category>
      <category>claude code</category>
      <category>Hook</category>
      <category>LLM</category>
      <category>scrooge</category>
      <category>근본원인분석</category>
      <category>디버깅</category>
      <category>상관과인과</category>
      <category>에이전트툴링</category>
      <category>토크나이저</category>
      <category>트러블슈팅</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/187</guid>
      <comments>https://kir93.tistory.com/entry/%EC%9E%AC%ED%98%84-%EC%95%88-%EB%90%98%EB%8A%94-%EB%B2%84%EA%B7%B8%EC%9D%98-%EC%A0%95%EB%8B%B5%EC%9D%80-%EB%B2%84%EA%B7%B8%EA%B0%80-%EC%95%84%EB%8B%88%EC%97%88%EB%8B%A4-%E2%80%94-%EB%B9%84%EC%9E%90%EB%AA%85%ED%95%9C-%EB%94%94%EB%B2%84%EA%B9%85-%EA%B8%B0%EB%A1%9D-Scrooge-3%ED%8E%B8#entry187comment</comments>
      <pubDate>Sun, 14 Jun 2026 17:46:17 +0900</pubDate>
    </item>
    <item>
      <title>안 깎는 것이 실력이다 &amp;mdash; 압축 도구가 거부해야 할 출력 (Scrooge 2편)</title>
      <link>https://kir93.tistory.com/entry/%EC%95%88-%EA%B9%8E%EB%8A%94-%EA%B2%83%EC%9D%B4-%EC%8B%A4%EB%A0%A5%EC%9D%B4%EB%8B%A4-%E2%80%94-%EC%95%95%EC%B6%95-%EB%8F%84%EA%B5%AC%EA%B0%80-%EA%B1%B0%EB%B6%80%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%B6%9C%EB%A0%A5-Scrooge-2%ED%8E%B8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;  scrooge&lt;/a&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 &amp;mdash; 압축 도구의 진짜 위험은 &quot;덜 압축&quot;이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 도구를 만들 때 본능적으로 좇는 목표는 &quot;최대한 깎기&quot;다. 그런데 출력 압축에는 일반적인 성능 최적화에 없는 위험이 하나 있다. &lt;b&gt;너무 잘 깎으면 사람이 다친다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 LLM이 이런 출력을 내야 하는 상황을 생각해 보자. &quot;이 명령은 데이터베이스를 복구 불가능하게 삭제합니다. 실행 전 백업을 확인하세요.&quot; 압축 규칙이 이걸 &quot;DB 삭제 주의&quot;로 깎았다고 하자. 토큰은 줄었다. 하지만 &lt;i&gt;복구 불가능&lt;/i&gt;이라는 사실과 &lt;i&gt;백업 확인&lt;/i&gt;이라는 행동 지시가 사라졌다. 사용자가 이 경고를 가볍게 보고 명령을 실행하면, 줄인 토큰 몇 개가 데이터 전체와 맞바꿔진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 잘 만든 압축 도구의 핵심 설계는 &quot;어떻게 더 깎을까&quot;가 아니라 &lt;b&gt;&quot;무엇을 절대 깎지 않을까&quot;&lt;/b&gt;다. 이 글에서 당신이 얻어갈 것은, 출력 도구에 &lt;b&gt;&quot;거부 영역&quot;을 계약처럼 박는 방법&lt;/b&gt;과 그 경계를 어디에 긋는지에 대한 구체적 기준이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; register, 그리고 &quot;압축하지 않는다&quot;는 약속&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;용어 1줄 정의 &amp;mdash; register:&lt;/b&gt; 언어학에서 상황에 맞는 말투&amp;middot;격식 수준을 뜻한다. 여기서는 &quot;출력이 띠어야 할 표현 양식&quot; 정도로 읽으면 된다. 압축된 register, 격식 있는 register, &lt;i&gt;안전을 위한 normal prose register&lt;/i&gt; 등.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrooge에는 &lt;b&gt;safety register&lt;/b&gt;라는 컨벤션이 있다(CLAUDE.md에 명문화). 골자는 한 줄이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 종류의 출력은, 압축 강도(dial)가 아무리 공격적이어도 normal prose로 유지한다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 압축 도구이면서도 &quot;여기는 압축 안 함&quot;을 &lt;b&gt;규칙으로 선언&lt;/b&gt;해 둔 것이다. 이건 도구의 약점이 아니라, 도구가 신뢰받을 수 있는 근거다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;안전성 계약&quot;이라는 프레임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이 글의 핵심 프레임이 나온다. 무엇을 압축하지 않을지를 정하는 일은 단순한 예외 처리가 아니라 &lt;b&gt;계약(contract)&lt;/b&gt;이다. 사용자는 scrooge를 켤 때 암묵적으로 이렇게 믿는다. *&quot;토큰은 아껴주되, 내가 다칠 수 있는 정보는 온전히 보여주겠지.&quot;* 안전 register는 그 믿음을 코드와 규칙으로 보증하는 명문 조항이다. 계약이 깨지면 &amp;mdash; 즉 압축이 안전 정보를 깎으면 &amp;mdash; 도구는 &quot;토큰 아끼는 도구&quot;가 아니라 &quot;위험한 도구&quot;가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 &amp;mdash; 경계를 어디에 긋는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;압축 대상&lt;/b&gt;과 &lt;b&gt;안전 register&lt;/b&gt;를 명확히 분리하는 것이다. dial이 올라가면 전자는 점점 더 깎이지만, 후자는 어느 강도에서도 normal prose를 유지한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;safety-register.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCy4d4/dJMcageMtys/x24jXl2Q27iKiBKuTvQNK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCy4d4/dJMcageMtys/x24jXl2Q27iKiBKuTvQNK0/img.png&quot; data-alt=&quot;압축 강도가 올라가도 안전 register&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCy4d4/dJMcageMtys/x24jXl2Q27iKiBKuTvQNK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCy4d4%2FdJMcageMtys%2Fx24jXl2Q27iKiBKuTvQNK0%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;760&quot; height=&quot;440&quot; data-filename=&quot;safety-register.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;압축 강도가 올라가도 안전 register&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전 register에 들어가는 것 (압축 제외)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 부류가 명시적으로 &quot;안 깎는&quot; 영역이다.&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; credential 노출 위험, 위험한 명령, 권한 상승 같은 경고. 압축돼 흐려지면 사용자가 위험을 과소평가한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;되돌릴 수 없는 동작:&lt;/b&gt; 삭제&amp;middot;덮어쓰기&amp;middot;배포처럼 한 번 하면 못 무르는 행위에 대한 안내. &lt;i&gt;복구 불가능성&lt;/i&gt;과 &lt;i&gt;선행 확인 절차&lt;/i&gt;가 압축으로 증발하면 안 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모호하면 위험한 다단계 절차:&lt;/b&gt; 순서가 틀리면 사고가 나는 절차. 압축이 단계를 뭉치거나 생략하면 명료성(auto-clarity)이 깨진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관통하는 기준은 하나다. &lt;b&gt;&quot;압축이 명료성을 깎았을 때, 사람이 잘못 행동할 수 있는 출력인가?&quot;&lt;/b&gt; 그렇다면 안전 register다. 여기서 &lt;b&gt;auto-clarity가 압축률보다 항상 우선&lt;/b&gt;한다는 우선순위가 register 레벨에 박힌다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한자 가드 &amp;mdash; &quot;압축 규칙&quot;이 아니라 &quot;정합성 가드&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrooge에는 한국어 출력에서 한자(漢字)가 새어 나오는 것을 막는 규칙이 있다. 흥미로운 분류 결정은 이걸 &lt;b&gt;압축 규칙이 아니라 정합성(correctness) 가드로 처리&lt;/b&gt;한 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이 구분이 중요한가. 한자 leakage 가드를 &quot;압축 규칙&quot;으로 분류하면, 압축 강도를 낮추면 가드도 느슨해질 수 있다는 잘못된 함의가 생긴다. 하지만 한글 출력에 한자가 섞이는 건 &lt;i&gt;압축 강도와 무관하게&lt;/i&gt; 틀린 출력이다. 그래서 이건 압축 다이얼에 종속되는 규칙이 아니라, &lt;b&gt;모든 dial에서 똑같이 지켜야 할 정합성 조건&lt;/b&gt;으로 박았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 여기서 의도적이고 명시적인 비대칭이 하나 있다. 한자 가드는 ko의 every dial(lite&amp;middot;full 모두)에 적용하되, &lt;b&gt;en에는 의도적으로 넣지 않았다&lt;/b&gt;(이 결정을 정리한 세션에서 en은 N/A로 명시). 한자 leakage는 CJK(한&amp;middot;중&amp;middot;일) 토크나이저를 공유하는 환경에서 한국어 같은 언어에 나타나는 &lt;b&gt;CJK 한정 실패 모드&lt;/b&gt;라, 영어 출력에는 해당하지 않기 때문이다. 중요한 건 이 예외를 &quot;그냥 안 넣음&quot;으로 흘리지 않고 &amp;mdash; &lt;b&gt;bilingual parity(두 언어 동등 대우)의 예외임을 명시적으로 플래그 했다는&lt;/b&gt; 점이다. parity는 기본 원칙이되, 깨야 할 땐 &lt;i&gt;왜 깨는지를 기록&lt;/i&gt;으로 남긴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 &amp;mdash; 거부 영역을 규칙으로 박기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서 &quot;코드 예제&quot; 자리는 register 정책과 가드 규칙이 채운다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Good Practice &amp;mdash; 안전 register를 dial과 분리해 선언&lt;/h3&gt;
&lt;pre class=&quot;ldif&quot;&gt;&lt;code&gt;# rules/ko/full.md (발췌, 개념적 표현)

## 압축 정책 (dial: full &amp;mdash; 공격적)
- 인사&amp;middot;맥락 복창&amp;middot;완곡어법 제거
- 부연은 핵심만, 산문은 압축 표현으로

## 안전 register (압축 제외 &amp;mdash; 모든 dial 공통)
다음은 압축 강도와 무관하게 normal prose로 유지한다:
- 보안 경고(credential&amp;middot;위험 명령&amp;middot;권한)
- 되돌릴 수 없는 동작(삭제/덮어쓰기/배포)의 위험성과 선행 확인
- 순서가 틀리면 위험한 다단계 절차

## 정합성 가드 (압축 규칙 아님 &amp;mdash; 모든 dial 공통)
- 한국어 출력은 한글 전용. 한자(漢字) leakage 금지.
#   ※ 이 가드는 ko 전용. en은 CJK 한정 실패라 N/A (parity 예외, 의도적).&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;안전 register&quot;와 &quot;정합성 가드&quot;가 &lt;b&gt;압축 정책과 다른 섹션에, 모든 dial 공통으로&lt;/b&gt; 적혀 있다는 점이다. dial을 바꿔도 이 두 섹션은 그대로다 &amp;mdash; 그게 계약이라는 증거다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ Anti-Pattern &amp;mdash; 안전 정보를 압축 대상에 섞기&lt;/h3&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;# 잘못된 규칙
## 압축 정책 (dial: full)
- 모든 출력을 최대한 짧게. 경고&amp;middot;주의도 한 줄로 요약.   # &amp;larr; 위험
- 절차는 단계 합쳐서 압축.                              # &amp;larr; 모호해지면 사고
- 한자 써도 됨 (토큰 효율 좋음).                         # &amp;larr; 정합성 위반&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제인가. 안전 정보를 압축 대상에 함께 두면, &lt;b&gt;압축 강도를 올릴 때마다 안전성이 함께 깎인다.&lt;/b&gt; &quot;경고도 한 줄로&quot;는 &lt;i&gt;복구 불가능&lt;/i&gt; 같은 결정적 단어를 날리고, &quot;단계 합쳐서&quot;는 순서 의존 절차를 망가뜨린다. &quot;한자 써도 됨&quot;은 토큰은 줄지만 한국어 독자에게 읽기 비용을 떠넘긴다(0화의 접근성 프레임과 정면충돌). 셋 다 토큰 몇 개와 사용자의 안전&amp;middot;이해를 맞바꾸는 거래다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 결과 &amp;mdash; 계약이 있을 때의 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전 register가 박혀 있으면, dial을 full로 올려도 보안 경고는 온전한 문장으로 나온다. 사용자는 &quot;압축 모드인데도 위험 정보는 또렷하다&quot;는 일관성을 경험하고, 그게 도구를 신뢰하는 근거가 된다. 반대로 정합성 가드 덕에 한국어 출력에 한자가 새지 않으므로, ko 사용자는 압축의 이득을 &lt;i&gt;읽기 비용 증가 없이&lt;/i&gt; 온전히 가져간다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&amp;middot;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ 압축 강도와 무관하게 안전성이 보장됨 (신뢰의 근거)&lt;/td&gt;
&lt;td&gt;✗ 절감률 수치는 안전 register만큼 낮아짐 (덜 화려한 숫자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 정합성/안전과 압축을 분리해 규칙이 명료해짐&lt;/td&gt;
&lt;td&gt;✗ &quot;무엇이 안전 register인가&quot;의 경계 판단에 지속적 노력 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ parity 예외를 기록으로 남겨 결정이 추적 가능&lt;/td&gt;
&lt;td&gt;✗ 예외(en N/A 등)가 늘면 규칙 일관성 관리 부담&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;실무 팁 &amp;mdash; 거부 영역 설계 체크리스트&lt;/b&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;&quot;압축이 명료성을 깎으면 사람이 잘못 행동하나?&quot;&lt;/b&gt;를 단일 기준으로 삼는다. 그렇다면 안전 register.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안전 register는 압축 정책과 &lt;i&gt;다른 섹션&lt;/i&gt;에&lt;/b&gt; 둔다. 같은 섹션에 두면 dial을 따라 함께 깎일 위험이 구조적으로 생긴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;틀린 출력&quot;과 &quot;덜 압축된 출력&quot;을 구분&lt;/b&gt;한다. 한자 leakage처럼 &lt;i&gt;틀림&lt;/i&gt;에 해당하는 건 압축 규칙이 아니라 정합성 가드로 분류해 모든 dial에 건다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;parity를 깰 땐 이유를 기록&lt;/b&gt;한다. &quot;그냥 안 넣음&quot;이 아니라 &quot;CJK 한정 실패라 en은 N/A&quot;처럼 명시적 플래그를 남긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 3줄 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;압축 도구의 진짜 설계 난제는 &quot;어떻게 더 깎을까&quot;가 아니라 &lt;b&gt;&quot;무엇을 절대 안 깎을까&quot;&lt;/b&gt;이며, 이건 사용자와의 &lt;b&gt;안전성 계약&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;보안 경고&amp;middot;되돌릴 수 없는 동작&amp;middot;모호한 다단계 절차는 &lt;b&gt;모든 dial에서 normal prose로 유지&lt;/b&gt;하고, auto-clarity를 압축률보다 우선한다.&lt;/li&gt;
&lt;li&gt;한자 leakage처럼 &lt;i&gt;틀림&lt;/i&gt;에 해당하는 건 압축 규칙이 아니라 &lt;b&gt;정합성 가드&lt;/b&gt;로 분류하며, parity를 깰 땐 그 예외를 &lt;b&gt;기록으로 명시&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 &lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;에 공개돼 있고, 이슈&amp;middot;PR&amp;middot;새 언어 rule 기여 모두 환영한다.&lt;/p&gt;</description>
      <category>AI 엔지니어링</category>
      <category>ax</category>
      <category>i18n</category>
      <category>LLM</category>
      <category>register</category>
      <category>scrooge</category>
      <category>가드레일</category>
      <category>안전성</category>
      <category>정합성</category>
      <category>출력설계</category>
      <category>프롬프트엔지니어링</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/186</guid>
      <comments>https://kir93.tistory.com/entry/%EC%95%88-%EA%B9%8E%EB%8A%94-%EA%B2%83%EC%9D%B4-%EC%8B%A4%EB%A0%A5%EC%9D%B4%EB%8B%A4-%E2%80%94-%EC%95%95%EC%B6%95-%EB%8F%84%EA%B5%AC%EA%B0%80-%EA%B1%B0%EB%B6%80%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%B6%9C%EB%A0%A5-Scrooge-2%ED%8E%B8#entry186comment</comments>
      <pubDate>Sat, 13 Jun 2026 17:42:38 +0900</pubDate>
    </item>
    <item>
      <title>측정하지 않으면 압축이 아니다 &amp;mdash; 출력 압축 도구의 효과를 재는 법 (Scrooge 1편)</title>
      <link>https://kir93.tistory.com/entry/%EC%B8%A1%EC%A0%95%ED%95%98%EC%A7%80-%EC%95%8A%EC%9C%BC%EB%A9%B4-%EC%95%95%EC%B6%95%EC%9D%B4-%EC%95%84%EB%8B%88%EB%8B%A4-%E2%80%94-%EC%B6%9C%EB%A0%A5-%EC%95%95%EC%B6%95-%EB%8F%84%EA%B5%AC%EC%9D%98-%ED%9A%A8%EA%B3%BC%EB%A5%BC-%EC%9E%AC%EB%8A%94-%EB%B2%95-Scrooge-1%ED%8E%B8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;  scrooge&lt;/a&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 &amp;mdash; &quot;더 압축하자&quot;가 위험한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;압축 도구를 며칠 굴리다 보면 자연스러운 다음 생각이 떠오른다. *&quot;영문 출력을 좀 더 깎을 수 있지 않을까?&quot;* 규칙을 한두 줄 더 공격적으로 쓰면 토큰이 더 줄 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 충동은 합리적으로 보이지만, 실은 &lt;b&gt;방향을 모르는 최적화&lt;/b&gt;다. 더 깎았을 때 토큰이 정말 줄어드는지, 줄어든다면 얼마나 줄어드는지, 그 대가로 무엇이 깨지는지 &amp;mdash; 이걸 모르는 채로 규칙만 더 공격적으로 쓰는 건 도박이다. 압축 도구에서 &quot;규칙을 더 공격적으로&quot;는 곧 &lt;b&gt;출력의 명료성(auto-clarity)과 안전 register를 깎는 쪽으로 압력&lt;/b&gt;을 주는 일이기 때문이다. 이득은 불확실한데 위험은 확실하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 scrooge 개발 중 &quot;더 압축하자&quot;는 plan을 &lt;b&gt;의도적으로 보류했다.&lt;/b&gt; 먼저 한 일은 압축이 아니라 &lt;b&gt;측정&lt;/b&gt;이었다. 이 글에서 당신이 얻어갈 것은, 프롬프트&amp;middot;출력처럼 &quot;정확한 수치를 매기기 애매한&quot; 대상의 효과를 어떻게 &lt;b&gt;재현 가능한 벤치마크로 환원&lt;/b&gt;하는가에 대한 구체적 방법이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; 프롬프트형 도구는 왜 측정이 어려운가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 코드 최적화와 다른 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 하나를 최적화했다면 측정은 명확하다. 같은 입력에 실행 시간을 재서 before/after를 비교하면 된다. 결정론적이고, 단위가 분명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 출력 압축은 그렇지 않다. 두 가지가 측정을 흐린다.&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; 같은 프롬프트라도 출력이 매번 다르다. 한 번 재서 &quot;30% 줄었다&quot;라고 말하면 그건 그날 그 샘플의 우연일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위의 모호함:&lt;/b&gt; &quot;압축됐다&quot;를 무엇으로 잴 것인가? 글자 수? 토큰 수? 토큰이 곧 비용이므로 &lt;b&gt;토큰 수가 정답&lt;/b&gt;이지만, 그러려면 baseline이 명확히 정의돼 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 통찰: baseline 없는 % 는 의미가 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;50% 절감&quot;이라는 말이 성립하려면 &lt;b&gt;무엇 대비&lt;/b&gt; 50%인지가 고정돼야 한다. scrooge가 택한 baseline은 압축을 전혀 걸지 않은 &lt;b&gt;normal(일반) 출력&lt;/b&gt;이다. 그리고 그 사이에 약하게 압축한 &lt;b&gt;terse&lt;/b&gt;를 두고, 본 제품인 &lt;b&gt;scrooge&amp;times; {언어} &amp;times; {강도}&lt;/b&gt; 출력들을 같은 입력에 대해 나란히 생성한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;용어 1줄 정의 &amp;mdash; 코퍼스(corpus):&lt;/b&gt; 여기서는 &quot;같은 입력 프롬프트 묶음 + 그걸 각 모드로 돌린 출력 묶음&quot;을 뜻한다. 측정의 기준 표본 집합이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &quot;scrooge ko full이 normal 대비 얼마나 줄었나&quot;가 &lt;b&gt;같은 입력 위에서&lt;/b&gt; 계산되는, 비교 가능한 수치가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 &amp;mdash; 코퍼스로 ground truth 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정의 뼈대는 단순하다. &lt;b&gt;하나의 입력을 여러 출력 모드로 돌리고, 토큰 수를 집계해 baseline과 비교한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;corpus-structure.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZyY3H/dJMcag6TSsF/l21Kv38d4m5nANQJmh4peK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZyY3H/dJMcag6TSsF/l21Kv38d4m5nANQJmh4peK/img.png&quot; data-alt=&quot;절감률 측정 코퍼스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZyY3H/dJMcag6TSsF/l21Kv38d4m5nANQJmh4peK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZyY3H%2FdJMcag6TSsF%2Fl21Kv38d4m5nANQJmh4peK%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;760&quot; height=&quot;430&quot; data-filename=&quot;corpus-structure.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 설계 포인트가 이 측정을 &quot;그럴듯한 숫자&quot;가 아니라 &quot;재현 가능한 ground truth&quot;로 만든다.&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;① 입력에 verbose-prone edge를 일부러 넣는다.&lt;/b&gt;&lt;br /&gt;압축 도구의 진짜 실력은 &quot;원래 짧은 답&quot;이 아니라 &lt;b&gt;&quot;가만 두면 장황해지는 답&quot;&lt;/b&gt;에서 드러난다. 그래서 코퍼스 입력에 장황해지기 쉬운 엣지 프롬프트를 의도적으로 포함한다. 평균적인 입력만 재면 도구가 가장 일하는 구간을 놓친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② baseline&amp;middot;terse&amp;middot;scrooge를 한 표본 위에 나란히 둔다.&lt;/b&gt;&lt;br /&gt;&lt;code&gt;normal / terse / scrooge&amp;times; {ko, en} &amp;times; {lite, full}&lt;/code&gt; 조합을 같은 입력으로 생성한다. normal은 절감의 기준점(0%), terse는 &quot;단순 간결 지시&quot;만으로 얼마나 줄어드는지의 대조군, scrooge 계열은 본 제품의 실측치다. terse라는 중간 대조군이 중요한 이유는, &lt;b&gt;&quot;규칙 묶음 없이 그냥 짧게 써&quot;만 해도 얻는 절감&lt;/b&gt;과 scrooge 규칙이 &lt;i&gt;추가로&lt;/i&gt; 사주는 절감을 분리해 보여주기 때문이다. 이게 없으면 scrooge의 공을 과대평가하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 측정 결과는 커밋하지 않는다(gitignore).&lt;/b&gt;&lt;br /&gt;코퍼스 입력과 리포트 도구는 저장소에 두되, 산출된 절감 수치 자체는 gitignore 했다. 이유는 정직성이다. 절감률은 &lt;b&gt;모델&amp;middot;시점&amp;middot;샘플링에 따라 달라지는 값&lt;/b&gt;이라, 한 번 잰 숫자를 저장소에 박아두면 &quot;고정된 사실&quot;처럼 오해된다. 대신 도구를 저장소에 두고 &lt;b&gt;재현은 &quot;다시 측정&quot;으로&lt;/b&gt; 하게 만든다. README가 절감 %를 단정하지 않고 &lt;b&gt;&quot;추정(estimated)&quot;&lt;/b&gt;으로 표기하는 태도와 같은 결의 결정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 &amp;mdash; 측정 파이프라인 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈에서 &quot;코드 예제&quot; 자리는 설정&amp;middot;구조&amp;middot;측정 코퍼스가 채운다. 1화의 예시는 절감률 측정 파이프라인의 골격이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Good Practice &amp;mdash; baseline을 고정하고 재현 가능하게 측정&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# 1) 코퍼스: 입력 + 모드 매트릭스 (장황해지기 쉬운 입력 포함)
inputs = load(&quot;benchmark/inputs/&quot;)        # verbose-prone edge 포함
modes  = [&quot;normal&quot;, &quot;terse&quot;,
          &quot;scrooge:ko:lite&quot;, &quot;scrooge:ko:full&quot;,
          &quot;scrooge:en:lite&quot;, &quot;scrooge:en:full&quot;]

# 2) 같은 입력을 모든 모드로 생성하고 토큰 수 집계
for inp in inputs:
    for m in modes:
        out = generate(inp, mode=m)
        record(inp.id, m, count_tokens(out))   # 글자수 아님 &amp;mdash; 토큰수

# 3) baseline(normal) 대비 절감률 산출
#    절감률 = (normal_tokens - mode_tokens) / normal_tokens
report = reduction_vs_baseline(baseline=&quot;normal&quot;)

# 4) 산출된 수치는 gitignore &amp;mdash; 도구만 저장소에, 재현은 &quot;다시 측정&quot;으로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 (a) baseline이 코드에 명시돼 있고, (b) 토큰 수로 재며, (c) 같은 입력 위에서 모드를 비교하고, (d) 결과를 박제하지 않는다는 네 가지다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ Anti-Pattern &amp;mdash; 인상으로 절감률을 말하기&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 흔한 실수
# - 출력 두어 개 눈으로 보고 &quot;한 절반 줄었네&quot; &amp;rarr; 표본 1~2개, 비결정성 무시
# - 글자 수로 비교 &amp;rarr; 비용 단위(토큰)와 어긋남
# - baseline 없이 &quot;scrooge는 60% 절감&quot; &amp;rarr; 무엇 대비 60%인지 불명
# - 측정해보지도 않고 규칙부터 더 공격적으로 &amp;rarr; auto-clarity&amp;middot;안전 register만 깎임&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제인가. 이렇게 나온 &quot;60%&quot;는 &lt;b&gt;반증 불가능한 마케팅 문구&lt;/b&gt;다. 누가 &quot;정말?&quot;이라고 물으면 재현할 방법이 없다. 그리고 측정 없이 규칙부터 공격적으로 바꾸면, 줄어든 토큰의 이득보다 깨진 명료성&amp;middot;안전성의 손해가 클 수 있는데 그걸 &lt;b&gt;알 길조차 없다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 결과 &amp;mdash; 측정이 사주는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정 파이프라인을 갖추면 두 가지가 생긴다. 첫째, &quot;scrooge ko full은 normal 대비 약 N% 절감(추정, 이 코퍼스&amp;middot;이 모델 기준)&quot;처럼 &lt;b&gt;조건이 붙은 정직한 수치&lt;/b&gt;를 말할 수 있다. 둘째 &amp;mdash; 그리고 이게 더 중요한데 &amp;mdash; &quot;여기를 더 압축하면 토큰은 X만큼 더 줄지만 명료성 체크가 깨진다&quot;는 &lt;b&gt;트레이드오프가 눈에 보인다.&lt;/b&gt; 측정은 자랑할 숫자를 주는 게 아니라, &lt;b&gt;어디를 건드리면 안 되는지를 알려주는 지도&lt;/b&gt;를 준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table style=&quot;height: 77px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;장점&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;단점&amp;middot;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✓ 절감 주장이 재현 가능한 근거를 갖는다&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✗ 코퍼스&amp;middot;리포트 도구를 먼저 만드는 선투자가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✓ &quot;더 압축&quot; 결정의 트레이드오프가 가시화된다&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✗ 비결정성 탓에 표본을 충분히 모아야 신뢰구간이 좁아짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✓ terse 대조군이 도구의 &lt;i&gt;순효과&lt;/i&gt;를 분리해준다&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;✗ 모델&amp;middot;시점이 바뀌면 수치도 바뀌어 재측정이 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;실무 팁 &amp;mdash; 측정 도입 체크리스트&lt;/b&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;baseline을 코드에 명시&lt;/b&gt;한다. &quot;무엇 대비&quot;가 함수 시그니처에 드러나야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;글자 수가 아니라 토큰 수&lt;/b&gt;로 잰다. 비용 단위와 측정 단위를 일치시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장황해지기 쉬운 입력을 코퍼스에 일부러 넣는다.&lt;/b&gt; 평균 입력만으로는 도구의 일하는 구간을 못 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순효과 대조군(terse)&lt;/b&gt;을 둔다. &quot;그냥 짧게 써&quot;와 도구의 차이를 분리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수치를 박제하지 않는다.&lt;/b&gt; 도구를 커밋하고 결과는 gitignore. 발표할 땐 &quot;추정 + 측정 조건&quot;을 함께 적는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 3줄 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프롬프트&amp;middot;출력 도구의 효과는 비결정성과 단위 모호함 때문에 측정이 어렵지만, &lt;b&gt;같은 입력 + 고정 baseline + 토큰 수 집계&lt;/b&gt;로 재현 가능하게 만들 수 있다.&lt;/li&gt;
&lt;li&gt;terse 같은 &lt;b&gt;중간 대조군&lt;/b&gt;이 도구의 순효과를 분리하고, 산출 수치는 &lt;b&gt;gitignore + &quot;추정&quot; 표기&lt;/b&gt;로 정직하게 다룬다.&lt;/li&gt;
&lt;li&gt;측정의 진짜 가치는 자랑할 숫자가 아니라, &lt;b&gt;&quot;더 압축해도 되는 곳과 건드리면 안 되는 곳&quot;의 지도&lt;/b&gt;다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 &lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;에 공개돼 있고, 이슈&amp;middot;PR&amp;middot;새 언어 rule 기여 모두 환영한다.&lt;/p&gt;</description>
      <category>AI 엔지니어링</category>
      <category>ax</category>
      <category>eval</category>
      <category>LLM</category>
      <category>scrooge</category>
      <category>벤치마크</category>
      <category>재현성</category>
      <category>출력압축</category>
      <category>측정방법론</category>
      <category>토큰최적화</category>
      <category>프롬프트엔지니어링</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/185</guid>
      <comments>https://kir93.tistory.com/entry/%EC%B8%A1%EC%A0%95%ED%95%98%EC%A7%80-%EC%95%8A%EC%9C%BC%EB%A9%B4-%EC%95%95%EC%B6%95%EC%9D%B4-%EC%95%84%EB%8B%88%EB%8B%A4-%E2%80%94-%EC%B6%9C%EB%A0%A5-%EC%95%95%EC%B6%95-%EB%8F%84%EA%B5%AC%EC%9D%98-%ED%9A%A8%EA%B3%BC%EB%A5%BC-%EC%9E%AC%EB%8A%94-%EB%B2%95-Scrooge-1%ED%8E%B8#entry185comment</comments>
      <pubDate>Fri, 12 Jun 2026 15:53:40 +0900</pubDate>
    </item>
    <item>
      <title>토큰은 돈이다 &amp;mdash; 한국어를 위한 LLM 출력 압축 도구 (Scrooge 0편)</title>
      <link>https://kir93.tistory.com/entry/Scrooge-%EC%9E%91%EC%97%85%EA%B8%B0-0%ED%8E%B8-%ED%86%A0%ED%81%B0%EC%9D%80-%EB%8F%88%EC%9D%B4%EB%8B%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;  scrooge&lt;/a&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 &amp;mdash; 왜 이 이야기가 중요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 길게 답하지 말라고 시켜본 적 있을 것이다. &quot;간결하게&quot;, &quot;불릿으로&quot;, &quot;200자 이내로&quot;. 이게 단순한 취향 문제가 아닌 이유는, &lt;b&gt;출력 토큰이 곧 비용이자 지연시간&lt;/b&gt;이기 때문이다. 같은 정보를 절반의 토큰으로 전달할 수 있다면, 그건 API 청구서와 응답 속도에 직접 꽂히는 최적화다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;LLM 출력을 압축하자&quot;는 도구들이 등장했다. 그런데 이들을 들여다보다가 한 가지가 걸렸다. &lt;b&gt;압축의 상당 부분이 영어의 약어 관습, 심하면 한문(Classical Chinese)식 함축에 기대고 있었다.&lt;/b&gt; 토큰을 줄이는 영리한 트릭이지만, 그 트릭을 읽어내려면 독자가 그 언어 문화의 소양을 갖고 있어야 한다. 압축된 출력이 &lt;i&gt;누군가에게는&lt;/i&gt; 더 읽기 어려워진다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 그 지점에서 출발해 만든 도구 &lt;b&gt;scrooge&lt;/b&gt;(구두쇠, &lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;)의 0화다. 시리즈 전체는 &quot;토큰은 돈이다 &amp;mdash; KO-first LLM 출력 압축 도구 scrooge 만들기&quot;이고, 0화부터 5화까지 총 6편으로 이어진다. 이 글에서 당신이 얻어갈 것은 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;재정의 하나:&lt;/b&gt; 출력 압축을 &quot;영리한 압축 트릭&quot;이 아니라 &lt;b&gt;&quot;accessibility(접근성) 문제&quot;&lt;/b&gt;로 보면 설계가 어떻게 달라지는가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결정 하나:&lt;/b&gt; 다국어(i18n)를 나중에 &quot;지원 추가&quot;하는 대신 &lt;b&gt;첫 커밋부터 아키텍처에 깔면&lt;/b&gt; 무엇이 쉬워지는가.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scrooge는 공개 저장소(&lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;)다. 다만 이 글에서는 특정 커밋 해시를 인용하지 않는다 &amp;mdash; 해시는 리팩터링되면 깨지고 독자가 따라갈 대상도 아니기 때문이다. 대신 &quot;초기 커밋 단계에서 무엇이 이미 결정되어 있었는가&quot;라는 사실만 근거로 쓴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 &amp;mdash; &quot;압축&quot;을 다시 정의하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 압축이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM의 답변은 기본적으로 장황하다. 인사하고, 맥락을 복창하고, 친절한 부연을 단다. &lt;b&gt;출력 압축&lt;/b&gt;은 이 장황함을 규칙으로 깎아 같은 의미를 더 적은 토큰으로 만드는 일이다. scrooge의 경우, 사용자가 압축 강도(dial)를 고르면 그에 맞는 규칙 묶음(rule)을 LLM에게 시스템 지시로 주입하는 방식으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영감의 출처는 분명히 밝혀둔다. scrooge는 &lt;b&gt;caveman&lt;/b&gt;(&lt;a href=&quot;https://github.com/JuliusBrussee/caveman&quot;&gt;github.com/JuliusBrussee/caveman&lt;/a&gt;, MIT, &amp;copy; Julius Brussee)에서 &quot;출력을 압축한다&quot;는 &lt;i&gt;개념&lt;/i&gt;을 빌렸다. caveman은 단순한 &quot;짧게 말하기 팁&quot;이 아니라, 여러 에이전트에서 동작하는 출력 압축 도구로 자신을 설명하며 &lt;code&gt;lite&lt;/code&gt;&amp;middot;&lt;code&gt;full&lt;/code&gt;&amp;middot;&lt;code&gt;ultra&lt;/code&gt;&amp;middot;&lt;code&gt;wenyan&lt;/code&gt;(classical Chinese) 같은 모드를 제공한다. scrooge는 코드나 규칙 문구를 그대로 가져온 게 아니라(verbatim copy 아님), &lt;b&gt;개념만 참고해 i18n-first로 독립 재구현&lt;/b&gt;했다. 차이는 뒤에서 설명한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;압축 트릭&quot;과 &quot;접근성&quot;은 다른 문제다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이 시리즈를 관통하는 첫 번째 프레임이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 도구의 암묵 전제는 대략 이렇다. *&quot;독자는 영어 약어와 함축적 표현을 무리 없이 읽는다.&quot;* 영어권 사용자에겐 합리적이다. 하지만 한국어로 일하는 사람에게 영어식으로 압축된 출력은 &lt;b&gt;토큰은 줄었지만 인지 비용은 오히려 올라간&lt;/b&gt; 결과물일 수 있다. 압축의 이득이 독자의 모국어 바깥에서 새는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 scrooge의 출발점은 *&quot;어떻게 더 압축할까&quot;&lt;i&gt;가 아니라 *&lt;/i&gt;&quot;누구의 토큰을, 누구가 읽을 수 있게 줄이는가&quot;&lt;b&gt;였다. 압축률을 높이는 기술 문제가 아니라, **자기 언어로 토큰을 절약할 수 있게 하는 접근성 문제&lt;/b&gt;로 재정의한 것이다. 이름이 &quot;구두쇠(scrooge)&quot;인 이유도 여기 있다 &amp;mdash; 토큰 한 톨까지 아끼되, 정작 읽는 사람이 손해 보면 그건 아낀 게 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비슷해 보이지만 다른 것&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;일반적인 출력 압축 도구&lt;/th&gt;
&lt;th&gt;scrooge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;압축의 근거&lt;/td&gt;
&lt;td&gt;영어 약어&amp;middot;함축 관습 (암묵 전제)&lt;/td&gt;
&lt;td&gt;언어별 규칙을 명시적으로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다국어&lt;/td&gt;
&lt;td&gt;사후에 &quot;지원 추가&quot;&lt;/td&gt;
&lt;td&gt;첫 커밋부터 아키텍처에 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;문제 정의&lt;/td&gt;
&lt;td&gt;토큰 수 최소화&lt;/td&gt;
&lt;td&gt;모국어로 토큰 절약 (accessibility)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;언어 추가 비용&lt;/td&gt;
&lt;td&gt;코어 수정 동반&lt;/td&gt;
&lt;td&gt;rule 파일 1개 + registry 1줄&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 &amp;mdash; i18n을 &quot;코어&quot;가 아니라 &quot;데이터&quot;로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재정의가 말뿐이 되지 않으려면 아키텍처가 그걸 강제해야 한다. scrooge의 핵심 결정은 &lt;b&gt;언어를 코드에 하드코딩하지 않고, registry라는 매핑 테이블의 데이터로 다룬 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 단순하다. &lt;code&gt;language &amp;times; dial&lt;/code&gt;의 조합 하나가 정확히 하나의 규칙 파일 경로로 1:1 매핑된다. 코어 로직은 &quot;어떤 언어인지&quot;를 모른다. 그저 registry에서 경로를 찾아 해당 규칙을 로드할 뿐이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;registry-mapping.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ORRNw/dJMcabEsXJl/kg2k27cC9TuCZhT1969950/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ORRNw/dJMcabEsXJl/kg2k27cC9TuCZhT1969950/img.png&quot; data-alt=&quot;registry mapping&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ORRNw/dJMcabEsXJl/kg2k27cC9TuCZhT1969950/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FORRNw%2FdJMcabEsXJl%2Fkg2k27cC9TuCZhT1969950%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;760&quot; height=&quot;420&quot; data-filename=&quot;registry-mapping.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;registry mapping&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;이 설계의 효과는 &lt;b&gt;언어를 하나 추가할 때 드러난다.&lt;/b&gt; 일본어(ja)를 넣고 싶다면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;rules/ja/lite.md&lt;/code&gt;, &lt;code&gt;rules/ja/full.md&lt;/code&gt; &amp;mdash; 규칙 파일을 작성하고&lt;/li&gt;
&lt;li&gt;registry에 &lt;code&gt;ja&lt;/code&gt; 엔트리를 한 줄 추가한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코어 코드 변경은 0이다.&lt;/b&gt; 언어가 if-else 분기나 switch 문에 박혀 있었다면 언어를 늘릴 때마다 코어를 건드려야 했겠지만, registry 매핑은 그 결합을 끊는다. 이게 &quot;i18n-first&quot;의 실질적 의미다 &amp;mdash; 다국어가 기능이 아니라 &lt;i&gt;구조&lt;/i&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결정은 사후에 끼워넣은 게 아니다. scrooge의 아주 초기 커밋들이 이미 이 골격을 깔고 있다. 저장소 구조와 i18n 아키텍처를 세운 첫 커밋, 그리고 영어 규칙 골격에서 caveman 특유의 내부자(insider) 표현을 걷어낸 후속 커밋이 그 증거다. 무엇보다 README 최초 버전에는 아예 *&quot;Why Scrooge &amp;mdash; positioning is accessibility&quot;&lt;i&gt;라는 섹션이 있다. 즉 &quot;접근성으로서의 압축&quot;은 글을 쓰려고 나중에 붙인 서사가 아니라, *&lt;/i&gt;첫날부터 명시된 포지셔닝**이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 &amp;mdash; registry 매핑이라는 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 FE 컴포넌트 코드 대신 &lt;b&gt;설정&amp;middot;구조 예시&lt;/b&gt;가 그 자리를 채운다. 0화의 예시는 registry 패턴 그 자체다. 이건 scrooge만의 트릭이 아니라, 다국어&amp;middot;다설정 도구라면 어디든 적용되는 일반 패턴이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ Good Practice &amp;mdash; 언어를 데이터로 다루기&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# registry (개념적 표현)
ko.lite  &amp;rarr; rules/ko/lite.md
ko.full  &amp;rarr; rules/ko/full.md
en.lite  &amp;rarr; rules/en/lite.md
en.full  &amp;rarr; rules/en/full.md

# 코어 로직 (언어를 모른다)
def resolve(language, dial):
    path = registry[f&quot;{language}.{dial}&quot;]   # 매핑 조회
    return load(path)                        # 규칙 로드

# 언어 추가 = 위 매핑에 줄 추가 + 파일 작성. 코어는 그대로.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;resolve&lt;/code&gt; 함수 어디에도 &lt;code&gt;&quot;ko&quot;&lt;/code&gt;나 &lt;code&gt;&quot;en&quot;&lt;/code&gt; 같은 리터럴이 없다는 점이다. 코어는 &quot;조회하고 로드한다&quot;만 안다. 언어 지식은 전부 registry라는 &lt;b&gt;데이터&lt;/b&gt;에 산다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ Anti-Pattern &amp;mdash; 언어를 코드에 박기&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 흔한 실수: 분기문에 언어를 하드코딩
def resolve(language, dial):
    if language == &quot;ko&quot;:
        if dial == &quot;lite&quot;: return load(&quot;rules/ko/lite.md&quot;)
        else:              return load(&quot;rules/ko/full.md&quot;)
    elif language == &quot;en&quot;:
        if dial == &quot;lite&quot;: return load(&quot;rules/en/lite.md&quot;)
        else:              return load(&quot;rules/en/full.md&quot;)
    # ja를 추가하려면? 이 함수를 또 수정해야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제인가. 언어가 늘 때마다 &lt;b&gt;코어 함수를 수정&lt;/b&gt;해야 하고, 그 수정은 곧 회귀(regression) 위험이다. 언어 지식이 코드 곳곳에 흩어지면(파서에 하나, 검증기에 하나, UI에 하나&amp;hellip;) &quot;ja 추가&quot;는 그 모든 곳을 빠짐없이 찾아 고치는 작업이 된다. 하나라도 놓치면 버그다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 결과 &amp;mdash; 무엇이 달라지나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;registry 패턴에서 &quot;ja 추가&quot;는 &lt;b&gt;파일 2개 + 매핑 1줄&lt;/b&gt;짜리 작업으로 수렴한다. 코어는 손대지 않으므로 기존 언어(ko/en)가 깨질 위험이 구조적으로 차단된다. 추가 비용이 &quot;언어 수에 비례하는 코드 수정&quot;에서 &quot;언어당 고정된 데이터 추가&quot;로 바뀌는 것 &amp;mdash; 이게 i18n-first가 사주는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&amp;middot;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ 언어 추가가 코어 수정 없이 가능 (rule 파일 + registry 1줄)&lt;/td&gt;
&lt;td&gt;✗ 초기에 추상화 레이어(registry)를 먼저 깔아야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 언어 지식이 한곳(데이터)에 모여 일관성 유지&lt;/td&gt;
&lt;td&gt;✗ 규칙이 코드가 아닌 외부 파일이라, 규칙-registry 동기화를 따로 강제해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ &quot;접근성으로서의 압축&quot;이라는 포지셔닝이 구조로 뒷받침됨&lt;/td&gt;
&lt;td&gt;✗ 도구가 한두 언어로 끝날 거면 over-engineering일 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;실무 팁 &amp;mdash; i18n-first가 정당화되는 조건&lt;/b&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; registry 패턴은 값을 한다. 한 언어로 끝날 도구라면 분기문이 더 솔직하다.&lt;/li&gt;
&lt;li&gt;registry는 &lt;b&gt;계약(contract)&lt;/b&gt;이다. 규칙 파일을 옮기거나 이름을 바꾸면 같은 변경에서 registry도 동기화해야 한다. 이 동기화를 사람의 기억에 맡기지 말고 검증 게이트로 강제하는 게 좋다. (이 규율이 실제로 무엇을 막았는지는 시리즈 마지막 5화에서 다룬다.)&lt;/li&gt;
&lt;li&gt;&quot;1:1 매핑&quot;을 깨고 싶은 유혹(한 규칙을 여러 언어가 공유 등)이 오면, 그게 접근성 약속을 깨는지 먼저 따져본다. 언어별로 압축 관습이 다르다는 게 이 도구의 출발점이었음을 기억할 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 3줄 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;출력 압축을 &quot;영리한 트릭&quot;이 아니라 &lt;b&gt;&quot;모국어로 토큰을 아끼는 접근성 문제&quot;&lt;/b&gt;로 재정의하면 설계 우선순위가 바뀐다.&lt;/li&gt;
&lt;li&gt;그 재정의를 말이 아니라 구조로 강제하려면, 언어를 코드가 아닌 &lt;b&gt;데이터(registry 매핑)&lt;/b&gt;로 다뤄 i18n을 첫 커밋부터 깐다.&lt;/li&gt;
&lt;li&gt;효과는 &quot;언어 추가&quot; 순간 드러난다 &amp;mdash; 코어 수정 0, 파일 2개 + 매핑 1줄.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어로 토큰을 아껴보고 싶다면 scrooge를 직접 써보는 게 가장 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 &lt;a href=&quot;https://github.com/Kir93/scrooge-mode&quot;&gt;github.com/Kir93/scrooge-mode&lt;/a&gt;에 공개돼 있고, 이슈&amp;middot;PR&amp;middot;새 언어 rule 기여 모두 환영한다.&lt;/p&gt;</description>
      <category>AI 엔지니어링</category>
      <category>ax</category>
      <category>i18n</category>
      <category>LLM</category>
      <category>registry패턴</category>
      <category>scrooge</category>
      <category>개발도구제작</category>
      <category>아키텍처설계</category>
      <category>출력압축</category>
      <category>토큰최적화</category>
      <category>프롬프트엔지니어링</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/184</guid>
      <comments>https://kir93.tistory.com/entry/Scrooge-%EC%9E%91%EC%97%85%EA%B8%B0-0%ED%8E%B8-%ED%86%A0%ED%81%B0%EC%9D%80-%EB%8F%88%EC%9D%B4%EB%8B%A4#entry184comment</comments>
      <pubDate>Wed, 10 Jun 2026 13:30:27 +0900</pubDate>
    </item>
    <item>
      <title>Next.js v14 &amp;rarr; v15 마이그레이션 작업기</title>
      <link>https://kir93.tistory.com/entry/Nextjs-v14-%E2%86%92-v15-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9E%91%EC%97%85%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 도입부 (Why This Matters)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메이저 버전 업그레이드는 보통 하나씩 한다. Next.js 먼저 올리고, 안정화되면 React, 그다음 Node. 교과서적이고 안전하다. 그런데 우리는 &lt;b&gt;Next.js 14&amp;rarr;15, React 18&amp;rarr;19, Node.js 20&amp;rarr;24, ESLint 8&amp;rarr;9를 단일 PR로 동시에&lt;/b&gt; 올렸다. 88개 파일이 한 번에 바뀌었고, 프로덕션 에러율은 0%를 유지했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무모해 보인다면 정확한 직관이다. 다만 이 네 축은 서로 강하게 결합돼 있어서 따로 떼면 오히려 &quot;중간 상태&quot;가 더 위험해진다. React 19 타입은 Next.js 15가 요구하고, Next.js 15의 비동기 API는 Node 런타임과 맞물리고, ESLint 9는 그 위에서 새 코드를 검증한다. 절반만 올린 브랜치를 며칠씩 들고 있는 것 자체가 리스크였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 &quot;동시에 올려도 된다&quot;는 주장이 아니라, &lt;b&gt;무엇을 한 번에 묶고 무엇을 회피&amp;middot;고정(pin)했는지&lt;/b&gt;에 대한 결정 기록이다. 읽고 나면 본인 프로젝트의 메이저 업그레이드에서 &quot;이건 묶고, 이건 핀으로 우회한다&quot;를 스스로 판단할 수 있게 되는 게 목표다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽는 시간: 약 8분.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 버전 고지: 이 글의 마이그레이션은 2026년 1월 Next.js 15.5 기준으로 수행됐다. 작성 시점(2026-06) 기준 최신 stable은 Next.js 16.2이며, 16부터는 Turbopack이 기본 번들러이고 React Compiler가 1.0 stable로 포함된다. 본문 마지막에서 이 방향성을 따로 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 개념 (What &amp;amp; Why)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 &quot;동시&quot;가 오히려 안전할 수 있나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메이저 업그레이드를 순차로 하면 각 단계마다 &quot;반쪽짜리 호환 상태&quot;가 만들어진다. 예를 들어 React만 19로 올리고 Next.js를 14에 두면, Next.js 14는 React 19의 비동기 &lt;code&gt;params&lt;/code&gt;를 전제하지 않으므로 타입과 런타임이 어긋난 채로 며칠을 버텨야 한다. 이 중간 상태는 정식 호환 매트릭스에 존재하지 않는 조합이라, 버그가 나도 &quot;이게 React 탓인지 Next 탓인지&quot; 분리가 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 강하게 결합된 축들을 한 번에 올리면, 검증해야 할 조합이 &quot;확정된 최종 상태 하나&quot;로 줄어든다. 핵심은 &lt;b&gt;결합도가 높은 것은 묶고, 결합도가 낮거나 리스크가 큰 것은 외부로 밀어내는 것&lt;/b&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; &amp;rarr; 사내 디자인 시스템 패키지를 React 19 호환 canary 버전으로 선반영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서드파티 타입 충돌&lt;/b&gt; &amp;rarr; &lt;code&gt;overrides&lt;/code&gt;로 타입을 강제 주입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;React Compiler 도입&lt;/b&gt; &amp;rarr; 이번 PR에서 제외, 정책만 설계 (부록 참조)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Turbopack &lt;code&gt;next build&lt;/code&gt;&lt;/b&gt; &amp;rarr; 이번 PR에서는 dev에만 적용, build 전환은 보류 (5장 판단표 참조)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;한 PR로 동시에&quot;는 모든 걸 한꺼번에 한다는 뜻이 아니라, &lt;b&gt;코어는 원자적으로 묶고 주변부 비호환은 회피 전략으로 격리한다&lt;/b&gt;는 뜻이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유사 개념과의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 말하는 &quot;빅뱅 마이그레이션&quot;과 다르다. 빅뱅은 &quot;전부 한 번에&quot;지만, 여기서는 코어 4축만 원자적이고 나머지는 의도적으로 분리했다. 점진적 마이그레이션(strangler fig)과도 다르다. 점진적 방식은 결합도 높은 프레임워크 코어에는 적용하기 어렵기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 동작 원리 (How It Works)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결합 구조와 작업 분해&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biQe0A/dJMcaci1pA1/8nSgkTbCuoCgjebXFrrTxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biQe0A/dJMcaci1pA1/8nSgkTbCuoCgjebXFrrTxk/img.png&quot; data-alt=&quot;Next.js v14 &amp;amp;rarr; v15 마이그레이션 작업기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biQe0A/dJMcaci1pA1/8nSgkTbCuoCgjebXFrrTxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiQe0A%2FdJMcaci1pA1%2F8nSgkTbCuoCgjebXFrrTxk%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;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Next.js v14 &amp;rarr; v15 마이그레이션 작업기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;#1&lt;/code&gt;이 모든 작업의 선행 조건이다. 일단 코어 버전을 올리면 &lt;code&gt;#2~#6&lt;/code&gt;은 서로 독립적으로 병렬 가능하고, &lt;code&gt;#7&lt;/code&gt;(CI/CD)은 &lt;code&gt;#5&lt;/code&gt;(설정)에, &lt;code&gt;#8&lt;/code&gt;(디자인 시스템)은 &lt;code&gt;#3&lt;/code&gt;(타입)에만 의존한다. 실제로는 단일 PR로 통합 배포했지만, 내부적으로는 이 의존 그래프대로 진행해야 충돌 지점을 예측할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디서 무엇이 깨지는가 &amp;mdash; 레벨별 분해&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레벨&lt;/th&gt;
&lt;th&gt;깨지는 지점&lt;/th&gt;
&lt;th&gt;원인&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;런타임(서버)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cookies()&lt;/code&gt;, &lt;code&gt;headers()&lt;/code&gt;가 동기 &amp;rarr; 비동기&lt;/td&gt;
&lt;td&gt;Next.js 15가 동적 API를 Promise로 전환 (요청 단위 캐싱&amp;middot;스트리밍 최적화 목적)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라우팅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;params&lt;/code&gt; / &lt;code&gt;searchParams&lt;/code&gt;가 Promise&lt;/td&gt;
&lt;td&gt;동일 &amp;mdash; 동적 데이터의 지연 평가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;미들웨어&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NextRequest.headers&lt;/code&gt;가 읽기 전용&lt;/td&gt;
&lt;td&gt;요청 헤더 불변성 강화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;타입(컴파일)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RefObject&amp;lt;T&amp;gt;&lt;/code&gt; &amp;rarr; &lt;code&gt;RefObject&amp;lt;T | null&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;React 19의 ref 제네릭 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의존성&lt;/td&gt;
&lt;td&gt;서드파티의 &lt;code&gt;@types/react&lt;/code&gt; 18 고정&lt;/td&gt;
&lt;td&gt;일부 라이브러리가 React 19 타입 미반영&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;런타임 깨짐(비동기 API)과 컴파일 깨짐(타입)이 서로 다른 종류의 문제&lt;/b&gt;라는 점이다. 비동기 API는 &lt;code&gt;await&lt;/code&gt;를 빠뜨리면 런타임에 조용히 틀린 값을 반환할 수 있어 더 위험하고, 타입 충돌은 빌드에서 즉시 멈추므로 오히려 안전하다. 그래서 비동기 API 전환을 가장 신중하게 다뤘다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실무 적용 (Practical Examples)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 실제 마이그레이션 변경분을 일반화한 예시다(핵심 패턴만 표시).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 비동기 Dynamic API &amp;mdash; &lt;code&gt;cookies()&lt;/code&gt; / &lt;code&gt;headers()&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// lib/auth/auth-server.ts

import { cookies, headers } from 'next/headers';

// ❌ Before (Next.js 14) &amp;mdash; 동기 함수
export function isAuthenticated(): boolean {
  const cookieStore = cookies();
  const authToken = cookieStore.get('SESSION_TOKEN')?.value;
  return authToken !== undefined;
}

// ✅ After (Next.js 15) &amp;mdash; async + await
export async function isAuthenticated(): Promise&amp;lt;boolean&amp;gt; {
  const cookieStore = await cookies(); // cookies()가 Promise 반환
  const authToken = cookieStore.get('SESSION_TOKEN')?.value;
  return authToken !== undefined;
}

export async function getNextUrl(): Promise&amp;lt;string&amp;gt; {
  const headerList = await headers(); // headers()도 동일
  const xPathname = headerList.get('x-next-url');
  return xPathname ?? `${process.env.APP_DOMAIN}/`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변경의 진짜 함정은 함수 시그니처가 &lt;code&gt;Promise&lt;/code&gt;로 바뀌는 순간 &lt;b&gt;이 함수를 호출하는 모든 곳이 연쇄적으로 async가 되어야 한다는 것&lt;/b&gt;이다. 미들웨어를 보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// middleware.ts

// ❌ Before &amp;mdash; 동기 호출
const authMiddleware = (req: NextRequest) =&amp;gt; {
  if (!isAuthenticated()) {
    /* 리다이렉트 */
  }
  return intlMiddleware(req);
};

// ✅ After &amp;mdash; async 전파 + 읽기 전용 헤더 우회
const authMiddleware = async (req: NextRequest) =&amp;gt; {
  if (!(await isAuthenticated())) {
    /* 리다이렉트 */
  }
  return intlMiddleware(req);
};

export default async function middleware(req: NextRequest) {
  const { pathname, search } = req.nextUrl;
  const nextUrl = createRedirectUrl(pathname, search);

  // ⚠️ NextRequest.headers는 읽기 전용 &amp;rarr; 직접 .set() 불가
  // 새 Headers를 만들어 복사 후 수정해야 한다
  const requestHeaders = new Headers(req.headers);
  requestHeaders.set('x-next-url', nextUrl);

  // ... 분기 ...
  return await authMiddleware(req); // 호출부도 await
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;실행 결과&lt;/b&gt;: &lt;code&gt;await&lt;/code&gt;를 빠뜨리면 &lt;code&gt;isAuthenticated()&lt;/code&gt;가 &lt;code&gt;Promise&amp;lt;boolean&amp;gt;&lt;/code&gt; 객체를 반환하고, 객체는 항상 truthy라서 &lt;code&gt;if (!isAuthenticated())&lt;/code&gt;가 &lt;b&gt;언제나 false&lt;/b&gt;가 된다. 즉 미인증 사용자가 보호 페이지를 통과한다. 빌드는 통과하지만 인증이 뚫리는 사일런트 버그라, 이 부분은 라우트별로 수동 점검했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. &lt;code&gt;searchParams&lt;/code&gt; / &lt;code&gt;params&lt;/code&gt; &amp;rarr; Promise&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 컴포넌트와 클라이언트 컴포넌트의 처리 방식이 다르다. 이게 React 19의 &lt;code&gt;use()&lt;/code&gt; Hook이 빛을 보는 지점이다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// ✅ 서버 컴포넌트 &amp;mdash; await로 푼다
interface PageProps {
  searchParams: Promise&amp;lt;{ documentId: string }&amp;gt;;
}

export async function generateMetadata({ searchParams }: PageProps): Promise&amp;lt;Metadata&amp;gt; {
  const { documentId } = await searchParams; // await
  // ...
}

export default async function Page({ searchParams }: PageProps) {
  const { documentId } = await searchParams; // await
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// ✅ 클라이언트 컴포넌트 &amp;mdash; use() Hook으로 푼다 (async 불가하므로)
'use client';
import { use, useState } from 'react';

interface PageProps {
  searchParams: Promise&amp;lt;{ initIndex: number; size: 'sm' | 'md' | 'lg' }&amp;gt;;
}

export default function Page(props: PageProps) {
  const searchParams = use(props.searchParams); // &amp;larr; Promise를 use()로 언랩
  const { initIndex, size } = searchParams;
  const [data, setData] = useState&amp;lt;Record&amp;lt;string, unknown&amp;gt;&amp;gt;();
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;Anti-Pattern&lt;/b&gt;: 클라이언트 컴포넌트를 억지로 &lt;code&gt;async function&lt;/code&gt;으로 만들려는 시도. 클라이언트 컴포넌트는 async가 될 수 없다. Promise prop은 반드시 &lt;code&gt;use()&lt;/code&gt;로 풀어야 한다. 이 구분(서버=await / 클라이언트=use())을 명확히 두지 않으면 페이지마다 헤매게 된다. 참고로 Next.js 기본 ESLint 설정의 &lt;code&gt;@next/next/no-async-client-component&lt;/code&gt; 규칙이 이 실수를 잡아주므로, 뒤에서 다룰 Flat Config 전환은 이런 마이그레이션 사고의 안전장치이기도 하다. 영향 범위는 7개 라우트 페이지 + 1개 레이아웃이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. React 19 타입 &amp;mdash; &lt;code&gt;RefObject&amp;lt;T | null&amp;gt;&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// ❌ Before (React 18)
positionRef: React.RefObject&amp;lt;HTMLDivElement&amp;gt;;

// ✅ After (React 19) &amp;mdash; 제네릭에 | null 명시
positionRef: React.RefObject&amp;lt;HTMLDivElement | null&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19는 &lt;code&gt;useRef&lt;/code&gt;의 초기값과 제네릭 정합을 강화하면서 &lt;code&gt;RefObject&amp;lt;T&amp;gt;&lt;/code&gt;를 &lt;code&gt;RefObject&amp;lt;T | null&amp;gt;&lt;/code&gt;로 바꿨다. 15개 이상 컴포넌트에서 ref 타입 선언을 일괄 수정했다. 이건 컴파일 에러로 전부 잡히므로 &lt;code&gt;tsc --noEmit&lt;/code&gt;을 가이드 삼아 기계적으로 처리할 수 있다 &amp;mdash; 위험하지 않은 종류의 깨짐이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 서드파티 타입 충돌 회피 &amp;mdash; &lt;code&gt;overrides&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 실용적인 트릭. 일부 서드파티 라이브러리가 내부적으로 &lt;code&gt;@types/react@18&lt;/code&gt;을 끌고 오면서 React 19 타입과 충돌했다. 라이브러리 업데이트를 기다리는 대신 &lt;b&gt;타입을 강제로 통일&lt;/b&gt;했다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;# pnpm-workspace.yaml  (pnpm 11+ 기준)
overrides:
  '@types/react': ^19
  '@types/react-dom': ^19

  # 특정 패키지가 오래된 React 타입을 끌고 올 때만 좁혀서 강제
  'some-legacy-lib&amp;gt;@types/react': ^19&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  버전 위치 참고: pnpm 11부터는 &lt;code&gt;overrides&lt;/code&gt;를 &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;에서 읽으며, &lt;code&gt;package.json&lt;/code&gt;의 &lt;code&gt;pnpm&lt;/code&gt; 필드는 더 이상 읽지 않는다. pnpm 10 이하 프로젝트라면 기존처럼 &lt;code&gt;package.json&lt;/code&gt;의 &lt;code&gt;&quot;pnpm&quot;: { &quot;overrides&quot;: { ... } }&lt;/code&gt;에 둔다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트에 박은 버전을 자식 의존성 전체로 전파하고 싶을 때는, 버전 문자열을 다시 쓰는 대신 루트 의존성을 참조하는 &lt;code&gt;$&lt;/code&gt; 표기를 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// (pnpm 10 이하) package.json &amp;mdash; $ 참조로 루트 버전 계승
{
  &quot;pnpm&quot;: {
    &quot;overrides&quot;: {
      &quot;@types/react&quot;: &quot;$@types/react&quot;,
      &quot;@types/react-dom&quot;: &quot;$@types/react-dom&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;$@types/react&quot;&lt;/code&gt;는 &quot;루트 &lt;code&gt;dependencies&lt;/code&gt;/&lt;code&gt;devDependencies&lt;/code&gt;에 선언된 &lt;code&gt;@types/react&lt;/code&gt; 버전을 그대로 자식 트리에 계승하라&quot;는 의미다. 버전 숫자를 한 곳(루트)에서만 관리하게 되므로, 숫자를 양쪽에 중복으로 적고 어긋나는 실수를 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 의존성 트리 전체에서 &lt;code&gt;@types/react&lt;/code&gt;가 단일 버전으로 평탄화된다. 사내 디자인 시스템은 한발 더 나아가 &lt;b&gt;React 19 호환을 미리 반영한 canary 버전을 핀&lt;/b&gt;으로 고정했다. &quot;정식 릴리스를 기다린다&quot;가 아니라 &quot;호환 버전을 선반영하고 핀으로 박는다&quot;가 핵심 회피 전략이었다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// canary 의존성은 범위(^)가 아니라 exact pin으로
{
  &quot;dependencies&quot;: {
    &quot;@internal/design-system&quot;: &quot;0.1.7-canary.2&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;canary 같은 prerelease는 semver 범위 매칭에서 쉽게 누락되거나 예상치 못한 버전으로 점프할 수 있으므로, &lt;b&gt;정확한 버전 핀&lt;/b&gt;이 더 예측 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. ESLint 9 Flat Config&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.eslintrc.json&lt;/code&gt;은 ESLint 9에서 지원이 끊긴다. 다만 &lt;code&gt;next/core-web-vitals&lt;/code&gt; 같은 기존 공유 설정은 아직 레거시 포맷이라, &lt;code&gt;FlatCompat&lt;/code&gt; 브릿지로 감싸 점진 전환했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// eslint.config.mjs
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({ baseDirectory: __dirname });

const eslintConfig = [
  // 레거시 공유 설정을 FlatCompat로 브릿지
  ...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
  {
    rules: {
      '@typescript-eslint/no-explicit-any': 'warn',
      'react/react-in-jsx-scope': 'off', // React 19: 자동 JSX 런타임
      'prefer-const': 'error',
      eqeqeq: ['error', 'smart'],
    },
  },
  { ignores: ['.next/**', 'out/**', 'build/**', 'next-env.d.ts'] },
];

export default eslintConfig;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 함정: ESLint 9 Flat Config 전환은 설정 파일 이름만 바꾸는 일이 아니다. 아직 v9 rule API를 따르지 않은 플러그인에서는 &lt;code&gt;context.getScope is not a function&lt;/code&gt; 같은 오류가 날 수 있다. 이런 경우 &lt;code&gt;FlatCompat&lt;/code&gt; 같은 호환 레이어로 감싸거나 구성을 재배치해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. Turbopack SVGR 호환 (dev 한정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 PR에서 Turbopack은 &lt;b&gt;dev에만&lt;/b&gt; 적용했고 &lt;code&gt;next build&lt;/code&gt;는 webpack 경로를 유지했다(전환 보류 이유는 5장 판단표 참조). Turbopack은 webpack 로더 설정을 그대로 읽지 않으므로, SVG를 컴포넌트로 쓰던 SVGR 설정을 &lt;code&gt;turbopack.rules&lt;/code&gt;로 별도 선언하고, webpack 빌드 경로도 폴백으로 함께 유지했다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// next.config.mjs
const nextConfig = {
  turbopack: {
    rules: {
      '*.svg': { loaders: ['@svgr/webpack'], as: '*.js' },
    },
  },
  // next build는 아직 webpack 경로 &amp;rarr; 동일 로더를 중복 선언해 폴백 유지
  webpack: (config) =&amp;gt; {
    config.module.rules.push({ test: /\.svg$/i, use: ['@svgr/webpack'] });
    return config;
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 장단점 및 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무엇을 묶고 무엇을 canary로 미룰까 &amp;mdash; 판단 기준표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 사례의 결정을 일반화하면, 새 프로젝트에서도 그대로 쓸 수 있는 판단표가 된다. 기준은 단순하다. &lt;b&gt;타입&amp;middot;런타임&amp;middot;린트 경계가 서로 맞물리는 것은 한 번에 묶고, 지원 단계가 beta/experimental이거나 운영 환경 편차가 큰 것은 canary로 분리한다.&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;판단 항목&lt;/th&gt;
&lt;th&gt;한 번에 묶기 좋은 경우&lt;/th&gt;
&lt;th&gt;canary로 미루기 좋은 경우&lt;/th&gt;
&lt;th&gt;이번 사례의 권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next 15 + React 19&lt;/td&gt;
&lt;td&gt;App Router, 타입 정렬, async API 정리가 함께 움직일 때&lt;/td&gt;
&lt;td&gt;Pages Router 비중이 크고 React 18 유지가 전략적으로 필요할 때&lt;/td&gt;
&lt;td&gt;&lt;b&gt;묶기&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@types/react&lt;/code&gt; 정렬&lt;/td&gt;
&lt;td&gt;서드파티가 React 18 타입을 끌고 와 충돌할 때&lt;/td&gt;
&lt;td&gt;충돌 범위가 좁고 임시 허용이 가능할 때&lt;/td&gt;
&lt;td&gt;&lt;b&gt;묶기&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESLint 9 + Flat Config&lt;/td&gt;
&lt;td&gt;Next 15.5 이상, &lt;code&gt;next lint&lt;/code&gt; 의존 탈피가 필요할 때&lt;/td&gt;
&lt;td&gt;플러그인 호환성이 크게 불안정할 때&lt;/td&gt;
&lt;td&gt;&lt;b&gt;대체로 묶기&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turbopack dev&lt;/td&gt;
&lt;td&gt;로컬 DX 개선이 목적일 때&lt;/td&gt;
&lt;td&gt;custom webpack 진입점에 강하게 의존할 때&lt;/td&gt;
&lt;td&gt;&lt;b&gt;묶기&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Turbopack &lt;code&gt;next build&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;parity 테스트가 충분하고 알려진 차이를 감당할 때&lt;/td&gt;
&lt;td&gt;CSS ordering&amp;middot;번들 최적화 격차&amp;middot;custom webpack 리스크가 클 때&lt;/td&gt;
&lt;td&gt;&lt;b&gt;canary 권장 (이번 PR 보류)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 24 + Alpine 베이스&lt;/td&gt;
&lt;td&gt;native addon이 단순하고 musl 검증이 끝났을 때&lt;/td&gt;
&lt;td&gt;glibc 의존&amp;middot;보안 릴리스 즉시 반영&amp;middot;멀티아키 민감도가 높을 때&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Node 24는 묶고 Alpine 전환은 주의&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 분기점은 Turbopack이다. dev는 안정 단계라 묶었지만, &lt;code&gt;next build --turbopack&lt;/code&gt;은 CSS ordering 차이와 일부 번들 최적화 격차가 알려져 있어 이번 PR에서는 의도적으로 보류했다. &quot;Turbopack을 채택했다&quot;가 아니라 &quot;dev에만 적용했다&quot;가 정확한 서술이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트레이드오프 요약&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점 / 비용&lt;/th&gt;
&lt;th&gt;예방책&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ 검증 대상이 &quot;최종 상태 하나&quot;로 수렴 &amp;mdash; 반쪽 호환 상태 제거&lt;/td&gt;
&lt;td&gt;✗ 단일 PR이 88개 파일로 비대 &amp;rarr; 리뷰 부담&lt;/td&gt;
&lt;td&gt;의존 그래프 순서대로 커밋을 쪼개 리뷰 동선 확보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 결합도 높은 축을 원자적으로 묶어 디버깅 시 변인 분리&lt;/td&gt;
&lt;td&gt;✗ 문제 발생 시 롤백 단위가 큼&lt;/td&gt;
&lt;td&gt;배포 전 스테이징에서 핵심 라우트 E2E 통과 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ canary 핀 + overrides로 서드파티 대기 시간 0&lt;/td&gt;
&lt;td&gt;✗ canary 의존은 임시 부채&lt;/td&gt;
&lt;td&gt;정식 stable 배포 시점에 핀 해제 캘린더 등록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✓ 정량 성과 명확 (아래)&lt;/td&gt;
&lt;td&gt;✗ 비동기 API 사일런트 버그는 자동 검출 불가&lt;/td&gt;
&lt;td&gt;인증&amp;middot;권한 분기는 라우트별 수동 점검 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정량 성과&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지표&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;변화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dev 서버 시작&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;개선 (Turbopack, Vercel 공식 벤치마크: 대형 앱 기준 최대 76.7% 빠름)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev HMR (Fast Refresh)&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;개선 (Vercel 공식 벤치마크: 최대 96.3% 빠름)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 빌드 시간&lt;/td&gt;
&lt;td&gt;평균 4분&lt;/td&gt;
&lt;td&gt;평균 2분 20초&lt;/td&gt;
&lt;td&gt;약 42%&amp;darr; (자체 CI 실측)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로덕션 에러율&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;무장애 유지 (자체 관측)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 42% 단축의 내역(자체 attribution): Next.js 15 빌드 최적화 ~20초 + Node.js 24 V8 개선 ~30초 + GitHub Actions &lt;code&gt;.next/cache&lt;/code&gt; 캐시(&lt;code&gt;actions/cache@v4&lt;/code&gt;) ~50초.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 수치 고지: 76.7%&amp;middot;96.3%는 Vercel이 대형 Next.js 앱에서 측정한 &lt;b&gt;공식 참고 수치&lt;/b&gt;로, 프로젝트 규모&amp;middot;환경에 따라 체감은 크게 달라진다(본 프로젝트의 독립 실측치가 아니다). 빌드 42%는 자체 CI 환경 실측이며, 내역 분해는 내부 attribution 가설이다. &quot;에러율 0%&quot;는 어떤 관측 도구&amp;middot;기간&amp;middot;분모 기준인지 함께 밝혀야 엄밀하다(여기서는 배포 후 안정화 관측 창 기준).&lt;/p&gt;
&lt;/blockquote&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;비동기 API(&lt;code&gt;cookies&lt;/code&gt;/&lt;code&gt;headers&lt;/code&gt;/&lt;code&gt;params&lt;/code&gt;/&lt;code&gt;searchParams&lt;/code&gt;) 호출부를 전수 조사했는가 &amp;mdash; &lt;code&gt;await&lt;/code&gt; 누락은 빌드를 통과하는 사일런트 버그다.&lt;/li&gt;
&lt;li&gt;인증&amp;middot;권한처럼 boolean을 반환하던 동기 함수가 async로 바뀌었다면, 그 호출부의 조건문을 직접 점검했는가.&lt;/li&gt;
&lt;li&gt;서드파티 타입 충돌은 라이브러리 업데이트를 기다리지 말고 &lt;code&gt;overrides&lt;/code&gt;로 평탄화했는가.&lt;/li&gt;
&lt;li&gt;CI/로컬/Docker의 Node 버전을 &lt;code&gt;.nvmrc&lt;/code&gt; 단일 소스로 일원화했는가.&lt;/li&gt;
&lt;li&gt;빌드 캐시(&lt;code&gt;.next/cache&lt;/code&gt;)를 CI에 붙였는가 &amp;mdash; 가장 가성비 높은 빌드 단축 수단이다.&lt;/li&gt;
&lt;li&gt;Turbopack을 dev에만 적용했는지 build까지 켰는지 명확히 구분해 기록했는가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론 및 다음 단계&lt;/h2&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;메이저 동시 업그레이드의 성패는 &quot;전부 묶기&quot;가 아니라 &lt;b&gt;결합도 높은 코어는 원자적으로 묶고, 비호환 주변부는 canary 핀&amp;middot;&lt;code&gt;overrides&lt;/code&gt;로 격리&lt;/b&gt;하는 분리 설계에 있다.&lt;/li&gt;
&lt;li&gt;가장 위험한 깨짐은 빌드를 멈추는 타입 에러가 아니라, &lt;b&gt;빌드를 통과하는 비동기 API의 &lt;code&gt;await&lt;/code&gt; 누락&lt;/b&gt;이다. 인증 같은 곳은 수동 점검이 필수다.&lt;/li&gt;
&lt;li&gt;정량 측정(빌드 42%&amp;darr;, 에러율 0%)을 붙이고, 공식 참고 수치와 내부 실측을 분리해 표기하면 &quot;무장애&quot;가 주장이 아니라 결과가 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 단계 제안: 본인 코드베이스에서 &lt;code&gt;cookies()&lt;/code&gt; / &lt;code&gt;headers()&lt;/code&gt; / &lt;code&gt;searchParams&lt;/code&gt; 사용처를 먼저 grep으로 전수 조사하라. 영향 범위를 알면 묶을지 나눌지가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심화 학습 키워드: React Compiler, PPR(Partial Prerendering), &lt;code&gt;use()&lt;/code&gt; Hook, Turbopack, pnpm overrides.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부록 &amp;mdash; React Compiler를 열어둔 useMemo/useCallback 정책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 PR에서 React Compiler는 &lt;b&gt;의도적으로 제외&lt;/b&gt;했다. 대신 &quot;나중에 켜도 손해 보지 않는&quot; 코드 정책을 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Compiler는 컴포넌트를 자동 메모이제이션하므로, 켜지는 순간 수동 &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; 상당수가 잉여가 된다. 그렇다고 기존 메모이제이션을 미리 대량으로 지우는 건 위험하다 &amp;mdash; 컴파일러를 켜기 전까지는 그 메모이제이션이 실제로 동작 중이고, 수백 개를 한꺼번에 지웠다가 재생성 버그나 렌더 루프를 쫓는 비용이 더 크다. 그래서 &lt;b&gt;기존 구문은 보존&lt;/b&gt;하되, 신규 코드에서는 &lt;b&gt;성능을 위한 선제적 메모이제이션을 금지&lt;/b&gt;하고, &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt;은 (a) 참조 동일성이 의미상 필요한 경우(의존성 배열, &lt;code&gt;key&lt;/code&gt;, context value), (b) 실측으로 병목이 확인된 경우에만 쓰도록 했다. 이렇게 하면 컴파일러를 켰을 때 제거할 부채가 자연히 줄어든다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향성: 작성 시점(2026-06) 기준 React Compiler는 1.0 stable이고 Next.js 16부터 빌드에 내장됐지만, 여전히 기본 비활성이다. 도입 시에는 compiler를 켜기 전에 ESLint compiler linter(&lt;code&gt;preserve-manual-memoization&lt;/code&gt; 등)를 먼저 도입해 기존 메모이제이션을 보존하면서 점진 전환하는 경로가 권장된다. 위 정책은 Next 16 전환 시 그대로 활용된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처 (외부 수치)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/blog/turbopack-for-development-stable&quot;&gt;Turbopack Dev is Now Stable | Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/blog/turbopack&quot;&gt;Turbopack: High-performance bundler for React &amp;amp; TypeScript &amp;mdash; Vercel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/blog/next-16&quot;&gt;Next.js 16 | Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler&quot;&gt;next.config.js: reactCompiler | Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/versions&quot;&gt;React Versions &amp;mdash; React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pnpm.io/settings&quot;&gt;pnpm settings (pnpm-workspace.yaml) &amp;mdash; pnpm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Next.js</category>
      <category>EsLint9</category>
      <category>FlatConfig</category>
      <category>nextjs15</category>
      <category>Nodejs24</category>
      <category>pnpm</category>
      <category>react19</category>
      <category>turbopack</category>
      <category>마이그레이션</category>
      <category>무장애배포</category>
      <category>프로트엔드아키텍처</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/183</guid>
      <comments>https://kir93.tistory.com/entry/Nextjs-v14-%E2%86%92-v15-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9E%91%EC%97%85%EA%B8%B0#entry183comment</comments>
      <pubDate>Sat, 6 Jun 2026 12:20:11 +0900</pubDate>
    </item>
    <item>
      <title>개발자 원칙(확장판) - 박성철 , 강대명 , 공용준 , 김정 , 박미정 , 박종천 , 장동수 , 이동욱(향로) , 이동욱(네피림)</title>
      <link>https://kir93.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%9B%90%EC%B9%99%ED%99%95%EC%9E%A5%ED%8C%90-%EB%B0%95%EC%84%B1%EC%B2%A0-%EA%B0%95%EB%8C%80%EB%AA%85-%EA%B3%B5%EC%9A%A9%EC%A4%80-%EA%B9%80%EC%A0%95-%EB%B0%95%EB%AF%B8%EC%A0%95-%EB%B0%95%EC%A2%85%EC%B2%9C-%EC%9E%A5%EB%8F%99%EC%88%98-%EC%9D%B4%EB%8F%99%EC%9A%B1%ED%96%A5%EB%A1%9C-%EC%9D%B4%EB%8F%99%EC%9A%B1%EB%84%A4%ED%94%BC%EB%A6%BC</link>
      <description>&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/dB2AYM/dJMcabqUvVm/kfVkbZCmtfhWuanWNZDYjk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dB2AYM/dJMcabqUvVm/kfVkbZCmtfhWuanWNZDYjk/img.jpg&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;654&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4294%; margin-right: 10px;&quot; data-widthpercent=&quot;50.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dB2AYM/dJMcabqUvVm/kfVkbZCmtfhWuanWNZDYjk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdB2AYM%2FdJMcabqUvVm%2FkfVkbZCmtfhWuanWNZDYjk%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;458&quot; height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FEYGe/dJMcaaS320j/zOMmeieTVCZUA4vApjwxs0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FEYGe/dJMcaaS320j/zOMmeieTVCZUA4vApjwxs0/img.jpg&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;1200&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4078%;&quot; data-widthpercent=&quot;49.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FEYGe/dJMcaaS320j/zOMmeieTVCZUA4vApjwxs0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFEYGe%2FdJMcaaS320j%2FzOMmeieTVCZUA4vApjwxs0%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;840&quot; height=&quot;1200&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;p data-ke-size=&quot;size16&quot;&gt;이 책은 &quot;좋은 개발자란 누구인가&quot;라는 질문에 기술 스택이 아니라 &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-ke-size=&quot;size16&quot;&gt;컬리&amp;middot;카카오&amp;middot;무신사&amp;middot;몰로코&amp;middot;인프런 등에서 일해온 국내 테크 리더 9인이 각자 한 가지 원칙을 에세이로 풀어냈고, 확장판은 여기에 0장 「선배와의 인터뷰」와 각 장 말미의 「출간 후 2년, 그다음 이야기」를 더했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 책은 &quot;무엇을 코딩할까&quot;보다 &quot;어떻게 개발자로 살아갈까&quot;에 가까운, 일종의 직업 정체성 안내서입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0장. 선배와의 인터뷰&lt;/h2&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;: 확장판에 새로 들어간 부분으로, 9인 저자 전원에게 같은 질문 세 가지를 던집니다. &quot;좋은 개발자(좋은 개발 조직)란 무엇인가&quot;, &quot;언제 가장 즐거웠나&quot;, &quot;이 일을 계속하게 하는 원동력은 어디서 오는가&quot;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 각자의 답이 본문 9개 원칙의 출발점이 됩니다. 박성철은 개발자를 &quot;현실의 문제를 해결하는 사람&quot;으로 규정하고, 강대명은 &quot;더 깊고 자세히, 더 많이 공부하라&quot;는 한 문장으로 자기 태도를 압축합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 본문보다 인터뷰가 더 흥미로운 대목이 있을 만큼, 저자들의 사람됨과 현재 직무 변화 자체가 책의 중요한 맥락입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1장. 덕업일치를 넘어서&lt;/h2&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;: 박성철(컬리 본부장)은 &quot;좋아함(덕업일치)&quot;은 출발점일 뿐이며, 직업으로서의 정체성을 의식적으로 탐구해야 지속 가능하다고 말합니다. 급여&amp;middot;복지 같은 위생 요인(Hygiene Factors: 부족하면 불만이 되지만 충족돼도 동기가 되지 않는 환경 요소)은 커트라인일 뿐, 진짜 만족은 성장을 자극하는 내재 동기(Intrinsic Motivation: 행동 자체에서 오는 즐거움)에서 나옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: SK플래닛 &amp;rarr; 우아한형제들 &amp;rarr; 컬리로 이어진 이직마다 '북극성'처럼 삼은 가치를 점검한 회고가 인상적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 번아웃은 에너지를 무조건 아끼기보다, 회복 탄력성을 키워 총량을 넓히는 방식으로 다스립니다. AI가 코드를 쏟아내는 시대일수록 &quot;어떤 개발자가 될 것인가&quot;라는 질문을 멈추지 말라고 당부합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2장. 오류를 만날 때가 가장 성장하기 좋을 때다&lt;/h2&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;: 강대명(레몬트리 기술책임자)은 장애와 오류를 방해물이 아니라 시스템 내부를 깊이 들여다볼 최고의 학습 자원으로 봅니다. AI가 준 정답 코드를 그대로 복사&amp;middot;붙여 넣는 습관은 장기적으로 자생력을 갉아먹는다고 경고합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 네이버&amp;middot;카카오&amp;middot;위버스의 데이터 플랫폼을 거치며 만난 난해한 오류들을, 짐작이 아니라 프레임워크의 소스 코드(Source Code: 프로그램의 원본 텍스트)를 한 줄씩 읽어 해결한 과정을 보여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 오류를 고친 뒤에는 반드시 글로 정리해 지식을 내재화해야 합니다. 문제를 만나면 AI에 의존하기 전에 내부 동작을 먼저 추론해보는 훈련이 권장됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3장. 소프트웨어 디자인 원칙&lt;/h2&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;: 공용준(카카오 클라우드 디렉터)은 거창한 패턴 카탈로그가 아니라, 실무에서 반복적으로 통하는 최소한의 설계 원칙(추상화&amp;middot;결합도 관리&amp;middot;확장성)을 정리합니다. 디자인을 곧 '의사소통'으로 보는 관점이 핵심입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 카카오톡 규모의 트래픽을 운영한 경험을 바탕으로, 비즈니스 요구와 시스템 설계가 충돌하는 전형적 딜레마와 그 조율 과정을 다룹니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 생성형 AI가 늘수록 사전 설계는 오히려 더 중요해집니다. 다만 거시적 설계를 다루는 만큼, 주니어가 한 번에 소화하기엔 진입 장벽이 느껴질 수 있는 장입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4장. 나의 메이저 버전을 업그레이드하는 마이너 원칙들&lt;/h2&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;: 김정(코드스쿼드 대표)은 자신을 소프트웨어에 비유해, 작은 마이너 패치를 꾸준히 쌓으면 메이저 버전이 올라간다고 봅니다. 시맨틱 버저닝(Semantic Versioning: 메이저&amp;middot;마이너&amp;middot;패치로 버전을 체계화하는 규칙)을 성장에 투영한 것입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: &quot;업무 관련 50% / 약하게 관련 30% / 관심사 20%&quot;라는 학습 시간 가계부, 그리고 &quot;개구리를 해부하지 말고 직접 만들어라&quot;는 학습관이 구체적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 결과에 함몰되지 말고 과정을 기록하라(v0.5.0), 정답보다 해답을 찾으라는 조언은 직장인 누구나 바로 적용할 수 있는 자기계발 팁입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5장. 이직, 분명한 이유가 필요해&lt;/h2&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;: 박미정(무신사 개발실장)은 이직이 좋은 도구이지만, 분명한 이유와 방향이 없으면 '도망'이 된다고 말합니다. 성장 단계별로 적합한 이직 이유가 다르다는 4단계 프레임을 제시합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: LG CNS &amp;rarr; 네이버 &amp;rarr; 쿠팡 &amp;rarr; 코빗 &amp;rarr; 배달의민족 베트남 &amp;rarr; 무신사로 이어진 본인의 경로를 단계별 이유와 함께 솔직하게 공유합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: &quot;왜 떠나는가&quot;보다 &quot;무엇을 얻는가&quot;를 먼저 쓰고, 같은 이유로 두 번 이직하지 말 것. 주인의식을 발휘할 수 없거나 개발 문화에 기여하기 어려울 때가 이직의 적기입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6장. 목표를 달성하는 나만의 기준, GPAM&lt;/h2&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;: 박종천(몰로코 헤드 아키텍트)은 막연한 목표가 부르는 실행 지연을 깨기 위해, 개발 사이클을 일상에 접목한 GPAM(Goal&amp;middot;Plan&amp;middot;Action&amp;middot;Measure: 목표&amp;middot;계획&amp;middot;실행&amp;middot;측정) 프레임워크를 제안합니다. S.M.A.R.T.로 목표를 쪼개고 GPAM으로 돌립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 한컴 &amp;rarr; 블리자드 &amp;rarr; 넥슨 &amp;rarr; 삼성전자 &amp;rarr; 몰로코로 이어진 30여 년 경력, 그리고 '개발자의 7가지 고민'을 GPAM으로 분해하는 워크북식 적용이 실용적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 측정(Measure)이 빠진 목표 관리는 결국 추측이 됩니다. 분기마다 한 사이클을 돌리는 것만으로도 정체를 깰 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7장. 프로덕트 중심주의&lt;/h2&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;: 이동욱(네피림, 당근 엔지니어)은 개발자의 장기 성장 단위가 '기술 스택'이 아니라 '내가 끝까지 만든 프로덕트(Product: 완성된 제품)'라고 강조합니다. 그 프로덕트가 꼭 회사 업무일 필요도 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 플러터(Flutter) 학습을 예로, &quot;문법책을 완독한다&quot;는 추상적 계획과 &quot;내 여행 기록 앱을 시장에 배포한다&quot;는 제품 중심 계획이 만들어내는 결과물의 밀도 차이를 대조합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 포트폴리오를 '기능'이 아니라 '완성된 프로덕트' 단위로 정리하세요. 완벽한 준비로 망설이기보다 빠르게 출시하고 반복 개선하는 감각이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8장. 제어할 수 없는 것에 의존하지 않기&lt;/h2&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;: 이동욱(향로, 인프랩 CTO)은 코드 설계든 이직이든 조직 매니징이든, 내 힘으로 제어할 수 없는 외부 변수에 의존하면 시스템이 깨지기 쉽다고 말합니다. 외부 의존을 줄일수록 견고해집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 많은 기업이 주민등록번호를 데이터베이스의 기본 키(Primary Key: 레코드를 고유하게 식별하는 키)로 삼았다가, 수집 금지법 개정으로 시스템 전체를 갈아엎어야 했던 사례가 대표적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 도메인 핵심 로직은 두텁게, 외부 통신&amp;middot;UI는 모킹(Mocking: 테스트용 가짜 객체로 격리하는 기법) 등으로 얇게 격리하세요. 커리어 결정도 &quot;내가 제어할 수 없는 것&quot;을 먼저 적어보고 시작하면 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9장. 달리는 기차의 바퀴를 갈아 끼우기&lt;/h2&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;: 장동수(수수한기술 대표)는 운영 중인 서비스를 멈추지 않으면서 기술 부채를 점진적으로 갚는 현실적 리팩토링 철학을 다룹니다. &quot;은탄환은 없다&quot;는 인정에서 출발합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 사례/에피소드&lt;/b&gt;: 세계 최초의 웹 기반 오피스 'Thinkfree Office Live' 개발, 그리고 패스트캠퍼스에서 60여 명 개발 조직을 맨땅에서 구축한 경험이 비유에 무게를 더합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기억할 점&lt;/b&gt;: 처음부터 100점을 노리느라 일정을 어기기보다, 기한 내 80~90점으로 확실히 동작시키고 점진 개선하세요. 발견했을 때보다 한 단계 깨끗하게 두고 떠나는 보이스카우트 규칙이 핵심 습관입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;9인의 원칙은 코딩을 넘어 목표 관리와 조직 운영으로까지 확장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 확장판의 추가분은 AI를 핑계로 기존 원칙을 폐기하지 않고, 오히려 설계&amp;middot;학습&amp;middot;문제 정의의 중요성을 더 강하게 만든다는 점에서 책의 신뢰도를 높입니다.&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;바로 적용해볼 만한 행동 포인트&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애를 겪으면 24시간 안에 원인 기록(RCA)을 남기고, 소스 코드 레벨까지 직접 확인한다.&lt;/li&gt;
&lt;li&gt;구현 전 1페이지짜리 설계 문서를 먼저 쓴다.&lt;/li&gt;
&lt;li&gt;분기마다 GPAM으로 개인 목표를 재설정하고 '측정' 항목을 반드시 넣는다.&lt;/li&gt;
&lt;li&gt;이직을 검토할 때 &quot;왜 떠나는가&quot;보다 &quot;무엇을 얻는가&quot;를 먼저 적는다.&lt;/li&gt;
&lt;li&gt;의사결정마다 통제 가능한 것과 불가능한 것을 분리해 적어본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추천 독자&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연차에 비해 성장이 정체돼 돌파구를 찾는 &lt;b&gt;3~7년 차 주니어&amp;middot;미들 개발자&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;생성형 AI로 직업 정체성에 혼란을 느끼는 &lt;b&gt;소프트웨어 직군&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;팀장 승진을 앞둔 시니어, 혹은 조직을 갓 꾸리는 &lt;b&gt;스타트업 초기 CTO&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리 평&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;강점&lt;/b&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;middot;카카오&amp;middot;무신사&amp;middot;몰로코의 의사결정 스타일을 비교할 수 있는 다관점 자극.&lt;/li&gt;
&lt;li&gt;GPAM, &quot;제어할 수 없는 것에 의존하지 않기&quot;, &quot;프로덕트 중심주의&quot;, &quot;밥값 개발자&quot;처럼 한 줄로 외울 수 있는 프레임이 많아, 회고나 1:1 면담에서 바로 인용하기 좋습니다.&lt;/li&gt;
&lt;li&gt;같은 저자가 직접 쓴 '출간 후 2년' 후기로 원칙을 시간 검증한 구성.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아쉬운 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저자 대부분이 국내 IT 대기업&amp;middot;유니콘 출신이라, SI&amp;middot;금융 같은 도메인과는 거리감이 있습니다.&lt;/li&gt;
&lt;li&gt;챕터당 25~30쪽의 분량 제약 탓에 사례가 단편적으로 끝나는 장이 있고, 챕터 간 문체&amp;middot;깊이 편차도 느껴집니다.&lt;/li&gt;
&lt;li&gt;AI 시대 대응이 주로 인터뷰와 후기 코너에 분산돼 있어, 본문 9개 원칙 자체가 AI를 정면으로 다루지는 않습니다.&lt;/li&gt;
&lt;li&gt;시니어에게는 새로운 기술 지식이라기보다 &quot;이미 알지만 정리하지 못한 원칙의 재문장화&quot;에 가깝습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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-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;성장 방향을 정리하려는 3~7년 차나 예비 리드라면 강하게 추천하고, 깊은 아키텍처&amp;middot;테스트 전략을 찾는 시니어라면 보조 텍스트로 곁들이길 권합니다.&lt;/p&gt;</description>
      <category>서평</category>
      <category>개발자원칙</category>
      <category>골든래빗</category>
      <category>소프트웨어설계</category>
      <category>시대개발자</category>
      <category>이직고민</category>
      <category>주니어개발자</category>
      <category>책서평</category>
      <category>커리어성장</category>
      <category>테크리더십</category>
      <category>항로</category>
      <author>Kir93</author>
      <guid isPermaLink="true">https://kir93.tistory.com/182</guid>
      <comments>https://kir93.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%9B%90%EC%B9%99%ED%99%95%EC%9E%A5%ED%8C%90-%EB%B0%95%EC%84%B1%EC%B2%A0-%EA%B0%95%EB%8C%80%EB%AA%85-%EA%B3%B5%EC%9A%A9%EC%A4%80-%EA%B9%80%EC%A0%95-%EB%B0%95%EB%AF%B8%EC%A0%95-%EB%B0%95%EC%A2%85%EC%B2%9C-%EC%9E%A5%EB%8F%99%EC%88%98-%EC%9D%B4%EB%8F%99%EC%9A%B1%ED%96%A5%EB%A1%9C-%EC%9D%B4%EB%8F%99%EC%9A%B1%EB%84%A4%ED%94%BC%EB%A6%BC#entry182comment</comments>
      <pubDate>Wed, 3 Jun 2026 12:27:16 +0900</pubDate>
    </item>
  </channel>
</rss>