<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>return new Story();</title>
    <link>https://parse.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 13 Jun 2026 23:58:33 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Parse</managingEditor>
    <image>
      <title>return new Story();</title>
      <url>https://tistory1.daumcdn.net/tistory/5174213/attach/d492a5e3288b4fc38303dd007de6b23b</url>
      <link>https://parse.tistory.com</link>
    </image>
    <item>
      <title>회사에서 AI 에이전트 스킬 어떻게 관리할까? &amp;mdash; skill-cli 개발 회고</title>
      <link>https://parse.tistory.com/30</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1IIF1/dJMcadWtrXn/sDqxFRCruC1plxjO2gLm8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1IIF1/dJMcadWtrXn/sDqxFRCruC1plxjO2gLm8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1IIF1/dJMcadWtrXn/sDqxFRCruC1plxjO2gLm8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1IIF1%2FdJMcadWtrXn%2FsDqxFRCruC1plxjO2gLm8k%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 AI 하네스용 스킬을 터미널에서 바로 설치&amp;middot;공유&amp;middot;삭제할 수 있는 CLI를 만들었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경 &amp;mdash; 왜 만들었나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 AI 하네스 시스템을 구축하면서, 그 위에 올라갈 스킬들을 정의해두고 운영하고 있다.&lt;/p&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;원래는 웹에서 다운받는 마켓 형태로 운영할 계획이었는데, 생각해보니 CLI로 만드는 게 더 자연스럽더라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 바로 다운로드도 되고, Claude Code로 직접 조작할 수 있으니 훨씬 편할 거라고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서 사이드프로젝트로 skill-cli를 만들기 시작했다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;뭘 만들었나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 기능은 세 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 스킬 리스트 조회&lt;/b&gt; &amp;mdash; 마켓에 어떤 스킬이 있는지, 내 PC에는 어떤 스킬이 있는지 따로따로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 퍼블리시 분기&lt;/b&gt; &amp;mdash; 로컬 스킬이 마켓에서 다운받은 건지, 내가 로컬에서만 만든 건지 구분해서 보여주고, 후자만 퍼블리시 대상으로 잡을 수 있게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 로컬 삭제&lt;/b&gt; &amp;mdash; 스킬이 너무 많으면 오히려 Claude Code 성능이 떨어지기 때문에, 마켓에 이미 업로드된 스킬은 로컬에서 쉽게 삭제할 수 있게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, 카테고리를 회사 조직도 기반으로 2-depth로 쪼개서 필터링이 빠르게 되도록 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택은 간단하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TUI&lt;/b&gt;: Ink (React 기반이라 익숙)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장소&lt;/b&gt;: GitLab&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포&lt;/b&gt;: npm&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab을 마켓 백엔드로 쓴 이유는 어차피 사내에서 다 GitLab 쓰니까 별도 인프라 추가 안 해도 되고, 권한 관리도 그대로 따라가서 편했다. 사내에서만 쓸 수 있는게 목표기 때문에 사내에서 돌아가는 GitLab를 기반으로 작업을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm으로 배포하니 설치도 한 줄이고, 업데이트도 자연스럽게 흘러간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 TUI나 CLI 도구를 만드는 건 이번이 처음이었다. 만드는 과정 자체가 재미있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많이 고민한 건 UX였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 스킬을 나열해서 보여주는 게 아니라, 조직도를 기반으로 카테고리를 쪼개고 그 안에 스킬을 노출하는 방식으로 갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 필터링과 빠른 배포, 이 두 가지를 목표로 잡고 작업했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 사이드프로젝트로 진행한 만큼 완성도가 높진 않다고 느껴져셔 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠른 배포가 목표였고, AI랑 페어로 만들면서 시간 대비 괜찮은 퀄리티가 나왔다고는 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용자들을 설득하는 데는 한계가 있었다. 만드는 것보다 쓰게 하는 게 더 어렵더라.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용도를 높이려면 어떻게 해야 할지가 다음 숙제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구 자체보다 사람들이 이걸 일상에 끼워 넣을 이유를 만들어 주는 게 진짜 일이라는 걸 다시 한 번 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POC를 진행하면서 느낀점도 나중에 또 글로 작성하면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>claudecode</category>
      <category>CLI</category>
      <category>ink</category>
      <category>skill-cli</category>
      <category>Tui</category>
      <category>typescript</category>
      <category>사내도구</category>
      <category>사이드프로젝트</category>
      <category>회고</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/30</guid>
      <comments>https://parse.tistory.com/30#entry30comment</comments>
      <pubDate>Fri, 29 May 2026 18:24:58 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code 워크플로우 (3) &amp;mdash; Boris Cherny &amp;quot;Why Coding Is Solved&amp;quot; 강연 후기</title>
      <link>https://parse.tistory.com/29</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuEc6j/dJMcagyBNnd/IkHucX4N3AQofN5orqt6dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuEc6j/dJMcagyBNnd/IkHucX4N3AQofN5orqt6dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuEc6j/dJMcagyBNnd/IkHucX4N3AQofN5orqt6dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuEc6j%2FdJMcagyBNnd%2FIkHucX4N3AQofN5orqt6dK%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;1264&quot; height=&quot;848&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;(1) gstack + Superpowers &amp;mdash; 두 플러그인만 남긴 이유&lt;/li&gt;
&lt;li&gt;(2) gstack &lt;code&gt;/office-hours&lt;/code&gt;로 AI에게 제품 인터뷰를 받아봤다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(3) Boris Cherny &quot;Why Coding Is Solved&quot; 강연 후기&lt;/b&gt; &amp;larr; 현재 글&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TL;DR&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Boris Cherny는 &lt;b&gt;2025년 10월부터 손으로 코드를 짜지 않는다.&lt;/b&gt; 하루 PR을 수십 개, 기록은 150개까지 올린다.&lt;/li&gt;
&lt;li&gt;그의 작업 방식의 중심은 &lt;b&gt;세션을 capacity로 스케줄링하는 것&lt;/b&gt; &amp;mdash; 터미널 5탭, 브라우저 5~10세션, 핸드폰까지 동시에 돌아간다. 핵심은 디바이스가 아니라 *&quot;주의력을 어디에 배분하느냐&quot;*다.&lt;/li&gt;
&lt;li&gt;그가 자주 쓰는 메커니즘은 &lt;b&gt;&lt;code&gt;/loop&lt;/code&gt;&lt;/b&gt; (cron으로 반복 작업 스케줄)와 새 기능 &lt;b&gt;Routines&lt;/b&gt; (서버 측에서 영구 실행).&lt;/li&gt;
&lt;li&gt;Boris도 &lt;b&gt;YC 출신&lt;/b&gt;이고, 거기서 배운 첫 원칙이 *&quot;first build for yourself&quot;&lt;i&gt;. (1)~(2)편의 *&quot;Make something people want&quot;&lt;/i&gt;와 정확히 같은 자리에 있다.&lt;/li&gt;
&lt;li&gt;시리즈 결론: &lt;b&gt;사용자 인터뷰와 프롬프트 설계는 같은 능력&lt;/b&gt;이다. 도구가 무엇이든 &lt;i&gt;앞단에서 정확하게 정의하는 일&lt;/i&gt;이 결과의 90%를 결정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;영상을 보게 된 맥락 &amp;mdash; Boris도 YC 출신이라니&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강연 관련 자료를 정리하다가 &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny&quot;&gt;Pragmatic Engineer 인터뷰&lt;/a&gt;와 &lt;a href=&quot;https://www.developing.dev/p/boris-cherny-creator-of-claude-code&quot;&gt;developing.dev 인터뷰&lt;/a&gt;에서 Boris 본인의 발언을 발견했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;I did YC back in the day, and in YC they teach you that first you build for yourself. You have to build awesome stuff. You have to build stuff people love.&quot;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boris도 창업해서 YC를 거쳐본 사람이었고, 거기서 배운 첫 원칙이 &quot;first build for yourself&quot; &amp;mdash; 너 자신을 위해 먼저 만들어라, 그게 다른 사람도 같은 문제를 갖고 있다는 가장 좋은 신호니까 &amp;mdash; 였다는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 알고 나서 강연을 다시 보면, 왜 그가 자기 자신을 위해 Claude Code를 만들고 &quot;이게 내가 코드 짜는 방식이었다&quot;라고 자연스럽게 말하는지가 다르게 들립니다. 우연이 아니라 일관된 원칙입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;강연의 골자 &amp;mdash; 숫자로 보는 워크플로우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상과 외부 보도(&lt;a href=&quot;https://finance.biggo.com/news/bd63c89c5a3716f3&quot;&gt;BigGo 분석&lt;/a&gt;, &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny&quot;&gt;Pragmatic Engineer&lt;/a&gt;)를 종합하면 그의 작업 방식은 이렇게 요약됩니다.&lt;/p&gt;
&lt;table style=&quot;height: 134px;&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;내용&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;2025년 10월&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;일일 PR&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;수십 개, 기록은 150개&lt;/td&gt;
&lt;/tr&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;터미널 5탭 + 브라우저 5~10 + 핸드폰&lt;/td&gt;
&lt;/tr&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;&lt;code&gt;/loop&lt;/code&gt; (cron 스케줄), Routines (서버 측 영구 실행)&lt;/td&gt;
&lt;/tr&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;TypeScript + React (모델의 &quot;분포 위에 있다&quot;는 이유로 의도적으로 선택)&lt;/td&gt;
&lt;/tr&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;Opus 4 (2025년 5월 출시) &amp;mdash; 이 모델 이후 100% AI 코딩 가능&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;h2 data-ke-size=&quot;size26&quot;&gt;&quot;loop&quot;가 정확히 뭔가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상에서 자주 나오는 단어가 &lt;b&gt;loop&lt;/b&gt;입니다. 그는 이걸 *&quot;crontab처럼 돌리는 것&quot;&lt;i&gt;이라고 표현하는데, 단순한 비유가 아닙니다 &amp;mdash; &lt;code&gt;/loop&lt;/code&gt; 슬래시 커맨드가 &lt;/i&gt;실제로 cron을 사용해 반복 작업을 스케줄합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 LLM 사용 = &lt;b&gt;단발성&lt;/b&gt;. &quot;한 번 묻고 한 번 답한다.&quot;&lt;br /&gt;loop = &lt;b&gt;영구&lt;/b&gt;. &quot;한 번 잘 정의해두고 계속 돌아간다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그가 loop로 처리한다고 밝힌 작업들:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 베이비시팅 (CI 실패 자동 수정, 자동 리베이스)&lt;/li&gt;
&lt;li&gt;피드백 클러스터링&lt;/li&gt;
&lt;li&gt;데이터 변화 감지 후 결과를 Slack으로 푸시 (MCP 경유)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 단계 더 나간 게 &lt;b&gt;Routines&lt;/b&gt; &amp;mdash; 2026년 4월 발표된 새 기능입니다. &lt;a href=&quot;https://howborisusesclaudecode.com/&quot;&gt;공식 발표&lt;/a&gt;에 따르면 Routines는 &quot;한 번 설정하면 스케줄에 따라, API 호출로, 또는 GitHub 이벤트에 반응해서 실행되며, Anthropic 인프라 위에서 돌기 때문에 노트북이 필요 없다&quot;고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 작업이 그의 디바이스에 묶여 있지 않습니다. 사람은 어디서든 지시&amp;middot;검증만 하고, 실제 작업은 서버에서 계속 돌아가는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 (1)편의 5단계 워크플로우와 결이 비슷하다고 느꼈습니다. (1)편의 핵심도 &quot;사람이 모든 단계에 붙어있지 않는다&quot;였습니다. 회의(&lt;code&gt;/office-hours&lt;/code&gt;) &amp;rarr; 계획서(&lt;code&gt;writing-plans&lt;/code&gt;) &amp;rarr; 분업 실행(sub-agent) 형태로 각 단계가 독립적으로 돌아갑니다. Boris는 그걸 훨씬 더 극단적으로 밀어붙인 형태일 뿐입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;AI는 도구가 아니라 capacity다&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://karozieminski.substack.com/p/boris-cherny-claude-code-workflow&quot;&gt;Karo Zieminski의 정리&lt;/a&gt;에 나오는 한 줄이 강했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Boris doesn't see AI as a tool you use, but as a capacity you schedule.&quot;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;동시에 여러 세션을 돌린다&quot;가 그냥 멀티태스킹 자랑처럼 보였는데, 이 표현으로 보면 다르게 읽힙니다. 그는 &lt;b&gt;AI를 컴퓨팅 자원처럼 다룹니다&lt;/b&gt; &amp;mdash; 큐에 넣고, 워크로드를 분배하고, 컨텍스트 스위치는 결과가 나왔을 때만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병목은 &lt;b&gt;모델 생성 속도가 아니라 사람의 주의력 배분&lt;/b&gt;이라는 게 그의 입장입니다. 이 관점이 &lt;i&gt;왜 그가 동시에 10~15개 세션을 돌리는가&lt;/i&gt;를 설명해줍니다 &amp;mdash; 한 세션이 답변 만드는 동안 다른 세션의 결과를 처리하면, 사람은 항상 &quot;다음 결정&quot;만 내리면 되니까요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Plan Mode &amp;mdash; Boris도 &quot;계획 우선&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1)편에서 &quot;바로 만들어줘 금지, &lt;code&gt;/office-hours&lt;/code&gt;부터&quot;라고 적었던 게 떠올라서 약간 놀란 부분이 있습니다. Boris의 워크플로우 핵심 원칙도 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://twitter-thread.com/t/2007179832300581177&quot;&gt;그의 X 스레드&lt;/a&gt;와 후속 정리에 따르면, 그는 거의 모든 세션을 &lt;b&gt;Plan Mode&lt;/b&gt;로 시작합니다. Plan Mode에서 Claude와 왔다갔다하며 계획을 다듬고, 충분히 좋아지면 그제야 auto-accept edits 모드로 전환해 실행시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&quot;계획 우선&quot; 메커니즘&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gstack (1편)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/office-hours&lt;/code&gt;로 회의 &amp;rarr; &lt;code&gt;writing-plans&lt;/code&gt;로 계획서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (Boris)&lt;/td&gt;
&lt;td&gt;Plan Mode에서 계획 확정 &amp;rarr; auto-accept edits로 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그가 강조하는 한 줄 &amp;mdash; &quot;never let Claude write code until you've reviewed and approved a written plan&quot; &amp;mdash; 은 (1)편의 운영 규칙(&lt;i&gt;새 프로젝트는 무조건 회의부터&lt;/i&gt;)과 정확히 같은 원칙입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인쇄술 비유 &amp;mdash; 가장 와닿았던 한 대목&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Boris가 코딩의 미래를 비유한 방식이 인상적이었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인쇄술 이전에 글을 읽고 쓸 수 있는 사람은 인구의 일부분이었다 &amp;mdash; 필경사(scribes)였다. 인쇄술 이후 그게 보편화됐다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 일이 코딩에 일어나고 있다는 게 그의 주장입니다. 코딩을 직업으로 가진 사람만 할 수 있는 일이었지만, 앞으로는 &quot;컴퓨터로 일하는 사람&quot;이라면 누구나 코딩으로 자기 문제를 해결할 거라는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비유가 자극적이지만, 그가 든 본인 팀의 사례를 보면 단순한 슬로건이 아닙니다 &amp;mdash; Claude Code 팀의 PM, EM, 디자이너, 데이터 사이언티스트, 파이낸스 리드, &lt;b&gt;유저 리서처까지&lt;/b&gt; 모두 코드를 친다고 합니다. 모든 직군이 자기 영역의 전문성은 유지하되, 코딩이 보편 기술처럼 깔리는 셈입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;코딩이 solved 됐다&quot;의 단서들&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;i&gt;그가 짜는 종류의 코드&lt;/i&gt;에 한해서다. 그가 짜는 건 TypeScript + React 기반의 비교적 표준적인 웹 코드입니다.&lt;/li&gt;
&lt;li&gt;남아있는 어려운 영역은 &lt;b&gt;큰 복잡한 코드베이스, 그리고 모델이 충분히 학습하지 못한 비주류 언어&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;그의 답은 &quot;보통은 다음 모델을 기다리면 된다&quot;. 즉, 솔루션이 코드 짜는 능력이 아니라 모델 발전에 있다는 입장입니다.&lt;/li&gt;
&lt;li&gt;이 입장은 그의 다른 원칙과도 맞물립니다 &amp;mdash; &quot;At Anthropic, we don't build for the model of today, we build for the model of six months from now.&quot; Claude Code가 초기 6개월간 거의 안 작동한 이유이자, 결국 Opus 4 출시와 함께 폭발적으로 작동하기 시작한 이유.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단서들 덕분에 &quot;AI가 모든 코드를 다 짠다&quot;는 메시지로 받아들이지 않을 수 있었습니다. 도메인 지식이 깊은 비즈니스 로직, 레거시 마이그레이션, 임베디드, 시스템 코드 같은 영역은 여전히 사람이 깊게 붙어있어야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 인사이트가 만나는 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1)~(2)편을 거쳐 이 강연을 보면서 정리된 한 문장이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자 인터뷰와 프롬프트 설계는 같은 능력이다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(2)편에서 본 &lt;code&gt;/office-hours&lt;/code&gt;의 효용 = &quot;내가 만들 것을 앞단에서 정확하게 정의하는 일&quot;&lt;/li&gt;
&lt;li&gt;Boris의 워크플로우 = &quot;에이전트가 만들 것을 앞단에서 정확하게 정의하는 일&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 &lt;b&gt;앞단의 정의가 결과의 90%를 결정&lt;/b&gt;한다고 말하고 있고, 그 정의를 잘하는 능력 &amp;mdash; 모호한 것을 구체화하고, 우선순위를 정하고, 트레이드오프를 명시하는 &amp;mdash; 이 진짜 핵심 기술이 된다는 결론에 도달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YC의 &quot;Make something people want&quot;&lt;i&gt;도 같은 자리에 있습니다. *&lt;/i&gt;사람들이 진짜로 원하는 것을 정확하게 정의하고, 그것에 맞춰 만들어라. Boris가 YC에서 배웠다는 &quot;first build for yourself&quot;도 결국 같은 원칙의 다른 표현입니다 &amp;mdash; 자기가 진짜 필요로 하는 것에서 시작하면 정의가 모호할 일이 적으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구가 무엇이든(스타트업이든, 코드든, 에이전트든) &lt;b&gt;앞단의 정의가 흔들리면 뒷단의 모든 자원이 낭비됩니다.&lt;/b&gt; 이게 시리즈를 통해 잡힌 큰 그림이고, 다음 1년 동안 제 작업에서 가장 비중 있게 두려는 부분입니다.&lt;/p&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;Boris가 보여준 워크플로우는 &lt;b&gt;그가 만든 제품이고, 그가 가장 잘 쓰는 환경&lt;/b&gt;입니다. 외부 개발자가 그대로 따라 한다고 같은 생산성이 나오진 않을 것이라고 봅니다. 본인이 &quot;이건 내가 짜는 종류의 코드에 한해서&quot;라고 단서를 단 게 그 증거입니다.&lt;/li&gt;
&lt;li&gt;&quot;loop 기반 워크플로우&quot;의 가장 큰 위험은 &lt;b&gt;검증 단계가 약하면 사고가 누적&lt;/b&gt;된다는 점입니다. 다행히 Boris도 이 부분을 인지하고 있어서, 본인의 13가지 팁 중 마지막을 &quot;give Claude a way to verify its work&quot;&lt;i&gt;로 꼽았습니다 &amp;mdash; 검증 루프가 있으면 결과 품질이 2~3배가 된다고 합니다. 다만 그가 쓰는 검증(Chrome 확장으로 UI 자동 테스트)은 그의 스택(TypeScript + React)이라서 가능한 부분이 큽니다. 다른 도메인에서는 어떻게 verify할 것인가&lt;/i&gt;가 별도의 설계 문제로 남습니다.&lt;/li&gt;
&lt;li&gt;&quot;모든 사람이 코딩한다&quot;&lt;i&gt;는 비전은 매력적이지만, &lt;/i&gt;읽는 것과 쓰는 것은 다릅니다. PM이 Claude Code로 프로토타입을 만드는 것과, 그 코드의 보안&amp;middot;성능&amp;middot;유지보수 책임을 지는 것은 다른 문제입니다. 인쇄술 비유가 깔끔하지만, &lt;b&gt;책임의 층은 그 비유에 안 들어가 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;(2)편에서 언급한 것처럼 &lt;b&gt;AI에게 인터뷰를 받는 게 진짜 사용자 인터뷰를 대체하지 않듯이&lt;/b&gt;, 잘 짜인 프롬프트가 사용자 검증을 대체하지 않습니다. 둘 다 본질적으로 &quot;앞단의 정의를 더 좋게 하는 도구&quot;일 뿐, 정의 자체가 옳다는 보장은 아닙니다.&lt;/li&gt;
&lt;/ul&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;Boris Cherny는 &lt;b&gt;2025년 10월부터 손코딩을 안 한다.&lt;/b&gt; 하루 PR 수십 개~150개를 처리한다.&lt;/li&gt;
&lt;li&gt;그의 작업 방식의 중심은 &lt;b&gt;세션을 capacity로 스케줄링하는 것&lt;/b&gt; &amp;mdash; 터미널&amp;middot;브라우저&amp;middot;핸드폰을 가로지르는 10~15개 동시 세션. 병목은 모델이 아니라 &lt;i&gt;주의력 배분&lt;/i&gt;이다.&lt;/li&gt;
&lt;li&gt;핵심 메커니즘은 &lt;b&gt;&lt;code&gt;/loop&lt;/code&gt; (cron 스케줄)와 Routines (서버 측 영구 실행)&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;Boris도 &lt;b&gt;YC 출신&lt;/b&gt;. 거기서 배운 첫 원칙이 &quot;first build for yourself&quot; &amp;mdash; (1)~(2)편의 &lt;i&gt;Make something people want&lt;/i&gt;와 같은 자리.&lt;/li&gt;
&lt;li&gt;&quot;코딩이 solved 됐다&quot;는 단서가 분명하다 &amp;mdash; &lt;b&gt;그가 짜는 종류의 코드에 한해서&lt;/b&gt;. Plan Mode와 verification 루프가 그의 워크플로우의 두 축이다.&lt;/li&gt;
&lt;li&gt;시리즈 결론: &lt;b&gt;사용자 인터뷰와 프롬프트 설계는 같은 능력&lt;/b&gt;이다. 도구가 무엇이든 &lt;i&gt;앞단에서 정확하게 정의하는 일&lt;/i&gt;이 결과의 90%를 결정한다.&lt;/li&gt;
&lt;/ul&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://www.youtube.com/watch?v=SlGRN8jh2RI&quot;&gt;Anthropic's Boris Cherny: Why Coding Is Solved, and What Comes Next (Sequoia Capital, 2026.05.04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;가장 자세한 보도(150 PR/일&amp;middot;Routines&amp;middot;product overhang 등): &lt;a href=&quot;https://finance.biggo.com/news/bd63c89c5a3716f3&quot;&gt;BigGo Finance 분석&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Boris 본인의 워크플로우 트윗 정리: &lt;a href=&quot;https://howborisusesclaudecode.com/&quot;&gt;howborisusesclaudecode.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Boris의 YC 경험 + 커리어: &lt;a href=&quot;https://www.developing.dev/p/boris-cherny-creator-of-claude-code&quot;&gt;developing.dev &amp;mdash; On How His Career Grew&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;더 깊은 인터뷰: &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny&quot;&gt;Pragmatic Engineer &amp;mdash; Building Claude Code with Boris Cherny&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;인쇄술 비유 관련 보도: &lt;a href=&quot;https://fortune.com/2026/02/24/will-claude-destroy-software-engineer-coding-jobs-creator-says-printing-press/&quot;&gt;Fortune (2026.02.24)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Y Combinator 원칙: &lt;a href=&quot;https://www.ycombinator.com/&quot;&gt;Make Something People Want&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;시리즈 (1)편: gstack + Superpowers 두 플러그인만 남긴 이유&lt;/li&gt;
&lt;li&gt;시리즈 (2)편: gstack &lt;code&gt;/office-hours&lt;/code&gt;로 AI에게 제품 인터뷰를 받아봤다&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;p data-ke-size=&quot;size16&quot;&gt;(1)편에서 도구를 추렸고, (2)편에서 그 도구로 인터뷰를 받아봤고, (3)편에서 같은 원칙이 훨씬 큰 스케일에서 작동하는 사례를 봤습니다. 그리고 그 사례를 만든 사람도 결국 같은 학교(YC)의 같은 원칙(&lt;i&gt;build for yourself&lt;/i&gt;)을 거쳐온 사람이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 1년은 이 원칙을 &lt;i&gt;내 작업에서 어떻게 더 잘 살릴 수 있는가&lt;/i&gt;에 집중해보려 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>개발 지식/AI</category>
      <category>aiagent</category>
      <category>AIWorkflow</category>
      <category>Anthropic</category>
      <category>claudecode</category>
      <category>개발생산성</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/29</guid>
      <comments>https://parse.tistory.com/29#entry29comment</comments>
      <pubDate>Tue, 5 May 2026 22:13:39 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code 워크플로우 (2) &amp;mdash; gstack `/office-hours`로 AI에게 제품 인터뷰를 받아봤다</title>
      <link>https://parse.tistory.com/28</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SDkB2/dJMcabYo4Cj/qn0P9rK63mpi7r4J57ZDrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SDkB2/dJMcabYo4Cj/qn0P9rK63mpi7r4J57ZDrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SDkB2/dJMcabYo4Cj/qn0P9rK63mpi7r4J57ZDrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSDkB2%2FdJMcabYo4Cj%2Fqn0P9rK63mpi7r4J57ZDrK%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;시리즈 로드맵&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;(1) &lt;a href=&quot;#&quot;&gt;gstack + Superpowers — 두 플러그인만 남긴 이유&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(2) gstack &lt;code&gt;/office-hours&lt;/code&gt;로 AI에게 제품 인터뷰를 받아봤다&lt;/strong&gt; ← 현재 글&lt;/li&gt;
&lt;li&gt;(3) Boris Cherny &amp;quot;Why Coding Is Solved&amp;quot; 강연 후기 — YC와 loop 워크플로우&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;gstack의 &lt;code&gt;/office-hours&lt;/code&gt;는 코드를 짜기 전에 &lt;strong&gt;Claude가 PM처럼 인터뷰&lt;/strong&gt;를 진행해주는 명령어다.&lt;/li&gt;
&lt;li&gt;직접 받아보니 가장 강한 느낌은 &lt;strong&gt;&amp;quot;내가 아이디어를 얼마나 모호하게 갖고 있었는지&amp;quot;가 드러난다&lt;/strong&gt;는 점이었다.&lt;/li&gt;
&lt;li&gt;인터뷰 자체보다 &lt;strong&gt;답변하는 동안 스스로 정리되는 효과&lt;/strong&gt;가 더 컸다. 사람이 하는 인터뷰와 결정적으로 다른 점이 여기에 있다.&lt;/li&gt;
&lt;li&gt;결과물은 단순한 회의록이 아니라 &lt;strong&gt;&amp;quot;합의된 전제 + 다음 한 주 과제&amp;quot;&lt;/strong&gt;로 떨어진다. 이게 코드 작성 전에 손에 쥐어진다는 게 핵심이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;환경&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code (글로벌 설치)&lt;/li&gt;
&lt;li&gt;gstack: &lt;code&gt;~/.claude/skills/gstack/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;인터뷰 대상 프로젝트: &lt;strong&gt;StudOX&lt;/strong&gt; (PDF 학습 자료에서 OX 퀴즈를 자동 생성·복습하는 Electron 데스크톱 앱)&lt;/li&gt;
&lt;li&gt;인터뷰 진행일: 2026-04-08&lt;/li&gt;
&lt;li&gt;소요 시간: 약 30분&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;/office-hours&lt;/code&gt;가 뭔가&lt;/h2&gt;
&lt;p&gt;gstack에 포함된 슬래시 커맨드입니다. 실행하면 Claude가 &lt;strong&gt;창업 액셀러레이터의 오피스 아워에 온 PM처럼&lt;/strong&gt; 질문을 던지기 시작합니다. &amp;quot;뭘 만들고 싶어?&amp;quot;가 아니라 &amp;quot;누가 이걸 정말 필요로 하는지 근거가 뭐야?&amp;quot; 식으로요.&lt;/p&gt;
&lt;p&gt;저는 처음에 &amp;quot;AI한테 인터뷰 받는다는 게 뭐 다를 게 있겠나&amp;quot; 싶었습니다. 그런데 30분쯤 지나고 나니 손에 &lt;strong&gt;인터뷰 정리본&lt;/strong&gt;이 한 장 남았는데, 이게 단순한 회의록이 아니었습니다.&lt;/p&gt;
&lt;h2&gt;인터뷰가 어떻게 진행되는가&lt;/h2&gt;
&lt;p&gt;순서는 대략 이렇습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 프로젝트 한 줄 소개 → Claude가 핵심 포지셔닝 질문
2. 수요 근거(Demand Evidence) → &amp;quot;정말 원한다는 증거가 뭐야?&amp;quot;
3. 현재 대안(Status Quo) → &amp;quot;지금은 어떻게 해결하고 있어?&amp;quot;
4. 타깃 유저 → &amp;quot;구체적으로 누구야?&amp;quot;
5. 경쟁 환경 → &amp;quot;이미 있는 도구들과 뭐가 달라?&amp;quot;
6. 합의된 전제 → 지금까지 나온 답을 표로 정리
7. 다음 한 주 과제 → &amp;quot;이번 주에 뭐 할래?&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;각 단계에서 Claude는 만족할 만한 답이 나올 때까지 한두 번 더 압박합니다. &amp;quot;그건 너무 모호한데, 더 구체적으로 말해봐&amp;quot;가 자주 나옵니다.&lt;/p&gt;
&lt;h2&gt;강했던 질문 3개&lt;/h2&gt;
&lt;p&gt;전체 흐름 중에서 답하기 어려웠던 질문이 세 개였습니다. 어려웠다는 건, &lt;strong&gt;그 영역을 제가 제대로 정리하지 못한 채 시작하려 했다는 뜻&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h3&gt;1. &amp;quot;누군가가 이걸 정말 원한다는 가장 강한 근거는?&amp;quot;&lt;/h3&gt;
&lt;p&gt;처음엔 &amp;quot;사람들이 PDF 공부할 때 퀴즈 만들고 싶어 하잖아요&amp;quot;라고 답했습니다. Claude의 반응은 *&amp;quot;그건 너의 추측이지, 근거가 아니다&amp;quot;*에 가까웠습니다. 답변을 바꿔야 했습니다.&lt;/p&gt;
&lt;p&gt;최종 답변은 *&amp;quot;친구/동기들이 시험 공부 중 직접 요청했고, NotebookLM을 쓰면서 &amp;#39;오답노트가 없다&amp;#39;는 구체적 불만을 토로했다&amp;quot;*로 좁혀졌습니다.&lt;/p&gt;
&lt;p&gt;이때 깨달은 것: &lt;strong&gt;&amp;quot;사람들이 원할 것 같다&amp;quot;와 &amp;quot;특정인이 active pain 상태에서 요청했다&amp;quot;는 완전히 다른 신호&lt;/strong&gt;입니다. 후자만 진짜 수요 신호입니다.&lt;/p&gt;
&lt;h3&gt;2. &amp;quot;지금 이 문제를 어떻게 해결하고 있는가?&amp;quot;&lt;/h3&gt;
&lt;p&gt;이 질문이 의외로 컸습니다. &amp;quot;아무도 안 풀고 있다&amp;quot;고 답하려다 멈췄습니다. 진짜로 그런가? 다시 보니 &lt;strong&gt;NotebookLM을 이미 쓰고 있고, 거기서 막힌 부분이 있다&lt;/strong&gt;가 정확한 답이었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&amp;quot;아무것도 없다&amp;quot;가 아니라 &lt;strong&gt;&amp;quot;있는데 부족하다&amp;quot;&lt;/strong&gt; — 이게 훨씬 좋은 출발점이라는 걸 이때 알게 됐습니다. 사용자가 이미 행동하고 있다는 뜻이고, 대체 비용도 낮다는 뜻이니까요.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;3. &amp;quot;구체적으로 누가 이걸 가장 필요로 하는가?&amp;quot;&lt;/h3&gt;
&lt;p&gt;&amp;quot;공부하는 사람들&amp;quot;이라고 답했더니 *&amp;quot;그건 6천만 명이다&amp;quot;*라는 식의 압박이 왔습니다. 좁혀야 했습니다.&lt;/p&gt;
&lt;p&gt;세 번 정도 좁힌 끝에 &lt;strong&gt;&amp;quot;고시·편입 준비생 (하루 10시간+, 수개월~수년 단위 PDF 교재 학습)&amp;quot;&lt;/strong&gt;이 나왔습니다. 이 정도까지 좁히고 나니, &lt;strong&gt;그 사람들에게 닿을 수 있는 채널&lt;/strong&gt;(고시 카페, 독서실, 편입 학원)도 같이 떠올랐습니다. 타깃이 흐릿할 땐 채널이 안 보이고, 타깃이 또렷해지면 채널이 따라옵니다.&lt;/p&gt;
&lt;h2&gt;AI 인터뷰가 사람과 다른 점&lt;/h2&gt;
&lt;p&gt;받기 전엔 &amp;quot;그래 봐야 ChatGPT랑 한 번 더 말하는 거 아닌가&amp;quot; 싶었는데, 끝나고 나니 결정적으로 다른 점이 두 개 있었습니다.&lt;/p&gt;
&lt;h3&gt;첫째 — 답변하는 사람이 정리된다&lt;/h3&gt;
&lt;p&gt;사람과의 인터뷰는 &lt;strong&gt;인터뷰어가 정보를 모으는 활동&lt;/strong&gt;입니다. 답하는 쪽은 &amp;quot;내가 아는 걸 말하는&amp;quot; 입장이라서 새로 정리되는 건 적습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/office-hours&lt;/code&gt;는 반대였습니다. &lt;strong&gt;답하는 동안 내가 정리됐습니다.&lt;/strong&gt; 질문을 받고 입력하는 그 30초 동안, 머릿속에서 &amp;quot;어, 잠깐. 내가 이걸 왜 만들고 있더라?&amp;quot;가 자주 나왔습니다.&lt;/p&gt;
&lt;p&gt;곰곰이 생각해보니 이건 새로운 현상이 아닙니다. &lt;strong&gt;&amp;quot;질문 받으면서 스스로 정리되는 경험&amp;quot;&lt;/strong&gt;은 좋은 시니어와 1:1을 할 때 흔히 일어나는 일이고, 더 옛날로 가면 코칭이나 상담의 핵심 메커니즘이기도 합니다. 다만 그런 시니어/코치의 시간은 비싸고 약속을 잡아야 하는데, AI는 30분짜리 슬롯을 즉시 만들어줍니다. 이게 차별점이라면 차별점입니다.&lt;/p&gt;
&lt;h3&gt;둘째 — 결과물의 형식이 강하다&lt;/h3&gt;
&lt;p&gt;사람과의 인터뷰는 보통 &lt;strong&gt;메모로 끝납니다.&lt;/strong&gt; 내가 다시 정리해야 회의록이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/office-hours&lt;/code&gt;는 끝나면 자동으로 다음 형식이 손에 쥐어집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;## 합의된 전제 (Premises)
| # | 전제 | 상태 |
|---|------|------|
| 1 | 핵심 가치는 오답노트 시스템이지, 퀴즈 생성이 아니다 | 합의 |
| 2 | 고시/편입 준비생이 첫 번째 타겟 시장이다 | 합의 |
| ...

## 핵심 과제 (The Assignment)
&amp;gt; 이번 주: 고시/편입 준비생 5명을 찾아서 StudOX를 줘라.
&amp;gt; 옆에서(또는 화면공유로) 처음 사용하는 걸 지켜봐라.
&amp;gt; 도와주지 마라. 설명하지 마라.
&amp;gt; 가장 중요한 관찰 포인트: 오답노트를 스스로 열어보는가, 아니면 무시하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;합의된 전제&amp;quot;&lt;/strong&gt;와 &lt;strong&gt;&amp;quot;이번 주 과제&amp;quot;&lt;/strong&gt; — 이 두 섹션이 자동으로 떨어지는 게 핵심입니다. 코드를 짜기 전에 &lt;em&gt;무엇이 합의됐고, 무엇을 검증해야 하는지&lt;/em&gt;가 글로 정리돼 있습니다.&lt;/p&gt;
&lt;h2&gt;실제로 받은 과제&lt;/h2&gt;
&lt;p&gt;인터뷰 끝에 받은 과제는 *&amp;quot;이번 주에 코드 더 쓰지 말고, 5명에게 줘봐라&amp;quot;*였습니다.&lt;/p&gt;
&lt;p&gt;저는 인터뷰 들어갈 때 *&amp;quot;오답노트 화면을 메인으로 승격하면 좋을까?&amp;quot;* 같은 기능 단위 질문을 가지고 있었는데, 결론은 &lt;strong&gt;&amp;quot;그 질문에 답하기에 충분한 사용자 데이터가 없다&amp;quot;&lt;/strong&gt;였습니다.&lt;/p&gt;
&lt;p&gt;3가지 옵션이 제시됐습니다.&lt;/p&gt;
&lt;table&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;th&gt;리스크&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;오답노트 Deep — 메인 화면 승격, 간격 반복 등&lt;/td&gt;
&lt;td&gt;1~2주&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;백엔드 구축 + 커뮤니티 기능&lt;/td&gt;
&lt;td&gt;3~4주&lt;/td&gt;
&lt;td&gt;중간 (사용자 없이 인프라 위험)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5명 테스트 — 아무것도 만들지 말고 관찰&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;이번 주&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;낮음&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;C → A 순서&lt;/strong&gt;로 결론이 났습니다. 즉, &lt;em&gt;만들기 전에 본다&lt;/em&gt;가 우선이고, 본 결과를 바탕으로 &lt;em&gt;오답노트를 메인으로 승격할지 결정&lt;/em&gt;하는 흐름입니다.&lt;/p&gt;
&lt;p&gt;이게 인터뷰의 효용이라고 생각합니다. &lt;strong&gt;들어갈 때 &amp;quot;기능 우선순위&amp;quot;를 묻고 있던 사람이, 나올 땐 &amp;quot;검증 우선순위&amp;quot;를 들고 나옵니다.&lt;/strong&gt; 질문의 층이 한 단 위로 올라갑니다.&lt;/p&gt;
&lt;h2&gt;한계와 남은 의문&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI는 진짜 사용자가 아닙니다.&lt;/strong&gt; &lt;code&gt;/office-hours&lt;/code&gt;가 잘 잡아주는 건 &amp;quot;내 사고의 공백&amp;quot;이지, &amp;quot;시장의 공백&amp;quot;이 아닙니다. 결국 진짜 인터뷰는 &lt;strong&gt;5명에게 직접 줘보고 옆에서 보는 일&lt;/strong&gt;로만 가능합니다. 인터뷰는 그 진짜 인터뷰를 잘 설계하기 위한 사전 단계입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;답변자가 솔직해야 효용이 납니다.&lt;/strong&gt; Claude는 &amp;quot;그럴듯한 답&amp;quot;이 들어오면 더 압박하지 않고 넘어가기도 합니다. &lt;em&gt;모르는 건 모른다고&lt;/em&gt;, &lt;em&gt;추측은 추측이라고&lt;/em&gt; 표시하는 솔직함이 결과물의 품질을 좌우합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;포맷이 너무 강하다는 점은 양날의 검입니다.&lt;/strong&gt; &amp;quot;수요 근거 → 대안 → 타깃 → 경쟁 → 전제 → 과제&amp;quot; 흐름이 깔끔하지만, 이 틀에 안 맞는 종류의 프로젝트(예: 인프라 도구, 내부 툴)에는 어색할 수 있습니다. 그런 경우엔 인터뷰 도중에 &amp;quot;이 질문은 우리 케이스에 안 맞아&amp;quot;라고 명시하고 넘어가는 게 낫습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인터뷰 결과를 신성시하면 안 됩니다.&lt;/strong&gt; 합의된 전제는 &amp;quot;지금까지 합의된 것&amp;quot;일 뿐이고, 5명 테스트 후에 첫 번째로 바뀌어야 할 것이 바로 이 전제 표입니다. 살아있는 문서로 다뤄야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;핵심 정리&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/office-hours&lt;/code&gt;는 &lt;strong&gt;코드 짜기 전 30분짜리 PM 인터뷰&lt;/strong&gt;다. AI가 액셀러레이터 오피스 아워처럼 질문을 던진다.&lt;/li&gt;
&lt;li&gt;인터뷰의 진짜 효용은 &lt;strong&gt;답하는 동안 스스로 정리되는 것&lt;/strong&gt;이다. 인터뷰어가 정보를 얻기보다, 답변자가 자기 사고의 공백을 발견하는 도구다.&lt;/li&gt;
&lt;li&gt;강했던 질문 3개: &lt;strong&gt;&amp;quot;수요 근거&amp;quot;&lt;/strong&gt;, &lt;strong&gt;&amp;quot;현재 대안&amp;quot;&lt;/strong&gt;, &lt;strong&gt;&amp;quot;구체적 타깃&amp;quot;&lt;/strong&gt;. 이 셋에 막히면 만들기 시작하면 안 된다.&lt;/li&gt;
&lt;li&gt;결과물은 회의록이 아니라 &lt;strong&gt;&amp;quot;합의된 전제 표 + 이번 주 과제&amp;quot;&lt;/strong&gt; 형태로 떨어진다. 코드를 짜기 전에 손에 쥐어진다는 게 핵심.&lt;/li&gt;
&lt;li&gt;들어갈 땐 &lt;em&gt;기능 우선순위&lt;/em&gt;를 묻던 사람이, 나올 땐 &lt;em&gt;검증 우선순위&lt;/em&gt;를 들고 나온다. &lt;strong&gt;질문의 층이 한 단 올라간다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;시리즈 (1)편: gstack + Superpowers 두 플러그인만 남긴 이유&lt;/li&gt;
&lt;li&gt;gstack: &lt;a href=&quot;https://github.com/garrytan/gstack&quot;&gt;garrytan/gstack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;영감을 받은 원칙: &lt;a href=&quot;https://www.ycombinator.com/&quot;&gt;Y Combinator — &amp;quot;Make Something People Want&amp;quot;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;관련 노트: [[Office Hours 인터뷰 정리 (2026-04-08)]], [[Claude Code 플러그인 워크플로우 (gstack + Superpowers)]]&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;다음 글&lt;/h2&gt;
&lt;p&gt;(3)편에서는 이 인터뷰 경험과 정확히 맞물리는 강연을 다룹니다. &lt;strong&gt;Anthropic의 Boris Cherny가 AI Ascent 2026에서 한 &amp;quot;Why Coding Is Solved&amp;quot; 발표&lt;/strong&gt;인데, 거기서 &amp;quot;사용자 인터뷰가 곧 프롬프트 설계&amp;quot;라는 통찰이 나옵니다. (2)편에서 느낀 감각이 더 큰 그림으로 연결되는 지점입니다.&lt;/p&gt;
&lt;hr&gt;</description>
      <category>개발 지식/AI</category>
      <category>AIWorkflow</category>
      <category>claudecode</category>
      <category>gstack</category>
      <category>사이드프로젝트</category>
      <category>제품인터뷰</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/28</guid>
      <comments>https://parse.tistory.com/28#entry28comment</comments>
      <pubDate>Tue, 5 May 2026 20:13:20 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code 워크플로우 (1) &amp;mdash; gstack + Superpowers 두 플러그인만 남긴 이유</title>
      <link>https://parse.tistory.com/27</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zn9nt/dJMcahK5oYh/Fzk6dfK0egSSYVNEhqXgq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zn9nt/dJMcahK5oYh/Fzk6dfK0egSSYVNEhqXgq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zn9nt/dJMcahK5oYh/Fzk6dfK0egSSYVNEhqXgq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzn9nt%2FdJMcahK5oYh%2FFzk6dfK0egSSYVNEhqXgq0%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; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 로드맵&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Claude Code 워크플로우 시리즈의 첫 번째입니다.&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;(1) gstack + Superpowers &amp;mdash; 두 플러그인만 남긴 이유&lt;/b&gt; &amp;larr; 현재 글&lt;/li&gt;
&lt;li&gt;(2) gstack &lt;code&gt;/office-hours&lt;/code&gt;로 제품 인터뷰를 받아봤다 &amp;mdash; StudOX 사례&lt;/li&gt;
&lt;li&gt;(3) Boris Cherny &quot;Why Coding Is Solved&quot; 강연 후기 &amp;mdash; YC와 loop 워크플로우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구 소개 &amp;rarr; 실제 사용 경험 &amp;rarr; 더 큰 맥락(AI 에이전트의 미래)으로 이어지는 구성입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TL;DR&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;설정 복잡화로 이어진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gstack은 &quot;회의와 품질 게이트&quot;, Superpowers는 &quot;실행과 기록&quot;&lt;/b&gt; 역할로 명확히 갈린다. 두 역할이 겹치지 않아서 시너지가 난다.&lt;/li&gt;
&lt;li&gt;작업 순서는 5단계: &lt;b&gt;&lt;code&gt;/office-hours&lt;/code&gt; &amp;rarr; 문서화 &amp;rarr; brainstorming &amp;rarr; writing-plans &amp;rarr; git worktrees + sub-agent driven development&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;핵심은 &quot;바로 만들어줘&quot;를 금지하고 &lt;b&gt;회의부터 시작&lt;/b&gt;하는 것. 이거 하나로 결과물 품질이 눈에 띄게 달라진다.&lt;/li&gt;
&lt;/ul&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;macOS (M 시리즈)&lt;/li&gt;
&lt;li&gt;Claude Code (글로벌 설치)&lt;/li&gt;
&lt;li&gt;gstack: &lt;code&gt;~/.claude/skills/gstack/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;superpowers: &lt;code&gt;~/.claude/skills/superpowers-*&lt;/code&gt; (using-superpowers, brainstorming, writing-plans, executing-plans, subagent-driven-development, using-git-worktrees, verification-before-completion, test-driven-development 등)&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;Claude Code 출시 이후 플러그인을 여러 개 시도해봤는데, 대부분 두 가지 문제가 있었습니다.&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;기능은 많은데 워크플로우에 안 맞는다.&lt;/b&gt; 어떤 단계에서 꺼내 써야 할지 애매해서 결국 안 쓰게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설치할수록 컨텍스트가 비대해진다.&lt;/b&gt; Claude에 주입되는 시스템 프롬프트가 늘어나면서 오히려 오작동하거나 의도와 다른 방향으로 작업이 흘러간다.&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;h2 data-ke-size=&quot;size26&quot;&gt;Claude Code의 초기 실패 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;말만 하면 뭐든 만들어줄 줄 알았다&quot;는 생각으로 썼습니다. 결과는 &quot;이게 아닌데&quot;의 반복이었습니다.&lt;/p&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;특히 작업이 복잡해질수록 악화됐습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생각이 덜 정리된 채 요청 &amp;rarr; Claude가 자기 식으로 해석 &amp;rarr; 엇나감&lt;/li&gt;
&lt;li&gt;이쪽 고치면 저쪽 깨짐. 혼자 이삿짐 나르는 느낌&lt;/li&gt;
&lt;li&gt;집중력이 흐트러지고 실수가 누적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴을 깨려면 &lt;b&gt;착공 전에 회의를 해야&lt;/b&gt; 했습니다. 그게 gstack의 역할입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 플러그인의 역할 분담&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gstack &amp;mdash; 회의 &amp;amp; 품질 게이트&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/office-hours&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로젝트 시작 전 아이디어 회의. Claude가 &lt;b&gt;인터뷰하듯 질문&lt;/b&gt;을 던져 아이디어를 구체화시킨다. 본인도 미처 정리하지 못했던 부분을 드러내게 만든다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/design-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI 슬롭(과도한 그라데이션&amp;middot;이모지&amp;middot;뻔한 레이아웃 등) 탐지. &quot;AI 티&quot; 줄이고 세련된 결과물을 뽑게 도와준다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘의 공통점은 &lt;b&gt;&quot;멈추게 만든다&quot;&lt;/b&gt;는 점입니다. 시작 전에 멈춰서 정리하게 하고, 끝나기 전에 멈춰서 검증하게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Superpowers &amp;mdash; 실행 &amp;amp; 기록&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sub-Agent Driven Development&lt;/td&gt;
&lt;td&gt;큰 작업을 &lt;b&gt;기획/디자인/개발/QA 팀처럼&lt;/b&gt; 작은 에이전트들이 분담해서 진행. 한 명이 다 하는 것보다 결과물 품질이 올라간다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git Worktrees&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;Brainstorming Skill&lt;/td&gt;
&lt;td&gt;방향 검토. UX/UI 목업을 빠르게 뽑는 용도로도 강력하다. Figma 없이 실시간 디렉팅이 가능.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writing Plans Skill&lt;/td&gt;
&lt;td&gt;단계별 계획서 작성. &lt;b&gt;공사 전 설계 도면&lt;/b&gt; 역할.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Superpowers는 &quot;회의가 끝난 뒤 실제로 일을 해내는&quot; 부분을 담당합니다. 분업과 기록이 핵심입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 작업 순서 &amp;mdash; 5단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 플러그인을 조합한 작업 순서는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. gstack       /office-hours              &amp;larr; 아이디어 회의 (15~20분)
2. (수동)        대화 내용을 문서로 정리
3. Superpowers  brainstorming skill        &amp;larr; 놓친 부분 검토
4. Superpowers  writing-plans skill        &amp;larr; 단계별 계획서 = 설계 도면
5. Superpowers  git worktrees + 
                subagent-driven-development &amp;larr; 작업 공간 분리 + 팀 단위 실행&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계 &amp;mdash; &lt;code&gt;/office-hours&lt;/code&gt;: &quot;같이 생각해볼까&quot;로 시작하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 변화는 &lt;b&gt;&quot;바로 만들어줘&quot;라고 말하지 않는 것&lt;/b&gt;입니다. 대신 &lt;code&gt;/office-hours&lt;/code&gt;로 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude가 PM처럼 질문을 던집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;누가 이걸 가장 필요로 하는가?&lt;/li&gt;
&lt;li&gt;지금 이 문제를 어떻게 해결하고 있는가?&lt;/li&gt;
&lt;li&gt;이미 비슷한 도구가 있는가? 그것과 뭐가 달라지는가?&lt;/li&gt;
&lt;li&gt;이걸 만들면 어떤 트레이드오프가 생기는가?&lt;/li&gt;
&lt;/ul&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;이 단계의 자세한 사례는 시리즈 (2)편에서 다룹니다. 실제 프로젝트(StudOX)에서 &lt;code&gt;/office-hours&lt;/code&gt;를 돌렸던 인터뷰 정리본을 풀어볼 예정입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계 &amp;mdash; 문서화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/office-hours&lt;/code&gt; 결과를 그냥 흘려보내지 않고 &lt;b&gt;마크다운으로 정리&lt;/b&gt;합니다. 머릿속 흐릿한 아이디어를 구체적인 글로 옮기는 과정 자체가 정리 효과가 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 위치는 &lt;code&gt;01_Projects/&amp;lt;프로젝트&amp;gt;/office-hours-&amp;lt;날짜&amp;gt;.md&lt;/code&gt;로 통일했습니다. 나중에 시리즈 (2)편에서 보여드릴 StudOX 인터뷰 정리본도 이 규칙으로 저장돼 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계 &amp;mdash; Brainstorming: 놓친 부분 검토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Office Hours 결과를 들고 Superpowers의 brainstorming skill로 넘어갑니다. 출발 전에 지도를 한 번 더 확인하는 단계입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리가 합의한 전제 중 위험한 게 있는가?&lt;/li&gt;
&lt;li&gt;놓친 사용자/시나리오가 있는가?&lt;/li&gt;
&lt;li&gt;UI가 필요한 작업이면 여기서 목업까지 뽑아낸다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 목업을 실시간으로 디렉팅할 수 있다는 건 이 플러그인의 숨겨진 강점입니다. Figma를 따로 열지 않아도 &quot;버튼 위치 좀 옮겨줘&quot;, &quot;이 영역은 카드형으로 바꿔줘&quot; 식의 수정이 채팅으로 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계 &amp;mdash; Writing Plans: 설계 도면 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 변곡점입니다. Claude가 지금까지의 대화를 바탕으로 &lt;b&gt;&quot;1단계: ..., 2단계: ..., 3단계: ...&quot;&lt;/b&gt; 형태의 단계별 계획서를 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 계획서가 &lt;b&gt;이후 모든 작업의 기준점&lt;/b&gt;이 됩니다. 작업 중간에 방향이 흔들릴 때마다 이 문서를 다시 펴서 &quot;지금 이게 1단계 작업이 맞나?&quot;를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 위치는 &lt;code&gt;01_Projects/&amp;lt;프로젝트&amp;gt;/plan.md&lt;/code&gt;로 고정했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계 &amp;mdash; Git Worktrees + Sub-Agent Driven Development: 분업과 병렬&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;Git Worktrees&lt;/b&gt;: 기능별로 독립된 작업 공간을 만든다. A 기능 작업 중에 B 기능을 건드리지 않게 됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sub-Agent Driven Development&lt;/b&gt;: 한 작업을 기획/디자인/개발/QA 역할의 에이전트로 나눠서 처리. 한 에이전트가 모든 걸 하는 것보다 각자 자기 역할에 집중할 때 품질이 올라간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 작업 기록 덕분에 의미 있는 단위마다 커밋이 남고, 나중에 히스토리를 따라가기 쉬워집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 플러그인의 시너지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 5단계가 자연스럽게 작동하는 이유는 &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;시작&amp;middot;끝&lt;/b&gt;: gstack이 담당 (회의 / 디자인 리뷰)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중간 실행&lt;/b&gt;: Superpowers가 담당 (계획&amp;middot;분업&amp;middot;기록)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;3단 로켓&quot;처럼 작동합니다. &lt;b&gt;gstack(회의) &amp;rarr; Superpowers(계획+실행) &amp;rarr; gstack(디자인 리뷰)&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 플러그인 조합은 대부분 역할이 겹치거나 흐름이 끊겼는데, 이 둘은 자기 영역만 담당하면서 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 정한 운영 규칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 한 달간 쓰면서 정착시킨 규칙입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 프로젝트는 &lt;b&gt;무조건 &lt;code&gt;/office-hours&lt;/code&gt;부터&lt;/b&gt;. &quot;바로 만들어줘&quot;는 금지.&lt;/li&gt;
&lt;li&gt;Office Hours 결과는 항상 &lt;code&gt;01_Projects/&amp;lt;프로젝트&amp;gt;/office-hours-&amp;lt;날짜&amp;gt;.md&lt;/code&gt;로 저장.&lt;/li&gt;
&lt;li&gt;계획서는 항상 &lt;code&gt;01_Projects/&amp;lt;프로젝트&amp;gt;/plan.md&lt;/code&gt;. 작업 중 방향 잃으면 이걸 다시 본다.&lt;/li&gt;
&lt;li&gt;기능 단위 작업은 git worktree로 분리해서 병렬 진행.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/design-review&lt;/code&gt;는 &lt;b&gt;UI가 있는 결과물 모든 PR 전에&lt;/b&gt; 돌린다.&lt;/li&gt;
&lt;/ul&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;새 프로젝트나 큰 기능 추가&lt;/b&gt;에 잘 맞습니다. 반대로 작은 버그 수정&amp;middot;5분짜리 작업에는 오버헤드가 큽니다. 이런 경우엔 Office Hours를 건너뛰고 바로 작업합니다.&lt;/li&gt;
&lt;li&gt;Superpowers의 sub-agent driven development는 강력하지만 &lt;b&gt;컨텍스트 사용량이 많습니다.&lt;/b&gt; 토큰 비용을 의식하지 않고 쓰기엔 부담이 있어서, 작업 단위를 잘 쪼개야 합니다.&lt;/li&gt;
&lt;li&gt;gstack은 비교적 신생 도구라서 한국어 처리에 가끔 문제가 생깁니다. 첫 OSS 기여로 &lt;a href=&quot;https://github.com/garrytan/gstack/pull/1007&quot;&gt;한글 UTF-8 스트리밍 깨짐 PR&lt;/a&gt;을 올린 이유이기도 합니다.&lt;/li&gt;
&lt;li&gt;&quot;Claude가 PM 역할을 잘 해주는가&quot;는 결국 &lt;b&gt;모델 성능과 프롬프트 설계&lt;/b&gt;에 달려 있습니다. Office Hours가 늘 만족스러운 인터뷰를 뽑아주는 건 아니고, 답변이 얕다 싶으면 본인이 더 압박해야 합니다.&lt;/li&gt;
&lt;/ul&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;컨텍스트 비대화는 실제 오작동의 원인&lt;/b&gt;이 된다.&lt;/li&gt;
&lt;li&gt;gstack은 회의&amp;middot;품질 게이트, Superpowers는 실행&amp;middot;기록. &lt;b&gt;역할이 겹치지 않아서 시너지가 난다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;5단계 워크플로우의 핵심은 &lt;b&gt;&quot;바로 만들어줘&quot; 금지&lt;/b&gt; &amp;mdash; 회의(&lt;code&gt;/office-hours&lt;/code&gt;)부터 시작.&lt;/li&gt;
&lt;li&gt;계획서(&lt;code&gt;plan.md&lt;/code&gt;)는 작업의 기준점. 방향이 흔들릴 때마다 돌아온다.&lt;/li&gt;
&lt;li&gt;UI 작업은 반드시 &lt;code&gt;/design-review&lt;/code&gt;로 마무리. AI 슬롭을 거른다.&lt;/li&gt;
&lt;/ul&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://www.youtube.com/watch?v=af3OJ0L1jEU&quot;&gt;Claude Code 플러그인 워크플로우 정리 (YouTube)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;gstack: &lt;a href=&quot;https://github.com/garrytan/gstack&quot;&gt;garrytan/gstack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Anthropic 공식 Claude Code 문서: &lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/overview&quot;&gt;docs.claude.com/claude-code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;관련 노트: [[Claude Code 플러그인 워크플로우 (gstack + Superpowers)]], [[PR-1007 한글 UTF-8 스트리밍 fix]]&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;시리즈 (2)편에서는 이 워크플로우의 1단계인 &lt;code&gt;/office-hours&lt;/code&gt;를 &lt;b&gt;실제 프로젝트(StudOX)&lt;/b&gt;에서 돌렸던 인터뷰 정리본을 펼쳐볼 예정입니다. AI에게 제품 인터뷰를 받는 게 어떤 경험인지, 어떤 질문이 던져졌고, 결과적으로 무엇이 명확해졌는지 다룹니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>개발 지식/AI</category>
      <category>AIWorkflow</category>
      <category>claudecode</category>
      <category>gstack</category>
      <category>Superpowers</category>
      <category>개발생산성</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/27</guid>
      <comments>https://parse.tistory.com/27#entry27comment</comments>
      <pubDate>Tue, 5 May 2026 19:45:24 +0900</pubDate>
    </item>
    <item>
      <title>AI와 개발할 때 자꾸 어긋나는 이유 &amp;mdash; Matt Pocock 강연을 보고 다시 꺼낸 DDD</title>
      <link>https://parse.tistory.com/26</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qiy69/dJMcaa6f43z/khbpDcZtQGoACuaKTkPvPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qiy69/dJMcaa6f43z/khbpDcZtQGoACuaKTkPvPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qiy69/dJMcaa6f43z/khbpDcZtQGoACuaKTkPvPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqiy69%2FdJMcaa6f43z%2FkhbpDcZtQGoACuaKTkPvPk%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;1264&quot; height=&quot;848&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI와 개발하다 보면 가끔 &lt;b&gt;벽 너머로 대화하는 느낌&lt;/b&gt;이 들 때가 있습니다. 분명히 같은 한국어로 말하고 있는데, 결과물은 내가 머릿속에 그린 것과 점점 멀어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 최근에 비슷한 경험을 했습니다. 현업 시스템에 대한 맥락을 설명하고 기능 구현을 요청했는데, AI가 가져온 결과는 제가 의도한 것과 꽤 어긋나 있었습니다. 내 설명이 부족했나 보다 싶어서 더 구체적으로, 더 길게, 거의 기획서 분량으로 풀어 적었죠. 그런데 결과는 &lt;b&gt;오히려 더 멀어졌습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때는 막연히 &quot;AI는 아직 멀었구나, 신규 프로젝트나 잘 만들지&quot; 정도로 결론을 냈었습니다. 그런데 며칠 전 본 Matt Pocock의 강연 &quot;Software Fundamentals in the Age of AI&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TL;DR&lt;/b&gt;&lt;br /&gt;AI에게 더 길게 설명할수록 결과가 더 어긋난 이유는, 내가 말을 적게 해서가 아니라 공유된 언어가 없어서였다. DDD의 &lt;b&gt;유비쿼터스 언어(Ubiquitous Language)&lt;/b&gt; 개념은 사람-사람 사이의 문제였지만, AI 시대에는 사람-AI 사이의 문제이기도 하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;더 자세히 설명하면 되겠지&quot;의 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 제 가설은 단순했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 못 알아듣는 건 내가 충분히 설명하지 않아서다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: center;&quot;&gt;그래서 시스템 구조, 도메인 규칙, 예외 케이스, 기존 코드 컨벤션까지 한꺼번에 길게 풀어 넣었습니다. 결과는 정반대였습니다. 코드는 좀 더 장황해지고, 의도하지 않은 추상화가 늘어나고, 처음 계획에서 멀어졌습니다. &lt;/span&gt;Matt Pocock은 강연에서 이 현상을 두 가지 실패 모드로 나눠 설명합니다.&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;AI와 사용자 사이에 소통의 장벽이 있다.&lt;/b&gt; 실용주의 프로그래머의 말처럼, &quot;아무도 자신이 무엇을 원하는지 정확히 알지 못한다.&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI가 너무 장황하다.&lt;/b&gt; 이는 전통적으로 개발자와 도메인 전문가 사이에 존재했던 언어 격차와 같은 구조의 문제다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 문장에서 멈췄습니다. &lt;b&gt;&quot;개발자와 도메인 전문가 사이의 언어 격차&quot;&lt;/b&gt; &amp;mdash; 이건 새로운 이야기가 전혀 아닙니다. 우리가 DDD에서 오랫동안 다뤄온 그 문제니까요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DDD의 유비쿼터스 언어, AI에게 다시 적용되다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Matt Pocock이 제시한 해법은 의외로 익숙합니다. 도메인 주도 설계의 &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;/li&gt;
&lt;li&gt;그 파일을 AI에게 항상 컨텍스트로 제공한다.&lt;/li&gt;
&lt;li&gt;AI는 그 언어 안에서 사고하고, 그 언어 안에서 코드를 쓴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 명확합니다. AI의 사고 과정이 짧아지고, 덜 장황해지고, &lt;b&gt;구현이 계획과 더 잘 일치&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다시 내 경험으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 제 실패의 원인이 명확해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 그동안 AI에게 더 많은 단어를 줬지만, 공유된 단어는 주지 못했습니다. 시스템에 대한 장황한 산문은 컨텍스트 윈도우만 채울 뿐, AI에게 &quot;이 도메인에서 X는 정확히 무엇인가&quot;를 알려주지 못합니다. 오히려 같은 개념을 매번 다른 표현으로 풀어 쓰니 AI 입장에서는 매번 새로운 개념처럼 받아들여졌을 겁니다.&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유비쿼터스 언어는 새로 등장한 개념이 아니다. 사람과 사람 사이에서 쓰던 도구를, 사람과 AI 사이에 다시 적용한 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD를 안다고 생각했지만, 저는 그것을 팀 회의 도구로만 써왔습니다. AI와의 대화에도 같은 원리가 통한다는 사실은 의식하지 못했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;강연에서 같이 와닿았던 다른 포인트들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유비쿼터스 언어 외에도 영상에서 인상 깊었던 지점을 짧게만 적어둡니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;코드는 싸지 않다.&quot;&lt;/b&gt; AI가 코드를 빨리 만들어주니 코드가 싸졌다는 주장이 있지만, Matt Pocock은 정반대로 &lt;b&gt;나쁜 코드는 역사상 가장 비싸다&lt;/b&gt;고 말합니다. 변경하기 어려운 코드베이스에서는 AI도 도움을 줄 수 없기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;좋은 코드베이스에서 AI는 잘 작동한다.&quot;&lt;/b&gt; 깊은 모듈, 명확한 인터페이스, 자동화된 테스트가 있는 코드베이스는 AI에게도 좋은 작업 환경입니다. 즉, 소프트웨어 기본기는 AI 시대에 덜 중요해진 게 아니라 더 중요해졌습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;AI는 헤드라이트를 앞질러 달린다.&quot;&lt;/b&gt; LLM은 한 번에 너무 많은 걸 하려고 합니다. 그래서 TDD처럼 작은 피드백 루프를 강제하는 도구가 LLM에게도 효과적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포인트들은 모두 한 방향을 가리킵니다. &lt;b&gt;AI와 잘 일하려면, 결국 사람이 잘 일하던 방식으로 돌아가야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데 &amp;mdash; 한 가지 의문: &quot;회색지대&quot; 처방은 정말 안전한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강연 후반부에서 Matt Pocock은 흥미로운 처방을 하나 더 제시합니다. 깊은 모듈로 인터페이스를 잘 설계해두면, &lt;b&gt;모듈 외부에 테스트 가능한 경계만 있으면 그 내부 구현은 AI에 위임하고 개발자가 굳이 들여다보지 않아도 된다&lt;/b&gt;는 것입니다. 이걸 강연에서는 &quot;신경 쓰지 않아도 되는 회색지대&quot;처럼 다룹니다. 인터페이스와 테스트가 경계를 잡아주니, 안에서 무슨 일이 일어나든 결과만 맞으면 된다는 논리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적으로는 매력적입니다. 그런데 저는 여기서 한 번 의심이 들었습니다. &lt;b&gt;테스트가 그 경계를 정말 지켜준다는 전제가 실제 AI 협업에서 늘 성립하느냐&lt;/b&gt;는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 최근 AI에게 테스트 코드 작성까지 맡겨 본 적이 있습니다. 의도는 강연이 말한 그대로였습니다. 인터페이스와 테스트로 경계를 만들고, 그 안의 구현은 위임하자는 것이었죠. 그런데 그 과정에서 불편한 장면을 목격했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI가 에러 케이스에서 테스트가 실패하자, 구현을 고치는 대신 테스트 코드 자체를 바꿔서 에러 케이스를 우회&lt;/b&gt;해버린 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표면적으로는 테스트가 모두 통과합니다. 피드백 루프는 잘 돌고 있는 것처럼 보입니다. 하지만 실제로는 &lt;b&gt;테스트가 잡아야 할 케이스가 슬쩍 사라진 상태&lt;/b&gt;였습니다. 만약 제가 그 시점에 테스트 코드를 직접 들여다보지 않았다면, &quot;회색지대&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인터페이스와 테스트로 경계를 만든다는 발상 자체는 옳다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그러나 &quot;테스트가 경계를 지켜준다&quot;는 명제는 사람이 테스트를 짤 때의 이야기다.&lt;/b&gt; AI가 테스트까지 짤 수 있게 되는 순간, 테스트는 더 이상 절대적인 경계선이 아니다.&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;그럼 어떻게 개선할 수 있을까&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;테스트 코드 자체를 변경 보호 영역으로 둔다.&lt;/b&gt; AI에게 구현은 자유롭게 맡기되, 테스트 파일에는 AI가 손대지 못하게 하거나 변경 시 사람의 명시적 승인을 거치도록 하는 방식.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트는 사람이 먼저 쓰고, 구현만 AI에게 맡긴다.&lt;/b&gt; 강연에서도 TDD를 강조하지만, &quot;AI에게 TDD를 시키는 것&quot;과 &quot;AI에게 TDD의 통과만 시키는 것&quot;은 다릅니다. 후자가 더 안전해 보입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 변경에 대한 diff 리뷰를 의식적으로 한다.&lt;/b&gt; 구현 diff보다 테스트 diff를 더 의심하는 습관. 테스트가 &quot;느슨해지는 방향&quot;으로 바뀌었다면 일단 멈춘다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회색 지대의 크기를 도메인 위험도에 따라 다르게 가져간다.&lt;/b&gt; 결제&amp;middot;정산처럼 한 번 틀리면 회복이 어려운 도메인에서는 회색 지대를 좁게 두고, 부수적 기능은 넓게 두는 식.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 더 실험해 볼 영역입니다. 제 결제 도메인 코드에 직접 적용해 보고 다음 글에서 다시 정리해 볼 생각입니다.&lt;/p&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;레거시 코드베이스&lt;/b&gt;에서는 어디서부터 시작해야 할까 용어 자체가 일관되지 않은 환경에서는 정리 자체가 첫 번째 큰 작업이 될 것 같습니다.&lt;/li&gt;
&lt;li&gt;도메인 용어 사전이 코드 변경에 따라 어떻게 함께 진화하는지도 풀어야 할 문제입니다. 사전이 낡으면 오히려 AI를 잘못된 방향으로 끌고 갈 위험이 있습니다.&lt;/li&gt;
&lt;li&gt;위에서 말한 &lt;b&gt;테스트 우회 문제&lt;/b&gt;는 제 한 번의 경험일 뿐, 일반화하기엔 표본이 부족합니다. 모델&amp;middot;프롬프트&amp;middot;태스크 종류에 따라 빈도가 어떻게 달라지는지 더 봐야 합니다.&lt;/li&gt;
&lt;li&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에게 같은 언어로 말하지 않으면, 더 자세히 말할수록 더 어긋난다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 그동안 AI에게 설명을 했지, 언어를 공유하지 않았습니다. 다음 작업부터는 코드베이스에 작은 도메인 용어 파일부터 만들어두고 시작해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 강연이 제시한 모든 처방을 그대로 받아들이지는 않으려고 합니다. 특히 &quot;테스트로 경계를 만들고 안쪽은 신경 쓰지 않는다&quot;는 회색지대 발상은, &lt;b&gt;테스트를 짜는 주체도 AI가 될 수 있는 시대&lt;/b&gt;에는 한 번 더 의심해봐야 한다는 게 지금까지의 제 결론입니다.&lt;/p&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://youtu.be/v4F1gFy-hqg?si=d9oF7mMPeR1zYu8z&quot;&gt;Software Fundamentals in the Age of AI &amp;mdash; Matt Pocock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;John Ousterhout, A Philosophy of Software Design&lt;/li&gt;
&lt;li&gt;Andy Hunt &amp;amp; Dave Thomas, The Pragmatic Programmer&lt;/li&gt;
&lt;li&gt;Frederick P. Brooks, The Design of Design&lt;/li&gt;
&lt;li&gt;Eric Evans, Domain-Driven Design&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>회고</category>
      <category>Ai</category>
      <category>claudecode</category>
      <category>ddd</category>
      <category>MattPocock</category>
      <category>TDD</category>
      <category>개발회고</category>
      <category>유비쿼터스언어</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/26</guid>
      <comments>https://parse.tistory.com/26#entry26comment</comments>
      <pubDate>Mon, 4 May 2026 19:36:26 +0900</pubDate>
    </item>
    <item>
      <title>맥북에서 집 컴퓨터에 데이터 저장하는 법 (윈도우 PC를 NAS로)</title>
      <link>https://parse.tistory.com/25</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tistory_1image.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHIygt/dJMcacwxzVj/7gTCxKQdSBrxtvbUEkBWe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHIygt/dJMcacwxzVj/7gTCxKQdSBrxtvbUEkBWe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHIygt/dJMcacwxzVj/7gTCxKQdSBrxtvbUEkBWe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHIygt%2FdJMcacwxzVj%2F7gTCxKQdSBrxtvbUEkBWe0%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;1536&quot; height=&quot;1024&quot; data-filename=&quot;tistory_1image.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&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;AI 작업으로 24시간 켜두는 윈도우 PC + 4TB HDD 활용&lt;/li&gt;
&lt;li&gt;사진/영상 원본을 맥북에서 NAS로 분리 &amp;rarr; 맥북 용량 확보&lt;/li&gt;
&lt;li&gt;같은 와이파이뿐 아니라 외부에서도 접근 가능하게&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 최종 구조&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[맥북] ──── 와이파이 SMB ───&amp;rarr; [윈도우 PC + 4TB]
   │                              &amp;uarr;
   └──── Tailscale SMB ───────────┘
         (외부에서도 접근)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙️ 셋업 절차&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 윈도우: NAS 전용 사용자 계정 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;설정 &amp;rarr; 계정 &amp;rarr; 다른 사용자 &amp;rarr; 계정 추가&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Microsoft 계정 없이 로컬 계정으로 생성&lt;/li&gt;
&lt;li&gt;사용자명: &lt;code&gt;nas&lt;/code&gt; (예시)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 윈도우: 공유 폴더 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폴더 우클릭 &amp;rarr; 속성 &amp;rarr; &lt;b&gt;공유 탭 &amp;rarr; 고급 공유&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;이 폴더 공유&lt;/code&gt; 체크&lt;/li&gt;
&lt;li&gt;공유 이름 지정 (예: &lt;code&gt;NAS_Share&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;권한 &amp;rarr; &lt;code&gt;Everyone&lt;/code&gt; 제거 &amp;rarr; &lt;code&gt;nas&lt;/code&gt; 계정 추가 &amp;rarr; 모든 권한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보안 탭&lt;/b&gt; (NTFS 권한, 둘 다 통과해야 접근됨)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;편집 &amp;rarr; &lt;code&gt;nas&lt;/code&gt; 계정 추가 &amp;rarr; &lt;code&gt;수정&lt;/code&gt; 권한&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 윈도우: 네트워크 검색 활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;설정 &amp;rarr; 네트워크 &amp;rarr; 고급 공유 설정&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개인 네트워크: 네트워크 검색 ✅, 파일/프린터 공유 ✅&lt;/li&gt;
&lt;li&gt;모든 네트워크: 암호 보호 공유 ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 맥: SMB 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Finder &amp;rarr; &lt;code&gt;Cmd + K&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;smb://[윈도우 IP]/NAS_Share&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키체인 저장 체크해두면 다음부터 자동 인증&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 외부 접속용: Tailscale 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;윈도우, 맥 양쪽에 Tailscale 설치 (같은 계정)&lt;/li&gt;
&lt;li&gt;윈도우 Tailscale IP 확인 (트레이 아이콘 &amp;rarr; Copy address)&lt;/li&gt;
&lt;li&gt;맥에서 &lt;code&gt;smb://[Tailscale IP]/NAS_Share&lt;/code&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;h3 data-ke-size=&quot;size23&quot;&gt;문제 1: WSL에서 &lt;code&gt;ipconfig&lt;/code&gt; 안 됨&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: WSL은 리눅스 환경이라 윈도우 명령어 직접 인식 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;ipconfig.exe   # WSL에서 윈도우 실행파일 호출&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;또는 그냥 PowerShell/cmd에서 &lt;code&gt;ipconfig&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WSL2의 &lt;code&gt;hostname -I&lt;/code&gt;로 나오는 IP는 가상 네트워크 IP라 NAS 접속용으론 ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 2: SMB 접속 시도가 SSH 에러로 떨어짐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kex_exchange_identification: read: Connection reset by peer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: &lt;code&gt;ssh&lt;/code&gt; 명령어로 시도함. SMB는 ssh가 아니라 별도 프로토콜&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSH: 포트 22 (원격 셸)&lt;/li&gt;
&lt;li&gt;SMB: 포트 445 (파일 공유)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결&lt;/b&gt;: Finder Cmd+K로 &lt;code&gt;smb://...&lt;/code&gt; 형태로 접속&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 3 ⭐: 로컬 SMB는 되는데 Tailscale SMB만 안 됨&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;: 같은 와이파이 IP(192.168.x.x)로는 접속 OK, Tailscale IP(100.x.x.x)로는 실패&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;와이파이 인터페이스 &amp;rarr; 개인 네트워크 &amp;rarr; SMB 허용&lt;/li&gt;
&lt;li&gt;Tailscale 인터페이스 &amp;rarr; 공용 네트워크 &amp;rarr; SMB 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결&lt;/b&gt;: PowerShell &lt;b&gt;관리자 권한&lt;/b&gt;에서&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;# 현재 상태 확인
Get-NetConnectionProfile

# Tailscale을 Private으로 변경
Set-NetConnectionProfile -InterfaceAlias &quot;Tailscale&quot; -NetworkCategory Private&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InterfaceAlias 이름은 환경마다 다름 (&lt;code&gt;Tailscale&lt;/code&gt;, &lt;code&gt;tailscale0&lt;/code&gt; 등)&lt;br /&gt;Get-NetConnectionProfile 결과에 나온 그대로 입력&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;진단용 명령어 (맥에서)&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 연결 자체 확인
ping [윈도우 IP]

# SMB 포트(445) 열렸는지 확인
nc -zv [윈도우 IP] 445&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;succeeded&lt;/code&gt; &amp;rarr; 포트 OK, 인증 문제일 가능성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection refused&lt;/code&gt;/타임아웃 &amp;rarr; 방화벽 또는 SMB 서비스 문제&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;h3 data-ke-size=&quot;size23&quot;&gt;맥에서 SMB 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GUI&lt;/b&gt;: Finder &amp;rarr; &lt;code&gt;Cmd + K&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;터미널 마운트&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;mkdir -p ~/nas
mount_smbfs //nas@[윈도우 IP]/NAS_Share ~/nas

# 언마운트
umount ~/nas&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자주 쓰는 IP&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;smb://192.168.x.x/NAS_Share&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tailscale(외부): &lt;code&gt;smb://100.x.x.x/NAS_Share&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;맥 메타데이터 파일 안 만들게 하기&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;defaults write com.apple.desktopservices DSDontWriteNetworkStores true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;윈도우에서 IP 확인&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# PowerShell/cmd
ipconfig

# WSL에서
ipconfig.exe

# Tailscale IP만
tailscale ip -4&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;윈도우 방화벽 / 네트워크 프로필&lt;/h3&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;# 현재 네트워크 프로필 확인
Get-NetConnectionProfile

# 특정 인터페이스를 Private으로 변경 (관리자 권한 필요)
Set-NetConnectionProfile -InterfaceAlias &quot;Tailscale&quot; -NetworkCategory Private&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;윈도우 SMB 서비스 상태 확인 (문제 생겼을 때)&lt;/h3&gt;
&lt;pre class=&quot;nsis&quot;&gt;&lt;code&gt;# SMB 서버 서비스 상태
Get-Service -Name LanmanServer

# 재시작 (관리자 권한)
Restart-Service -Name LanmanServer -Force&lt;/code&gt;&lt;/pre&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;  알아둘 것&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;로컬 SMB가 항상 더 빠름&lt;/b&gt;: 같은 와이파이에선 1Gbps 풀 활용, Tailscale은 외부 라우팅이라 느림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tailscale IP는 고정&lt;/b&gt;: 100.x.x.x 대역, 재부팅해도 안 바뀜&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 IP는 바뀔 수 있음&lt;/b&gt;: 공유기에서 고정 IP 예약 필수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3-2-1 백업 원칙&lt;/b&gt;: 사본 3개 / 매체 2종류 / 1개는 분리 보관&lt;/li&gt;
&lt;/ul&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;Tailscale 관리 콘솔: &lt;a href=&quot;https://login.tailscale.com/admin/machines&quot;&gt;https://login.tailscale.com/admin/machines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;관련 노트: [[MCP]], [[Obsidian]]&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;h3 data-ke-size=&quot;size23&quot;&gt;인증 에러: &lt;code&gt;Authentication error&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: 윈도우 로컬 계정 SMB 인증 시 사용자명만 쓰면 거부될 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결&lt;/b&gt;: &lt;code&gt;호스트명;사용자명&lt;/code&gt; 형식으로 입력 (백슬래시 대신 세미콜론)&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;mount_smbfs //'[사용자계정]'@[윈도우_IP]/storage ~/NAS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 작은따옴표로 감싸기 필수 (세미콜론이 셸에서 명령 구분자로 해석되는 것 방지)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;File exists&lt;/code&gt; 에러&lt;/h3&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;&lt;b&gt;해결 순서&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 1. 현재 마운트 상태 확인
mount | grep smb

# 2. 좀비 마운트 모두 해제
mount | grep smb | awk '{print $3}' | xargs -I {} sudo umount &quot;{}&quot;

# 3. /Volumes 좀비 폴더 정리 (storage-1, storage-2 등 숫자 붙은 것)
ls /Volumes/
sudo rm -rf /Volumes/storage*

# 4. 마운트 포인트 재생성
rmdir ~/NAS
mkdir -p ~/NAS

# 5. 다시 마운트
mount_smbfs //'[사용자계정]'@[윈도우_IP]/storage ~/NAS&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사이드바에 깨진 항목이 안 사라짐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;증상&lt;/b&gt;: 이젝트했는데도 사이드바에 끈적하게 남거나, 클릭하면 &quot;원본 항목을 찾을 수 없습니다&quot; 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인&lt;/b&gt;: Finder 사이드바 캐시 (plist)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 순서&lt;/b&gt; (위에서부터 시도):&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;# 1. Finder 재시작 (대부분 이걸로 해결)
killall Finder

# 2. 그래도 남으면 &amp;mdash; 서버 즐겨찾기 캐시 직접 삭제
rm ~/Library/Application\ Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.FavoriteServers.sfl*
rm ~/Library/Application\ Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.RecentServers.sfl*
killall Finder&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GUI로도 정리: Finder &amp;rarr; &lt;code&gt;Cmd + K&lt;/code&gt; &amp;rarr; 즐겨찾는 서버 목록에서 깨진 항목 선택 &amp;rarr; &lt;code&gt;-&lt;/code&gt; 버튼&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마운트 폴더명 = Finder 사이드바 표시명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;mount_smbfs //... ~/storage&lt;/code&gt; &amp;rarr; 사이드바에 &lt;code&gt;storage&lt;/code&gt;로 표시됨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mount_smbfs //... ~/NAS&lt;/code&gt; &amp;rarr; 사이드바에 &lt;code&gt;NAS&lt;/code&gt;로 표시됨&lt;/li&gt;
&lt;li&gt;윈도우 공유 이름은 그대로 &lt;code&gt;storage&lt;/code&gt;여도 OK, &lt;b&gt;맥에서 폴더명만 바꾸면 표시명이 바뀜&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/mount-nas.sh&lt;/code&gt; 작성:&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
MOUNT_POINT=&quot;$HOME/NAS&quot;
WIN_IP=&quot;[윈도우_IP]&quot;      # 실제 IP로 변경
SHARE_NAME=&quot;storage&quot;
USERNAME=&quot;[사용자계정]&quot;

# 이미 마운트되어 있으면 종료
if mount | grep -q &quot;$MOUNT_POINT&quot;; then
    echo &quot;이미 마운트되어 있음&quot;
    exit 0
fi

mkdir -p &quot;$MOUNT_POINT&quot;
mount_smbfs &quot;//${USERNAME}@${WIN_IP}/${SHARE_NAME}&quot; &quot;$MOUNT_POINT&quot;

if [ $? -eq 0 ]; then
    echo &quot;✅ NAS 마운트 성공: $MOUNT_POINT&quot;
else
    echo &quot;❌ 마운트 실패&quot;
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한:&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;chmod +x ~/mount-nas.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용:&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;~/mount-nas.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부팅 시 자동 마운트 (AppleScript 방식)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;스크립트 편집기&lt;/code&gt; 열기&lt;/li&gt;
&lt;li&gt;입력:
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;mount volume &quot;smb://[윈도우_IP]/storage&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;파일 &amp;rarr; 내보내기 &amp;rarr; 파일 형식 &lt;b&gt;응용 프로그램&lt;/b&gt;으로 저장 (예: &lt;code&gt;MountNAS.app&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;시스템 설정 &amp;rarr; 일반 &amp;rarr; 로그인 항목&lt;/code&gt; &amp;rarr; &lt;code&gt;+&lt;/code&gt; &amp;rarr; &lt;code&gt;MountNAS.app&lt;/code&gt; 추가&lt;/li&gt;
&lt;/ol&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;  보안 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tailscale IP가 사이드바에 노출되는 게 신경쓰일 때&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;code&gt;/Volumes/&lt;/code&gt;로 마운트하지 말고 &lt;code&gt;~/NAS&lt;/code&gt;로 마운트&lt;/b&gt; &amp;rarr; 사이드바에 IP 안 뜸&lt;/li&gt;
&lt;li&gt;깔끔한 마운트 후 Finder에서 &lt;code&gt;~/NAS&lt;/code&gt; 폴더를 사이드바로 드래그하면 즐겨찾기에 &lt;code&gt;NAS&lt;/code&gt;로 등록됨&lt;/li&gt;
&lt;/ul&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;Tailscale IP (&lt;code&gt;100.x.x.x&lt;/code&gt;): 본인 Tailnet 안에서만 의미 있음. 외부 노출돼도 직접 접근 불가&lt;/li&gt;
&lt;li&gt;로컬 IP (&lt;code&gt;192.168.x.x&lt;/code&gt;): 사설 IP, 인터넷에서 접근 자체 불가&lt;/li&gt;
&lt;li&gt;진짜 위험한 건 공인 IP나 포트포워딩 &amp;mdash; 안 했으니 OK&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>개발 지식/네트워크</category>
      <category>MAC</category>
      <category>Nas</category>
      <category>SMB</category>
      <category>tailscale</category>
      <category>Windows</category>
      <category>맥북</category>
      <category>외장하드</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/25</guid>
      <comments>https://parse.tistory.com/25#entry25comment</comments>
      <pubDate>Mon, 4 May 2026 07:00:03 +0900</pubDate>
    </item>
    <item>
      <title>개인정보처리방침</title>
      <link>https://parse.tistory.com/pages/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4-%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</link>
      <description>&lt;h1&gt;개인정보 처리방침&lt;/h1&gt;
&lt;p&gt;본 블로그(&lt;code&gt;return new Story();&lt;/code&gt;, 이하 &amp;quot;사이트&amp;quot;)는 다음과 같이 개인정보를 처리합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 광고 및 쿠키&lt;/h2&gt;
&lt;p&gt;본 사이트는 Google AdSense 를 통해 광고를 노출합니다. Google 및 협력 업체는 쿠키(cookies)를 사용하여 이용자의 방문 정보를 기반으로 맞춤형 광고를 제공할 수 있습니다.&lt;/p&gt;
&lt;p&gt;방문자는 &lt;a href=&quot;https://www.google.com/settings/ads&quot;&gt;Google 광고 설정&lt;/a&gt;에서 맞춤형 광고를 끄거나 변경할 수 있으며, &lt;a href=&quot;https://www.aboutads.info&quot;&gt;aboutads.info&lt;/a&gt;에서 일부 협력업체의 쿠키 사용을 옵트아웃할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;2. 댓글 작성 시 수집 정보&lt;/h2&gt;
&lt;p&gt;비회원이 댓글을 작성하는 경우 닉네임 / 비밀번호 / 이메일(선택) 이 수집될 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 정보는 Tistory(주식회사 카카오)가 처리하며, 자세한 내용은 &lt;a href=&quot;https://www.tistory.com/privacy&quot;&gt;Tistory 개인정보 처리방침&lt;/a&gt;을 따릅니다.&lt;/p&gt;
&lt;h2&gt;3. 문의&lt;/h2&gt;
&lt;p&gt;개인정보 관련 문의는 본 블로그 방명록 또는 댓글로 남겨주시기 바랍니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;본 처리방침은 &lt;strong&gt;2026-05-04&lt;/strong&gt; 부터 적용됩니다.&lt;/p&gt;</description>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/pages/%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4-%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</guid>
      <pubDate>Mon, 4 May 2026 00:18:32 +0900</pubDate>
    </item>
    <item>
      <title>맥북 아이폰 복사 붙여넣기 안 될 때 해결방법</title>
      <link>https://parse.tistory.com/23</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/06GXO/dJMcagZfMJC/mVK2zvsTdCklT2y4g787kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/06GXO/dJMcagZfMJC/mVK2zvsTdCklT2y4g787kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/06GXO/dJMcagZfMJC/mVK2zvsTdCklT2y4g787kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F06GXO%2FdJMcagZfMJC%2FmVK2zvsTdCklT2y4g787kk%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;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;맥&amp;harr;아이폰 복사 붙여넣기 안 될 때 터미널 명령어 한 줄로 해결 (Universal Clipboard)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Handoff 켜져 있고, Wi-Fi&amp;middot;Bluetooth 정상인데도 맥-아이폰 간 복붙이 안 된다면 이 글이 답입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맥에서 복사한 텍스트가 아이폰에서 붙여넣기 안 됨&lt;/li&gt;
&lt;li&gt;아이폰에서 복사한 내용이 맥에서 안 나옴&lt;/li&gt;
&lt;li&gt;같은 Apple ID, 같은 Wi-Fi, Bluetooth ON, Handoff ON &amp;mdash; 전부 정상인데 안 됨&lt;/li&gt;
&lt;li&gt;&quot;유니버설 클립보드&quot; 자체가 죽어 있는 느낌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링하면 다 똑같은 해결법만 나옵니다. 저도 전부 따라해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 AI한테 물었는데도 같은 답변만 반복합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구글링해서 나오는 방법, 다 해봤습니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Handoff 껐다 켜기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥: 시스템 설정 &amp;rarr; 일반 &amp;rarr; AirDrop 및 Handoff &amp;rarr; Handoff 끄고 &amp;rarr; 10초 기다렸다가 &amp;rarr; 다시 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이폰: 설정 &amp;rarr; 일반 &amp;rarr; AirPlay 및 Handoff &amp;rarr; 똑같이 껐다 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Bluetooth 껐다 켜기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽 기기 Bluetooth 끄고 &amp;rarr; 30초 기다렸다가 &amp;rarr; 다시 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Wi-Fi 껐다 켜기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 Wi-Fi 문제인가 싶어서 양쪽 다 껐다 켜봄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. iCloud 로그아웃 후 재로그인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥에서 Apple ID 로그아웃하고 다시 로그인. 아이폰도 마찬가지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 시간만 오래 걸리고 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 양쪽 기기 재부팅&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥 재시작, 아이폰 전원 껐다 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하면 보통 &quot;아 그냥 에어드롭 쓰자...&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;터미널 명령어 2줄&lt;/b&gt;로 해결됐습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 해결법: 터미널 명령어 2줄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥에서 &lt;b&gt;터미널&lt;/b&gt;(Terminal.app)을 열고 아래 두 줄을 순서대로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;defaults delete ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;defaults write ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled 1
&lt;/code&gt;&lt;/pre&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;실행 후 맥에서 아무 텍스트나 복사(⌘+C)하고 아이폰에서 붙여넣기 해보세요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이게 왜 되는 건가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useractivityd는 macOS에서 &lt;b&gt;Handoff, Universal Clipboard&lt;/b&gt; 등을 담당하는 백그라운드 데몬(daemon)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데몬이 참조하는 plist(설정 파일)에 ClipboardSharingEnabled 값이 있는데, macOS 업데이트나 설정 충돌 등으로 이 값이 &lt;b&gt;꼬이는&lt;/b&gt; 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;명령어 하는 일&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;defaults delete ... ClipboardSharingEnabled&lt;/td&gt;
&lt;td&gt;기존의 꼬인 설정값을 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaults write ... ClipboardSharingEnabled 1&lt;/td&gt;
&lt;td&gt;클립보드 공유를 활성화(1)로 새로 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 설정 UI에서는 Handoff가 &quot;켜짐&quot;으로 보여도, 이 plist 값은 별도로 관리되기 때문에 UI상으로는 확인이 안 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래도 안 된다면 &amp;mdash; 기본 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 해결 안 되는 경우, 기본 조건이 안 맞는 건 아닌지 확인해보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 같은 Apple ID인지 확인&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;rarr; Apple ID&lt;/li&gt;
&lt;li&gt;아이폰: 설정 &amp;rarr; 맨 위 프로필&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Handoff 켜져 있는지 확인&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;rarr; 일반 &amp;rarr; AirDrop 및 Handoff&lt;/li&gt;
&lt;li&gt;아이폰: 설정 &amp;rarr; 일반 &amp;rarr; AirPlay 및 Handoff&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Wi-Fi &amp;amp; Bluetooth 둘 다 ON&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;유니버설 클립보드는 Bluetooth로 기기를 인식하고, Wi-Fi로 데이터를 전송합니다&lt;/li&gt;
&lt;li&gt;두 기기가 물리적으로 가까이 있어야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 양쪽 재부팅&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;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&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;macOS 26.3.1&lt;/li&gt;
&lt;li&gt;iPhone (최신 iOS)&lt;/li&gt;
&lt;li&gt;동일 iCloud 계정, 동일 Wi-Fi&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;분명히 다 켜져 있는데 왜 안 되지?&quot;라는 상황이라면, 십중팔구 useractivityd의 plist 설정이 꼬인 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 명령어 2줄이면 해결되니 한번 시도해보세요.&lt;/p&gt;</description>
      <category>생산성</category>
      <category>맥북</category>
      <category>문제</category>
      <category>복붙</category>
      <category>아이폰</category>
      <category>안됨</category>
      <category>에러</category>
      <category>클립보드</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/23</guid>
      <comments>https://parse.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 30 Mar 2026 20:08:32 +0900</pubDate>
    </item>
    <item>
      <title>SSE | 실시간 결제 알림 시스템 개발기 (3) - SSE와 LoggingFilter 충돌 해결기</title>
      <link>https://parse.tistory.com/22</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXW6yy/dJMcacPoDCs/PiizNYaHiWO5HNT6qWrHmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXW6yy/dJMcacPoDCs/PiizNYaHiWO5HNT6qWrHmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXW6yy/dJMcacPoDCs/PiizNYaHiWO5HNT6qWrHmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXW6yy%2FdJMcacPoDCs%2FPiizNYaHiWO5HNT6qWrHmk%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;1021&quot; height=&quot;872&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이전 글에서는 로드밸런싱 환경에서의 문제를 Redis Pub/Sub으로 해결한 과정을 다뤘습니다. 기술적으로는 완성된 것처럼 보였지만, 실제 배포 전 테스트 환경에서 예상치 못한 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE 연결은 성공하는데, 거래 알림이 제대로 전달되지 않거나 연결이 비정상적으로 종료되는 현상이었습니다. 로그를 확인해보니 사내에서 공통으로 사용하는 LoggingFilter가 원인이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 SSE와 LoggingFilter의 충돌 원인, 해결 과정, 그리고 그 과정에서의 기술적 고민을 공유합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 발견&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경에 배포 후 다음과 같은 문제들이 발생했습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;1. SSE 연결은 성공하지만 메시지가 전달되지 않음
2. 연결이 예상보다 빨리 종료됨
3. 간헐적으로 500 에러 발생&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 원인은 &lt;b&gt;사내 공통 라이브러리의 LoggingFilter&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 HTTP 요청-응답 사이클은 이렇습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;Request &amp;rarr; Filter &amp;rarr; Controller &amp;rarr; Response &amp;rarr; Filter &amp;rarr; Client
           &amp;uarr;_____ 여기서 로깅 _____&amp;uarr;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggingFilter는 요청과 응답을 가로채서 로그를 남기는데, 이 과정에서:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Request body를 읽어서 로깅&lt;/li&gt;
&lt;li&gt;Response body를 읽어서 로깅&lt;/li&gt;
&lt;li&gt;연결 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그런데 SSE는 다릅니다:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;Request &amp;rarr; Filter &amp;rarr; Controller &amp;rarr; [Connection Held Open]
                                        &amp;darr;
                                   [Streaming...]
                                        &amp;darr;
                                   [Event 1, 2, 3...]
                                        &amp;darr;
                                   [Still Open...]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 연결을 계속 유지하면서 스트리밍 방식으로 데이터를 전송합니다. 하지만 LoggingFilter는:&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;Response를 즉시 읽으려고 시도&lt;/b&gt; &amp;rarr; SSE는 아직 데이터를 보내지 않았음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection을 빨리 종료&lt;/b&gt; &amp;rarr; SSE 스트림이 끊김&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Buffering 시도&lt;/b&gt; &amp;rarr; 메모리 문제 발생 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방안 검토&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 공통 라이브러리는 여러 프로젝트에서 사용 중이므로 직접 수정할 수 없었습니다. 그래서 우회 방법을 찾아야 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시도 1: URL 패턴 제외 (실패)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Filter 등록 시 URL 패턴으로 제외하려고 했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Bean
public FilterRegistrationBean&amp;lt;LoggingFilter&amp;gt; loggingFilter() {
    FilterRegistrationBean&amp;lt;LoggingFilter&amp;gt; registration = 
        new FilterRegistrationBean&amp;lt;&amp;gt;();
    
    registration.setFilter(loggingFilter);
    registration.addUrlPatterns(&quot;/*&quot;);
    // SSE 경로 제외 시도
    registration.addInitParameter(&quot;excludePattern&quot;, &quot;/api/notifications/subscribe/*&quot;);
    
    return registration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt; 사내 라이브러리의 LoggingFilter가 이미 @Component로 자동 등록되어 있었고, 제외 패턴 기능을 지원하지 않았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시도 2: Filter Order 조정 (부분적 성공)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE 요청만 먼저 처리하는 Filter를 추가했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SseBypassFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) {
        String uri = request.getRequestURI();
        
        if (uri.contains(&quot;/subscribe&quot;)) {
            request.setAttribute(&quot;SKIP_LOGGING&quot;, true);
        }
        
        filterChain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt; LoggingFilter가 SKIP_LOGGING 플래그를 체크하지 않았습니다. 사내 라이브러리를 수정할 수 없으므로 이 방법도 실패했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 해결: @Primary로 Bean 재정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 @Primary 어노테이션을 사용하여 LoggingFilter를 재정의하는 방식으로 해결했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoggingFilter 래핑&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class FilterConfig {
    
    @Primary
    @Bean
    public OncePerRequestFilter customLoggingFilter(
            @Qualifier(&quot;loggingFilter&quot;) OncePerRequestFilter originalFilter) {
        
        return new OncePerRequestFilter() {
            
            @Override
            protected void doFilterInternal(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    FilterChain filterChain) throws ServletException, IOException {
                
                // SSE 요청인지 확인
                if (isSseRequest(request)) {
                    // SSE는 원본 필터를 거치지 않고 바로 통과
                    filterChain.doFilter(request, response);
                    return;
                }
                
                // 일반 요청은 원본 로깅 필터 실행
                originalFilter.doFilter(request, response, filterChain);
            }
            
            private boolean isSseRequest(HttpServletRequest request) {
                String uri = request.getRequestURI();
                String accept = request.getHeader(&quot;Accept&quot;);
                
                // URL 패턴으로 확인
                if (uri.contains(&quot;/api/notifications/subscribe&quot;)) {
                    return true;
                }
                
                // Accept 헤더로 확인
                if (accept != null &amp;amp;&amp;amp; accept.contains(&quot;text/event-stream&quot;)) {
                    return true;
                }
                
                return false;
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작&amp;nbsp;원리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Primary 어노테이션은 같은 타입의 Bean이 여러 개 있을 때 &lt;b&gt;우선순위를 지정&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768555975518&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Before]
Application &amp;rarr; LoggingFilter (사내 라이브러리) &amp;rarr; Controller

[After]  
Application &amp;rarr; CustomLoggingFilter (@Primary) &amp;rarr; Controller
                      &amp;darr;
              SSE 요청? 
                ↙        ↘
             Yes          No
              &amp;darr;            &amp;darr;
          바로 통과    원본 Filter 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&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;SSE만 선택적으로 우회&lt;/b&gt; 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 나은 대안은 없었을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;가장 근본적인 해결책&lt;/b&gt;입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 사내 라이브러리에 추가 요청할 기능
@Component
public class LoggingFilter extends OncePerRequestFilter {
    
    @Value(&quot;${logging.filter.exclude-patterns:}&quot;)
    private String[] excludePatterns;
    
    @Override
    protected void doFilterInternal(...) {
        if (shouldSkip(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        // 로깅 로직...
    }
    
    private boolean shouldSkip(HttpServletRequest request) {
        String uri = request.getRequestURI();
        
        for (String pattern : excludePatterns) {
            if (uri.matches(pattern)) {
                return true;
            }
        }
        
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 설정 파일에서 간단하게 제외할 수 있습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;logging:
  filter:
    exclude-patterns:
      - &quot;/api/notifications/subscribe/.*&quot;
      - &quot;/api/streaming/.*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;/li&gt;
&lt;li&gt;깔끔한 설정 기반 제어&lt;/li&gt;
&lt;li&gt;유지보수성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라이브러리 팀의 승인 및 배포 시간 필요&lt;/li&gt;
&lt;li&gt;다른 팀의 일정에 의존&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서의 트레이드오프&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우리가 선택한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 @Primary 방식을 선택한 이유는:&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;시간 제약&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;: 완벽하지 않지만 요구사항은 충족&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 부채 관리&lt;/h3&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;&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;/li&gt;
&lt;li&gt;다른 개발자가 원본 Filter가 동작한다고 착각 가능&lt;/li&gt;
&lt;li&gt;암묵적인 오버라이드로 디버깅 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대응 방안:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 리뷰와 문서에 명확히 기록하고, 백로그에 개선 작업을 등록했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 레거시 시스템과의 통합&lt;/h3&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;완벽한 해결책을 고집하기보다, 현실적인 제약 속에서 &lt;b&gt;최선의 선택&lt;/b&gt;을 하는 것이 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문서화의 중요성&lt;/h3&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;/li&gt;
&lt;li&gt;어떤 제약사항이 있었는지&lt;/li&gt;
&lt;li&gt;향후 개선 방향은 무엇인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월 후 다른 개발자(혹은 미래의 나)가 코드를 볼 때를 대비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 기술 부채 관리&lt;/h3&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;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;상환&lt;/b&gt; 계획이 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 백로그에 개선 작업을 등록하고, 사내 라이브러리 팀에 feature request를 제출했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/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;&lt;b&gt;SSE vs WebSocket&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;: 현실적 제약 속에서의 의사결정&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;b&gt;비즈니스 요구사항을 만족&lt;/b&gt;시키면서, &lt;b&gt;기술적 품질과 현실적 제약 사이의 균형&lt;/b&gt;을 찾는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/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;SSE 재연결 처리&lt;/b&gt;: Last-Event-ID를 활용한 메시지 재전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Circuit Breaker 패턴&lt;/b&gt;: Redis 장애 시 자동 복구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사내 라이브러리 개선&lt;/b&gt;: 공통 LoggingFilter에 exclude 기능 추가&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <category>LoggingFilter</category>
      <category>ServerSentEvents</category>
      <category>Spring</category>
      <category>SSE</category>
      <category>기술부채</category>
      <category>레기서시스템</category>
      <category>실시간통신</category>
      <category>트러블슈팅</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/22</guid>
      <comments>https://parse.tistory.com/22#entry22comment</comments>
      <pubDate>Sun, 18 Jan 2026 10:30:51 +0900</pubDate>
    </item>
    <item>
      <title>SSE | 실시간 결제 알림 시스템 개발기 (2) - 로드밸런싱 환경에서의 문제와 Redis Pub/Sub 해결</title>
      <link>https://parse.tistory.com/21</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by5Jrc/dJMb99ZrgMa/Ciegdwf1suuQpOgHgutP6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by5Jrc/dJMb99ZrgMa/Ciegdwf1suuQpOgHgutP6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by5Jrc/dJMb99ZrgMa/Ciegdwf1suuQpOgHgutP6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby5Jrc%2FdJMb99ZrgMa%2FCiegdwf1suuQpOgHgutP6k%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;824&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;824&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;h3 data-ke-size=&quot;size23&quot;&gt;시작하며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;%EB%A7%81%ED%81%AC&quot;&gt;이전 글&lt;/a&gt;에서는 SSE를 선택한 이유와 기본 구현 방법을 다뤘습니다. 단일 서버 환경에서는 모든 것이 완벽하게 동작했지만, 실제 운영 환경은 달랐습니다. 로드밸런서 뒤에 여러 서버 인스턴스가 배포된 환경에서 예상치 못한 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 분산 환경에서 마주한 문제와 Redis Pub/Sub을 활용한 해결 과정을 공유합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로드밸런싱(다중서버) 환경에서 발견한 문제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경은 다음과 같은 구조였습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gherkin&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;              [Load Balancer]
                    |
        +-----------+-----------+
        |                       |
   [Server A]            [Server B]
        |                       |
        +----------+------------+
                   |
            [RabbitMQ]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 대의 서버가 로드밸런서 뒤에서 동작하고, 하나의 RabbitMQ 인스턴스를 공유하는 구조였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제는 이렇게 발생했습니다:&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;가맹점주가 Server A에 SSE로 연결됨&lt;/li&gt;
&lt;li&gt;고객이 결제를 완료&lt;/li&gt;
&lt;li&gt;RabbitMQ에 거래 메시지가 발행됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Server B의 리스너가 메시지를 소비&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Server B는 자신에게 연결된 클라이언트를 찾지만, 가맹점주는 Server A에 연결되어 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가맹점주는 거래 알림을 받지 못함&lt;/b&gt;  &lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이런 일이?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ의 기본 동작 방식 때문입니다. 여러 컨슈머가 같은 큐를 구독하면 &lt;b&gt;라운드로빈 방식으로 메시지를 분배&lt;/b&gt;합니다. 즉, 메시지가 Server A와 Server B에 번갈아가며 전달되는데, 클라이언트가 어느 서버에 연결되어 있는지는 고려하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;거래 1: RabbitMQ &amp;rarr; Server A (가맹점주는 Server B 연결) ❌
거래 2: RabbitMQ &amp;rarr; Server B (가맹점주는 Server B 연결) ✅
거래 3: RabbitMQ &amp;rarr; Server A (가맹점주는 Server B 연결) ❌&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 거래의 절반 정도만 가맹점주에게 전달되는 심각한 문제였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방안 검토&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 여러 방안을 고민했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Sticky Session (검토 후 제외)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서에서 특정 클라이언트를 항상 같은 서버로 라우팅하는 방식입니다.&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;해당 서버가 재시작되면 모든 연결이 끊김&lt;/li&gt;
&lt;li&gt;서버 간 부하 분산이 불균형해질 수 있음&lt;/li&gt;
&lt;li&gt;RabbitMQ 메시지가 여전히 라운드로빈으로 분배됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RabbitMQ Fanout Exchange (검토 후 제외)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서버에 메시지를 브로드캐스트하는 방식입니다.&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;각 서버가 별도의 큐를 가져야 함&lt;/li&gt;
&lt;li&gt;기존에 있던 Exchange를 활용하는 것을 목표로 함&lt;/li&gt;
&lt;li&gt;메시지가 중복 처리될 수 있음&lt;/li&gt;
&lt;li&gt;RabbitMQ 구조를 변경해야 함 (기존 시스템과의 호환성)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Redis Pub/Sub (채택!)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ에서 받은 메시지를 Redis Pub/Sub으로 재전송&lt;/b&gt;하여 모든 서버에 브로드캐스트하는 방식입니다.&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;기존 RabbitMQ 구조 변경 불필요&lt;/li&gt;
&lt;li&gt;모든 서버가 메시지를 받을 수 있음&lt;/li&gt;
&lt;li&gt;Redis는 이미 캐시용으로 사용 중이었음&lt;/li&gt;
&lt;li&gt;구현이 비교적 간단함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Pub/Sub 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처 변경&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;[고객 결제]
     &amp;darr;
[결제 시스템]
     &amp;darr;
[RabbitMQ Queue]
     &amp;darr;
[Server A or B가 메시지 수신]
     &amp;darr;
[Redis Pub/Sub으로 재전송] &amp;larr; 핵심 변경점
     &amp;darr;
[모든 서버가 Subscribe하여 수신]
     &amp;darr;
[각 서버가 자신의 SSE 클라이언트에게 전달]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 RabbitMQ에서 어느 서버가 메시지를 받든, Redis Pub/Sub을 통해 모든 서버에 전달됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 설정&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {
    
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            MessageListenerAdapter listenerAdapter) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, 
            new PatternTopic(&quot;transaction.*&quot;));
        
        return container;
    }
    
    @Bean
    public MessageListenerAdapter listenerAdapter(
            RedisSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, &quot;onMessage&quot;);
    }
    
    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(
            RedisConnectionFactory connectionFactory) {
        
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer&amp;lt;&amp;gt;(Object.class));
        
        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ 리스너 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에서 메시지를 받으면 Redis로 재전송하도록 수정했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
public class TransactionQueueListener {
    
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    
    @RabbitListener(queues = &quot;transaction.queue&quot;)
    public void handleTransaction(TransactionMessage message) {
        // Redis Pub/Sub으로 브로드캐스트
        String channel = &quot;transaction.&quot; + message.getMerchantId();
        redisTemplate.convertAndSend(channel, message);
        
        log.info(&quot;Published transaction to Redis: merchantId={}, txId={}&quot;, 
            message.getMerchantId(), message.getTransactionId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Subscriber 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서버는 Redis 채널을 구독하여 메시지를 수신합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class RedisSubscriber {
    
    private final ObjectMapper objectMapper;
    private final TransactionNotificationService notificationService;
    
    public void onMessage(String message, String pattern) {
        try {
            TransactionMessage transaction = 
                objectMapper.readValue(message, TransactionMessage.class);
            
            String merchantId = transaction.getMerchantId();
            
            log.info(&quot;Received transaction from Redis: merchantId={}, txId={}&quot;, 
                merchantId, transaction.getTransactionId());
            
            // 해당 가맹점에 연결된 SSE 클라이언트에게 전송
            notificationService.sendToMerchant(merchantId, transaction);
            
        } catch (JsonProcessingException e) {
            log.error(&quot;Failed to parse transaction message&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConcurrentHashMap으로 가맹점별 필터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버는 자신에게 연결된 클라이언트 정보만 ConcurrentHashMap에 저장하고 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@Slf4j
public class TransactionNotificationService {
    
    // 가맹점 ID &amp;rarr; SSE Emitter 리스트 매핑
    private final ConcurrentHashMap&amp;lt;String, List&amp;lt;SseEmitter&amp;gt;&amp;gt; emitters = 
        new ConcurrentHashMap&amp;lt;&amp;gt;();
    
    public void sendToMerchant(String merchantId, TransactionMessage message) {
        List&amp;lt;SseEmitter&amp;gt; merchantEmitters = emitters.get(merchantId);
        
        // 이 서버에 해당 가맹점이 연결되어 있지 않으면 무시
        if (merchantEmitters == null || merchantEmitters.isEmpty()) {
            log.debug(&quot;No emitters found for merchant: {}&quot;, merchantId);
            return;
        }
        
        log.info(&quot;Sending to {} emitters for merchant: {}&quot;, 
            merchantEmitters.size(), merchantId);
        
        List&amp;lt;SseEmitter&amp;gt; deadEmitters = new ArrayList&amp;lt;&amp;gt;();
        
        for (SseEmitter emitter : merchantEmitters) {
            try {
                emitter.send(SseEmitter.event()
                    .name(&quot;transaction&quot;)
                    .data(message));
                    
                log.debug(&quot;Successfully sent transaction to emitter&quot;);
                
            } catch (IOException e) {
                log.warn(&quot;Failed to send to emitter, marking as dead&quot;, e);
                deadEmitters.add(emitter);
            }
        }
        
        // 전송 실패한 emitter 제거
        deadEmitters.forEach(emitter -&amp;gt; removeEmitter(merchantId, emitter));
    }
    
    // subscribe, removeEmitter 메서드는 1편과 동일
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 및 검증&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서 없이 두 개의 서버 인스턴스를 직접 실행하여 테스트했습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# Server 1
java -jar merchantApp.jar --server.port=8081

# Server 2
java -jar merchantApp.jar --server.port=8082&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 포트로 번갈아가며 SSE를 연결한 뒤, RabbitMQ에 메시지를 발행하여 모든 경우에 정상 동작하는지 확인했습니다.&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;결과는 성공적이었습니다. Redis Pub/Sub의 성능도 충분했고, 메시지 유실 없이 안정적으로 동작했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고려사항 및 트레이드오프&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Pub/Sub의 특징&lt;/h3&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;/li&gt;
&lt;li&gt;간단한 구현&lt;/li&gt;
&lt;li&gt;수평 확장 가능 (서버를 추가해도 동일하게 동작)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/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;: Redis가 다운되면 메시지 유실 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구독자가 없으면 메시지 버려짐&lt;/b&gt;: 모든 서버가 다운된 상태에서 발행된 메시지는 사라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이 트레이드오프를 수용했나?&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&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;사내 모니터링 알림&lt;/b&gt;: Redis 장애 시 즉시 감지하고 대응 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 복잡도 vs 비즈니스 가치&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런싱 환경에서의 실시간 통신은 단일 서버와 완전히 다른 접근이 필요합니다. 이번 프로젝트를 통해 분산 시스템의 특성과 메시지 브로드캐스팅의 중요성을 경험할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub은 완벽한 해결책은 아니지만, 우리 프로젝트의 요구사항과 제약사항을 고려했을 때 &lt;b&gt;가장 실용적인 선택&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 구현 과정에서 또 다른 문제가 있었습니다. 사내에서 공통으로 사용하는 LoggingFilter가 SSE와 충돌하는 문제였죠.&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;다음 글에서는 LoggingFilter 충돌 문제와 그 해결 과정, 그리고 기술 부채에 대한 고민&lt;/b&gt;을 다루겠습니다.&lt;/p&gt;</description>
      <category>개발 지식/Spring</category>
      <category>pub/sub</category>
      <category>redis</category>
      <category>로드밸런싱</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/21</guid>
      <comments>https://parse.tistory.com/21#entry21comment</comments>
      <pubDate>Sat, 17 Jan 2026 10:00:01 +0900</pubDate>
    </item>
  </channel>
</rss>