<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>개발일지 S_Soo100</title>
        <link>https://velog.io/</link>
        <description>Ai agent 설계를 잘 하고싶은 개발자</description>
        <lastBuildDate>Thu, 30 Apr 2026 12:48:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>개발일지 S_Soo100</title>
            <url>https://velog.velcdn.com/images/s_soo100/profile/3f66f818-c944-42c2-8f30-e0584c225b37/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 개발일지 S_Soo100. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/s_soo100" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Claude Code 실전 활용 팁, 환경 엔지니어링 ]]></title>
            <link>https://velog.io/@s_soo100/Claude-Code-%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9-%ED%8C%81-%ED%99%98%EA%B2%BD-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81</link>
            <guid>https://velog.io/@s_soo100/Claude-Code-%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9-%ED%8C%81-%ED%99%98%EA%B2%BD-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81</guid>
            <pubDate>Thu, 30 Apr 2026 12:48:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1년 가까이 Claude Code로 작업하면서, 처음에는 프롬프트를 잘 쓰는 데 집중했고 그다음엔 컨텍스트를 잘 만드는 데 집중했습니다.
그런데 지금 돌아보니, <strong>에이전트가 일하는 환경을 설계하는 일</strong>이 가장 중요한 것 같습니다.
환경을 어떻게 구성하면 에이전트가 더 잘 일하는지를 4가지 장치로 정리한 노트입니다.</p>
</blockquote>
<hr>
<h2 id="개요-환경을-고치면-결과가-바뀐다">개요: 환경을 고치면 결과가 바뀐다</h2>
<p>에이전트가 같은 실수를 반복할 때, 결과물을 사람이 고치는 건 초보적인 단계이며, 지속 가능하지 않습니다. 왜냐하면 ai에이전트의 생산속도는 우리가 리뷰하고 고치는 속도를 이미 뛰어넘었기 떄문입니다.
오히려 에이전트에게 같은 실수를 다시 못 하도록 환경을 고치는 게 현재 단계에서 가장 효과적인 전략입니다. 
물론, 다음 주만 되어도 이 전략이 구식이 될 수도 있습니다(그러길 바래요!)</p>
<p>1년 정도 에이전트로 일해본 현재 단계에서 여기저기 귀동냥 하고 공부하며 어떻게 구성하면 에이전트가 더 잘 일하는지를 제 기준으로 정리한 노트입니다.
메모리 룰, Don&#39;ts 룰을 비롯해서 여러 내/외부 도구를 운영하면서 만든 팁을 4가지 카테고리로 풀어봤습니다.</p>
<blockquote>
<p>4가지 장치 분류(Commands/Rules/Skills/Hooks)는 Anthropic의 <a href="https://www.anthropic.com/engineering/harness-design-long-running-apps">Harness design for long-running apps</a> 자료와 교차 검증해 정리했습니다.</p>
</blockquote>
<p>이미 이전에 제 velog에 작성한  <a href="https://velog.io/@s_soo100/Claude-Code-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%ED%8F%AD%EC%A3%BC%ED%95%A0-%EB%95%8C-%EC%A0%9C%ED%95%9C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">에이전트 폭주 방지</a>, <a href="https://velog.io/@s_soo100/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EB%84%8C-%EC%A0%84%EB%AC%B8%EA%B0%80%EC%95%BC%EB%9D%BC%EA%B3%A0-%EB%A7%90%ED%95%98%EB%A9%B4-%EC%A7%84%EC%A7%9C-%EC%9E%98%ED%95%A0%EA%B9%8C-%EB%85%BC%EB%AC%B8%EA%B3%BC-%EC%8B%A4%EC%A0%84%EC%9D%B4-%EB%A7%90%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%A7%84%EC%8B%A4">페르소나의 한계</a>, <a href="https://velog.io/@s_soo100/Claude-Code-Skill-%EC%8B%A4%EC%A0%84-%ED%8C%81-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%8B%A4%EC%88%98%EC%9C%A8%EC%9D%B4-%EC%A4%84%EC%96%B4%EB%93%AD%EB%8B%88%EB%8B%A4">살아남은 Skill 5종</a> 등 포스팅을 통해 공유한 팁의 총합본 같은 느낌입니다. </p>
<hr>
<h2 id="1-프롬프트--컨텍스트--환경">1. 프롬프트 &lt; 컨텍스트 &lt; &quot;환경&quot;</h2>
<p>필드에 있으면서 느낀 AI 엔지니어링의 무게중심은 이렇게 옮겨온 것 같습니다.
<strong>(더 빨리 옮겨졌을 수 있음!)</strong></p>
<table>
<thead>
<tr>
<th>단계</th>
<th>시기(대략)</th>
<th>핵심 질문</th>
<th>대표 기술</th>
</tr>
</thead>
<tbody><tr>
<td>프롬프트 엔지니어링</td>
<td>2022~23</td>
<td>&quot;무엇을 물어볼까?&quot;</td>
<td>CoT, Few-shot</td>
</tr>
<tr>
<td>컨텍스트 엔지니어링</td>
<td>2024~25</td>
<td>&quot;무엇을 보여줄까?&quot;</td>
<td>RAG, MCP, long-context</td>
</tr>
<tr>
<td><strong>환경 엔지니어링</strong></td>
<td><strong>2026~</strong></td>
<td><strong>&quot;어떤 환경에서 일하게 할까?&quot;</strong></td>
<td>Rules, Hooks, Skills, Commands, Subagents</td>
</tr>
</tbody></table>
<p>프롬프트와 컨텍스트는 <strong>휘발성</strong>입니다. 세션이 끝나면 사라집니다. 반면 환경 설계는 리포지토리에 남는 <strong>자산</strong>입니다. 
이게 무게중심 이동의 핵심 분기점입니다.</p>
<p>다음 호출에서도 살아남는 것을 만들기 위해서 기술도 발전하고, 기술을 사용하는 개발자들도 같이 발전한 것 같아요.</p>
<hr>
<h2 id="2-1년-운영하면서-굳어진-결론-세-가지">2. 1년 운영하면서 굳어진 결론 세 가지</h2>
<p>Flutter APP, SAAS web, Unity 게임, React + Supabase 웹앱 등을 포함해 여러 프로젝트를 매니징 해주고, 그 외에도 내가 귀찮아 하고 어려워 하는 마케팅·기획·CS·책 노트·요리까지 묶은 허브 리포지토리(나는 ideaBank라고 부르고, 다른 사람에게는 자비스라고 소개중)를 동시에 굴리면서 1년을 보냈습니다. 
도메인이 전혀 다른데도 같은 실수가 다른 모습으로 반복되더군요 — Unity 퀘스트 가드 누락이나 React 컴포넌트 prop drilling이나, 결국은 &quot;에이전트가 같은 패턴을 다시 만들고 있는데 이걸 막을 룰이 없다&quot;는 한 가지 문제로 수렴했습니다. 
<strong>룰을 어디에 어떻게 쌓느냐</strong>에서 나온다는 게 보였습니다.</p>
<p>이 시행착오를 압축하면, 제 개발 기록을 통해서는 3가지 정도 팁을 얻을 수 있습니다.</p>
<blockquote>
<p><strong>1. 에이전트의 실수는 코드의 문제가 아니라 환경 설계의 미비다.</strong></p>
</blockquote>
<ul>
<li>에이전트가 실수했을 때 결과물을 사람이 고치는 건 하수. 에이전트가 다시는 그 실수를 못 하도록 <strong>규칙·검증·훅을 수정</strong>하는 게 고수라고 개발자 선배가 그러더라구요? </li>
<li>예를 들어 개발중인 게임에서 퀘스트 보상이 두 번 지급되는 버그를 잡았을 때, 코드만 고치고 끝낸 게 아니라 &quot;퀘스트 조건 체크 시 <code>isCompleting</code> 가드 필수&quot;를 룰로 승격시킵니다. </li>
<li>이러면 어떤 모델과 일하는 지는 상관없이, 다음 작업에서는 자동으로 막힙니다. 
이걸 <code>donts.md</code>와 <code>feedback_*.md</code> 메모리 파일에 꼼꼼히 기재하도록 규칙을 세워 꾸준히 실수 패턴을 축적해보니 확실히 개선되는게 보였습니다(같은 실수를 두 번 보면 룰로 승격시킨다는 단순한 원칙입니다)</li>
</ul>
<blockquote>
<p><strong>2. 자율성과 통제는 반비례가 아니라 정비례다.</strong></p>
</blockquote>
<ul>
<li>모델이 똑똑해질수록 runaway(통제 불능) 위험도 같이 커집니다. 
다행히 전 그런 일이 없었는데, 모델이 마음대로 마케팅 강의를 결제하거나 하는 등 폭주한 사례는 이미 유명하죠?!</li>
<li>앞서서 &quot;폭주 방지&quot;에 대한 velog글도 썼는데, 그것도 여기서 출발했습니다. 
&quot;적합한&quot; 통제 장치가 잘 세팅되면 더 큰 작업을 안전하게 맡길 수 있습니다. 하네스 엔지니어링이라고 부르죠 이젠?!</li>
</ul>
<blockquote>
<p><strong>3. 개발자는 코드를 짜는 사람이 아니라, AI가 코드를 잘 짤 수 있는 환경을 짓는 건축가다.</strong></p>
</blockquote>
<ul>
<li>손으로 친 코드 라인 수보다, 에이전트가 잘 작동하도록 만든 룰·훅·도구의 코드 라인이 이제 비교할 수 없을 만큼 깁니다. 
우리는 &quot;AI-native 개발자 / Harness Engineer&quot;가 되기 위해 노력하고, 
개발을 잘 기획하는 사람이 되기 위해 노력해야 하는 시대가 되었다고 생각합니다.</li>
</ul>
<hr>
<h2 id="3-claude-사용-환경을-구성하는-4가지-장치">3. Claude 사용 환경을 구성하는 4가지 장치</h2>
<p>환경을 구성하는 4개의 카테고리입니다. 각 장치가 어떤 역할을 하는지, 제 시스템에서 어떻게 작동하는지 매핑해봤습니다.</p>
<table>
<thead>
<tr>
<th>장치</th>
<th>정의</th>
<th>제 시스템에서</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Commands</strong></td>
<td>반복 작업의 진입점 (슬래시 커맨드)</td>
<td><code>/검수</code>, <code>/loop</code>, <code>/critic</code> 등 — <a href="https://velog.io/@s_soo100/Claude-Code-Skill-%EC%8B%A4%EC%A0%84-%ED%8C%81-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%8B%A4%EC%88%98%EC%9C%A8%EC%9D%B4-%EC%A4%84%EC%96%B4%EB%93%AD%EB%8B%88%EB%8B%A4">이 글에서 정리</a></td>
</tr>
<tr>
<td><strong>Rules</strong></td>
<td>상시 주입되는 규약</td>
<td><code>CLAUDE.md</code>, <code>.claude/rules/</code> 전체 (프로젝트별 + 카테고리별 분리)</td>
</tr>
<tr>
<td><strong>Skills</strong></td>
<td>에이전트가 호출하는 외부 능력</td>
<td><code>tools/</code> 하위 25+ 스크립트 (gemini-cli, hatchet-art, design-report…)</td>
</tr>
<tr>
<td><strong>Hooks</strong></td>
<td>이벤트 기반 강제 검증/자동화</td>
<td>Stop hook(자동 커밋), UserPromptSubmit(키워드 라우팅) 등</td>
</tr>
</tbody></table>
<p>각 장치는 다른 조건을 책임집니다. Commands는 <strong>반복 진입의 일관성</strong>, Rules는 <strong>상시 판단의 기준</strong>, Skills는 <strong>확장된 능력</strong>, Hooks는 <strong>이벤트 기반 강제력</strong>. 4개가 한 세트로 갖춰지면 에이전트가 같은 작업에서 매번 같은 품질을 내고, 같은 실수를 반복하지 않으며, 사람이 일일이 검토하지 않아도 결과물이 안정됩니다.</p>
<p>하나라도 비면 다른 쪽에 부담이 쏠립니다. 예를 들어 Hook이 약하면 Rules에 &quot;이건 하지 마세요&quot;를 10배 적어야 해서 컨텍스트 예산만 잡아먹습니다. Skills가 빈약하면 같은 작업을 매번 프롬프트로 풀어 설명하느라 토큰을 낭비합니다.</p>
<hr>
<h2 id="4-4가지-장치로-본-제-시스템의-약한-지점">4. 4가지 장치로 본 제 시스템의 약한 지점</h2>
<p>잘 갖춰진 영역은 3부작에서 이미 써봤으니, 이번엔 <strong>비어 있는 쪽</strong>에 집중해봤습니다.
<em>*Claude가 분석해주었습니다</em></p>
<table>
<thead>
<tr>
<th>약점</th>
<th>증거</th>
<th>개선 방향</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Hook 커버리지 부족</strong></td>
<td>Stop hook(자동 커밋)은 있지만, 작업 완료 전 검증을 강제하는 훅은 없음</td>
<td>PostToolUse/Stop 훅에 <code>/검수 code</code> 자동 트리거 조건 추가</td>
</tr>
<tr>
<td><strong>규칙 드리프트</strong></td>
<td><code>feedback_*.md</code>가 30개 이상으로 누적, 일부가 <code>.claude/rules/</code>로 승격 안 됨</td>
<td>분기마다 메모리 → rules 승격 감사 프로세스</td>
</tr>
<tr>
<td><strong>Handoff 규격 부재</strong></td>
<td>Designer → Implementer 전달물이 자유형 텍스트라 포맷이 들쭉날쭉</td>
<td>구현 계획서 템플릿(파일/함수 수준) + YAML frontmatter 메타</td>
</tr>
<tr>
<td><strong>Harness Audit 부재</strong></td>
<td>모델이 업그레이드됐을 때 기존 규칙이 여전히 유효한지 재검토하는 절차가 없음</td>
<td><code>audit-harness.sh</code> — 컨텍스트 사이즈, 규칙 중복, 실효성 체크</td>
</tr>
<tr>
<td><strong>실수 패턴 룰화 지연</strong></td>
<td>병렬 에이전트 경계, 퀘스트 가드 등 실전 교훈이 메모리에만 있음</td>
<td>&quot;Don&#39;ts&quot; 룰 파일로 명문화 (일부는 이미 진행 중)</td>
</tr>
</tbody></table>
<p>이 표를 만들고 보니, 제 시스템은 <strong>Commands와 Rules는 촘촘한데, Hooks와 감사 루틴이 얇다</strong>는 비대칭이 있었습니다. 다음에 손봐야 할 우선순위가 여기서 나옵니다.</p>
<hr>
<h2 id="5-약점을-어떻게-메우고-있나">5. 약점을 어떻게 메우고 있나</h2>
<h3 id="51-보안-스캔으로-hook-커버리지-보완">5.1 보안 스캔으로 Hook 커버리지 보완</h3>
<p>현재 제가 쓰는 ideaBank 프로젝트엔 9개의 커스텀 에이전트 + 다수 hook + MCP 서버 7개(Context7, PixelLab, Notion 등) + 권한관련 설정(settings.json)이 있습니다. </p>
<p>그런데 가장 문제가 <strong>이 환경 자체를 정기 검사하는 수단이 없었습니다</strong>. 
뭘 더 좋아지게 만드려면 혹은 사고치는걸 막으려면 제대로 검사를 해야하는데.. 에이전트 정의에 시크릿이 흘렀는지, hook에 명령 주입 여지가 있는지, MCP 권한이 과한지 점검할 도구 자체가 없어서 고민이 좀 있었습니다.</p>
<p><a href="https://github.com/affaan-m/everything-claude-code">Everything Claude Code</a> 레포의 <code>security-scan</code> 스킬(AgentShield, 1282 tests / 102 rules)을 한 번 돌렸습니다. 결과:</p>
<ul>
<li>위험 명령 차단을 위한 deny list 보강</li>
<li>자동 커밋 hook이 토큰을 흘릴 수 있는 경로를 잡아 시크릿 필터 + 로테이션</li>
<li>노출 가능성 있는 Vercel 토큰 회수</li>
</ul>
<p>Hook 자체가 두꺼워진 건 아니지만, 기존 Hook의 약한 지점을 데이터로 본 것만으로도 다음 보강 우선순위가 명확해졌습니다.</p>
<h3 id="52-실수-패턴-방지-제도">5.2 실수 패턴 방지 제도</h3>
<p>(4번 표에선 두 약점으로 나눴지만 결국 같은 결의 문제 — 룰을 어떻게 쌓고 어떻게 승격시키느냐. 처방을 묶어서 다룹니다)</p>
<p><code>feedback_*.md</code> 메모리가 30+개로 누적되는데 일부만 정식 룰로 승격되는 게 가장 큰 문제였습니다. 그래서 두 가지 절차를 박았습니다.</p>
<p><strong>(1) 삼진아웃 제도</strong> — 같은 패턴이 몇 번 반복되면 룰로 올리는지 명시:</p>
<table>
<thead>
<tr>
<th>발생 횟수</th>
<th>조치</th>
</tr>
</thead>
<tbody><tr>
<td>1회</td>
<td>메모리에만 저장</td>
</tr>
<tr>
<td>2회</td>
<td><code>donts-audit.md</code>에 &quot;승격 후보&quot; 플래그</td>
</tr>
<tr>
<td>3회</td>
<td><code>donts.md</code> 또는 <code>donts/{기능}.md</code>에 정식 룰 등록</td>
</tr>
</tbody></table>
<p><strong>(2( 기능별 Don&#39;ts 분리</strong>: 
실수를 반복하지 않는 체계를 구축합니다. 
그리고 파일에 다 쌓으면 컨텍스트 부담이 커지니 아래처럼 분기해두면 좋습니다. </p>
<ul>
<li><code>donts.md</code> — 전역 (모든 작업 공통)</li>
<li><code>donts/game.md</code> — 게임/Unity 개발</li>
<li><code>donts/someProject.md</code> — 특정 프로젝트 개발</li>
<li><code>donts/marketing.md</code> — 마케팅/콘텐츠 아티클 발행 및 수정</li>
<li><code>donts/images.md</code> — 이미지/에셋</li>
</ul>
<p>이젠 작업 진입 시 해당 파일만 로드하니 평소 컨텍스트가 가벼워졌습니다. 
룰화 자체를 더 자동화하는 건 ECC <code>/learn</code> 패턴 시범 검토 중.</p>
<h3 id="53-컨텍스트-예산-진단harness-audit-부재관련">5.3 컨텍스트 예산 진단(Harness Audit 부재관련)</h3>
<p><code>/context-budget</code>을 한 번 돌려서 토큰 분포를 봤더니 매 세션 강제 로드는 ~3%로 양호했습니다. 그런데 부수 발견이 더 컸어요 — <strong>마케팅 에이전트 3개(brand-manager, content-adapter, marketing-director)에 YAML frontmatter가 누락</strong>돼 있어 Claude Code가 자동 매칭으로 호출하지 못하는 상태였습니다. 진단한 같은 날 3개 모두 frontmatter 추가해 정상화.</p>
<p>진단 도구 한 번 돌리는 게 잠재 결함을 수면 위로 끌어올렸습니다. 정기 감사 루틴까진 아직이지만, 진단의 ROI는 확인됐어요.</p>
<h3 id="54-포매터-강제-handoff-규격">5.4 포매터 강제 (Handoff 규격)</h3>
<p>Designer → Implementer 전달물이 자유형 텍스트라 포맷이 들쭉날쭉한 문제는 에이전트 효율을 위해 중요한 문제라고 하더라구요.
그래서 이를 VLM 추론 결과 처리를 공부하며 3단 안전장치를 설계하고 적용하고 있습니다.
(이전 글에 작성한 내용입니다.)</p>
<p>  ① 모델에 JSON 응답 모드 강제(<code>responseMimeType: &#39;application/json&#39;</code>), 
  ② 정규식으로 첫 <code>{...}</code> 블록만 추출(prose/펜스 섞여 와도 잡힘), 
  ③ <code>JSON.parse</code> + 스키마 검증(필수 필드/타입 안 맞으면 명확한 에러로 차단). </p>
<p>  이 패턴을 공부하며 ideaBank등 기존 프로젝트들에도 적용하려고 합니다. 
  이를 통해서 Designer에이전트가 Implementer에게 핸드오프 할 떄 효율을 올려볼 계획입니다.</p>
<hr>
<h2 id="정리-환경을-개발하는-것이-자산이-되는-이유">정리: 환경을 개발하는 것이 자산이 되는 이유</h2>
<blockquote>
<p><strong>모델을 믿지 말고, 모델이 최고 성능을 낼 수밖에 없는 환경을 만들어봅시다.</strong></p>
</blockquote>
<p>모델은 비정기적으로 바뀝니다. 그래도 잘 구축한 환경(Commands, Rules, Skills, Hooks)은 다음 모델에서도 그대로 살아남을 확률이 높습니다. 새 모델이 나왔을 때 개선점만 집어넣고, 처음부터 다시 시작하지 않아도 됩니다.
안그래도 최근에 Opus의 버전업과 동시에 Claude쪽에 이슈가 있었다는 개발자님들이 의견이 많이 보였습니다. 
이러한 시행착오의 노트가 도움이 되기 바랍니다.</p>
<hr>
<h3 id="참고-자료">참고 자료</h3>
<ul>
<li><a href="https://velog.io/@s_soo100/Claude-Code-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%ED%8F%AD%EC%A3%BC%ED%95%A0-%EB%95%8C-%EC%A0%9C%ED%95%9C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">하네스 엔지니어링: 에이전트 폭주 방지 Tip</a></li>
<li><a href="https://velog.io/@s_soo100/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EB%84%8C-%EC%A0%84%EB%AC%B8%EA%B0%80%EC%95%BC%EB%9D%BC%EA%B3%A0-%EB%A7%90%ED%95%98%EB%A9%B4-%EC%A7%84%EC%A7%9C-%EC%9E%98%ED%95%A0%EA%B9%8C-%EB%85%BC%EB%AC%B8%EA%B3%BC-%EC%8B%A4%EC%A0%84%EC%9D%B4-%EB%A7%90%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%A7%84%EC%8B%A4">하네스 엔지니어링: 페르소나 설정은 정말 효율적인가?</a></li>
<li><a href="https://velog.io/@s_soo100/Claude-Code-Skill-%EC%8B%A4%EC%A0%84-%ED%8C%81-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%8B%A4%EC%88%98%EC%9C%A8%EC%9D%B4-%EC%A4%84%EC%96%B4%EB%93%AD%EB%8B%88%EB%8B%A4">하네스 엔지니어링: Claude Skill 작성 Tip</a></li>
<li><a href="https://www.anthropic.com/engineering/harness-design-long-running-apps">Anthropic - Harness design for long-running apps</a></li>
<li><a href="https://github.com/affaan-m/everything-claude-code">Everything Claude Code</a> : 앤쓰로픽 해커톤 우승작!!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[LLM 기초 스터디] 내가 보려고 쓰는 LLM 기초 메커니즘, 신뢰도와 토큰편]]></title>
            <link>https://velog.io/@s_soo100/LLM-%EA%B8%B0%EC%B4%88-%EC%8A%A4%ED%84%B0%EB%94%94LLM%EC%9D%B4-%ED%86%A0%ED%81%B0%EC%9D%84-%EA%B3%A0%EB%A5%B4%EB%8A%94-%EB%B0%A9%EB%B2%95-%EA%B8%B0%EC%B4%88-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</link>
            <guid>https://velog.io/@s_soo100/LLM-%EA%B8%B0%EC%B4%88-%EC%8A%A4%ED%84%B0%EB%94%94LLM%EC%9D%B4-%ED%86%A0%ED%81%B0%EC%9D%84-%EA%B3%A0%EB%A5%B4%EB%8A%94-%EB%B0%A9%EB%B2%95-%EA%B8%B0%EC%B4%88-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</guid>
            <pubDate>Thu, 30 Apr 2026 06:08:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>공부하면서 알게된, LLM/VLM을 실제로 쓰는 개발자가 자주 마주치는 파라미터와 구조 뒤에 어떤 메커니즘이 있는지를 정리해 봤습니다. </p>
</blockquote>
<hr>
<h2 id="개요">개요</h2>
<p> VLM(Vision-Language Model)으로 영상 행동 분류 PoC를 만들면서 모델 호출 옵션과 JSON 출력 안정화 코드를 직접 손대게 됐습니다. 
 에이전트로 컨트롤 하더라도 그 과정에서 코드에 자주 등장한 단어나, 개념들을 알아두어야 적절한 조정이 가능하기 때문에 직접 학습에 들어갔습니다.</p>
<p> 처음 보면 당황할 만한 단어들이 많으니, 기본 개념을 공부해봅시다.</p>
<ul>
<li><strong>temperature</strong> : 모델이 다음 토큰을 뽑을 때 확률 분포를 얼마나 뾰족하게 만들지 조절하는 파라미터</li>
<li><strong>topP(Top-P, Nucleus Sampling)</strong>: 후보 토큰을 누적 확률 P까지만 남기고 자르는 규칙</li>
<li><strong>JSON 출력 방어</strong>: 모델 응답을 JSON 형식으로 안정적으로 받기 위한 다층 설계</li>
</ul>
<p>이 단어들의 정확한 의미를 알고, 각 결정의 근거가 명확히 있어야 파라미터 선택이 추측이 아니게 됩니다.</p>
<hr>
<h2 id="어디에-쓰고-어디에-도움이-될까">어디에 쓰고, 어디에 도움이 될까?</h2>
<p>메커니즘을 알면 LLM 도구 결정이 추측에서 근거로 바뀝니다.
VLM으로 영상을 분류시킬 때 겪을 수 있는 다음 상황에서 의사결정의 근거가 됩니다.</p>
<ul>
<li><strong>&quot;프롬프트 이렇게 쓰면 좋다&quot; 류 팁이 어떤 건 통하고 어떤 건 안 통하는지 안 보일 때.</strong> 표층 팁 뒤의 메커니즘이 보이면 그 팁이 통하는 조건과 안 통하는 조건이 분리됩니다.</li>
<li><strong>같은 입력에 호출마다 다른 답이 나올 때.</strong> &quot;왜 결과가 흔들리지?&quot;를 &quot;다양성이 너무 높아서&quot;라는 일반론으로 넘기지 않고, 분포가 어떻게 만들어지고 어떻게 가공되는지를 알면 temperature 같은 파라미터를 손대는 근거가 생깁니다.</li>
<li><strong>파라미터 추천값을 그냥 따라 썼는데 효과를 설명하기 어려울 때.</strong> topP 0.95가 후보 토큰을 어떻게 자르는지 메커니즘을 알면 0.95냐 0.5냐 결정이 추측이 아닌 계산이 됩니다.</li>
<li><strong>JSON 출력이 가끔 깨져서 후처리가 멈출 때.</strong> 프롬프트만 바꾸다가 안 풀리는 이유, 그리고 SDK 설정·정규식 fallback이 왜 따로 필요한지가 보입니다. 3단 설계로 갈 근거가 명확해집니다.</li>
<li><strong>VLM이 confidence 값을 같이 주는데 그걸로 뭘 해야 할지 막막할 때.</strong> 모델 자체 점수와 우리가 정하는 cutoff 룰이 어떻게 다른지 구분이 되면, &quot;낮으면 재호출&quot;이 아니라 &quot;낮으면 사람 큐&quot;라는 설계 결정이 자연스럽게 따라옵니다.</li>
</ul>
<hr>
<h2 id="메커니즘-5가지">메커니즘 5가지</h2>
<h3 id="1-어휘집vocabulary과-토큰-분포">1. 어휘집(vocabulary)과 토큰 분포</h3>
<p><strong>개념:</strong> </p>
<ul>
<li>모델이 아는 모든 토큰의 사전을 어휘집(vocabulary)이라 합니다(Gemini 2.5 Flash 기준 약 25만 개)</li>
<li>한 토큰을 생성할 때마다 이 25만 개 전체에 확률 분포가 만들어집니다. 
즉, 매 스텝마다 25만 개 전체에 새 분포가 생기고 그 중 1개가 뽑힙니다.</li>
</ul>
<p><strong>비유:</strong> </p>
<ul>
<li>날씨 예보와 같습니다. 내일 날씨 예보는 &quot;비 70%, 흐림 15%, 맑음 10%, 눈 3%, 안개 2%&quot;처럼 모든 가능성에 확률을 부여합니다. 비만 후보인 게 아니라 눈에도 작은 확률이 있습니다. 토큰 분포도 마찬가지입니다.</li>
</ul>
<p><strong>실전:</strong> </p>
<ul>
<li>이 구조를 모르면 이후 공부할 temperature를 바꿔도 무슨 일이 벌어지는지 알 수 없습니다. 분포 가공 방법(temperature·topP)은 다음 항목에서 다룹니다.</li>
</ul>
<hr>
<h3 id="2-autoregressive-생성">2. Autoregressive 생성</h3>
<p><strong>개념:</strong> </p>
<ul>
<li>LLM은 토큰을 한 번에 다 만들지 않습니다. 직전까지 생성한 모든 토큰을 입력으로 받아 다음 토큰 하나를 예측하고, 그 토큰을 다시 입력에 추가해 다음을 예측하는 방식으로 순서대로 이어갑니다. 매 스텝마다 어휘집 전체 확률 분포를 새로 계산합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/7b537531-b36a-4295-b2b6-71a3e8644401/image.png" alt=""></p>
<ul>
<li>끝말잇기와 같습니다. 이전에 나온 단어를 받아 다음 단어를 고르고, 그 단어가 또 다음 사람의 입력이 됩니다. 단어를 미리 다 정해두지 않습니다.</li>
<li>차이가 있다면, 끝말잇기는 직전 단어 하나에만 의존하지만, LLM은 처음 입력부터 직전 토큰까지 전체 문맥을 참조합니다.</li>
<li>그러다보니 응답 앞부분에서 잘못된 방향이 잡히면 뒷부분도 그 방향으로 이어집니다. 출력 형식을 초반에 명확히 유도하는 것이 중요한 이유입니다.</li>
</ul>
<hr>
<h3 id="3-temperature와-topptop-p-nucleus-sampling">3. Temperature와 topP(Top-P, Nucleus Sampling)</h3>
<p><strong>개념:</strong> </p>
<ul>
<li>Temperature는 확률 분포 자체의 뾰족함을 조절하고, topP는 그 분포에서 후보 토큰 집합을 잘라냅니다. </li>
<li>즉, 같은 분포에 다른 방식으로 작용하는 두 파라미터 입니다.</li>
</ul>
<p>예시 이미지를 보며 공부해보죠
<img src="https://velog.velcdn.com/images/s_soo100/post/c15534c7-e555-40d5-8ede-808788e7456e/image.png" alt=""></p>
<p>위 이미지 상, topP 0.5에서 후보 토큰 집합이 &quot;0개&quot;가 아니라 1개인 이유는, 1등 토큰(0.70)이 cutoff(0.50)를 단독으로 이미 넘기 때문입니다.</p>
<p><strong>비유:</strong> </p>
<ul>
<li>topP는 정원이 정해진 엘리베이터입니다. 누적 확률이 P에 닿으면 그 뒤 후보는 들이지 않습니다. 1등 토큰 혼자 cutoff를 단독으로 넘으면 그 토큰만 후보로 남습니다.</li>
</ul>
<p><strong>실전:</strong> </p>
<ul>
<li>분류 task에서는 temperature 0.1 + topP 0.95 조합이 재현성과 꼬리 토큰 차단을 동시에 처리합니다. </li>
<li>temperature를 기본값(1.0)으로 두면 같은 입력에 다른 라벨이 나와 평가 자체가 noisy해집니다.</li>
</ul>
<hr>
<h3 id="4-confidence-value신뢰도와-threshold임계값">4. Confidence value(신뢰도)와 threshold(임계값)</h3>
<p><strong>개념:</strong> </p>
<ul>
<li>신뢰도(Confidence value)는 AI가 스스로 &quot;내가 고른 이 답이 정답일 거야!&quot;라고 확신하는 정도를 뜻하며, 앞서 우리가 이야기했던 &#39;이 단어가 다음에 올 확률(Probability)&#39;과 아주 밀접한 개념입니다.
VLM이 자기 답에 매기는 점수(0.0~1.0)로, 출력 JSON에 직접 포함됩니다. </li>
<li>임계값(Confidence threshold)은 어떤 변화가 일어나거나 통과시키기 위해 반드시 넘어야 하는 &#39;최소한의 기준선&#39;을 말하며, 우리가 일상에서 흔히 말하는 시험의 &#39;커트라인&#39;이나, 방에 들어가기 위해 넘어야 하는 &#39;문턱&#39;이라고 생각하면 아주 쉽습니다.
즉, &quot;value가 N 미만이면 자동 라벨로 채택하지 않는다&quot;는 판단을 하게 만들어주는 값으로 우리 코드에서 지정합니다. </li>
<li>만일 분석 결과 신뢰도가 임계값 미만일 때, 정석 처리는 재호출이 아니라 사람이 재라벨하는 큐(human re-label queue)로 보내는 것입니다. 
왜냐하면 temperature 0.1에서 같은 클립을 다시 호출해도 거의 같은 답이 나와서 사람이 직접 손을 대봐야 하는 단계기 때문입니다.</li>
<li>즉, confidence 후처리를 설계할 때 &quot;낮으면 재호출&quot; 대신 &quot;낮으면 사람 큐&quot;로 라우팅 설계를 잡아야 비용 낭비와 오답 반복을 막을 수 있습니다.</li>
</ul>
<hr>
<h3 id="5-json-출력-3단-방어">5. JSON 출력 3단 방어</h3>
<p><strong>왜 JSON 출력을 하는가요?</strong> </p>
<ul>
<li>LLM은 언어 모델이기 때문에(VLM도!) 출력 형식이 정해져있지 않습니다. 
출력 형식을 통일하면 해석하는데 비용도 줄고, 속도도 빨라져요!</li>
</ul>
<p><strong>개념</strong> 
<img src="https://velog.velcdn.com/images/s_soo100/post/9205d3cd-9335-4c69-a06f-dee4502289e5/image.png" alt=""></p>
<ul>
<li><p>LLM의 JSON 출력을 안정적으로 받으려면 단일 방어선으로는 부족합니다. 
각 층의 실패 모드가 다르기 때문입니다. </p>
</li>
<li><p>3층 레이어 구조로 방어해줘야 안정적으로 JSON을 받아낼 수 있습니다. </p>
</li>
<li><p>해당 레이어 구조는 아래와 같습니다.
  (1) 시스템 프롬프트에 &quot;JSON only, no prose, no markdown fences&quot; 명시
  (2) SDK의 <code>generationConfig.responseMimeType: 
  &#39;application/json&#39;</code>으로 constrained decoding을 강제  </p>
</li>
</ul>
<pre><code class="language-typescript">  // Gemini SDK generationConfig에 명시
  const result = await model.generateContent({
    contents: [{ role: &#39;user&#39;, parts: [{ text: userPrompt }] }],
    generationConfig: {
      responseMimeType: &#39;application/json&#39;, // 이렇게!!
      temperature: 0.1,
      topP: 0.95,
    },
  });</code></pre>
<p>(3) 응답에서 필요한 {...} 블록만 추출하는 regex fallback을 두기</p>
<p><strong>실전:</strong> </p>
<ul>
<li>이건 LLM호출의 여러 부분에 적용가능한데, 서브에이전트 등 호출시 응답 형식을 정해두면 서브에이전트를 사용하는 에이전트의 효율도 더 올라가는거 같습니다. </li>
<li>프롬프트에서 명시해도 JSON으로 안 나오는 경우가 있는데, 이거 때문에 이 팁을 연구한거고 대부분은 <strong>(2)항인 responseMimeType</strong>이 빠진 경우가 많습니다. </li>
<li>프롬프트 한 줄로 해결하려는 시도보다 SDK 설정이 조금 더 확실하게 출력 형식을 정해주는 거 같습니다.</li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>각 비유는 외우는 도구가 아닙니다. 오개념이 어디서 생기는지를 드러내는 도구입니다. topP 0.5에서 &quot;0개&quot;라고 떠오른다면 날씨 비유로 다시 돌아오고, confidence 미만에서 &quot;재호출&quot;이 먼저 나온다면 학생 시험지 비유로 돌아오면 됩니다. 메커니즘이 잡히면 파라미터 결정에 근거가 생깁니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트 개발자가 Supabase와 Postgres를 맡으면서 배운 것 — 멀티테넌시 전환과 RLS 무음 실패]]></title>
            <link>https://velog.io/@s_soo100/%ED%94%84%EB%A1%A0%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-Supabase%EC%99%80-Postgres%EB%A5%BC-%EB%A7%A1%EC%9C%BC%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B4-%EA%B2%83-%EB%A9%80%ED%8B%B0%ED%85%8C%EB%84%8C%EC%8B%9C-%EC%A0%84%ED%99%98%EA%B3%BC-RLS-%EB%AC%B4%EC%9D%8C-%EC%8B%A4%ED%8C%A8</link>
            <guid>https://velog.io/@s_soo100/%ED%94%84%EB%A1%A0%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-Supabase%EC%99%80-Postgres%EB%A5%BC-%EB%A7%A1%EC%9C%BC%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B4-%EA%B2%83-%EB%A9%80%ED%8B%B0%ED%85%8C%EB%84%8C%EC%8B%9C-%EC%A0%84%ED%99%98%EA%B3%BC-RLS-%EB%AC%B4%EC%9D%8C-%EC%8B%A4%ED%8C%A8</guid>
            <pubDate>Tue, 21 Apr 2026 02:17:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;프론트 잘하고 AI 잘 쓰는 개발자인데, 백엔드 약하면 결국 한계 아니에요?&quot;라는 질문을 자주 받습니다.
최근 SAAS 협업툴의 Postgres 레이어를 맡으면서, 답이 &quot;전문가 되기&quot;가 아니라는 걸 알았습니다.
이 글은 멀티테넌시 전환 한 건과, 초대 role 버그에서 마주친 RLS 무음 실패 한 건을 통해, AI 하네스를 낀 프론트가 어떻게 DB 레벨 판단을 뚫는지 기록한 것입니다.</p>
</blockquote>
<hr>
<h2 id="들어가며-실전-팀-단위-saas개발토이-프로젝트">들어가며, 실전 팀 단위 SAAS개발(토이 프로젝트)</h2>
<p>저는 Flutter/React 로 먹고살던 프론트엔드 개발자이고, 지금은 스스로를 AI-native 개발자 혹은 Harness Engineer라고 부릅니다. 최근 작성한 AI관련 블로그 포스팅이 주로 에이전트 시스템 설계에 치중되어 있었다면, 이 글은 같은 논지를 한 개인의 역량 확장에 적용한 버전입니다.</p>
<p>AI의 발달로 인해 러닝 커브는 극단적으로 짧아지고, 개발자도 본인이 잘하는 것만 하거나 개발만 하는 게 아니라 프로덕트를 제대로 이해하고 매니징할 수 있어야 하는 시대가 되었습니다.
그리고 이런 시대의 프론트엔드 출신 개발자는 무조건 이 질문과 조우하게 됩니다.</p>
<blockquote>
<p>&quot;AI 잘 쓰는 프론트라도, 백엔드 모르면 결국 반쪽 아니에요?&quot;</p>
</blockquote>
<p>결국에는 전 과정을 이해하고 있어야 합니다. 그리고 공부를 하지 않을 변명거리가 너무 궁색해져 버린 현실이기도 합니다. ai는 최고의 선생님이자 보조 개발자니까요.
공부도 마찬가지로 실전 프로젝트가 가장 빨리 늘겠죠? 저는 그래서 팀이 쓸 수 있는 가벼운 관리툴을 저렴하고 손쉬운 Supabase로 만들어보고 있었습니다.
이전에도 백엔드 공부를 독학으로 진행하고 있었지만, 토이 프로젝트이니 만큼 당장 무료플랜이 있느는 Supabase로 차근차근 진행하고 있는데, Supabase에 앱을 올리다 RLS가 막거나, 헬퍼 함수가 순환 참조로 터지거나, 트리거 하나가 흐름을 뒤집는 상황이 생깁니다.
프론트만 봐선 원인이 안 나오는 경우도 많고, 그러므로 우리는 나는 ㅁㅁ하는 사람이야 라고 스스로 선 긋기 보다는 넓게 공부하는게 맞는 것 같습니다.그렇다고 &quot;풀스택 백엔드 전문가&quot;가 되려고 공부하는 건 아닙니다.</p>
<blockquote>
<p><strong>전문가가 되기가 아니라, 충분히 판단할 수 있는 개발자가 되기.</strong></p>
</blockquote>
<p>제가 목표하는 건 이쪽입니다. AI는 설계 옵션을 나열해줍니다. 이 스키마로 갈지 저 RLS 조합으로 갈지, 어떤 헬퍼를 붙일지. 다만 <strong>어느 옵션을 고를지는 결국 사람이 판단</strong>해야 합니다.
트레이드오프를 읽고, 프로젝트 요구사항과 맞춰보고, 되돌리기 비용까지 가늠하는 일을 할 수 있어야 하는 시대인거 같습니다.. AI는 재료를 수십 배 빠르게 차려주지만, 선택의 책임을 대신 지지는 않습니다.</p>
<p>영역 확장 = AI 하네스로 판단 반경을 넓히는 것. 이 글은 그 프레임을 SAAS 협업툴에서 실제로 부딪힌 두 사건으로 풀어낸 기록입니다. 하나는 스키마 레벨의 큰 전환, 다른 하나는 &quot;버그 하나가 나를 이틀 잡아먹은&quot; 작은 사건. 둘 다 영역 확장의 다른 얼굴입니다.</p>
<p>토이라고 해서 문제 크기까지 토이는 아니었습니다. &quot;팀 단위로 격리해서 여러 곳이 같이 쓰게 해달라&quot; 같은 요구가 들어오면, 해결해야 할 DB 레벨 문제는 현장과 똑같아지거든요.</p>
<hr>
<h2 id="에피소드-1--단일-팀-전제로-만들었는데-야-이거-옆-팀도-쓰고싶대">에피소드 1 : 단일 팀 전제로 만들었는데 &quot;야 이거 옆 팀도 쓰고싶대&quot;</h2>
<p>첫 번째 덩어리는 &quot;단일 팀 전제로 만들어진 SAAS 협업툴을 여러 회사가 각자 격리된 상태로 쓸 수 있게 바꾸는 것&quot;이었습니다. 프론트 언어로 번역하면 &quot;전역 싱글톤 store를 회사별 scope로 나누는 것&quot;에 가깝습니다. 다만 대상이 Postgres입니다.</p>
<p>전환 전에는 모든 RLS가 <code>auth.uid()</code>만 믿고 &quot;본인 것만&quot;이라는 축으로 작성돼 있었고, <code>company_id</code> 컨셉 자체가 없었습니다. 회사 개념을 끼워 넣으려면 스키마·RLS·RPC를 전부 건드려야 합니다. <strong>되돌리기 비용이 큰 작업</strong>이었습니다.</p>
<h3 id="한-번에-안-하고-3파일로-쪼갰다">한 번에 안 하고 3파일로 쪼갰다</h3>
<p>처음엔 SQL 파일 하나를 크게 쓸까 싶었고 AI도 단일 파일 쪽을 선호했지만, 회귀 감지 축에서 보면 나쁜 설계였습니다. 어디서 터졌는지 원인을 끊어 보기 어렵거든요. 3파일로 나눴습니다.</p>
<table>
<thead>
<tr>
<th>Part</th>
<th>파일</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>multi_tenancy_1_schema.sql</code></td>
<td><code>companies</code> 테이블 신설 + 기존 업무 테이블에 <code>company_id</code> 추가</td>
</tr>
<tr>
<td>2</td>
<td><code>multi_tenancy_2_rls.sql</code></td>
<td>기존 RLS 정책 전체를 DROP 후 <code>company_id</code> 필터 버전으로 재생성</td>
</tr>
<tr>
<td>3</td>
<td><code>multi_tenancy_3_rpc.sql</code></td>
<td>모든 RPC 함수에 &quot;회사 검증&quot; 보일러플레이트 삽입</td>
</tr>
</tbody></table>
<p>목적은 단순합니다. <strong>Supabase SQL Editor에서 하나씩 실행하며 즉시 회귀를 감지하기 위해서</strong>. 스키마만 올리고 앱을 돌려보면 어떤 쿼리가 깨지는지 눈에 보이고, RLS를 올리면 또 다른 증상이 나옵니다. 원인을 <strong>실행 단위로 분리</strong>하는 것이 3파일 분할의 실체입니다. 프론트의 &quot;작은 PR로 쪼개기&quot;와 같은 감각. 다만 대상이 스키마라 실수의 비용이 훨씬 큽니다.</p>
<h3 id="헬퍼-함수에-security-definer를-붙인-이유">헬퍼 함수에 <code>SECURITY DEFINER</code>를 붙인 이유</h3>
<p>회사 범위로 필터링하려면 &quot;지금 로그인한 사용자의 회사 ID&quot;를 꺼내는 함수가 필요합니다.</p>
<pre><code class="language-sql">CREATE OR REPLACE FUNCTION get_my_company_id()
RETURNS UUID
LANGUAGE sql
SECURITY DEFINER     -- 함수 소유자 권한으로 실행
STABLE               -- 같은 쿼리 안에서 결과 캐시
AS $$
  SELECT company_id FROM profiles WHERE id = auth.uid()
$$;</code></pre>
<p><code>SECURITY INVOKER</code>(기본값)로 두면 RLS가 걸린 <code>profiles</code>를 <code>profiles</code> RLS 안에서 읽어 순환 참조가 납니다. <code>SECURITY DEFINER</code>가 이 순환을 끊는 장치입니다. <code>STABLE</code>은 쿼리 내 캐시용인데, 하나의 <code>FOR ALL</code> 정책이 네 종류 커맨드(SELECT/INSERT/UPDATE/DELETE)에 전부 걸리고 각 정책에서 이 함수가 여러 번 호출되기 때문에, 이 한 줄이 I/O에 제법 영향을 줍니다.</p>
<p>이 지점에서 AI는 함수로 뺄지, 서브쿼리 직삽할지, JWT claim으로 뺄지를 나란히 제시했습니다. <strong>고르는 건 제 몫</strong>이었고, 팀 전환 요구 때문에 JWT는 탈락, 직삽은 가독성 문제로 탈락이었습니다.</p>
<h3 id="rls-정책-텍스트가-곧-권한-명세가-된다">RLS 정책 텍스트가 곧 권한 명세가 된다</h3>
<p>기존 정책을 전부 갈아엎으면서 골격을 통일했습니다. 소속 체크는 <code>USING (company_id = get_my_company_id())</code>로 기본 필터를 박고, 역할 체크는 <code>EXISTS</code> 서브쿼리로 별도 확인. &quot;게스트는 민감 컬럼 못 봄&quot; 같은 규칙을 정책 텍스트에 그대로 박았습니다. <strong>권한을 별도 문서로 만드는 대신, RLS 정책 텍스트 자체가 권한 명세가 되는 구조</strong>입니다. 문서와 실코드의 드리프트가 생기지 않습니다. 프론트에서 권한을 Route Guard에 박아온 선호를 RLS에도 그대로 옮긴 셈입니다.</p>
<h3 id="role을-의도적으로-두-군데에-저장했다"><code>role</code>을 의도적으로 두 군데에 저장했다</h3>
<p>&quot;한명의 유저가 여러 팀 개념에 속할 수 있다&quot;는 요구가 추가되면서 <code>user_companies</code> junction 테이블이 생겼습니다. <code>role</code>을 <code>user_companies</code> 하나에만 둘지, 아니면 <code>profiles.role</code>과 이중으로 둘지 선택지가 있었고, 저는 <strong>이중 저장 + 단일 진입점</strong>을 택했습니다. 프론트 가드가 여러 군데서 <code>profiles.role</code>을 읽는 경로를 바꾸는 건 영향 범위가 넓어서, DB 쪽에서 한 스텝 감당하는 편이 합리적이었습니다. 대신 <code>update_member_role</code>, <code>switch_company</code> 같은 <strong>단일 진입점 RPC를 통해서만 역할이 바뀌게</strong> 걸었습니다. &quot;프로젝트 구조와 어디서 role을 읽고 있는가&quot;를 아는 사람만 할 수 있는 결정입니다. AI가 대신 짜주지 않습니다.</p>
<h3 id="cascade-대신-set-null을-쓴-이유">CASCADE 대신 <code>SET NULL</code>을 쓴 이유</h3>
<p><code>profiles.company_id</code> FK에 <code>ON DELETE CASCADE</code>가 아니라 <code>SET NULL</code>을 썼습니다. CASCADE였다면 &quot;회사 삭제 → 소속 프로필 전부 삭제 → 실질적 유저 삭제&quot;라는 재앙이 납니다. 반면 <code>SET NULL</code>이면 &quot;회사가 없는 유저&quot;라는 어색한 상태가 되지만, 복구 가능합니다. 둘 중 한 방향으로 터질 수밖에 없다면 복구 가능한 쪽으로 터지게 만든다는, fail-closed 원칙 그대로입니다.</p>
<h3 id="초대-테이블엔-아직도-찜찜한-정책-하나가-있다">초대 테이블엔 아직도 찜찜한 정책 하나가 있다</h3>
<p><strong>초대받은 사람은 아직 회사에 소속되지 않은 상태로 토큰을 수락</strong>합니다. 수락 시점에 <code>company_id</code>가 없습니다. 그래서 <code>invitations</code> RLS는 두 정책이 병렬로 걸립니다. admin이 자기 회사 초대를 관리하는 쪽은 <code>USING (company_id = get_my_company_id())</code>, 초대받은 사람이 자기 초대를 확인하는 쪽은 <code>USING (true)</code>. 후자는 토큰 UUID의 무작위성에 보안을 맡깁니다. &quot;토큰을 알면 조회 가능&quot;으로 풀고 토큰의 추측 불가능성에 기대는 식. 이 프로젝트 RLS에서 <strong>가장 불편한 트레이드오프</strong>이고, 에피소드 2의 복선이 되는 지점이기도 합니다.</p>
<h3 id="다음날-체크리스트-몇-개가-더-붙었다">다음날 체크리스트 몇 개가 더 붙었다</h3>
<p>정직하게 쓰자면, 이 작업을 끝낸 다음날 몇 가지 항목을 더 넣었습니다. 묻지 않으면 AI도 안 꺼내주는 종류였습니다. &quot;모르는 걸 묻지 않으면 AI도 안 꺼낸다&quot;는 감각이 또 박혔습니다. AI가 완벽한 초안을 주지 않기 때문에 <strong>판단자와 체크리스트가 더 중요해진다</strong>는 게 결론입니다.</p>
<hr>
<h2 id="에피소드-2--초대-role-버그-그리고-rls-무음-실패">에피소드 2 : 초대 role 버그, 그리고 RLS 무음 실패</h2>
<p>에피소드 1이 큰 덩어리 하나였다면, 에피소드 2는 작은 버그 하나가 이틀을 잡아먹은 이야기입니다.</p>
<h3 id="로그인은-되는데-전부-guest로-들어온다">로그인은 되는데 전부 <code>guest</code>로 들어온다</h3>
<p>프론트에서는 분명 <code>member</code> / <code>admin</code> 같은 역할을 선택해 초대 메일을 보냈는데, 받는 쪽 계정은 전부 <code>guest</code>로 저장됐습니다. 프론트 payload를 찍어봐도 role은 제대로 실려 있었고, <code>invitations</code> 테이블에 저장된 값도 맞았습니다. 그런데 유저가 토큰을 수락한 뒤 <code>profiles.role</code>을 열어보면 <code>guest</code>.</p>
<h3 id="범인은-트리거의-한-줄">범인은 트리거의 한 줄</h3>
<p>범인은 <code>auth.users</code>에 걸린 트리거였습니다.</p>
<pre><code class="language-sql">CREATE OR REPLACE FUNCTION handle_new_user() RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO profiles (id, email, role)
  VALUES (NEW.id, NEW.email, &#39;guest&#39;);  -- ← 무조건 guest, 하드코딩
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;</code></pre>
<p><code>NEW.raw_user_meta_data-&gt;&gt;&#39;role&#39;</code>을 읽지 않고 <code>guest</code>를 박아버립니다. 신규 가입자는 전원 guest로 태어납니다. 초대 role이 아무리 맞게 날아와도 트리거가 한 번 뭉개고 나면 끝.</p>
<h3 id="트리거는-건드리지-않고-rpc로-덮었다">트리거는 건드리지 않고 RPC로 덮었다</h3>
<p>&quot;원인을 알았으면 트리거를 고치면 되지 않나?&quot; 쪽이 자연스러워 보입니다. 그런데 <strong>트리거는 안 건드리기로</strong> 했습니다. 대신 <code>accept_invitation</code> RPC가 수락 시점에 <code>profiles.role</code>을 덮어쓰게 했습니다.</p>
<pre><code class="language-sql">CREATE FUNCTION accept_invitation(p_token TEXT, p_user_id UUID) ... AS $$
  SELECT * INTO v_inv FROM invitations
  WHERE token = p_token AND accepted_at IS NULL AND expires_at &gt; NOW();

  UPDATE profiles
     SET role = v_inv.role, company_id = v_inv.company_id
   WHERE id = p_user_id;
$$;</code></pre>
<p>왜 트리거를 안 고쳤느냐. 여기서부터는 <strong>추정</strong>입니다. (a) 트리거는 <code>auth.users</code>에 걸려 있어 건드리면 회원가입 흐름 전체에 영향을 줍니다. 되돌리기 비용이 큽니다. (b) 초대 없이 그냥 signUp한 유저는 실제로 <code>guest</code>가 맞습니다. 즉 하드코딩이 <strong>기본값 역할</strong>로는 정상 동작합니다. 고쳐야 할 것이 &quot;기본값&quot;이 아니라 &quot;초대 수락 시점의 덮어쓰기 경로 부재&quot;라면, 건드릴 곳은 트리거가 아니라 RPC 쪽이 됩니다.</p>
<p>판단을 한 줄로 적으면 이렇습니다. <strong>&quot;고쳐야 할 것을 고치지 않고 우회한다&quot;가 맞을 때가 있다.</strong> 영향 범위가 넓고 기본값으로는 정상인 경로는 건드리지 않고, 실제로 틀린 경로에 패치를 얹는 쪽이 되돌리기 비용이 낮습니다.</p>
<p>멀티팀 지원을 넣고 싶어지면서 이 RPC에 <code>user_companies</code> INSERT와 &quot;신규 유저만 <code>profiles.role</code>을 덮어쓴다&quot;는 가드도 더 붙었습니다. 기본 뼈대는 같습니다.</p>
<h3 id="진짜-오래-걸린-이유는-rls-무음-실패였다">진짜 오래 걸린 이유는 RLS 무음 실패였다</h3>
<p>사실 트리거와 RPC 구조를 파악하는 건 한나절이면 됐습니다. 제가 당시 이 버그에 이틀을 쓴 이유는 따로 있습니다.</p>
<p>처음 <code>accept_invitation</code>을 고쳤을 때, RPC는 <strong>성공했다고 응답</strong>했습니다. 에러 없음. 프론트는 조용히 다음 화면으로 넘어갔고, 재조회해 보니 <code>profiles.role</code>은 여전히 <code>guest</code>였습니다.</p>
<p>원인은 <code>UPDATE</code>가 RLS에 막혀서 <strong>0행이 갱신된 것</strong>이었습니다. Postgres의 <code>UPDATE</code>는 조건에 맞는 행이 0개여도 에러가 아닙니다. &quot;0 rows updated&quot;는 정상 결과이기 때문입니다. RLS가 행을 필터링해서 0행이 된 경우도 똑같이 &quot;정상 실행&quot;으로 처리됩니다. RPC 관점에서는 &quot;UPDATE 문이 정상 실행됐다&quot;가 사실이고, 프론트 관점에서는 &quot;RPC가 성공 응답을 줬다&quot;가 사실입니다. 양쪽 다 거짓말을 한 게 아닌데, 실제 DB 상태는 안 바뀌었습니다.</p>
<p>프론트 디버거로 payload를 아무리 찍어봐도 원인이 안 나옵니다. 네트워크 응답도 200입니다. 진짜 보스는 프론트도 RPC 내부 로직도 아니고, <strong>&quot;RLS에 막혀서 조용히 실패하는 mutation&quot;</strong>이었습니다.</p>
<h3 id="사건을-dont-노트에-박았다">사건을 Don&#39;t 노트에 박았다</h3>
<p>결국 사건이 끝난 뒤 내 Don&#39;t 노트에 룰을 하나 승격시켰습니다. <strong>&quot;Supabase mutation 후에는 <code>.select()</code> + row count 검증 필수&quot;</strong>. mutation 결과를 <code>.select()</code>로 받아와 실제로 몇 행이 바뀌었는지 확인하지 않으면, 다음에도 같은 함정에 다시 빠집니다.</p>
<blockquote>
<p><strong>RLS가 에러 없이 실패하는 세계에서, <code>.select()</code> 없는 mutation은 어둠 속 발사.</strong></p>
</blockquote>
<p>에피소드 2를 복기하면 배움이 세 가지로 압축됩니다. 첫째, <strong>증상의 출처와 원인의 출처가 다를 수 있다.</strong> 프론트 버그처럼 보였지만 범인은 DB 트리거였고, 그 다음 범인은 RLS였습니다. 둘째, <strong>고칠 곳을 고치지 않고 우회하는 판단이 맞을 때가 있다.</strong> 트리거는 영향 범위가 넓고 기본값으로는 정상이니, 건드릴 곳은 RPC였습니다. 셋째, <strong>침묵하는 실패가 가장 비싸다.</strong> RLS 무음 실패처럼 에러를 안 던지는 실패는 체크 코드로 강제 소음화해야만 발견됩니다. 그리고 한 번 당한 건 개인 지식으로 끝내지 않고 내 Don&#39;t 노트에 박아두는 게 재발 방지의 실체입니다.</p>
<hr>
<h2 id="모르는-영역을-조금씩-찍먹해-봅시다">모르는 영역을 조금씩 찍먹해 봅시다</h2>
<p>두 에피소드가 보여준 건 성격이 다른 두 종류의 판단입니다. 에피소드 1은 <strong>큰 덩어리를 어떻게 쪼갤지</strong>의 판단이었고, 에피소드 2는 <strong>작은 버그를 어디서 잡을지, 그리고 왜 안 잡히는지</strong>의 판단이었습니다. 스키마 설계와 디버깅, 결이 다른 영역이지만 공통점은 하나입니다. AI는 옵션을 나열해줬고, 선택의 책임은 저에게 있었다는 것.</p>
<p>이 두 판단을 하며 느낀 건 분명합니다. 백엔드 전문가가 된 게 아니라, <strong>백엔드를 두려워하지 않게 된 것</strong>입니다. 스키마를 쪼개고, 트리거와 RPC 사이에서 어느 쪽을 건드려야 되돌리기 비용이 낮은지 가늠하고, RLS가 에러 없이 실패할 수 있다는 걸 아는 것. 이 정도면 <strong>판단 가능한 프론트</strong>로서 프로덕트 전 과정에 참여할 수 있습니다. 그게 제가 말하는 풀스택의 <strong>교두보</strong>입니다.</p>
<p>AI 하네스는 이 과정에서 옵션 제공자 역할을 합니다. 선택과 책임은 엔지니어의 몫입니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Claude Code Skill 실전 팁! 이렇게 쓰면 실수율이 줄어듭니다.]]></title>
            <link>https://velog.io/@s_soo100/Claude-Code-Skill-%EC%8B%A4%EC%A0%84-%ED%8C%81-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%8B%A4%EC%88%98%EC%9C%A8%EC%9D%B4-%EC%A4%84%EC%96%B4%EB%93%AD%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@s_soo100/Claude-Code-Skill-%EC%8B%A4%EC%A0%84-%ED%8C%81-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%8B%A4%EC%88%98%EC%9C%A8%EC%9D%B4-%EC%A4%84%EC%96%B4%EB%93%AD%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Fri, 17 Apr 2026 03:52:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Claude Code에 Skill을 만들기 시작한 지 7개월. 처음엔 긴 프롬프트를 파일에 저장하는 정도였는데, 어느 순간 몇 개는 매일 쓰고 몇 개는 만들어놓고 쓴 적이 없다는 걸 알아챘습니다.</p>
</blockquote>
<hr>
<h2 id="들어가며--7개월-써보고-살아남은-것만-추렸습니다">들어가며 — 7개월 써보고 살아남은 것만 추렸습니다</h2>
<p>Claude Code에 Skill(슬래시 커맨드, 서브에이전트)을 만들기 시작한 지 7개월이 됐습니다. 처음엔 그냥 긴 프롬프트를 <code>.claude/commands/</code> 아래 파일로 저장하는 정도였는데, 시간이 지나면서 몇 개는 거의 매일 쓰고 몇 개는 만들어놓고 한 번도 다시 찾지 않는 게 갈렸습니다.</p>
<p>이번에 8,041개 세션 히스토리를 훑어서 &quot;실제로 살아남은 Skill&quot;만 추려봤습니다. 10개 이상 프로젝트를 가로질러 반복 등장한 패턴이 5가지였습니다. 게임(Unity), 웹앱(React/Supabase), Flutter 앱, 기획·마케팅 문서 작업까지, 전혀 다른 도메인에서도 같은 구조가 계속 나왔습니다.</p>
<p>이 글은 그 5가지 패턴의 실물 코드와, 왜 그게 살아남았는지를 정리한 것입니다. 복붙해 쓸 수 있는 Skill 파일 구조를 함께 뒀으니, Claude Code를 쓰고 있거나 쓸 생각이 있으면 바로 적용할 수 있습니다. (여기서 Skill은 슬래시 커맨드와 서브에이전트를 포함한 포괄적인 개념입니다.)</p>
<hr>
<h2 id="1-multi-critic--하나의-코드-변경에-비평가-4명-붙이기">1. Multi-Critic — 하나의 코드 변경에 비평가 4명 붙이기</h2>
<p>가장 오래 살아남은 Skill 1위는 Multi-Critic입니다. 6개 이상의 프로젝트에 그대로 복붙해 쓰고 있는 패턴이고, 기능 구현이 끝난 직후 코드 리뷰 단계에서 매번 호출합니다.</p>
<p>구조는 단순합니다. 하나의 코드 변경에 대해 관점이 서로 다른 4개의 비평 에이전트를 단일 메시지에서 병렬 스폰하고, 결과를 메인 Claude가 종합합니다. 각 에이전트는 자기 영역만 봅니다. 데이터 무결성 / 로직 정합성 / 보안 리스크 / UX 품질을 독립 축으로 분리한 겁니다.</p>
<p>이걸 쓰는 이유는 단일 시점의 리뷰가 놓치는 사각지대를 구조적으로 제거하기 때문입니다. 4명이 각자 관점에서 훑고 나서 <strong>2명 이상이 동일 이슈를 지적하면 확정 결함</strong>으로, 1명만 지적한 건 참고 사항으로 분류합니다. 오탐(false positive)이 줄고, 과잉 수정도 방지됩니다.</p>
<p>아래는 실제로 쓰는 <code>/critic</code> Skill의 뼈대입니다. 4개 에이전트를 정의하고 메인 Claude가 결과를 종합 분석하도록 지시하는 구조입니다.</p>
<pre><code class="language-markdown"># Critic — 4 에이전트 병렬 코드 비평

## 실행 절차

1. 대상 파일 결정: $ARGUMENTS 또는 `git diff --name-only HEAD~1`
2. 변경 파일 10개 이상이면 기능별 그룹핑 → 핵심 그룹만 전달
3. **4개 에이전트를 단일 메시지에서 병렬 스폰**
4. 전체 결과를 메인 Claude가 종합 분석

## 에이전트 A — 데이터 무결성 감사

- 관점: 모든 수치는 증명되기 전까지 거짓이다
- 검증 축: 산술 정합성, 데이터 변환 손실, 캐시-원본 불일치
- 출력: 점수(N/10), 이슈 목록(파일:라인 + 심각도), 판정

## 에이전트 B — 로직 정합성 질문자

- 관점: 아무것도 모른다. 다만 질문할 뿐이다
- 검증 축: 상태 전이 일관성, 워크플로우 모순, 엣지케이스

## 에이전트 C — 보안/리스크 비관주의자

- 관점: 이 시스템은 뚫린다
- 검증 축: 권한 상승 경로, 데이터 노출, 인증 취약점

## 에이전트 D — UX/성능 파괴자

- 관점: &#39;편리한 UI&#39;라는 우상을 부수겠다
- 검증 축: 클릭 경제성, 에러 복구 비용, 접근성

## 종합 분석 규칙 (메인 Claude 수행)

- 2+ 에이전트 공통 지적 → 교차 패턴 테이블로 정리
- 우선순위 매트릭스: 순위 | 항목 | 지적 에이전트 | 영향도 | 수정 난이도</code></pre>
<p><code>/critic</code> 한 번의 호출로 전체 파이프라인이 자동 실행됩니다. 대상 미지정 시에는 최근 커밋의 변경 파일을 자동으로 감지합니다.</p>
<hr>
<h2 id="2-architect-→-implement-→-review--설계와-구현을-분리하는-파이프라인">2. Architect → Implement → Review — 설계와 구현을 분리하는 파이프라인</h2>
<p>두 번째는 하나의 기능 요청을 <strong>설계(Architect) → 구현(Implement) → 검증(Critic) 3단계로 분리</strong>하는 패턴입니다. 각 단계에 전문화된 에이전트를 배치하고, 단계 사이에는 사용자 승인 게이트를 둡니다. 이것도 3개 이상 프로젝트에 똑같은 구조로 적용돼 있습니다.</p>
<p>핵심은 관심사 분리입니다. 설계 에이전트는 코드를 쓰지 않고, 구현 에이전트는 설계를 바꾸지 않습니다. 설계 문서가 사용자 명시 승인을 받기 전까진 구현 단계로 넘어가지 못합니다. 이게 의도하지 않은 구현을 구조적으로 막아줍니다. (이 분리는 제가 쓰는 CAOF(Claude Agent Orchestration Framework, 에이전트 오케스트레이션 규약)의 핵심입니다. 왜 이런 분리가 필요한지는 <a href="https://velog.io/@s_soo100/Claude-Code-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%ED%8F%AD%EC%A3%BC%ED%95%A0-%EB%95%8C-%EC%A0%9C%ED%95%9C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">2026-03-27 &#34;Claude Code 에이전트가 폭주할 때&#34;</a> 글에서 다뤘습니다.)</p>
<p>아래는 설계 에이전트 Skill 파일입니다. 코드를 못 쓰게 막고, 사용자 승인 게이트까지 강제하는 구조입니다.</p>
<pre><code class="language-markdown"># Architect — 시스템 아키텍트

## 페르소나

10년차 시니어 아키텍트. 코드를 직접 작성하지 않는다 — 설계만 한다.
5원칙: 회의적 질문자, 복잡성 감시자, 실패 시나리오 집착, 단순함 옹호, 코드 금지.

## 절차

1. 기존 코드베이스와 규칙 파일 탐색 (Read/Grep/Glob만 허용)
2. 최소 3개의 공격적 질문 (목표, 범위, 제약, 트레이드오프, 실패 시나리오)
3. 설계 문서 작성:
   - 수정 대상 파일 테이블
   - 핵심 함수/메서드 명세
   - 재사용 가능한 기존 코드
   - 경고사항 및 실패 시나리오
4. 승인 게이트 — 사용자가 &quot;approved&quot; 할 때까지 구현 금지

## 출력

설계 문서를 `planning/YYYY-MM-DD-기능명.md`에 저장
완료 후 안내: `/implement planning/설계문서.md` 로 구현 시작</code></pre>
<p>구현 에이전트는 별도 파일(<code>/implement</code>)로 분리돼 있고, 페르소나는 &quot;8년차 시니어 개발자, 아키텍트의 설계를 충실히 실행한다&quot;입니다. 핵심 제약은 세 가지입니다. 계획에 없는 코드 금지, &quot;일단 고쳐보겠다&quot; 금지(근본 원인 명시 + 선택지 제시 필수), 규칙 파일 위반 금지. 구현이 끝나면 변경 파일 목록과 설계 대비 이탈 사항, 자가점검 결과를 보고서로 남깁니다.</p>
<p>전체 체인은 <code>/architect</code> → (사용자 승인) → <code>/implement 설계문서.md</code> → <code>/critic</code> 순으로 진행됩니다. 각 단계 전환이 명시적 승인을 거치기 때문에, 잘못된 방향으로 15분 달리다 &quot;아 이게 아닌데&quot; 하고 되돌리는 일이 크게 줄었습니다.</p>
<hr>
<h2 id="3-cross-model-parallel-review--ai-모델-4개에-동시에-물어보기">3. Cross-Model Parallel Review — AI 모델 4개에 동시에 물어보기</h2>
<p>세 번째는 동일한 코드 또는 기획 문서를 <strong>서로 다른 AI 모델 4개(Gemini, Claude Opus, Sonnet, Haiku)에 동시에 리뷰</strong>시키는 패턴입니다. 기획서 완성 후 구현 착수 전 Planning Review나, 대규모 코드 변경 후 Code Review에서 씁니다.</p>
<p>왜 모델을 나누냐면, 같은 모델이 자기 출력을 리뷰하면 자기 동의 편향(self-agreement bias)이 생겨서 사실상 쓸모없기 때문입니다. 모델마다 blind spot이 달라서, 다른 모델이 그 사각지대를 보완해줍니다. (이 문제를 페르소나 관점에서 어떻게 다뤄야 하는지는 <a href="https://velog.io/@s_soo100/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EB%84%8C-%EC%A0%84%EB%AC%B8%EA%B0%80%EC%95%BC%EB%9D%BC%EA%B3%A0-%EB%A7%90%ED%95%98%EB%A9%B4-%EC%A7%84%EC%A7%9C-%EC%9E%98%ED%95%A0%EA%B9%8C-%EB%85%BC%EB%AC%B8%EA%B3%BC-%EC%8B%A4%EC%A0%84%EC%9D%B4-%EB%A7%90%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%A7%84%EC%8B%A4">2026-04-08 &#34;AI 에이전트에게 넌 전문가야라고 말하면 진짜 잘할까&#34;</a> 글에서 다뤘습니다.)</p>
<p>각 모델에 역할을 다르게 줍니다. Gemini는 구조적 일관성, Claude Opus는 깊은 논리 분석, Sonnet은 실용성/구현 가능성, Haiku는 직관적 이상 탐지 필터. 아래는 실제 쓰는 <code>/검수</code> Skill 구조입니다.</p>
<pre><code class="language-markdown"># 4모델 병렬 검수

## 모드 A — 기획서 검수 (기본)

1. 대상 문서 감지: specs/ 디렉토리 또는 $ARGUMENTS
2. 4개 에이전트 병렬 실행:
   - Gemini (외부 CLI): 구조적 일관성 검토
   - Claude Opus (서브에이전트): 깊은 논리적 분석
   - Claude Sonnet (서브에이전트): 실용성/구현 가능성 검토
   - Claude Haiku (서브에이전트): 직관적 이상 탐지 필터
3. 4개 보고서 수집 → 교차 분석 → 최종 보고서 저장

## 모드 B — 코드 검수 (`/검수 code`)

1. `git diff`로 변경 사항 수집
2. 동일한 4개 모델로 코드 레벨 리뷰
3. 최종 보고서 저장

## 판정 기준

- 3+ 모델 합의 → 확정적 이슈 (반드시 수정)
- 2 모델 합의 → 검토 필요 이슈
- 1 모델만 지적 → 참고 사항</code></pre>
<p>다수결 기반 신뢰도 판정이 실전에선 꽤 잘 작동합니다. 3개 이상 모델이 같이 지적하면 거의 확정 결함이라 그냥 고치고, 1개만 지적한 건 &quot;참고&quot;로만 보고 넘어갑니다. 이 분류만으로도 &quot;우선순위 어떻게 잡지&quot; 고민이 사라졌습니다.</p>
<hr>
<h2 id="4-domain-specific-automation--반복-작업을-슬래시-커맨드-한-줄로">4. Domain-Specific Automation — 반복 작업을 슬래시 커맨드 한 줄로</h2>
<p>네 번째는 프로젝트에서 반복적으로 하는 작업을 <strong>하나의 Skill로 캡슐화</strong>하는 패턴입니다. 경험상 ROI가 가장 높았던 유형입니다. 5~10단계의 수동 작업이 단일 명령으로 축소됩니다.</p>
<p>대표 사례 두 개만 보여드리겠습니다. 첫 번째는 버전 업데이트 자동화입니다. Flutter 앱을 배포할 때마다 <code>pubspec.yaml</code> 버전 올리고, 관련 설정 파일 3곳 동기화하고, 릴리즈 노트 쓰는 걸 매번 수동으로 했는데, 한 번 묶어놓으니 <code>/version-up</code> 한 줄로 끝납니다.</p>
<pre><code class="language-markdown"># Version Up — 원클릭 버전 관리

## 절차

1. 현재 버전 읽기 (pubspec.yaml)
2. 마이너 버전 +1, 빌드 넘버 +1
3. 관련 설정 파일 3곳 동기화 (네이티브 빌드 설정 포함)
4. 클린 빌드 실행
5. 최근 10개 커밋 분석 → 릴리즈 노트 자동 생성
   - 앱스토어용 영문 카피
   - 다국어 4줄 업데이트 노트</code></pre>
<p>두 번째는 다국어 처리 자동화입니다. 하드코딩된 자국어 문자열을 찾아서 번역 키를 만들고, 영문 번역을 붙여서 번역 사전에 등록하고, 원본 코드에서 문자열을 키 호출로 교체하는 5단계 작업입니다. 이것도 <code>/i18n</code> 한 줄로 압축해뒀습니다.</p>
<pre><code class="language-markdown"># i18n — 하드코딩 텍스트 다국어 전환

## 절차

1. 대상 파일에서 하드코딩된 자국어 문자열 검출
2. 번역 키 자동 생성 (섹션\_의미 네이밍 규칙)
3. 영문 번역 생성
4. 번역 사전 파일에 양방향 등록
5. 원본 코드에서 문자열을 번역 키 호출로 교체
6. 결과 보고 (변환 건수, 누락 건수)</code></pre>
<p>이런 Automation Skill의 진짜 가치는 <strong>실수 방지</strong>입니다. 수동으로 할 때 &quot;3곳 중 2곳만 동기화&quot;하거나 &quot;번역 키 등록 누락&quot; 같은 실수가 생기는데, 이걸 명령 하나에 묶어두면 그런 누락이 사라집니다. 프로젝트 맥락을 잠시 잊은 상태에서도 명령만 실행하면 결과가 나와서 컨텍스트 복원 비용이 줄어드는 것도 큰 이점입니다.</p>
<hr>
<h2 id="5-hook-based-auto-routing--프롬프트-키워드로-에이전트-자동-배치">5. Hook-Based Auto-Routing — 프롬프트 키워드로 에이전트 자동 배치</h2>
<p>다섯 번째는 사용자가 입력하는 자연어 프롬프트의 키워드를 실시간으로 감지해서, 적절한 에이전트 조합을 <strong>자동으로 컨텍스트에 주입</strong>하는 패턴입니다. 명시적으로 에이전트를 호출하지 않아도 시스템이 키워드를 보고 알아서 올바른 워크플로우로 유도합니다.</p>
<p>아래는 실제 쓰는 Hook 두 개입니다. 첫 번째는 프롬프트 감지 Hook으로, 모든 사용자 입력을 받아 키워드 매칭이 되면 에이전트 조합을 주입합니다.</p>
<pre><code class="language-bash">#!/bin/bash
# UserPromptSubmit Hook — 모든 프롬프트에서 실행

PROMPT=&quot;$1&quot;

# 도메인 키워드 감지 → 에이전트 조합 자동 주입
if echo &quot;$PROMPT&quot; | grep -qE &#39;구현해|버그|스크립트&#39;; then
  echo &#39;{&quot;additionalContext&quot;: &quot;CAOF 라우팅: Designer + Implementer 배치&quot;}&#39;
  exit 0
fi

# 이미지 생성 키워드 → 사전 승인 요청 주입
if echo &quot;$PROMPT&quot; | grep -qE &#39;이미지.*만들|생성&#39;; then
  echo &#39;{&quot;additionalContext&quot;: &quot;이미지 생성 전 반드시 사용자 승인 필요&quot;}&#39;
  exit 0
fi

exit 0  # 매칭 없으면 무시</code></pre>
<p>두 번째는 위험 명령 차단 Hook입니다. 되돌릴 수 없는 명령(force push, hard reset 등)을 자동으로 차단합니다.</p>
<pre><code class="language-bash">#!/bin/bash
# PreToolUse Hook — 모든 Bash 명령 실행 전 검사

COMMAND=&quot;$1&quot;

# 되돌릴 수 없는 명령 차단
if echo &quot;$COMMAND&quot; | grep -qE &#39;git reset --hard|git push --force|git clean -f&#39;; then
  echo &quot;BLOCKED: 되돌릴 수 없는 명령입니다. 사용자에게 확인하세요.&quot;
  exit 2
fi

exit 0</code></pre>
<p><code>settings.json</code>의 <code>hooks</code> 섹션에 등록해두면 모든 세션에서 자동으로 돌아갑니다. 에이전트 호출을 깜빡해도 시스템이 알아서 라우팅하고, 파괴적 명령은 실행 전에 막힙니다. 설정 후엔 가장 편하지만, <strong>처음 세팅에는 한 번 앉아서 Hook 구조를 이해하고 붙여야 한다</strong>는 점이 주의할 부분입니다. 앞선 4개 Skill보다 진입장벽이 조금 있어서, 저도 가장 나중에 손댔습니다.</p>
<hr>
<h2 id="skill-유형별-비교-요약">Skill 유형별 비교 요약</h2>
<table>
<thead>
<tr>
<th>유형</th>
<th>주요 가치</th>
<th>실행 방식</th>
<th>사용 빈도</th>
<th>난이도</th>
</tr>
</thead>
<tbody><tr>
<td>Multi-Critic</td>
<td>교차 검증으로 결함 누락 방지</td>
<td><code>/critic</code> → 4 에이전트 병렬</td>
<td>매 기능 완료 시</td>
<td>중</td>
</tr>
<tr>
<td>Architect-Implement Pipeline</td>
<td>설계/구현 분리로 품질 확보</td>
<td><code>/architect</code> → 승인 → <code>/implement</code></td>
<td>신규 기능마다</td>
<td>중</td>
</tr>
<tr>
<td>Cross-Model Review</td>
<td>모델 편향 상쇄</td>
<td><code>/검수</code> → 4 모델 병렬</td>
<td>기획서 완성 시</td>
<td>상</td>
</tr>
<tr>
<td>Domain Automation</td>
<td>반복 작업 제거</td>
<td><code>/version-up</code>, <code>/i18n</code> 등</td>
<td>배포/텍스트 작업 시</td>
<td>하</td>
</tr>
<tr>
<td>Hook Auto-Routing</td>
<td>워크플로우 자동 유도</td>
<td>항상 (백그라운드)</td>
<td>모든 세션</td>
<td>하 (설정 후)</td>
</tr>
</tbody></table>
<hr>
<h2 id="skill은-프롬프트-저장소가-아니라-판단-구조의-라이브러리입니다">Skill은 &quot;프롬프트 저장소&quot;가 아니라 &quot;판단 구조의 라이브러리&quot;입니다</h2>
<p>7개월 써보고 얻은 제일 큰 깨달음은 이겁니다. Skill은 단순히 긴 프롬프트를 저장하는 게 아니라, <strong>누가 / 무엇을 / 어떤 기준으로 판단하는가</strong>를 구조화한 것이라는 점입니다.</p>
<p>같은 Multi-Critic 구조가 웹앱, 모바일앱, 게임, 데이터 분석 등 전혀 다른 도메인에 재적용되면서 검증 축만 도메인에 맞게 교체됐습니다. Architect-Implement 분리도 마찬가지입니다. 어떤 언어·프레임워크에서도 &quot;설계자는 코드를 쓰지 않는다, 구현자는 설계를 바꾸지 않는다&quot;는 판단 경계는 그대로 유지됩니다. 바뀌는 건 도메인 용어뿐입니다.</p>
<p>결국 Skill 라이브러리는 <strong>재사용 가능한 판단 구조의 저장소</strong>에 가깝습니다. 이게 제가 찾은 자동화의 3계층 구조입니다.</p>
<pre><code>Layer 3: Hook (항상 실행, 키워드 감지 → 라우팅)
Layer 2: Pipeline (사용자 호출, 다단계 워크플로우)
Layer 1: Single Skill (사용자 호출, 단일 작업)</code></pre><p>이 3계층이 결합되면서 &quot;적절한 에이전트가 적절한 타이밍에 적절한 깊이로&quot; 개입하는 체계가 만들어집니다.</p>
<hr>
<h2 id="어느-것부터-시작하면-좋을까">어느 것부터 시작하면 좋을까</h2>
<p>&quot;5개 다 해보고 싶은데 뭐부터 손대지?&quot;라는 질문을 받으면 저는 이렇게 답합니다. <strong>Domain Automation부터 시작하는 게 제일 낫습니다.</strong> 난이도가 가장 낮고 ROI가 가장 빠릅니다. 매번 하는 반복 작업 하나를 골라서 <code>.claude/commands/</code> 아래 슬래시 커맨드 파일 하나만 만들어보면, 다음 주부터 바로 체감됩니다. 버전 업데이트든 번역 키 교체든, 매번 손이 가는 작업이 있다면 그게 1순위 후보입니다.</p>
<p>그 다음은 Multi-Critic이나 Architect-Implement Pipeline을 얹고, Cross-Model Review는 기획서를 자주 쓰게 되는 단계에서 추가하면 됩니다. Hook-Based Auto-Routing은 가장 나중에 손대시는 걸 권합니다. 가치는 가장 크지만 세팅 한 번에 시간이 좀 들어서, 앞의 4개 Skill을 충분히 써보고 &quot;어떤 키워드에서 어떤 에이전트가 자동 배치되면 좋을지&quot; 감이 생겼을 때 붙이는 게 효율이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어찌보면 Ai 최고의 수혜자! Claude Code + Supabase MCP 입문서]]></title>
            <link>https://velog.io/@s_soo100/%EC%96%B4%EC%B0%8C%EB%B3%B4%EB%A9%B4-Ai-%EC%B5%9C%EA%B3%A0%EC%9D%98-%EC%88%98%ED%98%9C%EC%9E%90-Claude-Code-Supabase-MCP-%EC%9E%85%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@s_soo100/%EC%96%B4%EC%B0%8C%EB%B3%B4%EB%A9%B4-Ai-%EC%B5%9C%EA%B3%A0%EC%9D%98-%EC%88%98%ED%98%9C%EC%9E%90-Claude-Code-Supabase-MCP-%EC%9E%85%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Mon, 13 Apr 2026 10:28:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프론트엔드 개발자가 MCP로 DB 작업 빠르게 끝낸 후기</p>
</blockquote>
<h2 id="왜-supabase인가">왜 Supabase인가</h2>
<p>요즘 AI로 프로젝트 시작하는 사람이 진짜 많아지고 있다. 
예전엔 백엔드/DB 쪽에서 가장 진입장벽이 낮은 게 Firebase였을지 모르지만, 현재는 Supabase라고 생각한다.</p>
<h4 id="이유는-개인적으로는-다음과-같다"><strong>이유는.. 개인적으로는 다음과 같다.</strong></h4>
<ul>
<li><strong>PostgreSQL 기반</strong>인데 SQL 몰라도 Dashboard에서 클릭으로 테이블 만들 수 있고, <em><strong>엑셀</strong></em> 만 할 줄 알면 배울 수 있다!</li>
<li><strong>인증, 스토리지, 실시간 구독</strong>이 다 내장되어 있어서 별도 서버 구축이 필요 없고, 심지어 Firebase처럼 소셜로그인도 도와준다.(물론 사업자나 개발자 계정 필요)</li>
<li><strong>클라이언트 SDK</strong>가 잘 되어 있어서 프론트엔드 개발자가 바로 쓸 수 있다
(이게 가장 큰 장점중 하나인 것 같다)</li>
<li><strong>무료 티어가 꽤 넉넉하다</strong> — 프로젝트 2개, DB 500MB, Storage 1GB, 월 5만 API 호출. 사이드 프로젝트나 MVP 단계에서는 과금 걱정 없이 충분하다</li>
<li><strong>✨️M✨️C✨️P✨️가 있다</strong> - 그저 빛✨️.. </li>
</ul>
<h3 id="supabase-프로젝트-생성--연결까지">Supabase 프로젝트 생성 ~ 연결까지</h3>
<p><strong>1단계: 프로젝트 만들기</strong></p>
<ol>
<li><a href="https://supabase.com">supabase.com</a> 접속 → <strong>Start your project</strong> 클릭</li>
<li>GitHub 계정으로 로그인(SSO로 해도 된다 그냥 되는거로!)</li>
<li><strong>New Project</strong> 클릭</li>
<li>Organization 선택 (처음이면 자동 생성됨)</li>
<li>프로젝트 이름, DB 비밀번호, Region 설정 → <strong>Create new project</strong></li>
<li>2~3분 기다리면 프로젝트가 올라온다
<img src="https://velog.velcdn.com/images/s_soo100/post/1bf89cb2-952b-4556-8017-4a33a6683ad9/image.png" alt=""></li>
</ol>
<p><strong>2단계: project_ref 확인</strong></p>
<p>프로젝트가 만들어지면 Dashboard URL이 이런 형태다:</p>
<pre><code>https://supabase.com/dashboard/project/abcdefghijklmnop
                                       ^^^^^^^^^^^^^^^^
                                       이 부분이 project_ref</code></pre><p>Settings &gt; General 순서로 눌러서 확인해보자</p>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/c23af45a-5fda-4336-b5ad-30add19dc689/image.png" alt=""></p>
<p><strong>3단계: 클라이언트 연결 키 확인</strong></p>
<p>Settings &gt; API에 가면 두 가지 키가 있다:</p>
<table>
<thead>
<tr>
<th>키</th>
<th>용도</th>
<th>클라이언트 노출</th>
</tr>
</thead>
<tbody><tr>
<td><code>anon</code> (public)</td>
<td>프론트엔드 SDK용. RLS가 적용됨</td>
<td>OK</td>
</tr>
<tr>
<td><code>service_role</code> (secret)</td>
<td>관리자/백엔드용. RLS 우회</td>
<td><strong>절대 노출 금지</strong></td>
</tr>
</tbody></table>
<p>프론트엔드 앱에서는 <code>anon</code> 키 + Project URL만 있으면 바로 연결 가능하다.
<img src="https://velog.velcdn.com/images/s_soo100/post/31f5a54d-b957-43fa-802c-c71be0e9c01f/image.png" alt=""></p>
<pre><code class="language-ts">import { createClient } from &#39;@supabase/supabase-js&#39;

const supabase = createClient(
  &#39;https://abcdefghijklmnop.supabase.co&#39;,  // Project URL
  &#39;eyJhbGciOiJIUzI1NiIsInR5...&#39;            // anon key
)</code></pre>
<p>여기까지 오면 Supabase 쓸 준비 완료인데, MCP로 연결하면 이거보다 더 빨리 할 수도 있다. </p>
<p><strong>Another 3단계: Claude Code + MCP로 터미널에서 바로 DB 조작하기</strong></p>
<p>근데 이 글의 핵심은 Dashboard를 왔다갔다하는 게 아니라, <strong>Claude Code 안에서 DB까지 한 번에 다루는 것</strong>이며, AI로 Supabase를 사용하는 대부분이 그것 때문에 이 글을 읽는다고 생각한다. 그걸 가능하게 해주는 게 Supabase MCP 서버이며 너무 간단하다.</p>
<p>Supabase 대시보드의 상단에서 (Connect) 버튼을 눌러서 MCP로 가면 친절하게 Claude와 연동되는 커맨드를 준다.
<img src="https://velog.velcdn.com/images/s_soo100/post/46d848dd-30d3-4e46-a304-1092674b50f4/image.png" alt=""></p>
<p>이대로 진행하면 대부분 Claude가 알아서 해주며, 너무너무 쉽고 빠르다.</p>
<p>하지만 FM(?)대로 연동하는 설명도 갖고왔다.
프로젝트 루트에 <code>.mcp.json</code> 파일 만들고 2단계에서 찾은 프로젝트 레퍼런스를 집어넣는다.</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;supabase&quot;: {
      &quot;type&quot;: &quot;url&quot;,
      &quot;url&quot;: &quot;https://mcp.supabase.com/mcp?project_ref=아까_확인한_project_ref&quot;
    }
  }
}</code></pre>
<p>그 다음 Claude Code를 실행하면 처음에 브라우저가 뜨면서 OAuth 인증을 요청하는데, 연동을 진행하면 웹 브라우저로 이동해서 Supabase 계정으로 승인 한 번만 하면 끝이다. 이후로는 Claude Code가 <code>mcp__supabase__*</code> 도구들을 자동으로 인식하며 날 도와준다.</p>
<p>연결 확인은 Claude Code에서 &quot;테이블 목록 보여줘&quot; 라고 하면 <code>list_tables</code>가 작동하면서 현재 DB 상태를 바로 가져온다. 그러면 실질적으로는 Dashboard 열 필요가 거의 없다.</p>
<p>이 상태에서 &quot;이 JSON 파일 읽어서 DB에 넣어줘&quot;라고 하면, Claude가 로컬 파일 읽기부터 SQL 생성(대부분 초보자들이 어려워 하는), 그리고 마지막으로 MCP로 실행까지 한 호흡에 처리 가능하다! 아마 대부분 이렇게 하지 않았을까 싶다.</p>
<hr>
<h2 id="실전-supabase-적용-flutter와-연동한다면">실전 Supabase 적용, Flutter와 연동한다면?</h2>
<p>Flutter 앱에 하드코딩된 로컬 데이터(Dart 상수 + JSON 파일)를 Supabase DB로 옮겨야 했다. 테이블 14개, 시드 데이터 253행, RLS 정책 32개.</p>
<p>기존이라면 이런 흐름이다:</p>
<pre><code>VS Code에서 JSON 열기 → 브라우저에서 Supabase Dashboard 열기
→ 머리로 SQL 변환 → SQL Editor에 붙여넣기 → 에러
→ 다시 VS Code로 돌아와서 JSON 확인 → 반복</code></pre><p><strong>Supabase MCP</strong>를 쓰면 이렇게 바뀐다:</p>
<pre><code>Claude가 로컬 JSON 읽기 → Claude가 SQL 생성 + 실행 → 에러 시 즉시 수정
→ 전 과정 한 대화에서 완료</code></pre><p>&quot;Dashboard 안 열어도 된다&quot;도 좋은 포인트지만, 내가 보기에는 <strong>로컬 코드와 DB 사이의 컨텍스트가 끊기지 않는 것</strong>이 핵심인 것 같다. 
Claude가 양쪽을 동시에 보면서 변환하니까 초보적인 실수인 데이터 타입 불일치나 FK 누락이 거의 안 생겼다.</p>
<hr>
<p>그렇다면 이제 Supabase를 사용하기 위한 기초 지식만 공부해보자.
&quot;DB란 대충 데이터를 많이 넣는 창고구나?&quot; 라는 정도만 알고있다면 충분히 할 수 있다.</p>
<h2 id="먼저-편의점으로-이해하는-db-기초">먼저, 편의점으로 이해하는 DB 기초</h2>
<p>PostgreSQL 핵심 개념을 편의점에 빗대서 빠르게 정리한다. 이미 아는 사람은 <a href="#%EC%8B%A4%EC%A0%84-%EB%A1%9C%EC%BB%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-db%EB%A1%9C-%EC%98%AE%EA%B8%B4-%EC%A0%84%EC%B2%B4-%EA%B3%BC%EC%A0%95">실전 파트</a>로 넘어가도 된다.</p>
<h3 id="테이블--진열대">테이블 = 진열대</h3>
<p>편의점에 진열대가 여러 개 있다. 음료 진열대, 과자 진열대, 도시락 진열대. DB에서 <strong>테이블</strong>이 바로 그것이다. 각 진열대(테이블)에는 같은 종류의 상품(데이터)이 정해진 규격으로 놓여 있는 느낌이다.</p>
<table>
<thead>
<tr>
<th>편의점</th>
<th>PostgreSQL</th>
</tr>
</thead>
<tbody><tr>
<td>편의점 전체</td>
<td>데이터베이스</td>
</tr>
<tr>
<td>진열대 (음료, 과자, 도시락)</td>
<td>테이블 (products, sales, partners)</td>
</tr>
<tr>
<td>상품 한 개</td>
<td>로우(row) 한 건</td>
</tr>
<tr>
<td>가격표 항목 (이름, 가격, 유통기한)</td>
<td>컬럼 (name, price, expiry)</td>
</tr>
</tbody></table>
<p>각 컬럼에는 타입이 정해져 있다. 가격에 &quot;맛있음&quot;이라고 적을 수 없듯이, <code>price</code> 컬럼에는 숫자만 들어간다. TypeScript의 타입 시스템과 같은 느낌이다.</p>
<h3 id="fkforeign-key--발주서의-납품업체-코드">FK(Foreign Key) = 발주서의 납품업체 코드</h3>
<p>편의점에서 발주서를 작성할 때, 납품업체를 이름으로 적지 않고 <strong>업체 코드</strong>로 적는다. &quot;업체 코드 A001의 콜라 30박스&quot; 이런 식이다. 그 업체 코드가 <strong>FK(Foreign Key)</strong>다.</p>
<pre><code class="language-sql">-- 납품업체 진열대 (부모)
CREATE TABLE suppliers (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL
);

-- 발주서 진열대 (자식) — supplier_id가 FK
CREATE TABLE orders (
  id UUID PRIMARY KEY,
  supplier_id TEXT REFERENCES suppliers(id),  -- &quot;이 발주는 이 업체 거&quot;
  item TEXT,
  quantity INT
);</code></pre>
<p><code>REFERENCES suppliers(id)</code> = &quot;이 값은 반드시 suppliers 테이블에 있는 업체여야 한다.&quot; 존재하지 않는 업체에 발주할 수 없는 것처럼, FK가 데이터의 정합성을 지켜준다.</p>
<p><strong>부모-자식 규칙도 편의점으로:</strong></p>
<ul>
<li>납품업체(부모) 등록 → 그 다음에 발주서(자식) 작성 가능</li>
<li>업체를 삭제하려면? 그 업체 발주서(자식)부터 정리해야 한다</li>
<li><code>ON DELETE CASCADE</code> = 업체 삭제하면 관련 발주서도 자동 폐기</li>
</ul>
<h3 id="rls--알바생-vs-점장-권한">RLS = 알바생 vs 점장 권한</h3>
<p>편의점 POS 시스템에서 알바생은 결제만 할 수 있고, 점장만 매출 보고서를 볼 수 있다. <strong>RLS(Row Level Security)</strong>가 정확히 이것이다. 같은 데이터인데 누가 접근하느냐에 따라 보이는 게 달라진다.</p>
<pre><code class="language-sql">-- 점장만 매입 단가를 볼 수 있다
CREATE POLICY &quot;점장만 매입가 조회&quot; ON products
  FOR SELECT
  USING (
    (SELECT role FROM profiles WHERE id = auth.uid()) = &#39;manager&#39;
  );</code></pre>
<p>프론트엔드에서 <code>if (user.role !== &#39;manager&#39;) return []</code> 하는 거랑 뭐가 다르냐면 — 프론트는 데이터가 이미 브라우저에 왔는데 그냥 안 보여주는 것이다. 개발자 도구 열면 다 보인다. RLS는 <strong>DB 단에서 아예 안 준다</strong>. 데이터가 네트워크를 타지도 않는다.</p>
<h3 id="뷰view--일일-매출-요약표">뷰(View) = 일일 매출 요약표</h3>
<p>점장이 매일 아침 보는 매출 요약표 같은 느낌이다. 여러 진열대(테이블)의 데이터를 한 장으로 모아놓은 것이다. 원본 데이터를 건드리지 않고, 필요한 정보만 뽑아서 보여주는 <strong>가상 테이블</strong>이다.</p>
<pre><code class="language-sql">CREATE VIEW daily_sales_summary AS
SELECT date, SUM(amount) as total, COUNT(*) as order_count
FROM sales
GROUP BY date;</code></pre>
<hr>
<h2 id="mcp-핵심-도구-3개">MCP 핵심 도구 3개</h2>
<p>편의점 비유는 여기까지다. 이제부터 실전에서 쓰는 MCP 도구를 알아보자.</p>
<table>
<thead>
<tr>
<th>도구</th>
<th>용도</th>
<th>편의점 비유</th>
</tr>
</thead>
<tbody><tr>
<td><code>apply_migration</code></td>
<td>DDL (CREATE TABLE, ALTER)</td>
<td>새 진열대 설치</td>
</tr>
<tr>
<td><code>execute_sql</code></td>
<td>DML (INSERT, SELECT)</td>
<td>상품 진열/조회</td>
</tr>
<tr>
<td><code>list_tables</code></td>
<td>테이블 목록 + 행 수 + RLS 확인</td>
<td>매장 배치도 + 재고 요약</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>주의</strong>: 스키마 변경(CREATE/ALTER)은 반드시 <code>apply_migration</code>으로 해야 한다. <code>execute_sql</code>로 하면 마이그레이션 이력에 안 남아서 나중에 추적이 안 된다.</p>
</blockquote>
<hr>
<h2 id="실전-로컬-데이터를-db로-옮긴-전체-과정">실전: 로컬 데이터를 DB로 옮긴 전체 과정</h2>
<p>아까 비유했던 편의점, 이제 진짜로 만들어보자. 편의점 POS 시스템을 예시로 진행한다.</p>
<h3 id="step-1--스키마-설계--테이블-생성">Step 1 — 스키마 설계 + 테이블 생성</h3>
<p>아까 편의점에서 진열대 만든다고 했는데, SQL로는 이렇게 생겼다:</p>
<pre><code class="language-sql">-- apply_migration(name: &quot;create_store_tables&quot;)
CREATE TABLE product_categories (
  id          TEXT PRIMARY KEY,
  name        TEXT NOT NULL,       -- 음료, 과자, 도시락, 생활용품
  sort_order  INT DEFAULT 0
);

CREATE TABLE products (
  id            TEXT PRIMARY KEY,
  category_id   TEXT REFERENCES product_categories(id),  -- FK: 이 상품은 이 카테고리 소속
  name          TEXT NOT NULL,       -- 콜라, 새우깡, 삼각김밥
  price         INT NOT NULL,
  barcode       TEXT
);</code></pre>
<p>이걸 직접 다 짤 필요는 없다. Claude에게 이렇게 요청하면 된다:</p>
<blockquote>
<p>💬 <strong>Claude에게 이렇게 말한다:</strong></p>
<p>&quot;내 프로젝트의 <code>docs/supabase-schema.md</code> 파일을 읽어서 테이블을 만들어줘. FK 의존성 순서대로 마이그레이션을 분리해서 실행해.&quot;</p>
</blockquote>
<p>설계 문서가 없어도 괜찮다. 기존 코드를 보여주면서 요청할 수도 있다:</p>
<blockquote>
<p>💬 <strong>설계 문서 없이 요청하는 경우:</strong></p>
<p>&quot;<code>src/data/models/</code> 폴더에 있는 모델 클래스들을 읽어서, 이 구조에 맞는 Supabase 테이블을 만들어줘. FK 관계도 잡아주고.&quot;</p>
</blockquote>
<p>Claude가 모델 클래스를 분석해서 테이블 구조를 제안하고, 승인하면 <code>apply_migration</code>으로 바로 실행한다.</p>
<p><strong>FK 의존성 순서</strong>가 중요하다. 납품업체 등록 안 하고 발주서 쓸 수 없는 것처럼, 부모 테이블부터 만들어야 한다:</p>
<pre><code>product_categories → products → product_details
suppliers → inventory
(카테고리 먼저)    (상품 다음)  (상세 정보는 상품 다음에)</code></pre><p>마이그레이션도 역할별로 분리했다:</p>
<pre><code>1. create_store_tables    → 상품/카테고리/납품업체 테이블
2. create_sales_tables    → 매출/매출항목 테이블
3. create_staff_tables    → 직원/근무기록 테이블
4. create_indexes         → 인덱스
5. enable_rls             → RLS 정책</code></pre><p>한 방에 다 넣으면 어디서 에러가 터졌는지 찾기 어렵다. 이 분리도 Claude에게 &quot;역할별로 나눠서 마이그레이션 해줘&quot;라고 하면 알아서 해준다.</p>
<h3 id="step-2--데이터-시딩-insert">Step 2 — 데이터 시딩 (INSERT)</h3>
<p>여기서 MCP의 진가가 나온다. Claude에게 이렇게 말하면 된다:</p>
<blockquote>
<p>💬 <strong>Claude에게 이렇게 말한다:</strong></p>
<p>&quot;<code>assets/data/products.json</code> 파일을 읽어서 products 테이블에 넣어줘.&quot;</p>
</blockquote>
<p>이러면 Claude가 로컬 JSON을 읽고 → SQL INSERT문을 만들고 → <code>execute_sql</code>로 바로 실행한다. 한 흐름이다.</p>
<p>소스 코드에 하드코딩된 데이터도 마찬가지다:</p>
<blockquote>
<p>💬 <strong>하드코딩된 상수를 옮기는 경우:</strong></p>
<p>&quot;<code>lib/data/product_repository.dart</code>에 하드코딩된 상품 데이터를 읽어서 products 테이블에 INSERT 해줘. category_id FK도 맞춰서.&quot;</p>
</blockquote>
<p>Claude가 소스 코드의 상수를 분석해서 SQL로 변환하고, FK 관계까지 맞춰서 넣어준다. 이게 수동으로 하면 가장 귀찮은 작업인데, Claude가 양쪽 파일을 동시에 보고 있으니까 타입 변환 실수가 안 생긴다.</p>
<p><strong>데이터 소스 매핑 예시:</strong></p>
<pre><code>하드코딩 (product_repository.dart)    → products 테이블
JSON (assets/data/categories.json)   → product_categories 테이블
JSON (assets/data/suppliers.json)    → suppliers 테이블
JSON (assets/data/inventory.json)    → inventory 테이블</code></pre><p>JSON의 중첩 객체도 JSONB로 깔끔하게 들어간다:</p>
<pre><code class="language-sql">INSERT INTO products (id, category_id, name, price, nutrition) VALUES (
  &#39;cola-500&#39;,
  &#39;beverage&#39;,
  &#39;코카콜라 500ml&#39;,
  1800,
  &#39;{&quot;calories&quot;:210,&quot;sugar&quot;:53,&quot;caffeine&quot;:60}&#39;::jsonb
);</code></pre>
<p>대량 데이터는 배치 분할이 필요하다. 이것도 직접 나눌 필요 없이:</p>
<blockquote>
<p>💬 <strong>대량 데이터 요청:</strong></p>
<p>&quot;<code>assets/data/inventory.json</code>에서 전체 재고 데이터를 inventory 테이블에 넣어줘. 양이 많으면 배치로 나눠서.&quot;</p>
</blockquote>
<p>데이터가 100개 이상이면 한 번에 넣을 때 SQL 길이 제한에 걸리는데, Claude가 알아서 <strong>배치로 나눠서</strong> 실행해준다.</p>
<p>각 INSERT 후에는 확인도 한마디면 된다:</p>
<blockquote>
<p>💬 &quot;지금까지 넣은 데이터 행 수 확인해줘&quot;</p>
</blockquote>
<pre><code>product_categories: 4 ✓
products: 120 ✓
suppliers: 8 ✓
inventory: 350 ✓</code></pre><h3 id="step-3--rls-정책">Step 3 — RLS 정책</h3>
<p>RLS도 Claude에게 맡길 수 있다:</p>
<blockquote>
<p>💬 <strong>Claude에게 이렇게 말한다:</strong></p>
<p>&quot;상품/카테고리 테이블은 누구나 읽을 수 있게, 매출/근무기록 테이블은 해당 직원 본인 데이터만 접근 가능하게 RLS 설정해줘.&quot;</p>
</blockquote>
<p>Claude가 테이블 구조를 보고 적절한 패턴을 골라서 적용해준다. 결과적으로 두 패턴이 사용된다:</p>
<p><strong>패턴 A: 공개 읽기</strong> — 상품 정보는 누구나 볼 수 있다:</p>
<pre><code class="language-sql">ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY &quot;Public read&quot; ON products
  FOR SELECT USING (true);
-- INSERT/UPDATE/DELETE 정책 없음 → 관리자(service_role)만 수정 가능</code></pre>
<p><strong>패턴 B: 본인 데이터만</strong> — 내 근무기록은 나만 볼 수 있다:</p>
<pre><code class="language-sql">ALTER TABLE shift_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY &quot;Own data only&quot; ON shift_logs
  FOR SELECT USING (auth.uid() = staff_id);
CREATE POLICY &quot;Own insert&quot; ON shift_logs
  FOR INSERT WITH CHECK (auth.uid() = staff_id);</code></pre>
<p>테이블에 <code>staff_id</code>가 없는 경우(자식 테이블)는 부모를 타고 올라간다:</p>
<pre><code class="language-sql">-- sale_items에는 staff_id가 없다. sale_id만 있다.
-- → sales 테이블을 경유해서 소유권 확인
CREATE POLICY &quot;Own sale items&quot; ON sale_items
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM sales
      WHERE sales.id = sale_items.sale_id
        AND sales.staff_id = auth.uid()
    )
  );</code></pre>
<blockquote>
<p><strong>삽질 포인트</strong>: 정책만 만들고 <code>ENABLE ROW LEVEL SECURITY</code>를 빠뜨리면 RLS가 작동 안 한다. <code>list_tables</code>의 <code>rls_enabled</code> 필드로 꼭 확인해야 한다.</p>
</blockquote>
<hr>
<h2 id="삽질-방지-체크리스트">삽질 방지 체크리스트</h2>
<p>실전에서 걸린 것들을 정리한다:</p>
<table>
<thead>
<tr>
<th>삽질</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td><code>apply_migration</code> vs <code>execute_sql</code> 혼동</td>
<td>스키마(CREATE/ALTER) = migration, 데이터(INSERT) = execute</td>
</tr>
<tr>
<td>FK 순서 안 지킴</td>
<td>부모 테이블 먼저. categories → products → product_details</td>
</tr>
<tr>
<td>RLS ENABLE 빠뜨림</td>
<td><code>list_tables</code>로 <code>rls_enabled</code> 확인</td>
</tr>
<tr>
<td>OAuth URL 줄 바꿈</td>
<td>터미널에서 긴 URL이 잘림. <code>open &quot;전체URL&quot;</code> (macOS) 또는 브라우저에 직접 붙여넣기</td>
</tr>
<tr>
<td><code>auth.uid()</code> 안 됨</td>
<td>로그인 구현 전이면 직원 테이블은 RLS에 막힘. 상품 데이터 먼저 연동</td>
</tr>
</tbody></table>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<ol>
<li><strong>MCP 도구 3개</strong>만 기억하자: <code>apply_migration</code>(스키마), <code>execute_sql</code>(데이터), <code>list_tables</code>(검증)</li>
<li><strong>FK 순서</strong>: 부모 먼저, 자식 나중. 마이그레이션은 역할별로 분리한다</li>
<li><strong>RLS 패턴 2개</strong>: 공개 읽기(<code>USING (true)</code>) / 본인 데이터(<code>auth.uid() = user_id</code>)</li>
<li><strong>설계 문서 먼저</strong>: CREATE TABLE DDL을 미리 정리해두면 MCP 실행은 복붙 수준이다</li>
<li><strong>레퍼런스 vs 유저 데이터 분리</strong>: 인증 없이 바로 쓸 수 있는 것과 로그인 후 쓸 수 있는 것을 나눠두면 단계적 연동이 가능하다</li>
</ol>
<h2 id="한-줄-감상">한 줄 감상</h2>
<p>솔직히 제일 놀란 건 속도보다 에러가 안 난다는 것 이었다. 기존에는 JSON 보고 SQL 쓰면서 타입 틀리고, FK 빠뜨리고, 작은따옴표 이스케이프 깜빡하고... 그런 자잘한 삽질이 작업 시간의 절반이었는데, Claude가 양쪽 파일을 동시에 보고 있으니까 그런 실수가 구조적으로 안 생긴다.</p>
<p>&quot;AI가 코드를 짜준다&quot; 아니면 &quot;AI가 딸깍해준다&quot;보다 <strong>&quot;AI가 두 시스템 사이의 번역기가 된다&quot;</strong>가 MCP의 진짜 가치인 것 같다. 
DB 작업이 어려운 게 SQL 문법이 어려워서가 아니라, 내 코드와 DB 사이를 왔다갔다하면서 컨텍스트를 유지하는 게 어려운 것 같다. 우선 기초만 내 머릿속에 들어가 있다면 이제 나머지는 다 쉬워진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LLM API로 앱에 AI 채팅 넣기 (Groq + Flutter 실전)]]></title>
            <link>https://velog.io/@s_soo100/LLM-API%EB%A1%9C-%EC%95%B1%EC%97%90-AI-%EC%B1%84%ED%8C%85-%EB%84%A3%EA%B8%B0-Groq-Flutter-%EC%8B%A4%EC%A0%84</link>
            <guid>https://velog.io/@s_soo100/LLM-API%EB%A1%9C-%EC%95%B1%EC%97%90-AI-%EC%B1%84%ED%8C%85-%EB%84%A3%EA%B8%B0-Groq-Flutter-%EC%8B%A4%EC%A0%84</guid>
            <pubDate>Fri, 10 Apr 2026 09:45:39 GMT</pubDate>
            <description><![CDATA[<h1 id="앱-안에-ai-채팅을-넣었습니다--내부-데이터-우선-llm은-보조">앱 안에 AI 채팅을 넣었습니다 — 내부 데이터 우선, LLM은 보조</h1>
<blockquote>
<p>앱에 이미 쌓아둔 데이터를 먼저 쓰고, 거기서 답이 안 나올 때만 LLM을 부르는 하이브리드 채팅을 Flutter로 만든 과정입니다. 
지금은 혼자 보니까 무료 API(Groq)만 갖고 돌아갑니다.</p>
</blockquote>
<hr>
<h2 id="정적-데이터만-갖고-부족한데-어쩌지">정적 데이터만 갖고 부족한데 어쩌지?</h2>
<p>개인적으로 키우는 도마뱀과 동물들을 위해 반려동물 건강 가이드 앱을 만들고 있습니다. 앱 안에 동물 별 건강 정보를 JSON으로 넣어뒀는데, 이런 질문에는 답을 줄 수 없었습니다.</p>
<ul>
<li>&quot;골든리트리버 관절 영양제 뭐가 좋아?&quot;</li>
<li>&quot;강아지가 구토를 하는데 언제 병원 가야 해?&quot;</li>
<li>&quot;포메라니안 슬개골 탈구 예방법은?&quot;</li>
</ul>
<p>정적 위키는 &quot;ㅁㅁ견종의 적정 체중: 5~8kg&quot;이라고만 알려줍니다. <strong>상황에 따른 구체적 질문</strong>에는 무력합니다.</p>
<p>그렇다고 LLM에게 모든 걸 맡기면? 앱 안에 검증해둔 데이터가 있는데 그걸 무시하고 LLM의 일반 지식에만 의존하게 됩니다. 할루시네이션 위험은 덤이고요.</p>
<p><strong>원하는 구조는 이겁니다:</strong></p>
<pre><code>사용자 질문
  → 앱 내부 데이터에서 관련 정보 찾기
  → 있으면: 그 데이터를 LLM에게 컨텍스트로 넘겨서 답변 생성
  → 없으면: LLM 일반 지식으로 답변 + &quot;일반 지식 기반&quot; 표시</code></pre><p>LLM을 &quot;만능 답변기&quot;로 쓰는 게 아니라, <strong>내 데이터를 읽고 설명해주는 도우미</strong>로 쓰는 겁니다.</p>
<hr>
<h2 id="전체-구조">전체 구조</h2>
<pre><code>┌──────────────────────────────────────────┐
│            사용자 질문 입력                  │
└───────────────┬──────────────────────────┘
                │
                ▼
┌──────────────────────────────────────────┐
│  1. 질문 분석 — 무슨 주제에 대한 질문인가?        │
│     키워드 매칭으로 품종(breed) + 카테고리 감지   │
└───────────────┬──────────────────────────┘
                │
                ▼
┌──────────────────────────────────────────┐
│  2. 내부 데이터 검색                         │
│     감지된 품종 + 카테고리로 JSON 데이터 조회     │
│     → 관련 스니펫 추출                       │
└───────────────┬──────────────────────────┘
                │
        ┌───────┴───────┐
        ▼               ▼
   데이터 있음        데이터 없음
        │               │
        ▼               ▼
┌─────────────┐  ┌─────────────┐
│ 시스템 프롬프트 │  │ 시스템 프롬프트 │
│ + 내부 데이터  │  │ (데이터 없이)  │
│ + 질문       │  │ + 질문       │
└──────┬──────┘  └──────┬──────┘
       │                │
       └───────┬────────┘
               ▼
┌──────────────────────────────────────────┐
│  3. LLM API 호출 (Groq, 무료)              │
└───────────────┬──────────────────────────┘
                │
                ▼
┌──────────────────────────────────────────┐
│  4. 앱 코드가 출처/경고를 강제 첨부              │
│     데이터 있었음 → &quot;📎 출처: PetMD...&quot;       │
│     데이터 없었음 → &quot;⚠️ 일반 지식 기반&quot;         │
└──────────────────────────────────────────┘</code></pre><p>핵심은 <strong>LLM을 항상 부르되, 내부 데이터가 있으면 컨텍스트로 넘긴다</strong>는 점입니다. 내부 데이터만으로 답변을 완성하는 게 아니라, LLM이 내부 데이터를 &quot;참고&quot;해서 자연어 답변을 만들도록 하는 구조입니다.</p>
<hr>
<h2 id="1단계-무료-llm-프로바이더-세팅">1단계: 무료 LLM 프로바이더 세팅</h2>
<h3 id="groq를-선택한-이유">Groq를 선택한 이유</h3>
<table>
<thead>
<tr>
<th>프로바이더</th>
<th>무료 티어</th>
<th>OpenAI 호환</th>
<th>한국어</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Groq</strong></td>
<td>1,000 요청/일, 카드 불필요</td>
<td>✅</td>
<td>양호</td>
</tr>
<tr>
<td>DeepSeek</td>
<td>크레딧제</td>
<td>✅</td>
<td>양호</td>
</tr>
<tr>
<td>Gemini</td>
<td>15 RPM</td>
<td>❌ (자체 SDK)</td>
<td>중간</td>
</tr>
<tr>
<td>OpenAI</td>
<td>유료</td>
<td>네이티브</td>
<td>우수</td>
</tr>
</tbody></table>
<p>Groq는 가입만 하면 하루 1,000번 무료로 쓸 수 있고, OpenAI 호환 API라서 나중에 프로바이더를 바꾸고 싶으면 URL과 모델명만 교체하면 됩니다.</p>
<h3 id="flutter에서-api-호출">Flutter에서 API 호출</h3>
<pre><code class="language-dart">// llm_api_repository.dart
Future&lt;String&gt; sendChat(List&lt;Map&lt;String, String&gt;&gt; messages) async {
  final response = await http.post(
    Uri.parse(&#39;https://api.groq.com/openai/v1/chat/completions&#39;),
    headers: {
      &#39;Authorization&#39;: &#39;Bearer $apiKey&#39;,
      &#39;Content-Type&#39;: &#39;application/json&#39;,
    },
    body: jsonEncode({
      &#39;model&#39;: &#39;llama-3.3-70b-versatile&#39;,
      &#39;messages&#39;: messages,
      &#39;max_tokens&#39;: 1024,
      &#39;temperature&#39;: 0.3,  // 사실 기반 답변 → 낮게
    }),
  );

  final data = jsonDecode(response.body);
  return data[&#39;choices&#39;][0][&#39;message&#39;][&#39;content&#39;];
}</code></pre>
<p>OpenAI SDK 없이 <code>http</code> 패키지 하나로 충분합니다. <code>temperature: 0.3</code>은 창의적 답변보다 정확한 답변이 중요한 도메인 특화 앱에 맞는 설정입니다.</p>
<hr>
<h2 id="2단계-질문-분석--키워드-매칭">2단계: 질문 분석 — 키워드 매칭</h2>
<p>LLM에게 &quot;이 질문이 무슨 주제야?&quot;라고 물어볼 수도 있지만, 그러면 API를 두 번 부르게 됩니다. 대신 키워드 매칭으로 로컬에서 처리합니다.</p>
<pre><code class="language-dart">// context_builder.dart

// 카테고리 키워드
static const _categoryKeywords = {
  &#39;joints&#39;:      [&#39;관절&#39;, &#39;슬개골&#39;, &#39;고관절&#39;, &#39;디스크&#39;, &#39;연골&#39;],
  &#39;skin&#39;:        [&#39;피부&#39;, &#39;알러지&#39;, &#39;탈모&#39;, &#39;가려움&#39;, &#39;아토피&#39;],
  &#39;diet&#39;:        [&#39;사료&#39;, &#39;간식&#39;, &#39;영양제&#39;, &#39;칼슘&#39;, &#39;급여량&#39;],
  &#39;vaccination&#39;: [&#39;백신&#39;, &#39;접종&#39;, &#39;예방&#39;, &#39;항체&#39;, &#39;부스터&#39;],
  // ...
};

Set&lt;String&gt; detectCategories(String question) {
  final matched = &lt;String&gt;{};
  for (final entry in _categoryKeywords.entries) {
    for (final keyword in entry.value) {
      if (question.contains(keyword)) {
        matched.add(entry.key);
        break;
      }
    }
  }
  return matched;
}</code></pre>
<p>&quot;골든리트리버 관절 영양제 뭐가 좋아?&quot; → <code>joints</code> + <code>diet</code> 카테고리 감지. 이 결과로 내부 데이터에서 관절·영양 관련 정보만 꺼냅니다.</p>
<h3 id="후속-질문-대응">후속 질문 대응</h3>
<p>&quot;관절 영양제 뭐가 좋아?&quot; 다음에 &quot;그러면 겨울에는?&quot;이라고 물으면 키워드가 없습니다. 이때는 이전 대화 4개를 스캔해서 카테고리를 계승합니다.</p>
<pre><code class="language-dart">if (categories.isEmpty) {
  final history = chatRepo.getRecentMessages(conversationId, limit: 4);
  for (final msg in history) {
    categories = detectCategories(msg.content);
    if (categories.isNotEmpty) break;
  }
}</code></pre>
<p>형태소 분석 없는 단순 키워드 매칭이지만, 도메인이 좁으면 이 정도로 충분합니다.</p>
<hr>
<h2 id="3단계-컨텍스트-빌딩--핵심">3단계: 컨텍스트 빌딩 — 핵심</h2>
<p>여기가 이 구조의 가치를 결정하는 부분입니다. <strong>같은 LLM이라도 컨텍스트 품질에 따라 답변이 극적으로 달라집니다.</strong></p>
<pre><code class="language-dart">BuildContextResult buildContext(String question, String? breedId) {
  // 1. 품종 + 카테고리 감지
  final breed = detectBreed(question) ?? breedId;
  final categories = detectCategories(question);

  // 2. 내부 데이터에서 관련 스니펫 추출
  String? healthSnippet;
  List&lt;String&gt; sources = [];

  if (breed != null &amp;&amp; categories.isNotEmpty) {
    final healthInfo = healthInfoRepo.getHealthInfo(breed);
    healthSnippet = extractSnippet(healthInfo, categories);
    sources = filterSources(categories);  // 카테고리 관련 출처만
  }

  // 3. 시스템 프롬프트 조립
  final systemPrompt = &#39;&#39;&#39;
반려동물 건강 전문 AI. 주요 견종별 건강 정보 제공.
${healthSnippet != null ? &#39;\n[앱 데이터]\n$healthSnippet&#39; : &#39;&#39;}

규칙:
- [앱 데이터]가 있으면 참고하되, &quot;앱 데이터에 따르면&quot; 같은 메타 언급 없이 바로 답변.
- [앱 데이터]가 없어도 일반 지식으로 성실히 답변.
- 한국어, 간결체. 출처표기 금지 — 출처는 앱이 자동으로 붙입니다.
&#39;&#39;&#39;;

  return BuildContextResult(
    messages: [
      {&#39;role&#39;: &#39;system&#39;, &#39;content&#39;: systemPrompt},
      ...recentHistory,  // 최근 6개 메시지
      {&#39;role&#39;: &#39;user&#39;, &#39;content&#39;: question},
    ],
    hasHealthData: healthSnippet != null,
    sources: sources,
  );
}</code></pre>
<p>포인트 세 가지:</p>
<p><strong>① 내부 데이터를 시스템 프롬프트에 <code>[앱 데이터]</code>로 삽입합니다.</strong> LLM은 이걸 &quot;자기가 아는 것&quot;처럼 자연스럽게 답변에 녹여냅니다.</p>
<p><strong>② <code>hasHealthData</code> 플래그를 반환합니다.</strong> 이 값으로 나중에 &quot;출처 있음/없음&quot;을 앱 코드가 결정합니다. LLM에게 판단을 맡기지 않습니다.</p>
<p><strong>③ 출처도 카테고리별로 필터링합니다.</strong> 관절 질문에 백신 관련 출처를 보여주면 신뢰가 떨어집니다.</p>
<hr>
<h2 id="4단계-출처와-신뢰도--llm에게-맡기지-않는-것들">4단계: 출처와 신뢰도 — LLM에게 맡기지 않는 것들</h2>
<p>이 구조에서 가장 중요한 설계 결정입니다.</p>
<p>처음에는 프롬프트에 &quot;답변 끝에 출처를 달아줘&quot;라고 시켰습니다. 결과:</p>
<table>
<thead>
<tr>
<th>시도</th>
<th>프롬프트</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>v1</td>
<td>&quot;답변 기반이 앱 데이터인지 구분 표시해줘&quot;</td>
<td>&quot;앱 데이터에 따르면...&quot; 장황한 서문</td>
</tr>
<tr>
<td>v3</td>
<td>&quot;출처 규칙 5개항 상세 지시&quot;</td>
<td>규칙을 일관되게 안 따름</td>
</tr>
<tr>
<td>v4</td>
<td><strong>&quot;출처표기 금지 — 출처는 앱이 자동으로 붙입니다&quot;</strong></td>
<td><strong>해결</strong></td>
</tr>
</tbody></table>
<p><strong>LLM은 답변 생성에만 집중시키고, 메타데이터는 앱 코드가 처리합니다:</strong></p>
<pre><code class="language-dart">// chat_providers.dart — AI 응답 수신 후
String finalContent = aiResponse;

if (contextResult.hasHealthData) {
  final sourceText = contextResult.sources
      .map((s) =&gt; &#39;- $s&#39;)
      .join(&#39;\n&#39;);
  finalContent += &#39;\n\n📎 출처:\n$sourceText&#39;;
} else {
  finalContent += &#39;\n\n⚠️ 일반 지식 기반 답변입니다&#39;;
}</code></pre>
<p>이렇게 하면 출처 표시 일관성이 100%가 됩니다. LLM 재량에 맡겼을 때는 40% 정도였습니다.</p>
<p><strong>사용자가 보는 화면:</strong></p>
<ul>
<li>내부 데이터 기반 답변 → 답변 끝에 <code>📎 출처: PetMD, AKC Canine Health</code></li>
<li>일반 지식 답변 → 답변 끝에 <code>⚠️ 일반 지식 기반 답변입니다</code></li>
</ul>
<p>사용자는 <strong>이 답변이 검증된 데이터에 근거한 건지, LLM의 일반 지식인지</strong> 바로 알 수 있습니다.</p>
<hr>
<h2 id="5단계-대화에서-지식-쌓기">5단계: 대화에서 지식 쌓기</h2>
<p>여기까지면 &quot;잘 만든 챗봇&quot;입니다. 한 단계 더 간 부분은 <strong>대화에서 재사용 가능한 지식을 자동으로 추출·축적</strong>하는 시스템입니다.</p>
<pre><code>Day 1: &quot;강아지 피부에 빨간 반점이 생겼어&quot; → AI 답변 → 지식으로 저장 (confidence: 0.5)
Day 3: &quot;알러지인지 어떻게 구분해?&quot;       → 기존 지식을 컨텍스트에 추가 → 더 정확한 답변
Day 10: 비슷한 피부 질문               → 캐시 히트! API 호출 없이 즉시 응답</code></pre><p>반복 질문에 대한 답변이 점점 좋아지고, 일정 수준 이상이면 API를 부르지 않아 한도도 아낍니다.</p>
<h3 id="오답이-영원히-반복되면">오답이 영원히 반복되면?</h3>
<p>이 시스템의 가장 큰 위험입니다. 틀린 답변이 캐시되면 계속 틀린 답을 줍니다. 차단 장치 세 가지:</p>
<ol>
<li><strong>confidence 최대 0.9</strong> — 자동으로 1.0에 도달 불가 (사람 검증 없이는)</li>
<li><strong>&quot;부정확&quot; 버튼</strong> — 사용자가 누르면 confidence -0.3</li>
<li><strong>자동 삭제</strong> — confidence ≤ 0.1이면 엔트리 삭제 (신고 3회면 거의 확실히 삭제)</li>
</ol>
<hr>
<h2 id="토큰-예산--무료-티어에서-살아남기">토큰 예산 — 무료 티어에서 살아남기</h2>
<p>Groq 무료 티어는 하루 1,000 요청, 500K 토큰입니다. 한 번 요청에 얼마나 쓰는지 관리해야 합니다.</p>
<table>
<thead>
<tr>
<th>세그먼트</th>
<th>~토큰</th>
</tr>
</thead>
<tbody><tr>
<td>시스템 프롬프트</td>
<td>200</td>
</tr>
<tr>
<td>내부 데이터 스니펫</td>
<td>400~800</td>
</tr>
<tr>
<td>대화 히스토리 (최근 6개)</td>
<td>800~1,200</td>
</tr>
<tr>
<td>사용자 질문</td>
<td>50~100</td>
</tr>
<tr>
<td><strong>응답 예산</strong></td>
<td><strong>1,024</strong></td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td><strong><del>3,000</del>4,000</strong></td>
</tr>
</tbody></table>
<p>Groq의 Llama 3.3 70B는 128K 컨텍스트이므로 매우 여유 있습니다. 내부 데이터 스니펫을 카테고리별로 필터링하는 이유가 여기에도 있습니다 — 전체 데이터를 때려넣으면 토큰 낭비입니다.</p>
<hr>
<h2 id="이-구조를-다른-앱에-쓰려면">이 구조를 다른 앱에 쓰려면</h2>
<p>도메인에 종속된 부분과 아닌 부분을 분리하면 재사용할 수 있습니다.</p>
<p><strong>그대로 쓸 수 있는 것:</strong></p>
<ul>
<li>LLM API 호출 모듈 (OpenAI 호환이면 URL만 교체)</li>
<li>지식 축적/캐시 시스템 (confidence + 피드백)</li>
<li>출처 강제 첨부 패턴 (hasHealthData 분기)</li>
<li>대화 저장/한도 관리</li>
</ul>
<p><strong>도메인별로 바꿔야 하는 것:</strong></p>
<ul>
<li>카테고리 키워드 목록</li>
<li>엔티티 키워드 (여기서는 견종)</li>
<li>시스템 프롬프트</li>
<li>내부 데이터 구조와 스니펫 빌더</li>
<li>출처 URL 매핑</li>
</ul>
<p>인터페이스 하나로 정리하면:</p>
<pre><code class="language-dart">abstract class ChatConfig {
  String get systemPrompt;
  Map&lt;String, List&lt;String&gt;&gt; get categoryKeywords;
  Map&lt;String, List&lt;String&gt;&gt; get entityKeywords;
  Future&lt;({String snippet, List&lt;String&gt; sources})&gt; buildDataSnippet(
    String entityId, Set&lt;String&gt; categories);
}</code></pre>
<p>이 인터페이스만 구현하면 요리 레시피 앱이든, 건강 관리 앱이든, 법률 상담 앱이든 같은 구조로 돌아갑니다.</p>
<hr>
<h2 id="결과물-미리보기">결과물 미리보기</h2>
<ul>
<li>조금 더 다듬어야 하겠지만, 컨텍스트도 잘 이어지고 원하는대로 잘 동작하고 있습니다.</li>
<li>예시 질문은 레오파드 게코로 했습니다. 귀엽거든요.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/454f141c-1262-4d29-97a3-83930208c4d0/image.png" alt=""></p>
<hr>
<h2 id="정리">정리</h2>
<ol>
<li><strong>LLM을 만능으로 쓰지 않습니다.</strong> 내부 데이터가 있으면 컨텍스트로 넘기고, LLM은 그걸 자연어로 풀어주는 역할만 합니다.</li>
<li><strong>메타데이터는 앱 코드가 결정합니다.</strong> 출처 표기, 신뢰도 표시를 LLM에게 맡기면 일관성이 깨집니다. 코드로 강제하면 100%입니다.</li>
<li><strong>무료 API로 충분합니다.</strong> Groq 무료 티어 + 키워드 기반 컨텍스트 빌딩 + 지식 캐시로, 비용 없이 쓸 만한 AI 채팅을 앱에 넣을 수 있습니다.
(나중에 유저가 많아지면 갈아끼워요!)</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI 에이전트에게 "넌 전문가야"라고 말하면 진짜 잘할까? — 논문과 실전이 말하는 불편한 진실]]></title>
            <link>https://velog.io/@s_soo100/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EB%84%8C-%EC%A0%84%EB%AC%B8%EA%B0%80%EC%95%BC%EB%9D%BC%EA%B3%A0-%EB%A7%90%ED%95%98%EB%A9%B4-%EC%A7%84%EC%A7%9C-%EC%9E%98%ED%95%A0%EA%B9%8C-%EB%85%BC%EB%AC%B8%EA%B3%BC-%EC%8B%A4%EC%A0%84%EC%9D%B4-%EB%A7%90%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%A7%84%EC%8B%A4</link>
            <guid>https://velog.io/@s_soo100/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EB%84%8C-%EC%A0%84%EB%AC%B8%EA%B0%80%EC%95%BC%EB%9D%BC%EA%B3%A0-%EB%A7%90%ED%95%98%EB%A9%B4-%EC%A7%84%EC%A7%9C-%EC%9E%98%ED%95%A0%EA%B9%8C-%EB%85%BC%EB%AC%B8%EA%B3%BC-%EC%8B%A4%EC%A0%84%EC%9D%B4-%EB%A7%90%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%A7%84%EC%8B%A4</guid>
            <pubDate>Wed, 08 Apr 2026 10:37:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>저는 페르소나 설정의 신봉자였습니다. 모든 AI 에이전트에 정체성을 부여하고, 그게 당연히 성능을 높인다고 믿었습니다. 그런데 논문을 파보니, 이 믿음이 반만 맞았습니다.</p>
</blockquote>
<hr>
<h2 id="저는-이렇게-쓰고-있었습니다">저는 이렇게 쓰고 있었습니다</h2>
<p>저는 Claude Code 위에 CAOF(Claude Agent Orchestration Framework)라는 에이전트 오케스트레이션 시스템을 운영하고 있습니다. 게임 기획 에이전트, Unity 구현 에이전트, 웹앱 개발 에이전트 등 역할별로 전문 에이전트를 나누고, 각각에 페르소나를 부여합니다.</p>
<p>예를 들면 이런 식입니다:</p>
<ul>
<li><strong>game-designer</strong>: &quot;넌 비판적 기획자야. 기획 의도를 검증하고, 스코프가 현실적인지 따져&quot;</li>
<li><strong>unity-game-coder</strong>: &quot;넌 Unity C# 전문가야. 최소 변경 원칙을 따르고, New Input System만 써&quot;</li>
<li><strong>content-adapter</strong>: &quot;넌 콘텐츠 편집자야. 원본의 톤을 살리면서 플랫폼에 맞게 변환해&quot;</li>
</ul>
<p>이게 잘 작동한다고 느꼈고, 페르소나가 핵심이라고 생각했습니다. 그런데 &quot;왜 잘 되는 거지?&quot;를 논문으로 파보니, 제가 생각한 이유와 실제 이유가 달랐습니다.</p>
<hr>
<h2 id="페르소나-효과-있냐는-잘못된-질문이었습니다">&quot;페르소나 효과 있냐?&quot;는 잘못된 질문이었습니다</h2>
<p>&quot;AI에게 페르소나를 주면 효과가 있냐?&quot;는 &quot;약을 먹으면 낫냐?&quot;와 같은 질문입니다. 어떤 약을, 어떤 증상에 쓰느냐에 따라 답이 달라지듯, 페르소나도 태스크에 따라 정반대로 작용합니다.</p>
<p>USC 연구팀의 PRISM 논문(2026.03)이 이걸 명확하게 보여줬습니다.</p>
<table>
<thead>
<tr>
<th>태스크 유형</th>
<th>페르소나 효과</th>
</tr>
</thead>
<tbody><tr>
<td>글쓰기, 톤/스타일, 안전성</td>
<td><strong>향상</strong></td>
</tr>
<tr>
<td>수학, 코딩, 팩트 기반 QA</td>
<td><strong>저하</strong></td>
</tr>
<tr>
<td>서사 생성, 공감적 대화</td>
<td><strong>향상</strong></td>
</tr>
<tr>
<td>순수 논리 추론</td>
<td><strong>불안정</strong> (향상/저하 반반)</td>
</tr>
</tbody></table>
<p>이유는 직관적입니다. &quot;넌 전문가야&quot;라고 말해서 전문 지식이 생기는 건 아닙니다. 페르소나는 모델의 출력 분포를 특정 방향으로 밀어버리는데, 이게 스타일에는 도움이 되지만 팩트 검색에는 오히려 노이즈가 됩니다.</p>
<hr>
<h2 id="논문들이-말하는-5가지-원칙">논문들이 말하는 5가지 원칙</h2>
<p>아래 내용은 제 주장이 아닙니다. PRISM, Jekyll &amp; Hyde, RRP 등 관련 논문과 실전 사례를 종합한 결과입니다.</p>
<h3 id="1-스타일이-중요한-곳에만-페르소나를-씁니다">1. 스타일이 중요한 곳에만 페르소나를 씁니다</h3>
<p>페르소나가 효과적인 영역은 <strong>정렬(alignment) 태스크</strong>입니다. 톤 유지, 규칙 준수, 역할극, 글쓰기 — 이런 곳에서 페르소나는 확실히 작동합니다.</p>
<p>반대로, 코딩 정확도나 수학 문제 풀이에는 페르소나 대신 <strong>구체적인 규칙과 예시</strong>를 주는 게 낫습니다.</p>
<h3 id="2-모호한-페르소나는-효과가-없습니다">2. 모호한 페르소나는 효과가 없습니다</h3>
<pre><code>❌ &quot;넌 전문 개발자야.&quot;
✅ &quot;넌 React 테스트 엔지니어로, 소스코드는 절대 수정하지 않고, 아래 예시를 따른다.&quot;</code></pre><p>Agentic Thinking의 가이드에서 정리한 좋은 페르소나의 조건은 두 가지입니다:</p>
<ul>
<li><strong>Expertise</strong>: 에이전트가 깊이 아는 것 — 구체적이고 범위가 제한된 지식</li>
<li><strong>Process</strong>: 출력을 일관되게 만드는 단계별 방법론</li>
</ul>
<h3 id="3-페르소나--규칙-시스템을-결합해야-합니다">3. 페르소나 + 규칙 시스템을 결합해야 합니다</h3>
<p>Rule-based Role Prompting(RRP) 논문이 이걸 증명했습니다. 단독 페르소나보다 <strong>character-card + scene-contract + function calling 강제</strong>를 조합했을 때 성능이 가장 높았습니다(대화 에이전트 점수 0.571 vs 제로샷 0.519).</p>
<p>ESLint 창시자 Nicholas Zakas도 비슷한 결론에 도달했습니다. AI를 단일 어시스턴트가 아니라 <strong>전문가 팀</strong>으로 운영하되, 각 역할에 구체적인 규칙과 프로세스를 묶었을 때 생산성이 올라갔다고 합니다.</p>
<h3 id="4-llm이-만든-페르소나가-수작업보다-안정적입니다">4. LLM이 만든 페르소나가 수작업보다 안정적입니다</h3>
<p>Jekyll &amp; Hyde 논문(2024.08)과 PromptHub 테스트에서 공통으로 확인된 결과입니다. LLM에게 &quot;이 태스크에 최적화된 페르소나를 만들어줘&quot;라고 요청한 것이, 사람이 직접 설계한 것보다 더 안정적이었습니다.</p>
<p>직접 페르소나를 깎아 만들 시간에, LLM에게 초안을 뽑게 하고 그걸 다듬는 게 효율적입니다.</p>
<h3 id="5-코딩-에이전트는-정체성-선언보다-규칙에-무게를-둡니다">5. 코딩 에이전트는 정체성 선언보다 규칙에 무게를 둡니다</h3>
<p>RRP 논문의 결론은 명확합니다 — 페르소나 단독은 별로지만, <strong>페르소나 + 규칙 시스템의 조합</strong>이 최고 성능이었습니다. 그리고 코딩처럼 사전학습 지식에 의존하는 태스크에서는 그 비율이 규칙 쪽으로 기울어야 합니다.</p>
<p>이걸 알고 나서 제 에이전트 파일을 다시 보니, 흥미로운 패턴이 보였습니다:</p>
<ul>
<li><strong>기획 에이전트</strong> (game-designer): 정체성 선언(&quot;넌 비판적 기획자야&quot;)의 비중이 큼 → 톤, 관점 유지에 기여</li>
<li><strong>구현 에이전트</strong> (unity-game-coder): 정체성 선언은 짧고, 대부분이 &quot;New Input System만 써라&quot;, &quot;최소 변경 원칙&quot;, &quot;isCompleting 가드 필수&quot; 같은 구체적 규칙</li>
</ul>
<p>페르소나를 빼라는 게 아닙니다. 논문이 말하는 건, 코딩 에이전트에서는 <strong>&quot;넌 전문가야&quot;보다 &quot;이 규칙을 따라&quot;가 성능에 더 크게 기여한다</strong>는 것입니다. 저는 무의식적으로 이 비율을 맞추고 있었는데, 논문을 읽고 나서야 그 이유를 이해했습니다.</p>
<hr>
<h2 id="한-장으로-정리">한 장으로 정리</h2>
<pre><code>페르소나가 효과적인 곳     페르소나가 위험한 곳
━━━━━━━━━━━━━━━━━━    ━━━━━━━━━━━━━━━━━
톤/스타일 유지            코딩 정확도
규칙 준수                수학/논리 추론
역할극/서사 생성          팩트 기반 QA
공감적 대화              일반 지식 검색</code></pre><p>저는 여전히 모든 에이전트에 페르소나를 부여합니다. 다만 논문을 읽기 전에는 &quot;페르소나가 효과적이다&quot;라고만 생각했고, 읽은 후에는 <strong>&quot;왜 효과적인 곳과 아닌 곳이 나뉘는지&quot;</strong>를 이해하게 되었습니다.</p>
<p>&quot;넌 전문가야&quot;는 양날의 검입니다. 중요한 건 페르소나를 쓰냐 마냐가 아니라, <strong>어떤 태스크에서 페르소나와 규칙의 비율을 어떻게 잡느냐</strong>입니다.</p>
<hr>
<h3 id="참고-논문자료">참고 논문/자료</h3>
<ul>
<li><a href="https://arxiv.org/abs/2603.18507">PRISM: Expert Personas Improve LLM Alignment but Damage Accuracy</a> — USC, 2026.03</li>
<li><a href="https://arxiv.org/abs/2408.08631">Persona is a Double-edged Sword (Jekyll &amp; Hyde)</a> — 2024.08</li>
<li><a href="https://arxiv.org/abs/2509.00482">Rule-based Role Prompting (RRP)</a> — 2025.09</li>
<li><a href="https://arxiv.org/abs/2307.11760">EmotionPrompt: LLMs Understand Emotional Stimuli</a> — 2023.07</li>
<li><a href="https://humanwhocodes.com/blog/2025/06/persona-based-approach-ai-assisted-programming/">Nicholas Zakas: Persona-based AI-Assisted Programming</a></li>
<li><a href="https://agenticthinking.ai/blog/agent-personas/">Agentic Thinking: Agent Personas That Actually Work</a></li>
</ul>
<p>#AI실전 #프롬프트엔지니어링 #에이전트 #개발생산성 #ClaudeCode</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[교차 모델 리뷰 — 자기 동의 편향 깨기]]></title>
            <link>https://velog.io/@s_soo100/%EA%B5%90%EC%B0%A8-%EB%AA%A8%EB%8D%B8-%EB%A6%AC%EB%B7%B0-%EC%9E%90%EA%B8%B0-%EB%8F%99%EC%9D%98-%ED%8E%B8%ED%96%A5-%EA%B9%A8%EA%B8%B0</link>
            <guid>https://velog.io/@s_soo100/%EA%B5%90%EC%B0%A8-%EB%AA%A8%EB%8D%B8-%EB%A6%AC%EB%B7%B0-%EC%9E%90%EA%B8%B0-%EB%8F%99%EC%9D%98-%ED%8E%B8%ED%96%A5-%EA%B9%A8%EA%B8%B0</guid>
            <pubDate>Wed, 01 Apr 2026 09:59:36 GMT</pubDate>
            <description><![CDATA[<h2 id="핵심-요약">핵심 요약</h2>
<p>같은 모델이 자기 코드를 리뷰하면 결함을 못 잡는다. <strong>다른 모델로 리뷰</strong>하면 검출률이 올라간다.</p>
<h2 id="패턴">패턴</h2>
<h3 id="문제-자기-동의-편향-self-agreement-bias">문제: 자기 동의 편향 (Self-Agreement Bias)</h3>
<pre><code>Claude Opus가 코드 작성 → Claude Opus가 리뷰
→ &quot;네, 이 코드는 잘 작성되었습니다&quot; (자기가 쓴 거니까)</code></pre><p>사람도 자기 글을 교정할 때 오타를 못 보는 것과 같은 원리.
AI는 이 편향이 더 심하다 — 같은 모델은 같은 사고 패턴을 공유하므로, 같은 맹점을 가진다.</p>
<h3 id="해결-4모델-병렬-교차-리뷰">해결: 4모델 병렬 교차 리뷰</h3>
<pre><code>작업 완료 → Gemini가 리뷰 (구조적 관점)
          → Claude Opus가 리뷰 (깊은 논리 분석)
          → Claude Sonnet이 리뷰 (실용적 관점)
          → Claude Haiku가 리뷰 (직관적 필터)
          → 4개 결과 병합</code></pre><p>모델마다 역할이 다르다:
| 모델 | 역할 | 강점 |
|------|------|------|
| <strong>Gemini</strong> (API) | 구조적 일관성 | 수치 불일치, 네이밍 비일관성, 패턴 위반 |
| <strong>Claude Opus</strong> | 깊은 논리 분석 | 엣지케이스, 비즈니스 로직, 설계 판단 |
| <strong>Claude Sonnet</strong> | 실용적 관점 | 과잉 설계, 단순화, 구현 현실성 |
| <strong>Claude Haiku</strong> | 직관적 필터 | &quot;이거 정말 필요해?&quot;, 최대 위험 하나 |</p>
<h3 id="실전-구현">실전 구현</h3>
<h4 id="ideabank의-검수-시스템-4모델">ideaBank의 <code>/검수</code> 시스템 (4모델)</h4>
<pre><code>기획 검수:  Gemini + Opus + Sonnet + Haiku → 4개 병렬 실행 → 종합 보고서
코드 검수:  Gemini + Opus + Sonnet + Haiku → 4개 병렬 실행 → 종합 보고서</code></pre><p>보고서 5종 생성:</p>
<ol>
<li><code>*-gemini.md</code> — 구조적 일관성 발견</li>
<li><code>*-claude-opus.md</code> — 깊은 논리 분석 발견</li>
<li><code>*-claude-sonnet.md</code> — 실용성 평가</li>
<li><code>*-claude-haiku.md</code> — 직관적 판단</li>
<li><code>*-final.md</code> — 종합 (다수 공통 → 2모델 공통 → 단독 순으로 정리)</li>
</ol>
<h4 id="i-spider의-review-스킬">i-spider의 review 스킬</h4>
<ul>
<li>Architect(Opus)가 쓴 계획을 Implement(Sonnet)가 구현</li>
<li>Review 스킬이 &quot;계획 vs 실제&quot; 비교 — 다른 세션이므로 맥락이 분리됨</li>
<li>추가로 외부 모델(Codex 등)로 보완 리뷰 가능</li>
</ul>
<h2 id="결과">결과</h2>
<h3 id="교차-리뷰에서-실제로-잡힌-것들">교차 리뷰에서 실제로 잡힌 것들</h3>
<ul>
<li><strong>Gemini가 잡고 Claude가 놓친 것</strong>: 스펙 문서 간 수치 불일치, 네이밍 비일관성</li>
<li><strong>Claude가 잡고 Gemini가 놓친 것</strong>: 런타임 엣지케이스, 비동기 타이밍 이슈</li>
<li><strong>둘 다 잡은 것</strong>: 누락된 에러 처리, 미사용 코드 (이건 높은 신뢰도)</li>
<li><strong>Haiku의 직관</strong>: &quot;이 시스템 자체가 불필요하다&quot; 수준의 과감한 판단 (2026-03-20 개선안 검토에서 B, C를 깔끔하게 No 판정)</li>
</ul>
<h3 id="신뢰도-기준-4모델-체계">신뢰도 기준 (4모델 체계)</h3>
<ul>
<li><strong>3~4개 모델 공통 지적</strong>: 거의 확실한 결함 → 반드시 수정</li>
<li><strong>2개 모델 공통 지적</strong>: 높은 신뢰도 → 수정 권장</li>
<li><strong>1개 모델 단독 지적</strong>: 오탐 가능성 → 확인 후 판단</li>
</ul>
<h3 id="비용-대비-효과">비용 대비 효과</h3>
<ul>
<li>Gemini: 무료 한도 내 (Gemini Flash API)</li>
<li>Claude Opus/Sonnet/Haiku: 구독 내 포함</li>
<li>추가 비용 거의 없이 4개 관점 확보</li>
</ul>
<h2 id="교훈">교훈</h2>
<ol>
<li><strong>3개 이상 모델이 지적한 이슈는 거의 확실한 결함이다</strong> — 최우선 처리</li>
<li><strong>1개 모델만 지적한 이슈는 검토가 필요하다</strong> — 오탐(false positive)일 수 있음</li>
<li><strong>리뷰 결과는 파일로 저장한다</strong> — 대화 컨텍스트는 날아가지만 파일은 남는다</li>
<li><strong>자동화할수록 좋다</strong> — <code>/검수</code>처럼 한 명령으로 병렬 리뷰가 돌아가야 실제로 쓰게 된다</li>
</ol>
<h2 id="도입-최소-단위">도입 최소 단위</h2>
<pre><code class="language-bash"># 1단계: git diff를 다른 모델에게 보내기
git diff | ./tools/code-review.sh output.md  # Gemini 리뷰

# 2단계: 결과를 현재 세션에서 검토
# Claude가 Gemini 리뷰 결과를 읽고 동의/반박

# 3단계: /검수 커맨드로 원클릭 자동화</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI한테 코드 맡기고 롤백한 이야기 — 앱 개발에서 만난 5가지 함정]]></title>
            <link>https://velog.io/@s_soo100/AI%ED%95%9C%ED%85%8C-%EC%BD%94%EB%93%9C-%EB%A7%A1%EA%B8%B0%EA%B3%A0-%EB%A1%A4%EB%B0%B1%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-5%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95</link>
            <guid>https://velog.io/@s_soo100/AI%ED%95%9C%ED%85%8C-%EC%BD%94%EB%93%9C-%EB%A7%A1%EA%B8%B0%EA%B3%A0-%EB%A1%A4%EB%B0%B1%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-%EC%95%B1-%EA%B0%9C%EB%B0%9C%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%9C-5%EA%B0%80%EC%A7%80-%ED%95%A8%EC%A0%95</guid>
            <pubDate>Tue, 31 Mar 2026 09:01:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>AI 코딩 에이전트로 앱을 만들면서 겪은 실패 패턴 5가지. 코드를 대신 짜주는 건 해결됐지만, &quot;어떻게 시키느냐&quot;는 여전히 사람의 몫이었습니다.</p>
</blockquote>
<hr>
<h2 id="배경">배경</h2>
<p>AI 코딩 에이전트(Claude Code)로 사이드 프로젝트를 진행하고 있습니다. Flutter 앱과 React 웹앱을 동시에 개발하는데, UI 구현부터 상태 관리, API 연동까지 대부분의 코드를 AI가 작성합니다.</p>
<p>개발 속도는 확실히 빨라졌습니다. 하루 만에 로그인 화면, 리스트 UI, 상세 페이지, 상태 관리까지 돌아가는 프로토타입이 나왔으니까요.</p>
<p><strong>문제는 속도가 아니라 방향이었습니다.</strong> 빠르게 잘못된 곳으로 갈 수 있다는 걸, 하루 세션에서 여러 번 확인했습니다.</p>
<hr>
<h2 id="함정-1-개선해줘-→-화면-전체-재작성">함정 1: &quot;개선해줘&quot; → 화면 전체 재작성</h2>
<h3 id="무슨-일이-있었나">무슨 일이 있었나</h3>
<pre><code>나: &quot;이 주문 목록 화면 좀 개선해줘.&quot;
AI: 네, 전체 구조를 개선하겠습니다.
    - StatefulWidget → Riverpod AsyncNotifier로 전환
    - ListView → CustomScrollView + SliverList로 교체
    - 에러/로딩/빈 상태를 별도 위젯으로 분리
    ...</code></pre><p>이 한 마디에 AI가 화면 하나를 통째로 재작성했습니다. 상태 관리 방식이 바뀌었고, 스크롤 구조가 달라졌으며, 기존에 맞춰둔 애니메이션 타이밍이 전부 깨졌습니다.</p>
<p>결과? <strong>롤백하는 게 더 빠를 정도의 나쁜 수정.</strong></p>
<h3 id="왜-이렇게-됐나">왜 이렇게 됐나</h3>
<p>AI는 &quot;개선&quot;이라는 단어를 &quot;더 좋게 다시 짜기&quot;로 해석했습니다. 하지만 기존 코드에는 수십 번의 디바이스 테스트와 미세 조정이 녹아있었어요.</p>
<pre><code class="language-dart">// AI가 &quot;매직 넘버&quot;라고 판단하고 삭제한 코드
const kCardHeight = 72.0;    // 72px — 터치 타겟 최소 48px + 패딩. 갤럭시 폴드에서 검증
const kScrollThreshold = 0.85; // 85% — 0.9로 하면 느린 네트워크에서 로딩 스피너가 안 보임
const kAnimDuration = 180;    // 180ms — 200ms는 체감상 느림, 150ms는 끊겨 보임 (Galaxy A13 기준)

// AI가 &quot;깔끔하게&quot; 바꾼 코드
const kCardHeight = 56.0;      // Material 기본값
const kScrollThreshold = 0.9;  // 일반적인 값
const kAnimDuration = 300;     // Material 기본 애니메이션</code></pre>
<p>카드 높이, 스크롤 임계값, 애니메이션 시간 — 이것들은 여러 기기에서 테스트하고 나서 조정한 값이지 임의의 숫자가 아니었습니다.</p>
<h3 id="교훈">교훈</h3>
<blockquote>
<p><strong>&quot;개선&quot; 요청 전에, 진짜 문제가 뭔지 먼저 파악하세요.</strong></p>
</blockquote>
<p>실제로 급했던 건 목록 하단의 페이지네이션 버그였습니다. 화면 전체 재설계가 아니라 스크롤 리스너 조건문 하나 수정이면 될 일이었어요.</p>
<pre><code>// 이렇게 시켰어야 했다
나: &quot;주문 목록에서 마지막 페이지 로드가 안 돼.
    _onScroll 메서드의 페이지네이션 로직만 봐줘.
    다른 코드는 건드리지 마.&quot;</code></pre><p>AI에게 &quot;개선해줘&quot;라고 하면, AI는 자기가 할 수 있는 최대치를 합니다. <strong>변경 범위를 먼저 정해주지 않으면, 범위는 끝없이 커집니다.</strong></p>
<hr>
<h2 id="함정-2-코드만-보고-에셋설정은-안-봄">함정 2: 코드만 보고 에셋/설정은 안 봄</h2>
<h3 id="무슨-일이-있었나-1">무슨 일이 있었나</h3>
<p>앱에서 이미지가 간헐적으로 깨져서 나오는 문제가 있었습니다. AI는 이미지 로딩 코드를 분석하고 캐시 로직을 수정했습니다.</p>
<pre><code>나: &quot;프로필 이미지가 가끔 깨져서 나와.&quot;
AI: CachedNetworkImage의 캐시 정책을 분석해보겠습니다.
    → cacheManager의 maxAge가 너무 짧은 것이 원인입니다. 7일 → 30일로 수정했습니다.
나: &quot;아직 깨지는데?&quot;
AI: 추가로 placeholder와 errorWidget의 처리를 개선하겠습니다.
나: &quot;... 여전히 깨지는데?&quot;</code></pre><p>캐시 코드를 3번이나 수정했지만 <strong>핵심 문제는 그대로</strong>였습니다.</p>
<h3 id="왜-이렇게-됐나-1">왜 이렇게 됐나</h3>
<p>원인은 코드가 아니라 <strong>pubspec.yaml</strong>과 <strong>이미지 에셋</strong>이었습니다. <code>flutter_image_compress</code> 패키지 버전이 올라가면서 HEIC 포맷 처리가 바뀌었고, 특정 안드로이드 기기에서 변환이 실패하고 있었어요.</p>
<pre><code class="language-bash">$ git diff --stat
 pubspec.lock                    | 12 +++---  # ← AI가 무시한 변경
 lib/widgets/profile_image.dart  | 25 ++++++---
 lib/services/image_cache.dart   | 18 ++----</code></pre>
<p><code>git diff</code>에 <code>pubspec.lock</code> 변경이 찍혀 있었는데, AI는 <code>.dart</code> 파일에만 집중하느라 패키지 버전 변경을 무시했습니다.</p>
<h3 id="교훈-1">교훈</h3>
<blockquote>
<p><strong>&quot;코드가 맞는데 동작이 이상하다&quot; → 에셋·설정·패키지부터 확인 → 코드는 그 다음.</strong></p>
</blockquote>
<p>AI 코딩 에이전트는 본능적으로 코드를 먼저 봅니다. 하지만 앱 개발에서 &quot;특정 기기에서만 이상하다&quot;의 원인은 코드가 아니라 패키지 버전, 에셋 포맷, 빌드 설정인 경우가 많습니다. <code>git diff</code>에 <code>pubspec.lock</code>, <code>build.gradle</code>, <code>Podfile.lock</code> 변경이 있으면 반드시 확인하세요.</p>
<hr>
<h2 id="함정-3-ai가-만든-설계안을-그대로-믿을-뻔함">함정 3: AI가 만든 설계안을 그대로 믿을 뻔함</h2>
<h3 id="무슨-일이-있었나-2">무슨 일이 있었나</h3>
<p>앱이 커지면서 상태 관리가 복잡해졌고, AI에게 아키텍처 개선 보고서를 생성시켰습니다. 보고서가 꽤 그럴듯했어요:</p>
<pre><code class="language-markdown">## 아키텍처 개선 제안 (AI 생성)

1. Clean Architecture 도입 (Domain/Data/Presentation 3레이어 분리)
2. 모든 API 호출을 UseCase 클래스로 래핑
3. Repository 패턴 + DataSource 추상화
4. DI 컨테이너: get_it + injectable
5. 에러 처리: Either&lt;Failure, Success&gt; (dartz 패키지)
6. 라우팅: go_router + ShellRoute 기반 네비게이션 재설계</code></pre>
<p><strong>화면 8개, 개발자 1명인 사이드 프로젝트에 UseCase 클래스 20개, Repository 인터페이스 10개라니.</strong> 이건 대규모 엔터프라이즈 앱이지, 내 프로젝트가 아닙니다.</p>
<h3 id="왜-이렇게-됐나-2">왜 이렇게 됐나</h3>
<p>AI는 &quot;아키텍처 개선&quot;이라는 키워드만 보고 업계 베스트 프랙티스를 제안했습니다. 앱 규모, 팀 크기, 출시 일정 같은 맥락이 프롬프트에 충분히 들어가지 않았어요.</p>
<p>다행히 &quot;비판적으로 검수해봐&quot;라고 <strong>다른 모델</strong>에게 요청한 덕분에, 이런 피드백이 돌아왔습니다:</p>
<pre><code>검수 결과 (다른 모델):
❌ Clean Architecture 3레이어 — 화면 8개에 파일 수만 3배. 유지보수 비용 &gt; 이득
❌ UseCase 클래스 — API 호출을 그대로 전달하는 패스스루 클래스가 대부분일 것
❌ dartz Either — 러닝커브 대비 효과 미미. try-catch + sealed class로 충분
⚠️ Repository 패턴 — 서버가 1개인데 DataSource 추상화는 과잉. 직접 접근이 더 간단
✅ go_router — 네비게이션 개선은 유효. 단, ShellRoute 전면 재설계는 점진적으로</code></pre><p>요청하지 않았으면 그대로 설계에 반영했을 겁니다.</p>
<h3 id="교훈-2">교훈</h3>
<blockquote>
<p><strong>AI가 생성한 설계안은 &quot;초안&quot;이지 &quot;결정&quot;이 아닙니다.</strong></p>
</blockquote>
<p>AI는 &quot;기술적으로 가능한 최선&quot;을 제안하지, &quot;이 규모와 이 상황에서 현실적인 최선&quot;을 제안하지 않습니다. <strong>&quot;구현 가능한가&quot;뿐 아니라 &quot;우리 앱 규모에 맞는가&quot;를 반드시 검증하세요.</strong> 이 문제를 구조적으로 해결한 방법은 다음 글에서 다룹니다.</p>
<hr>
<h2 id="함정-4-문제를-새-패키지로-해결하려는-본능">함정 4: 문제를 새 패키지로 해결하려는 본능</h2>
<h3 id="무슨-일이-있었나-3">무슨 일이 있었나</h3>
<p>앱의 메인 리스트 화면이 버벅거린다는 피드백이 들어왔습니다.</p>
<pre><code>나: &quot;메인 피드가 스크롤할 때 버벅여. 개선 방안 제안해줘.&quot;
AI: 다음 3가지를 제안합니다:
    1. flutter_staggered_animations로 지연 렌더링 도입
    2. 상태 관리를 Provider → Riverpod으로 마이그레이션
    3. 이미지 로딩을 CachedNetworkImage → fast_cached_network_image로 교체</code></pre><p>전부 새로운 패키지 도입이나 대규모 마이그레이션이었습니다.</p>
<h3 id="왜-이렇게-됐나-3">왜 이렇게 됐나</h3>
<p>기존 코드를 살펴보니 이미 문제가 보였습니다:</p>
<pre><code class="language-dart">// 문제 1: ListView.builder를 안 쓰고 전체 리스트를 한 번에 빌드
Widget build(BuildContext context) {
  return ListView(
    children: items.map((item) =&gt; FeedCard(item: item)).toList(),
    // 아이템 200개면 → 위젯 200개를 한 번에 생성
  );
}

// 문제 2: 매 빌드마다 이미지 URL 파싱
class FeedCard extends StatelessWidget {
  Widget build(BuildContext context) {
    final url = Uri.parse(item.imageUrl);  // 스크롤할 때마다 파싱
    final resized = url.replace(queryParameters: {&#39;w&#39;: &#39;300&#39;});  // 매번 새 객체
    return CachedNetworkImage(imageUrl: resized.toString());
  }
}

// 문제 3: setState로 전체 화면 리빌드
void _onLike(String itemId) {
  setState(() {
    items = items.map((i) =&gt; i.id == itemId ? i.copyWith(liked: true) : i).toList();
    // 좋아요 1개 누르면 → 아이템 200개 전부 리빌드
  });
}</code></pre>
<p>실제 해결: <strong>새 패키지 0개.</strong></p>
<ul>
<li><code>ListView</code> → <code>ListView.builder</code> (화면에 보이는 것만 빌드)</li>
<li>이미지 URL 파싱을 모델 생성 시 한 번만 수행</li>
<li>좋아요를 개별 아이템의 <code>ValueNotifier</code>로 분리 → 해당 카드만 리빌드</li>
</ul>
<p>jank이 완전히 사라졌습니다.</p>
<h3 id="교훈-3">교훈</h3>
<blockquote>
<p><strong>&quot;화면이 버벅인다&quot; ≠ &quot;새 패키지가 필요하다&quot;</strong></p>
</blockquote>
<p>앱 개발자(그리고 AI)의 본능은 &quot;더 좋은 패키지로 교체&quot;이지만, 실제 필요한 건 기존 코드의 위젯 빌드 최적화인 경우가 많습니다.</p>
<p><strong>기존 코드에 이미 답이 있는데 새로 도입하려는 건, 사람도 AI도 똑같이 빠지는 함정입니다.</strong></p>
<hr>
<h2 id="잘-된-것-병렬-에이전트-대량-투입">잘 된 것: 병렬 에이전트 대량 투입</h2>
<p>실패 사례만 있는 건 아닙니다.</p>
<p>6개의 독립적인 화면을 구현할 때, AI 에이전트 6개를 동시에 투입했습니다.</p>
<pre><code>에이전트 A → 주문 상세 화면   (새 파일: order_detail_screen.dart)
에이전트 B → 결제 화면        (새 파일: payment_screen.dart)
에이전트 C → 리뷰 작성 화면   (새 파일: review_write_screen.dart)
에이전트 D → 알림 목록 화면   (새 파일: notification_list_screen.dart)
에이전트 E → 설정 화면        (새 파일: settings_screen.dart)
에이전트 F → 프로필 편집 화면 (새 파일: profile_edit_screen.dart)

규칙: &quot;각자 새 파일만 생성. 기존 파일 수정 금지. 공통 위젯과 모델만 import.&quot;</code></pre><p><strong>결과: ~5분 만에 전부 완료. 파일 충돌 0건.</strong></p>
<p>성공 요인은 단순했습니다:</p>
<ul>
<li>각 에이전트에 <strong>&quot;이 파일만 생성, 기존 파일 수정 금지&quot;</strong> 규칙</li>
<li>기존 공통 위젯과 모델만 import (에이전트 간 의존성 차단)</li>
<li>화면 스펙 문서를 에이전트에 직접 전달 (자율 구현)</li>
</ul>
<p><strong>AI에게 자율성을 줄 때, 경계를 명확히 정해주면 병렬 처리의 위력이 폭발합니다.</strong></p>
<hr>
<h2 id="정리-ai-코딩-에이전트-활용-체크리스트">정리: AI 코딩 에이전트 활용 체크리스트</h2>
<p>매 작업 전에 확인하는 목록입니다:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>체크</th>
<th>관련 함정</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>진짜 문제가 뭔지 파악했는가? (코드? 에셋? 설정? 패키지?)</td>
<td>함정 1, 2</td>
</tr>
<tr>
<td>2</td>
<td>변경 범위를 합의했는가? (전면 교체 vs 최소 수정)</td>
<td>함정 1</td>
</tr>
<tr>
<td>3</td>
<td>git diff에 pubspec.lock·설정 파일 변경이 있는가?</td>
<td>함정 2</td>
</tr>
<tr>
<td>4</td>
<td>AI 생성 설계안을 검수했는가? (앱 규모, 팀 크기, 현실성)</td>
<td>함정 3</td>
</tr>
<tr>
<td>5</td>
<td>기존 코드에 이미 답이 있지 않은가?</td>
<td>함정 4</td>
</tr>
<tr>
<td>6</td>
<td>새 패키지가 정말 필요한가? 기존 위젯 최적화로 해결 가능하지 않은가?</td>
<td>함정 4</td>
</tr>
<tr>
<td>7</td>
<td>병렬 투입 시 에이전트 간 경계가 명확한가?</td>
<td>성공 사례</td>
</tr>
</tbody></table>
<hr>
<h2 id="다음-글">다음 글</h2>
<p>함정 3에서 &quot;AI 설계안을 검수해야 한다&quot;고 했는데, 구체적으로 어떻게 검수할까요?</p>
<p><strong>같은 모델에게 &quot;이거 괜찮아?&quot;라고 물으면, &quot;네, 괜찮습니다&quot;라고 답합니다.</strong> 자기가 만든 걸 자기가 리뷰하면 결함을 못 잡아요.</p>
<p>다음 글에서는 이 &quot;자기 동의 편향&quot;을 깨는 방법 — <strong>교차 모델 리뷰</strong>에 대해 작성해 보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Claude Code 에이전트가 폭주할 때 : 제한이 아니라 전략으로 해결하기]]></title>
            <link>https://velog.io/@s_soo100/Claude-Code-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%ED%8F%AD%EC%A3%BC%ED%95%A0-%EB%95%8C-%EC%A0%9C%ED%95%9C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@s_soo100/Claude-Code-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%ED%8F%AD%EC%A3%BC%ED%95%A0-%EB%95%8C-%EC%A0%9C%ED%95%9C%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 27 Mar 2026 00:56:41 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>Claude Code로 서브에이전트를 활용한 개발 워크플로우를 운영하고 있다. 설계자(Architect)가 분석하고, 구현자(Implementer)가 코드를 쓰고, 검증자(Reviewer)가 리뷰하는 구조다. CAOF(Claude Agent Orchestration Framework)라는 이름을 붙였다.</p>
<p>잘 돌아갈 때는 마법 같다. 문제는 <strong>잘 안 돌아갈 때</strong>다.</p>
<hr>
<h2 id="사건-50분간-응답-없음">사건: 50분간 응답 없음</h2>
<p>React + Next.js 프로젝트(KARS)에서 <code>/review</code> 명령을 실행했다. 22개 파일이 변경된 상태에서 전체 코드 리뷰를 요청한 것이다.</p>
<p>50분이 지나도 응답이 없었다. 토큰은 계속 소비되고 있었다. 강제 중단하고 확인해보니, <strong>리뷰 작업 자체는 이미 끝나 있었다.</strong> 결과 보고서를 생성하는 과정에서 멈춘 것이다.</p>
<hr>
<h2 id="원인-3가지가-겹쳤다">원인: 3가지가 겹쳤다</h2>
<h3 id="1-22개-파일을-한-번에-읽었다">1. 22개 파일을 한 번에 읽었다</h3>
<p>에이전트가 <code>git diff</code>로 변경 파일 목록을 받고, 22개를 전부 Read했다. 컨텍스트 윈도우가 가득 차면 토큰 생성 속도가 극적으로 느려진다.</p>
<h3 id="2-빌드-실패-→-수정-→-재빌드-무한-루프">2. 빌드 실패 → 수정 → 재빌드 무한 루프</h3>
<p>&quot;빌드 실패 시 즉시 수정, 다음 단계 진행 금지&quot;라는 규칙이 있었는데, 중단 조건이 없었다. <code>npm run build</code>가 2<del>5분이니까, 5회 반복하면 그것만 20</del>30분.</p>
<h3 id="3-장문-보고서-생성-병목">3. 장문 보고서 생성 병목</h3>
<p>모든 에이전트에 테이블, 시나리오, 체크리스트가 포함된 상세 보고서를 요구했다. 작업은 끝났는데 <strong>보고서 쓰는 데 수십 분</strong>이 걸렸다.</p>
<hr>
<h2 id="첫-번째-시도-제한을-걸었다--그리고-의문이-생겼다">첫 번째 시도: 제한을 걸었다 — 그리고 의문이 생겼다</h2>
<p>처음에는 직관적인 해결책을 적용했다:</p>
<pre><code>- 파일 읽기 최대 10개
- 디렉토리 전체 탐색 금지
- docs/ 읽기 금지</code></pre><p>효과는 있었다. 폭주는 멈췄다. 하지만 <strong>역효과가 보이기 시작했다.</strong></p>
<p>BLE 센서 분석처럼 깊은 탐색이 필요한 작업에서 &quot;파일 10개 읽었으니 여기서 멈춥니다&quot;가 발생했다. 에이전트의 능력을 깎고 있었던 거다.</p>
<hr>
<h2 id="깨달음-제한이-아니라-전략">깨달음: 제한이 아니라 전략</h2>
<p>사용자가 직접 에이전트에게 이렇게 말했더니 정상 동작했다:</p>
<blockquote>
<p>&quot;overview 먼저 하고, 기능별로 나눠서 순차적으로 리뷰해&quot;</p>
</blockquote>
<p>파일 22개를 읽는 건 똑같은데, <strong>한 번에 읽느냐 나눠서 읽느냐</strong>의 차이였다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>예시</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td>❌ <strong>제한</strong></td>
<td>&quot;파일 10개만 읽어라&quot;</td>
<td>능력을 깎는다</td>
</tr>
<tr>
<td>✅ <strong>전략</strong></td>
<td>&quot;한 번에 다 읽지 말고 overview → 순차 처리&quot;</td>
<td>능력은 그대로, 접근법만 바꾼다</td>
</tr>
</tbody></table>
<p>이게 핵심이다. <strong>에이전트를 제한하면 안 되고, 에이전트의 접근법을 바꿔야 한다.</strong></p>
<hr>
<h2 id="해결-전략-→-간소화-→-안전장치">해결: 전략 → 간소화 → 안전장치</h2>
<h3 id="1-전략-대규모-변경-처리법-가장-효과적">1. 전략: 대규모 변경 처리법 (가장 효과적)</h3>
<pre><code>1. Overview: git diff --stat으로 파일 목록만 확인
2. 그룹핑: 기능별로 묶기 (품목 관리, 발주, 판매...)
3. 순차 처리: 그룹별로 관련 파일만 읽고 작업
4. 종합: 그룹별 결과를 합산</code></pre><p>이걸 에이전트 프롬프트에 기본 동작으로 넣었다:</p>
<pre><code>변경 파일 10개 이상일 때:
한 번에 전부 읽지 않는다. overview → 그룹핑 → 순차 처리.</code></pre><p>탐색도 마찬가지:</p>
<pre><code># 제한 (❌)
&quot;파일 읽기 최대 10개&quot;

# 전략 (✅)
&quot;Grep 먼저, Read 나중. 디렉토리를 통째로 읽지 않는다.&quot;</code></pre><h3 id="2-간소화-출력-형식-줄이기">2. 간소화: 출력 형식 줄이기</h3>
<p>에이전트 간에 주고받는 산출물은 <strong>기계가 읽는 것</strong>이다. 예쁘게 꾸밀 필요 없다.</p>
<pre><code># 변경 전 (리뷰 에이전트)
심각/경고/권장 분류 테이블 + 6개 체크리스트 + 빌드 상태 + 설계 일치도

# 변경 후
대상: path, path
빌드: TS ✅ | Lint ✅
이슈:
- 🔴 path:line — 설명
체크: 권한 ✅ | 캐시 ✅ | 날짜 ✅</code></pre><p>간결한 출력이 빠른 응답이다.</p>
<h3 id="3-안전장치-실패-제한-전략이-아니라-안전장치">3. 안전장치: 실패 제한 (전략이 아니라 안전장치)</h3>
<p>전략으로 해결되지 않는 영역도 있다. 빌드 무한 루프 같은 건 전략이 아니라 중단 조건이 필요하다:</p>
<pre><code>빌드 수정: 최대 3회. 초과 시 에러 목록 정리 후 중단.
에이전트 스폰 재시도: 최대 3회. 초과 시 메인 Claude가 직접 처리.</code></pre><h3 id="4-최후-수단-선택-claude-code-hook">4. 최후 수단 (선택): Claude Code Hook</h3>
<p>위 3가지로 대부분 해결된다. 하지만 에이전트 시스템이 복잡하거나(4+ 에이전트, 병렬 비평 등) 반복적으로 폭주가 발생하면 물리적 차단을 추가할 수 있다.</p>
<p>Claude Code의 <code>PreToolUse</code> Hook은 도구 호출 전에 스크립트를 실행하고, <code>deny</code>를 반환해서 차단한다.</p>
<pre><code class="language-bash">#!/bin/bash
# 세션 파일 크기로 컨텍스트 포화 감지
INPUT=$(cat)
TRANSCRIPT=$(echo &quot;$INPUT&quot; | jq -r &#39;.transcript_path // empty&#39;)
SIZE_MB=$(( $(stat -f%z &quot;$TRANSCRIPT&quot;) / 1048576 ))

if [[ $SIZE_MB -ge 6 ]]; then
  jq -n &#39;{
    hookSpecificOutput: {
      hookEventName: &quot;PreToolUse&quot;,
      permissionDecision: &quot;deny&quot;,
      permissionDecisionReason: &quot;세션 크기 초과. 새 세션에서 이어가세요.&quot;
    }
  }&#39;
fi</code></pre>
<p>모든 도구 호출마다 스크립트가 돌아가므로 오버헤드가 있다. 필요할 때만 도입하자.</p>
<hr>
<h2 id="왜-제한이-아니라-전략인가">왜 &quot;제한&quot;이 아니라 &quot;전략&quot;인가</h2>
<p>처음에는 직감적으로 제한을 걸었다. 그런데 적용하고 보니:</p>
<ul>
<li>&quot;파일 10개 제한&quot;은 <strong>더 깊이있는 호출에서 역효과</strong> — 깊은 탐색이 필요한데 능력을 깎았다</li>
<li>&quot;overview → 순차 처리&quot;는 <strong>동일한 효과를 내면서 능력은 보존</strong> — 22개 파일을 다 읽되, 나눠서 읽었다</li>
<li>숫자 제한은 프로젝트마다 적정값이 다름 — 유지보수 비용이 높다</li>
</ul>
<p>제한은 증상을 억누르고, 전략은 원인을 해결한다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>수단</th>
<th>필수 여부</th>
</tr>
</thead>
<tbody><tr>
<td><strong>전략</strong></td>
<td>overview → 그룹핑 → 순차 처리, Grep 먼저 Read 나중</td>
<td><strong>필수 (가장 효과적)</strong></td>
</tr>
<tr>
<td><strong>간소화</strong></td>
<td>출력 형식 줄이기, 에이전트 간 I/O 규격</td>
<td>필수</td>
</tr>
<tr>
<td><strong>안전장치</strong></td>
<td>빌드 루프 3회 제한, 스폰 재시도 3회</td>
<td>필수</td>
</tr>
<tr>
<td><strong>최후 수단</strong></td>
<td>PreToolUse Hook으로 물리적 차단</td>
<td>선택</td>
</tr>
</tbody></table>
<p>에이전트 오케스트레이션은 &quot;잘 돌아갈 때&quot;만 설계하면 안 된다. <strong>실패할 때 어떻게 멈추는가</strong>, 그리고 <strong>멈추는 방법이 에이전트의 능력을 깎지 않는가</strong>가 더 중요하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ AI한테 시킨 건 끝났는데 티켓은 '진행 중?', 69개 문서를 만들고 깨달은 것]]></title>
            <link>https://velog.io/@s_soo100/AI%ED%95%9C%ED%85%8C-%EC%8B%9C%ED%82%A8-%EA%B1%B4-%EB%81%9D%EB%82%AC%EB%8A%94%EB%8D%B0-%ED%8B%B0%EC%BC%93%EC%9D%80-%EC%A7%84%ED%96%89-%EC%A4%91-69%EA%B0%9C-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EA%B9%A8%EB%8B%AC%EC%9D%80-%EA%B2%83</link>
            <guid>https://velog.io/@s_soo100/AI%ED%95%9C%ED%85%8C-%EC%8B%9C%ED%82%A8-%EA%B1%B4-%EB%81%9D%EB%82%AC%EB%8A%94%EB%8D%B0-%ED%8B%B0%EC%BC%93%EC%9D%80-%EC%A7%84%ED%96%89-%EC%A4%91-69%EA%B0%9C-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EA%B9%A8%EB%8B%AC%EC%9D%80-%EA%B2%83</guid>
            <pubDate>Thu, 26 Mar 2026 04:15:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>AI 코딩 에이전트와 함께 일하면서, 작업 추적을 어떻게 해야 하는지 삽질하고 도달한 결론을 공유합니다.</p>
</blockquote>
<hr>
<h2 id="1-문제-ai가-코드를-짜는-시대에-작업-추적은-누가-하나">1. 문제: AI가 코드를 짜는 시대에, 작업 추적은 누가 하나?</h2>
<p>AI 코딩 에이전트(Claude Code, Cursor, Copilot 등)의 등장으로 개발 속도가 확 바뀌었습니다. &quot;이 기능 구현해줘&quot;라고 하면 코드 작성부터 커밋까지 해주는 세상이에요.</p>
<p>그런데 막상 써보면, 묘하게 불편한 순간이 옵니다.</p>
<blockquote>
<p>&quot;이 기능 구현해줘&quot; → AI가 코드를 완성하고 커밋까지 해줌
→ 다음 날 프로젝트 관리 도구를 열어보면... 티켓 상태는 &#39;진행 중&#39;</p>
</blockquote>
<p>코드는 이미 main에 머지됐는데, 프로젝트 보드는 &quot;아직 하고 있음&quot;이라고 말하고 있습니다. AI가 빠르게 구현해준 덕에 코드는 앞서가는데, 관리 체계는 그 속도를 못 따라가는 거예요.</p>
<p><strong>AI가 코드를 대신 짜주는 건 해결됐지만, &quot;지금 뭘 하고 있고, 어디까지 됐는지&quot; 추적하는 문제는 오히려 더 복잡해졌습니다.</strong></p>
<hr>
<h2 id="2-삽질기--69개-문서-11000줄">2. 삽질기 — 69개 문서, 11,000줄</h2>
<p>처음엔 &quot;제대로 해보자&quot;는 마음이었습니다. Jira나 Linear에서 보던 방식 — 큰 기능을 Epic으로 묶고, 구체적인 작업을 Ticket으로 쪼개고, 상태를 단계별로 전환하는 그 프로세스를 마크다운으로 옮겨서 AI 개발에 적용해봤습니다.</p>
<h3 id="구조">구조</h3>
<pre><code>planning/
├── epic-auth.md           # Epic (큰 기능 단위)
├── tickets/
│   ├── T-001.md           # Ticket (구체적 작업)
│   ├── T-002.md
│   └── ...
└── usecases/
    ├── usecase-login.md   # Usecase (사용자 시나리오)
    └── ...</code></pre><ul>
<li><strong>3계층</strong>: Epic → Ticket → Usecase</li>
<li><strong>5단계 상태 전환</strong>: <code>draft → ready → in-progress → review → done</code></li>
<li><strong>권한 분리</strong>: 사람은 상태 승인, AI는 구현</li>
</ul>
<p>결과물: <strong>69개 마크다운 파일, 총 11,000줄.</strong></p>
<p>AI에게 &quot;T-003 구현해줘&quot;라고 하면 정확히 무엇을 만들어야 하는지 알려줄 수 있었습니다. 그 부분은 확실히 좋았어요.</p>
<h3 id="그런데">그런데...</h3>
<p>한 달 뒤 상태를 점검해봤더니:</p>
<table>
<thead>
<tr>
<th>증상</th>
<th>구체적 사례</th>
</tr>
</thead>
<tbody><tr>
<td><strong>상태 불일치</strong></td>
<td>티켓 5개가 <code>review</code> 상태로 방치. 코드는 이미 main에 머지 완료.</td>
</tr>
<tr>
<td><strong>상태 불일치 (2)</strong></td>
<td>티켓 7개가 <code>draft</code> 상태. 구현 커밋이 이미 존재.</td>
</tr>
<tr>
<td><strong>과도한 문서화</strong></td>
<td>CSS 값 몇 개 고치는 버그 수정에 155줄짜리 티켓 문서 작성</td>
</tr>
<tr>
<td><strong>사문서</strong></td>
<td>Usecase 4개 전부 <code>draft</code>로 방치 — 3계층 중 1개가 통째로 미사용</td>
</tr>
<tr>
<td><strong>단위 불일치</strong></td>
<td>&quot;1 티켓 = 1 커밋&quot; 원칙인데, 실제론 7개 티켓을 한 커밋으로 구현</td>
</tr>
<tr>
<td><strong>무의미한 권한 분리</strong></td>
<td><code>review → done</code>은 &quot;사람이 승인&quot;해야 하는데, 1인 개발에서 내가 쓰고 내가 승인</td>
</tr>
</tbody></table>
<p><strong>69개 문서 중 80%의 상태가 실제 코드와 불일치.</strong> 티켓을 보고 판단하면 오히려 틀린 판단을 하게 되는 상황이었습니다.</p>
<hr>
<h2 id="3-왜-실패했나--수동-상태-관리는-반드시-실패한다">3. 왜 실패했나 — &quot;수동 상태 관리는 반드시 실패한다&quot;</h2>
<p>원인을 파고들면 결국 하나로 수렴합니다:</p>
<blockquote>
<p><strong>코드를 구현하는 행위와, 상태를 업데이트하는 행위가 별개의 작업이다.</strong></p>
</blockquote>
<p>AI가 코드를 짜고 커밋까지 했는데, 그 다음에 &quot;이제 티켓 파일 열어서 status를 <code>review</code>로 바꿔&quot;라는 건 <strong>코딩과 무관한 별도 작업</strong>입니다. 사람도, AI도 이걸 빼먹습니다.</p>
<pre><code>코드 구현 ──→ 커밋 ──→ (여기서 끝내고 싶음)
                        ↓
                  status 필드 수동 갱신 ← 이건 코딩이 아니라 행정이다
                        ↓
                  (귀찮아서 안 함)
                        ↓
                  상태 불일치 발생</code></pre><p>Jira든, Linear든, 마크다운 티켓이든 마찬가지입니다. <strong>별도 상태 필드가 존재하는 한, 반드시 코드와 어긋나게 됩니다.</strong></p>
<h3 id="다른-사람들은-어떻게-하고-있을까">다른 사람들은 어떻게 하고 있을까?</h3>
<p>이게 나만의 문제인지 궁금해서 11개 접근법을 조사해봤습니다.</p>
<ul>
<li><strong>Cursor Rules / Memory Bank</strong>: IDE 안에서 규칙 파일로 맥락을 유지하지만, 작업 이력 추적이 약함</li>
<li><strong>SDD(Spec-Driven Development)</strong>: Thoughtworks/GitHub 주도의 스펙 기반 개발. 구조적이되 유연한 방향</li>
<li><strong>AGENTS.md / Copilot Agent</strong>: 에이전트에게 맥락을 주는 데 집중하되, 상태 관리는 Git 기반으로 파생</li>
</ul>
<p>이름과 형태는 달라도, 공통 트렌드가 하나 보였습니다:</p>
<blockquote>
<p><strong>별도 상태 필드 대신, 기존 인프라(Git, 체크리스트)에서 상태를 파생(derive)하는 방향.</strong></p>
</blockquote>
<p>그래서 결론에 도달했습니다.</p>
<pre><code>&quot;상태를 어떻게 정확하게 관리할까?&quot;     ← 잘못된 질문
&quot;수동 상태 관리는 반드시 실패한다&quot;     ← 발견
&quot;상태 필드를 제거하면?&quot;               ← 발상의 전환
&quot;체크리스트 자체가 상태다&quot;             ← 해결</code></pre><hr>
<h2 id="4-해결-체크리스트--상태">4. 해결: 체크리스트 = 상태</h2>
<p>핵심 원리는 간단합니다.</p>
<h3 id="before-별도-상태-필드">Before (별도 상태 필드)</h3>
<pre><code class="language-markdown"># 소셜 로그인
status: review        ← 이걸 누가 언제 바꾸나? → 아무도 안 바꿈
assignee: ai
priority: high

## 수락 기준
- Google OAuth 연동
- 프로필 정보 저장
- 에러 핸들링</code></pre>
<h3 id="after-체크리스트가-곧-상태">After (체크리스트가 곧 상태)</h3>
<pre><code class="language-markdown"># 소셜 로그인

&gt; 사용자가 Google 계정으로 로그인할 수 있게 한다

## 수락 기준
- [x] Google OAuth 연동
- [x] 프로필 정보 저장
- [ ] 에러 핸들링</code></pre>
<p>이것만 보면 &quot;2/3 완료, 에러 핸들링만 남음&quot;이라는 상태가 <strong>자명합니다.</strong> 별도 필드가 필요 없어요.</p>
<h3 id="왜-이게-작동하는가">왜 이게 작동하는가</h3>
<ol>
<li><p><strong>AI가 코드를 구현하면서 항목을 체크하는 건 자연스러운 동작입니다.</strong> &quot;status 필드를 <code>review</code>로 바꿔&quot;가 아니라, 구현이 끝난 항목에 <code>[x]</code>를 넣는 건 작업의 연장선이에요.</p>
</li>
<li><p><strong>상태가 존재하는 곳이 딱 한 군데입니다.</strong> 이중 관리 자체가 불가능하므로, 불일치도 불가능합니다.</p>
</li>
<li><p><strong>&quot;완료 처리해줘&quot;라고 말할 필요가 없습니다.</strong> 체크리스트가 다 차면 끝입니다.</p>
</li>
</ol>
<h3 id="상태-파생-규칙">상태 파생 규칙</h3>
<table>
<thead>
<tr>
<th align="center">체크리스트 상태</th>
<th align="center">= 작업 상태</th>
</tr>
</thead>
<tbody><tr>
<td align="center">체크된 항목 0개</td>
<td align="center">아직 시작 안 함</td>
</tr>
<tr>
<td align="center">일부 체크됨</td>
<td align="center">진행 중</td>
</tr>
<tr>
<td align="center">전부 체크됨</td>
<td align="center">완료</td>
</tr>
</tbody></table>
<p><code>active/</code>, <code>done/</code> 같은 하위 폴더도 필요 없습니다. 폴더 이동도 수동 작업이니까, 역시 누락됩니다.</p>
<hr>
<h2 id="5-뭘-버렸고-왜-괜찮은가">5. 뭘 버렸고, 왜 괜찮은가</h2>
<p>한 파일이 기존의 Epic + Ticket + Usecase 역할을 모두 합니다. 3개 파일로 분산되던 정보가 하나로 합쳐지니, 동기화할 대상 자체가 없어요.</p>
<h3 id="스펙-파일-템플릿">스펙 파일 템플릿</h3>
<pre><code class="language-markdown"># {기능명}

&gt; {이 기능이 해결하는 문제를 한 줄로}

## 배경

왜 이 기능이 필요한지. 사용자 관점에서 설명.

## 수락 기준

- [ ] 기준 1 — 구체적이고 검증 가능한 조건
- [ ] 기준 2
- [ ] 기준 3
- [ ] 빌드 성공

## 설계 메모

구현 방향, 기술 선택, 참고할 기존 코드 등.
처음엔 비어 있어도 됨. AI가 구현하면서 채울 수도 있음.</code></pre>
<p>전체 10~30줄. 155줄짜리 티켓 문서와 비교해보세요.</p>
<h3 id="비교표">비교표</h3>
<table>
<thead>
<tr>
<th>기존 티켓에 있던 것</th>
<th>경량 스펙</th>
<th>왜 괜찮은가</th>
</tr>
</thead>
<tbody><tr>
<td>status 필드</td>
<td>없음</td>
<td>체크리스트가 상태</td>
</tr>
<tr>
<td>티켓 ID (T-001)</td>
<td>없음</td>
<td>파일명이 ID (<code>auth-social-login.md</code>)</td>
</tr>
<tr>
<td>우선순위 필드</td>
<td>없음</td>
<td>어떤 걸 먼저 할지는 구두로 지시</td>
</tr>
<tr>
<td>담당자 필드</td>
<td>없음</td>
<td>1~2명 팀에서 불필요</td>
</tr>
<tr>
<td>depends-on / blocks</td>
<td>참고란에 텍스트</td>
<td>강제 안 하고 참고만. 실제 의존성은 코드가 결정</td>
</tr>
<tr>
<td>브랜치 필드</td>
<td>없음</td>
<td><code>git branch --contains</code>로 파생</td>
</tr>
<tr>
<td>유즈케이스 문서</td>
<td>수락 기준</td>
<td>별도 파일 → 같은 파일 내 체크리스트로 통합</td>
</tr>
</tbody></table>
<h3 id="실제-예시">실제 예시</h3>
<p>69개 문서의 삽질 이후, 같은 프로젝트의 새 기능을 이 방식으로 관리해봤습니다:</p>
<pre><code class="language-markdown"># 시계열 메트릭 수집 및 조회

&gt; 운동 중 시간/거리 기반으로 속도·거리 데이터를 샘플링하여 서버에 저장

## 수락 기준

### 데이터 수집
- [x] MetricsCollector 구현 — 시간 기준(5초) + 거리 기준(1m) 샘플링
- [x] Basic Training에 collector 연동
- [x] Interval Training에 collector 연동
- [x] Course Training에 collector 연동

### API 연동
- [x] WorkoutDetail 모델 생성
- [x] History POST 시 메트릭 데이터 포함하여 전송
- [x] 전송 실패 시 메트릭 제거 후 History만 재시도 (fallback)

### 디버그 확인 (현 단계)
- [ ] History 조회 시 메트릭 포함하여 파싱
- [ ] 디버그 위젯으로 메트릭 표시

### 유저 시각화 (추후)
- [ ] 속도-시간 그래프 표시
- [ ] 속도-거리 그래프 표시</code></pre>
<p>이 파일 하나로 &quot;데이터 수집과 API 연동은 끝났고, 프론트엔드 표시가 남았다&quot;가 즉시 보입니다. 155줄짜리 티켓 3개를 만들 때보다 파악이 빨라요.</p>
<h3 id="스펙이-필요-없는-경우">스펙이 필요 없는 경우</h3>
<p>모든 작업에 스펙을 쓸 필요는 없습니다.</p>
<table>
<thead>
<tr>
<th>스펙 필요</th>
<th>스펙 불필요 (커밋 메시지로 충분)</th>
</tr>
</thead>
<tbody><tr>
<td>새 기능 (수락 기준 3개 이상)</td>
<td>버그 수정</td>
</tr>
<tr>
<td>여러 파일에 걸친 리팩토링</td>
<td>스타일 조정</td>
</tr>
<tr>
<td>설계 판단이 필요한 작업</td>
<td>설정 변경, 1~2줄 수정</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-도입-가이드--5분이면-됩니다">6. 도입 가이드 — 5분이면 됩니다</h2>
<h3 id="step-1-폴더--템플릿-1분">Step 1: 폴더 + 템플릿 (1분)</h3>
<pre><code class="language-bash">mkdir specs
# 위 템플릿을 specs/_template.md로 저장</code></pre>
<h3 id="step-2-ai-설정-파일에-규칙-추가-2분">Step 2: AI 설정 파일에 규칙 추가 (2분)</h3>
<p>CLAUDE.md, .cursorrules 등 사용 중인 AI 도구의 설정 파일에 추가합니다:</p>
<pre><code class="language-markdown">## 스펙 기반 개발

- `specs/` 폴더에 기능별 스펙 파일이 있다.
- 스펙 구현 요청 시: 스펙 읽기 → 구현 → 수락 기준 체크(✅) → 커밋
- 커밋 메시지: `{type}({스펙파일명}): {설명}`
- status 필드 없음. 체크리스트의 체크 상태가 곧 진행률이다.
- 전부 체크되면 완료. 작은 작업은 스펙 없이 바로 커밋.</code></pre>
<h3 id="step-3-첫-스펙-작성-2분">Step 3: 첫 스펙 작성 (2분)</h3>
<p>지금 진행 중인 기능 하나를 골라서 스펙으로 작성해보세요. AI에게 &quot;이 기능 코드를 분석해서 스펙으로 정리해줘&quot;라고 시켜도 됩니다.</p>
<h3 id="커밋-컨벤션-선택">커밋 컨벤션 (선택)</h3>
<pre><code>feat(auth-social-login): Google OAuth 연동 구현
fix(payment-refund): 환불 금액 계산 오류 수정</code></pre><p>스펙 파일명을 커밋에 포함하면 <code>git log --grep=&quot;auth-social-login&quot;</code>으로 해당 기능의 전체 이력을 추적할 수 있습니다.</p>
<h3 id="진행-상황-확인">진행 상황 확인</h3>
<pre><code class="language-bash"># 완료된 스펙 (미체크 항목 없음)
grep -rL &quot;\- \[ \]&quot; specs/*.md

# 진행 중인 스펙 (미체크 항목 있음)
grep -rl &quot;\- \[ \]&quot; specs/*.md</code></pre>
<hr>
<h2 id="7-이-방법이-안-맞는-경우">7. 이 방법이 안 맞는 경우</h2>
<p>만능은 아닙니다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>왜 안 맞나</th>
<th>대안</th>
</tr>
</thead>
<tbody><tr>
<td><strong>3명 이상 팀</strong></td>
<td>누가 어떤 스펙을 작업 중인지 추적 필요</td>
<td>Linear, GitHub Issues</td>
</tr>
<tr>
<td><strong>엄격한 의존성 관리</strong></td>
<td>&quot;A 완료 전 B 시작 불가&quot; 강제가 필요한 경우</td>
<td>의존성 필드가 있는 티켓 시스템</td>
</tr>
<tr>
<td><strong>감사(audit) 추적</strong></td>
<td>누가 언제 승인했는지 기록이 필요한 규제 환경</td>
<td>별도 상태 이력 관리</td>
</tr>
</tbody></table>
<p><strong>이 프로세스가 최적인 상황:</strong></p>
<ul>
<li>1인 개발 + AI</li>
<li>소규모 팀 (1~2명)</li>
<li>빠른 이터레이션이 중요한 프로젝트</li>
<li>사이드 프로젝트부터 중규모 앱까지</li>
</ul>
<hr>
<h2 id="8-정리">8. 정리</h2>
<pre><code>69개 문서, 11,000줄 → specs/ 폴더에 기능당 10~30줄짜리 파일 하나.
5단계 상태 전환 → 체크리스트가 곧 상태.
3계층 구조 → 한 파일이 전부.</code></pre><p>핵심은 이겁니다:</p>
<blockquote>
<p><strong>&quot;상태를 더 잘 관리하자&quot;가 아니라, &quot;상태 필드를 지우자&quot;가 답이었다.</strong></p>
</blockquote>
<p>AI와 함께 일하면서 가장 먼저 깨진 게 &quot;수동 상태 관리&quot;였고, 가장 효과적이었던 건 &quot;관리할 대상을 줄이는 것&quot;이었습니다.</p>
<p>지금 바로 <code>mkdir specs</code>를 치고, 진행 중인 기능 하나를 10줄짜리 스펙으로 적어보세요. 다음에 AI한테 &quot;이 스펙 구현해줘&quot;라고 했을 때, 체크리스트가 알아서 채워지는 걸 보면 감이 올 겁니다.</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[CAOF — Claude Agent Orchestration Framework: AI 코딩 에이전트의 실패를 구조로 해결하는 법]]></title>
            <link>https://velog.io/@s_soo100/CAOF-Claude-Agent-Orchestration-Framework-AI-%EC%BD%94%EB%94%A9-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%9D%98-%EC%8B%A4%ED%8C%A8%EB%A5%BC-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@s_soo100/CAOF-Claude-Agent-Orchestration-Framework-AI-%EC%BD%94%EB%94%A9-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%9D%98-%EC%8B%A4%ED%8C%A8%EB%A5%BC-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Wed, 25 Mar 2026 09:08:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>AI 코딩 에이전트가 복잡한 작업에서 반복 실패하는 문제를, 역할 분리와 게이트 시스템으로 해결한 실전 프레임워크.</p>
</blockquote>
<hr>
<h2 id="문제-인식-ai는-왜-같은-실수를-반복하는가">문제 인식: AI는 왜 같은 실수를 반복하는가</h2>
<p>Claude Code 같은 AI 코딩 에이전트에게 복잡한 작업을 맡기면 특유의 실패 패턴이 나타난다:</p>
<ol>
<li><strong>&quot;이미 알고 있다&quot;는 과신</strong> — 리서치를 건너뛰고 기억에 의존해 코드를 작성</li>
<li><strong>증상 패치의 반복</strong> — 근본 원인을 분석하지 않고 보이는 에러만 수정</li>
<li><strong>범위의 무한 확장</strong> — 한 파일 수정이 다른 파일로 번지고, 원래 의도와 동떨어진 변경이 쌓임</li>
</ol>
<p>실제로 한 게임 프로젝트에서 UI 전환 시스템을 구현할 때, AI에게 직접 코딩을 맡긴 결과 <strong>7회 연속 실패</strong>했다. 매번 다른 증상이 나타났지만, 근본 원인은 동일했다 — 분석 없이 코딩부터 시작하는 구조적 문제.</p>
<p><strong>바뀌어야 할 건 AI의 코딩 실력이 아니라, AI를 운용하는 프로세스였다.</strong></p>
<hr>
<h2 id="핵심-아이디어-역할-분리">핵심 아이디어: 역할 분리</h2>
<p>CAOF의 핵심은 단순하다. <strong>&quot;분석하는 AI&quot;와 &quot;구현하는 AI&quot;를 분리한다.</strong></p>
<table>
<thead>
<tr>
<th>역할</th>
<th>책임</th>
<th>하지 않는 것</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Researcher</strong></td>
<td>모르는 것을 찾음 (웹 검색 등)</td>
<td>판단, 구현</td>
</tr>
<tr>
<td><strong>Designer</strong></td>
<td>원인 분석 + 수정 방향 + 성공 기준 정의</td>
<td>코드 작성</td>
</tr>
<tr>
<td><strong>Implementer</strong></td>
<td>Designer의 방향대로만 구현</td>
<td>독자 판단으로 범위 확장</td>
</tr>
<tr>
<td><strong>Reviewer</strong></td>
<td>성공 기준 대비 결과 검증 + 반박 의무</td>
<td>무조건 통과</td>
</tr>
</tbody></table>
<p>왜 이게 작동하는가?</p>
<ul>
<li><strong>Designer가 코드를 쓰지 않으므로</strong> 분석에 집중할 수 있다</li>
<li><strong>Implementer가 분석하지 않으므로</strong> 계획서 범위 안에서만 움직인다</li>
<li><strong>Reviewer에게 반박 의무</strong>가 있으므로 &quot;돌아가니까 통과&quot;가 불가능하다</li>
</ul>
<p>이것은 소프트웨어 공학의 오래된 원칙 — <strong>관심사의 분리(Separation of Concerns)</strong> — 을 AI 에이전트 운용에 적용한 것이다.</p>
<hr>
<h2 id="모든-작업에-적용하지-않는다-트랙-시스템">모든 작업에 적용하지 않는다: 트랙 시스템</h2>
<p>CAOF의 실용성은 <strong>&quot;언제 적용하고 언제 건너뛰는가&quot;</strong>에 있다.</p>
<table>
<thead>
<tr>
<th>트랙</th>
<th>기준</th>
<th>파이프라인</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Trivial</strong></td>
<td>단일 파일, 기존 동작 변경 없음, 되돌리기 &lt; 5분</td>
<td>Implementer 직행</td>
<td>오타 수정, 로그 추가, 상수값 변경</td>
</tr>
<tr>
<td><strong>Standard</strong></td>
<td>2~3 파일 또는 기존 동작 변경, 되돌리기 &lt; 1시간</td>
<td>Designer 분석 → Implementer</td>
<td>버그 수정, 기존 기능 수정</td>
</tr>
<tr>
<td><strong>Critical</strong></td>
<td>4+ 파일, 새 시스템, 외부 의존성</td>
<td>풀 GATE (7단계)</td>
<td>새 기능, 아키텍처 변경</td>
</tr>
</tbody></table>
<p><strong>판단 기준은 코드 줄 수가 아니라 &quot;실패 시 되돌리기 비용&quot;이다.</strong></p>
<p>1줄 변경이라도 물리 엔진 파라미터나 DB 스키마 변경이면 Standard 이상. 100줄 변경이라도 새 유틸 함수 추가면 Trivial일 수 있다.</p>
<hr>
<h2 id="gate-파이프라인-critical-트랙의-7단계">GATE 파이프라인: Critical 트랙의 7단계</h2>
<p>Critical 트랙은 다음 7개 게이트를 순서대로 통과해야 한다. <strong>이전 단계의 산출물이 없으면 다음 단계를 시작할 수 없다.</strong></p>
<h3 id="gate-0--필요성-검증">GATE 0 — 필요성 검증</h3>
<blockquote>
<p>산출물: &quot;왜 필요한가&quot; 1문장</p>
</blockquote>
<ul>
<li>&quot;이걸 왜 만들어야 하는가?&quot;</li>
<li>&quot;기존 시스템으로 해결 가능하지 않은가?&quot;</li>
<li>가장 자주 스킵되지만, 가장 비용을 아끼는 단계</li>
</ul>
<h3 id="gate-1--리서치">GATE 1 — 리서치</h3>
<blockquote>
<p>산출물: 리서치 요약</p>
</blockquote>
<ul>
<li>기술적 불확실성을 웹 검색 등으로 조사</li>
<li><strong>&quot;이미 알고 있다&quot;는 스킵 사유가 아님</strong> — 목적은 &quot;모르는 것을 모르는 상태&quot; 방지</li>
<li>프로젝트 내 검증된 패턴은 코드베이스 탐색으로 대체 가능</li>
</ul>
<h3 id="gate-2--설계">GATE 2 — 설계</h3>
<blockquote>
<p>산출물: 구현 계획서 + 성공 기준</p>
</blockquote>
<p>Designer가 파일/함수 수준의 구체적인 계획서를 작성한다:</p>
<pre><code>- 수정 파일: 경로
- 수정 함수: 이름
- 근본 원인: (1줄)
- 수정 전 동작 / 수정 후 기대 동작
- 건드리지 않는 것: (명시)
- 성공 기준: (체크리스트)</code></pre><p><strong>&quot;건드리지 않는 것&quot;을 명시하는 게 핵심이다.</strong> Implementer의 범위 확장을 사전에 차단한다.</p>
<h3 id="gate-3--승인">GATE 3 — 승인</h3>
<blockquote>
<p>산출물: 사용자 &quot;승인&quot;</p>
</blockquote>
<p>계획 요약을 사용자에게 제시하고, 승인 후에만 구현을 시작한다.</p>
<h3 id="gate-4--구현">GATE 4 — 구현</h3>
<blockquote>
<p>산출물: 수정된 코드</p>
</blockquote>
<ul>
<li>반드시 Implementer 에이전트가 구현 (Designer가 직접 코딩하지 않음)</li>
<li>GATE 1 리서치 + GATE 2 계획서를 컨텍스트로 전달</li>
<li>구현 중 &quot;모르는 동작&quot; 발견 시 즉시 중단, GATE 1로 복귀</li>
</ul>
<h3 id="gate-5--검수">GATE 5 — 검수</h3>
<blockquote>
<p>산출물: 검수 보고서</p>
</blockquote>
<ul>
<li>GATE 2의 성공 기준 대비 결과를 대조</li>
<li><strong>교차 모델 검수 권장</strong> — 같은 모델의 자기 검수는 자기 동의 편향(self-agreement bias) 위험</li>
<li>Reviewer는 반박 의무가 있음</li>
</ul>
<h3 id="gate-6--테스트">GATE 6 — 테스트</h3>
<blockquote>
<p>산출물: 사용자 테스트 결과</p>
</blockquote>
<ul>
<li>사용자가 직접 테스트</li>
<li>버그 발견 시 버그 수정 파이프라인(Standard 트랙) 적용</li>
</ul>
<hr>
<h2 id="버그-수정-designer를-거치는-이유">버그 수정: Designer를 거치는 이유</h2>
<blockquote>
<p>Implementer에게 직접 버그를 넘기지 않는다.</p>
</blockquote>
<pre><code>버그 증상 + 로그
    ↓
Designer: 근본 원인 분석 + 수정 방향 + 영향 범위
    ↓
Implementer: 수정 방향대로만 구현
    ↓
검수 + 테스트</code></pre><p>Implementer는 코딩 전문이지 진단 전문이 아니다. 원인 분석 없이 넘기면 증상만 패치하게 된다. 이것이 &quot;7회 연속 실패&quot;의 직접적 원인이었다.</p>
<hr>
<h2 id="실패-에스컬레이션-3회면-멈춘다">실패 에스컬레이션: 3회면 멈춘다</h2>
<pre><code>1회 실패: Implementer가 원인 분석 후 재시도
2회 실패: Designer가 원인 재분석
           → 구현 문제: 다른 접근법으로 재구현
           → 설계 문제: GATE 2로 롤백
3회 실패: 즉시 중단 + 사용자에게 보고
           → 기능 범위 축소 또는 대안 기능으로 전환</code></pre><p><strong>&quot;접근 전환&quot;의 구체적 행동:</strong></p>
<ul>
<li>추가 리서치 (새 키워드, 다른 기술 스택)</li>
<li>Designer 교체 (다른 모델에 위임)</li>
<li>기능 범위를 MVP로 축소</li>
<li>최소 재현 테스트로 가설 검증</li>
</ul>
<p>중요한 건 <strong>&quot;빨리 해&quot;, &quot;바로 구현해&quot;는 게이트 스킵 승인이 아니라는 점</strong>이다. &quot;N단계 스킵 승인&quot;이라는 명시적 문구만 허용한다.</p>
<hr>
<h2 id="에이전트-설계-극단적-페르소나가-역할-분리를-만든다">에이전트 설계: 극단적 페르소나가 역할 분리를 만든다</h2>
<p>Claude Code의 커스텀 에이전트(<code>.claude/agents/</code>)를 활용해 역할을 구현한다.</p>
<h3 id="에이전트-카드-구조">에이전트 카드 구조</h3>
<pre><code class="language-markdown">---
name: [에이전트 이름]
description: [한 줄 설명]
model: [opus/sonnet — Designer는 opus, Implementer는 sonnet 권장]
tools: [사용 가능한 도구]
---

# 페르소나
[2~3줄로 사고방식과 가치관. 극단적일수록 좋다.]

## 사고 원칙
1. [최우선 원칙]
2. [두 번째 원칙]

## 범위 제약
- 이 에이전트가 하는 것
- 이 에이전트가 하지 않는 것 ← 이게 더 중요

## 과거 교훈
- [실패에서 학습한 내용 — 날짜와 함께]</code></pre>
<h3 id="4가지-설계-원칙">4가지 설계 원칙</h3>
<ol>
<li><strong>페르소나가 극단적이어야 역할 분리가 작동한다</strong> — &quot;꼼꼼한 개발자&quot;는 약하다. &quot;버그를 못 찾으면 잠을 못 자는 15년차 시니어&quot;는 강하다.</li>
<li><strong>&quot;하지 않는 것&quot;이 &quot;하는 것&quot;보다 중요하다</strong> — Designer가 코드를 쓰기 시작하면 역할 분리가 무너진다.</li>
<li><strong>모델 선택: Designer = Opus, Implementer = Sonnet</strong> — Designer는 깊은 분석이 필요하고, Implementer는 빠른 실행이 필요하다.</li>
<li><strong>교훈 섹션이 에이전트를 성장시킨다</strong> — 프로젝트에서 발견한 실수를 에이전트 카드에 축적하면, 같은 실수를 반복하지 않는 구조가 된다.</li>
</ol>
<hr>
<h2 id="모델-선택과-교차-검증">모델 선택과 교차 검증</h2>
<h3 id="designer--opus-implementer--sonnet">Designer = Opus, Implementer = Sonnet</h3>
<table>
<thead>
<tr>
<th>모델</th>
<th>강점</th>
<th>CAOF 역할</th>
</tr>
</thead>
<tbody><tr>
<td>Opus</td>
<td>깊은 분석, 복잡한 추론</td>
<td>Designer, Reviewer</td>
</tr>
<tr>
<td>Sonnet</td>
<td>빠른 코드 생성, 실행력</td>
<td>Implementer</td>
</tr>
<tr>
<td>Haiku</td>
<td>초고속, 저비용</td>
<td>보조 검수</td>
</tr>
</tbody></table>
<h3 id="교차-모델-검수의-필요성">교차 모델 검수의 필요성</h3>
<p>같은 모델이 자기 코드를 검수하면 <strong>자기 동의 편향(self-agreement bias)</strong>이 발생한다. 자신이 작성한 로직을 &quot;합리적&quot;으로 판단하는 경향이 있어, 실제 결함을 놓친다.</p>
<p>CAOF에서는 구현과 검수에 서로 다른 모델을 사용하는 교차 검증을 권장한다:</p>
<ul>
<li>Sonnet이 구현 → <strong>Opus + Gemini</strong>가 검수</li>
<li><strong>4모델 병렬 검수</strong> (Gemini + Opus + Sonnet + Haiku)로 다각도 리뷰</li>
</ul>
<hr>
<h2 id="실전-검증-데이터">실전 검증 데이터</h2>
<table>
<thead>
<tr>
<th>Phase</th>
<th>프레임워크</th>
<th>시도 횟수</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>Phase 0</td>
<td>없음 (AI 직접 코딩)</td>
<td>7회</td>
<td>반복 실패 후 프레임워크 도입</td>
</tr>
<tr>
<td>Phase 1</td>
<td>CAOF 적용</td>
<td>2회</td>
<td>성공 (1회 버그 → Designer 분석 → 수정)</td>
</tr>
<tr>
<td>Phase 2</td>
<td>CAOF 적용</td>
<td>1회</td>
<td>즉시 성공</td>
</tr>
<tr>
<td>Phase 3</td>
<td>CAOF 적용</td>
<td>1회</td>
<td>즉시 성공</td>
</tr>
<tr>
<td>Phase 4</td>
<td>CAOF 적용</td>
<td>1회</td>
<td>즉시 성공</td>
</tr>
</tbody></table>
<p>Phase 0에서 7회 실패하던 작업이, CAOF 도입 후 Phase 1<del>4에서 각각 1</del>2회만에 성공했다. <strong>변한 건 AI 모델이 아니라 프로세스다.</strong></p>
<hr>
<h2 id="새-프로젝트에-적용하는-법">새 프로젝트에 적용하는 법</h2>
<h3 id="step-1-프로젝트의-실패-비용-분석">Step 1: 프로젝트의 실패 비용 분석</h3>
<ul>
<li>이 프로젝트에서 <strong>가장 비싼 실수</strong>는 무엇인가?</li>
<li>실수 후 <strong>되돌리기에 얼마나 걸리는가?</strong></li>
<li>과거에 <strong>같은 실수를 반복</strong>한 적이 있는가?</li>
</ul>
<h3 id="step-2-designerimplementer-역할-결정">Step 2: Designer/Implementer 역할 결정</h3>
<table>
<thead>
<tr>
<th>질문</th>
<th>Designer</th>
<th>Implementer</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;왜 이렇게 해야 하는가?&quot;</td>
<td>○</td>
<td>×</td>
</tr>
<tr>
<td>&quot;이 코드를 어떻게 작성하는가?&quot;</td>
<td>×</td>
<td>○</td>
</tr>
<tr>
<td>&quot;이 버그의 원인은?&quot;</td>
<td>○</td>
<td>×</td>
</tr>
<tr>
<td>&quot;계획서대로 코드를 작성하라&quot;</td>
<td>×</td>
<td>○</td>
</tr>
</tbody></table>
<h3 id="step-3-에이전트-카드-작성">Step 3: 에이전트 카드 작성</h3>
<p><code>.claude/agents/</code> 디렉토리에 Designer와 Implementer 에이전트를 생성한다.</p>
<h3 id="step-4-claudemd에-caof-규칙-삽입">Step 4: CLAUDE.md에 CAOF 규칙 삽입</h3>
<p>트랙 분류 기준, 라우팅 트리, 실패 에스컬레이션 규칙을 프로젝트 설정에 추가한다.</p>
<h3 id="step-5-첫-기능으로-캘리브레이션">Step 5: 첫 기능으로 캘리브레이션</h3>
<p>첫 Critical 기능을 풀 GATE로 실행하면서 병목을 측정하고, <strong>5개 기능 완료 후 회고</strong>로 프레임워크를 조정한다.</p>
<hr>
<h2 id="프로젝트-유형별-적용-예시">프로젝트 유형별 적용 예시</h2>
<table>
<thead>
<tr>
<th>프로젝트 유형</th>
<th>Designer</th>
<th>Implementer</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>게임 (Unity/Godot)</td>
<td>게임 설계 에이전트 (Opus)</td>
<td>엔진 코더 에이전트 (Sonnet)</td>
<td>전용 에이전트 쌍</td>
</tr>
<tr>
<td>웹앱 (React/Next.js)</td>
<td>메인 Claude 또는 아키텍트 에이전트</td>
<td>프론트엔드 코더 에이전트</td>
<td>Designer 겸임 가능</td>
</tr>
<tr>
<td>모바일 앱 (Flutter)</td>
<td>설계 에이전트 공유 가능</td>
<td>Flutter 코더 에이전트</td>
<td>Designer 재사용</td>
</tr>
<tr>
<td>데이터 파이프라인</td>
<td>데이터 분석 에이전트</td>
<td>데이터 엔지니어 에이전트</td>
<td>도메인별 분리</td>
</tr>
<tr>
<td>인프라/DevOps</td>
<td>인프라 플래너</td>
<td>인프라 실행자</td>
<td>되돌리기 비용 높음 → Critical 비율 높음</td>
</tr>
</tbody></table>
<p>Designer가 없으면 메인 Claude가 겸임하되, <strong>교차 검증으로 자기 동의 편향을 보완</strong>한다.</p>
<hr>
<h2 id="핵심-교훈">핵심 교훈</h2>
<ol>
<li><strong>AI의 코딩 실력은 충분하다. 부족한 건 프로세스다.</strong> — 같은 모델이 프로세스만 바꿔도 성공률이 극적으로 올라간다.</li>
<li><strong>역할 분리의 핵심은 &quot;하지 않는 것&quot;을 정의하는 것이다.</strong> — Designer가 코드를 쓰는 순간 프레임워크가 무너진다.</li>
<li><strong>되돌리기 비용이 트랙을 결정한다.</strong> — 줄 수, 파일 수는 보조 지표일 뿐이다.</li>
<li><strong>&quot;빨리 해&quot;는 게이트 스킵이 아니다.</strong> — 급할수록 프로세스를 지켜야 한다. 7회 실패와 1회 성공 중 어느 쪽이 더 빠른가.</li>
<li><strong>에이전트는 경험으로 성장한다.</strong> — 실패 교훈을 에이전트 카드에 축적하면, 프로젝트가 진행될수록 에이전트가 똑똑해진다.</li>
<li><strong>자기 동의 편향은 실재한다.</strong> — 반드시 교차 모델 검수를 적용해야 한다.</li>
</ol>
<hr>
<p><em>CAOF는 Claude Code의 커스텀 에이전트 시스템 위에 구축된 실전 프레임워크입니다. 2026년 3월, 실제 프로젝트에서의 반복 실패를 계기로 설계되었으며, 도입 후 구현 성공률이 극적으로 개선되었습니다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter GetX] Getx Binding의 기초 개념과 의존성 관리 전략]]></title>
            <link>https://velog.io/@s_soo100/Flutter-GetX-Getx-Binding%EC%9D%98-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90%EA%B3%BC-Binding-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@s_soo100/Flutter-GetX-Getx-Binding%EC%9D%98-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90%EA%B3%BC-Binding-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Wed, 02 Jul 2025 00:54:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>주의!</strong> <strong>이미 GetX를 사용하고 있는 프로젝트</strong>에서 <strong>올바른 사용법</strong>을 제시하는 것이 목적입니다. 
절대로 새로운 프로젝트에 GetX 도입을 권장하는 것이 아닙니다.
새 프로젝트에서는 Provider, Riverpod 등의 권장 패턴을 써주세요!</p>
</blockquote>
<h2 id="개요">개요</h2>
<p>Bindings는 인스턴스의 의존성 주입과 라이프사이클을 관리하는 GetX의 핵심 메커니즘입니다. </p>
<p><strong>GetX Bindings의 주요 특징:</strong></p>
<ul>
<li><strong>의존성 주입 관리</strong>: 컨트롤러, 서비스, 저장소 등의 의존성을 자동으로 관리, Get.put()과 Get.find() 사용</li>
<li><strong>메모리 최적화</strong>: 페이지 생성 시에만 의존성을 주입하고, 페이지 제거 시 자동으로 메모리에서 해제</li>
<li><strong>생명주기 관리</strong>: 라우트 기반으로 의존성의 생명주기를 자동 관리</li>
</ul>
<p>이 글에서는 GetX 공식 문서의 Bindings 개념과 참여했던 실제 프로젝트에서 어떻게 적용했는지 비교 분석해보겠습니다.</p>
<hr>
<h2 id="getx-bindings란">GetX Bindings란?</h2>
<h3 id="공식-문서-정의공식문서">공식 문서 정의(<a href="https://pub.dev/packages/get/versions/4.7.2">공식문서</a>)</h3>
<p>Bindings는 인스턴스의 의존성 주입과 라이프사이클을 관리하는 GetX의 핵심 메커니즘 이며, 다음과 같은 특징을 가집니다:</p>
<ul>
<li><strong>의존성 주입 관리</strong>: 컨트롤러, 서비스, 저장소 등의 의존성을 자동으로 관리</li>
<li><strong>메모리 최적화</strong>: 페이지가 생성될 때만 의존성을 주입하고, 페이지가 제거될 때 자동으로 메모리에서 해제</li>
<li><strong>생명주기 관리</strong>: 라우트 기반으로 의존성의 생명주기를 자동 관리, 알아서 켜지고 꺼지고</li>
</ul>
<p>확실히 &#39;마법같은&#39; 기능을 제공하기로 유명한 만큼, 개발자들이 놓칠 수 있는 부분을 알아서 잡아주는 편리함이 눈에 띄네요.</p>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-dart">class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.put&lt;HomeController&gt;(HomeController());
    Get.lazyPut&lt;ApiService&gt;(() =&gt; ApiService());
  }
}</code></pre>
<p>이런 식으로 HomeBinding을 구성하고, HomePage의 라우트에 같이 묶어둡니다. 그리고 Get.put 메서드에는 주입하는 의존성의 타입을 정확하게 명시해주세요.</p>
<pre><code class="language-dart">static final routes = [
  GetPage(
    name: HomePage.routeName,
    page: () =&gt; const HomePage(),
    binding: HomeBinding()),
  ...
  ];</code></pre>
<p>이제 HomePage로 라우팅 할 때 HomeBinding에 붙어있는 의존성들이 줄줄히 인스턴스화 됩니다.
put메서드가 아니라 lazyPut메서드로 주입한건 &#39;필요할 때만 생성&#39;됩니다. 아래에서 또 언급 하겠습니다.</p>
<hr>
<h2 id="getmaterialapp과-bindings의-관계">GetMaterialApp과 Bindings의 관계</h2>
<h3 id="getmaterialapp의-핵심-역할">GetMaterialApp의 핵심 역할</h3>
<p>GetX의 모든 기능은 <code>GetMaterialApp</code> 내에서 동작합니다. 이는 일반 Flutter의 <code>MaterialApp</code>을 확장한 것으로, GetX의 상태관리, 라우팅, 의존성 주입 등의 기능을 제공합니다. 물론 Binding도 이 안에서 사용해야겠죠?</p>
<pre><code class="language-dart">class App extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: &#39;WheelyX&#39;,
      initialRoute: PageRouter.initial,
      getPages: PageRouter.routes,
      initialBinding: InitialBinding(),  // 앱 전체 초기 의존성
      // ... 기타 설정들
    );
  }
}</code></pre>
<h3 id="bindings의-계층-구조">Bindings의 계층 구조</h3>
<p>GetMaterialApp사용 하에 Bindings는 라우터에만 거는게 아니라, 다양한 방식으로 여러곳에 <strong>묶을</strong> 수 있습니다.</p>
<ol>
<li><strong>InitialBinding</strong>: 앱 시작 시 전역적으로 필요한 의존성 (상시 의존)</li>
<li><strong>Route Binding</strong>: 각 페이지별로 필요한 의존성 (페이지에 의존)</li>
</ol>
<p>GetMaterialApp 환경에서 Bindings는 다음과 같은 계층 구조로 관리됩니다:</p>
<pre><code>My App Service
├── InitialBinding (앱 전체)
│   ├── AuthStore (permanent: true)
│   ├── FCM Controller (permanent: true)
│   └── Repository Layer (fenix: true)
└── Route Bindings (페이지별)
    ├── HomeBinding
    ├── AuthBinding
    └── TrainingBinding</code></pre><hr>
<h2 id="initialbinding---앱-전체-초기-의존성">InitialBinding - 앱 전체 초기 의존성</h2>
<p>주로 앱 전체에서 공유되는 핵심 서비스들을 여기에 걸어둡니다.</p>
<pre><code class="language-dart">class InitialBinding extends Bindings {
  @override
  void dependencies() {
    // FCM 푸시메세지 관리자
    Get.put&lt;FcmFirebaseController&gt;(
      FcmFirebaseController(messaging: FirebaseMessaging.instance),
      permanent: true,  // 앱 종료까지 유지
    );

    // 항상 관리해야 하는 중요한 상태
    Get.put&lt;AuthStore&gt;(AuthStoreImpl(), permanent: true);
    Get.put&lt;HistoryStore&gt;(HistoryStoreImpl(), permanent: true);

    // Repository 계층
    Get.lazyPut&lt;AuthRepository&gt;(
      () =&gt; AuthRepositoryImpl(authDio: Get.find&lt;AuthDio&gt;()),
      fenix: true,  // 사용 후에도 메모리에 유지
    );
  }
}</code></pre>
<p>이렇게 선언후 위의 GetMaterilaApp의 initialBinding에 넣어주면 됩니다.
프로젝트에서 InitialBinding에 포함시켜야 할 주요 의존성은 대부분 전역에서 관리해야 하는 객체들로, 아래처럼 정리할 수 있을 것 같습니다.</p>
<p><strong>포함해야 할 의존성</strong>:</p>
<ul>
<li>앱 생명주기와 일치하는 서비스</li>
<li>전역 상태 관리 객체</li>
<li>시스템 서비스 (푸시 알림, 센서 연결 등)</li>
<li>인증 관련 서비스</li>
</ul>
<p><strong>제외해야 할 의존성</strong>:</p>
<ul>
<li>페이지별 ViewModel</li>
<li>초기화 비용이 높은 서비스</li>
<li>조건부로만 필요한 기능</li>
</ul>
<p>여기서 짚어봐야 할 점이 있습니다. 바로 <strong>Get.put</strong>과 <strong>lazyPut</strong>이라는 인스턴스의 <strong>두 개의 의존성 생산 전략</strong>과, 이들과 함께 붙는 <strong>permanent, fenix 파라미터</strong> 입니다.
잠시 체크하고 가겠습니다.</p>
<h3 id="getput-vs-getlazyput">Get.put() vs Get.lazyPut()</h3>
<p>생성 시점과 메모리 비용 효율성을 고려해서 어떻게 생성할 건지를 결정하는 메서드인 만큼, 잘 골라서 사용해야 합니다. </p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Get.put()</th>
<th>Get.lazyPut()</th>
</tr>
</thead>
<tbody><tr>
<td>생성 시점</td>
<td>즉시 생성</td>
<td>첫 사용 시 생성</td>
</tr>
<tr>
<td>메모리 사용</td>
<td>높음 (즉시 점유)</td>
<td>효율적 (필요시만)</td>
</tr>
<tr>
<td>적용 대상</td>
<td>필수 서비스</td>
<td>선택적 서비스</td>
</tr>
<tr>
<td>초기화 시간</td>
<td>앱 시작 시 부담</td>
<td>분산된 부담</td>
</tr>
</tbody></table>
<h3 id="permanent-vs-fenix-심화-이해">permanent vs fenix 심화 이해</h3>
<p>permanent의 경우는 말 그대로 true로 하는 경우 어떤 상황에도 종료되지 않고 항시 인스턴스가 유지되도록 해주는 기능입니다.
앱이 종료되기 전 까지 절대로 해제되지 않고 메모리에 상주시키며, 항상 접근이 가능한 기능입니다.</p>
<p>그렇다면 fenix(피닉스)는 lazyPut에 붙어서 사용 할 때마다 부활하는 특성을 보여주는 기능으로, 사용 후 메모리에서 해제되지만 재사용 시 기존 인스턴스를 그대로 활용하는 기능입니다.</p>
<p> 좀 유치하지만 <strong>&quot;불사조(Phoenix)&quot; 패턴</strong>이라고 부르던데.. 아래와 같은 동작방식을 가지니 알아두면 좋을 것 같습니다.</p>
<p> <strong>동작 방식:</strong></p>
<ol>
<li><strong>첫 번째 Get.find</strong>: 인스턴스가 생성되고 메모리에 저장됨</li>
<li><strong>사용 완료</strong>: 일반적으로 <code>lazyPut</code>은 사용 후 메모리에서 해제됨</li>
<li><strong>재사용 시</strong>: <code>fenix: true</code>가 있으면 <strong>기존 인스턴스를 재활용</strong></li>
<li><strong>메모리 효율성</strong>: 사용하지 않을 때는 메모리에서 해제되지만, 재사용 시 빠른 접근 가능</li>
</ol>
<p>즉, 인스턴스를 새로 만들지 않고, 기존 인스턴스가 잠깐 죽어있다가 부활한다는 의미입니다.</p>
<h4 id="authrepository에-왜-lazyput과-fenix를-사용했을까요">AuthRepository에 왜 lazyPut과 fenix를 사용했을까요?</h4>
<p>항시 켜놓아도(put과 permanent를 통해) 상관없을지 모르지만, repository계층의 특성을 생각하면 lazyPut이 적합하다고 판단했습니다.
하지만 인스턴스는 쓰던 놈을 계속 써야 하죠. 정리하자면 아래와 같은 이유가 있습니다.</p>
<ul>
<li><strong>네트워크 연결 재사용</strong>: HTTP 클라이언트(Dio) 연결을 재활용하여 성능 향상</li>
<li><strong>토큰 관리</strong>: 인증 토큰이 저장된 Repository 인스턴스를 재사용</li>
<li><strong>메모리 효율성</strong>: 사용하지 않을 때는 메모리에서 해제, 필요할 때만 유지</li>
<li><strong>빠른 응답</strong>: 재사용 시 새로운 인스턴스 생성 시간 절약</li>
</ul>
<p>실제 프로젝트에서의 활용 예시도 볼까요?</p>
<pre><code class="language-dart">// AuthDio - HTTP 클라이언트 (토큰 관리)
Get.lazyPut&lt;AuthDio&gt;(
  () =&gt; AuthDio(locale: Get.locale, dio: Dio(), storage: FlutterSecureStorage()),
  fenix: true,
);

// AuthRepository - 인증 관련 API 호출
Get.lazyPut&lt;AuthRepository&gt;(
  () =&gt; AuthRepositoryImpl(authDio: Get.find&lt;AuthDio&gt;()),
  fenix: true,
);</code></pre>
<p>이렇게 <code>fenix: true</code>를 사용하면 <strong>메모리 효율성과 성능을 동시에 확보</strong>할 수 있습니다.
주로 Repository, API 클라이언트같은** 무거운 객체<strong>, 네트워크 연결, 데이터베이스 연결 같은</strong> 초기화 비용이 높은 객체<strong>, 특정 기능에서만 **가끔 사용하는 객체</strong> 등을 등록해주시면 됩니다.</p>
<hr>
<h2 id="routebinding---라우트-기반-의존성-관리">RouteBinding - 라우트 기반 의존성 관리</h2>
<p>Route Bindings는 각 페이지별로 필요한 의존성을 관리하는 핵심 메커니즘입니다. InitialBinding에서 전역 서비스를 관리했다면, Route Bindings는 페이지 생명주기와 연결된 컨트롤러와 서비스를 관리합니다.</p>
<h3 id="기본-구현-패턴">기본 구현 패턴</h3>
<h4 id="1-단일-viewmodel-binding">1. 단일 ViewModel Binding</h4>
<pre><code class="language-dart">class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.put&lt;HomeViewModel&gt;(HomeViewModel(
      authRepository: Get.find&lt;AuthRepository&gt;(),
      authStore: Get.find&lt;AuthStore&gt;(),
    ));
  }
}</code></pre>
<p><strong>특징</strong>:</p>
<ul>
<li>페이지 진입 시 ViewModel 생성</li>
<li>페이지 종료 시 자동으로 메모리에서 해제</li>
<li>InitialBinding의 전역 의존성 활용</li>
</ul>
<h4 id="2-다중-의존성-binding">2. 다중 의존성 Binding</h4>
<p>여러 기능을 하나의 ViewModel에 묶어서 사용하는 경우 다중 의존성 Binding기법을 사용해주세요.</p>
<p>제 프로젝트에서는 운동을 수행할 때, 유저 정보, 히스토리(운동 기록)정보, 블루투스 연결, 운동을 수행하는 트레이닝 서비스 등등 많은 기능이 필요했습니다.</p>
<pre><code class="language-dart">class TrainingBinding extends Bindings {
  @override
  void dependencies() {
    // ViewModel 주입
    Get.put&lt;BasicTrainingViewModel&gt;(BasicTrainingViewModel(
      authRepository: Get.find&lt;AuthRepository&gt;(),
      historyRepository: Get.find&lt;HistoryRepository&gt;(),
      bluetoothStore: Get.find&lt;BluetoothStoreImpl&gt;(),
    ));

    // 페이지별 서비스 주입
    Get.put&lt;TrainingService&gt;(TrainingService());
    Get.put&lt;TimerService&gt;(TimerService());

    // Lazy 의존성 주입
    Get.lazyPut&lt;SensorDataProcessor&gt;(() =&gt; SensorDataProcessor());
  }
}</code></pre>
<h3 id="route-bindings-심화-패턴">Route Bindings 심화 패턴</h3>
<h4 id="1-조건부-bindings">1. 조건부 Bindings</h4>
<p>Get.arguments를 통해 매개변수를 전달 받아서 의존성을 조건부로 주입합니다. </p>
<pre><code class="language-dart">class HistoryDetailBinding extends Bindings {
  @override
  void dependencies() {
    // 기본 ViewModel
    Get.put&lt;HistoryDetailViewModel&gt;(HistoryDetailViewModel(
      historyRepository: Get.find&lt;HistoryRepository&gt;(),
    ));

    // 조건부 서비스 주입
    final args = Get.arguments as HistoryDetailArgs?;
    if (args?.isGameHistory == true) {
      Get.put&lt;GameAnalysisService&gt;(GameAnalysisService());
    }

    if (args?.needsComparison == true) {
      Get.lazyPut&lt;ComparisonService&gt;(() =&gt; ComparisonService());
    }
  }
}</code></pre>
<p>혹은 Get.parameters를 사용해도 됩니다.</p>
<pre><code class="language-dart">class CourseBinding extends Bindings {
  @override
  void dependencies() {
    // 트레이닝 코스를 판별
    final courseId = Get.parameters[&#39;courseId&#39;];

    // 특정 트레이닝 코스별로 기능 구분
    Get.put&lt;CourseViewModel&gt;(CourseViewModel(
      courseId: courseId,
      courseRepository: Get.find&lt;CourseRepository&gt;(),
    ));
    Get.lazyPut&lt;CourseAnalyzer&gt;(() =&gt; CourseAnalyzer(courseId: courseId));
  }
}</code></pre>
<h4 id="2-계층형-bindings">2. 계층형 Bindings</h4>
<p><code>BaseBinding</code>을 만들어서 코드의 중복을 줄이고, 공통 사용 의존성을 쉽게 관리합니다. <code>Bindings</code>도 클래스기 때문에 당연히 유효한 전략입니다.</p>
<pre><code class="language-dart">// 공통 기능을 담당하는 Base Binding
abstract class BaseTrainingBinding extends Bindings {
  void registerCommonDependencies() {
    Get.put&lt;SensorService&gt;(SensorService());
    Get.put&lt;AudioService&gt;(AudioService());
  }
}

// 구체적인 트레이닝별 Binding
class ATrainingBinding extends BaseTrainingBinding {
  @override
  void dependencies() {
      // 공통 의존성 등록 딸깍
    registerCommonDependencies();

    Get.put&lt;ATrainingViewModel&gt;(ATrainingViewModel(
      sensorService: Get.find&lt;SensorService&gt;(),
      audioService: Get.find&lt;AudioService&gt;(),
    ));
  }
}

class BTrainingBinding extends BaseTrainingBinding {
  @override
  void dependencies() {
      // 공통 의존성 등록 딸깍
    registerCommonDependencies();

    Get.put&lt;BTrainingViewModel&gt;(BTrainingViewModel(
      sensorService: Get.find&lt;SensorService&gt;(),
      audioService: Get.find&lt;AudioService&gt;(),
    ));

    // B 트레이닝에만 붙는 서비스
    Get.put&lt;SpecialTimerService&gt;(SpecialTimerService());
  }
}</code></pre>
<hr>
<h2 id="마무리-및-getx-bindings-사용-시-주의사항">마무리 및 GetX Bindings 사용 시 주의사항</h2>
<p>마무리로 Bindings사용 전략을 정리하고, 권장 패턴과 안좋은 패턴을 알아보고 끝내겠습니다.</p>
<ol>
<li><strong>과도한 전역 의존성 지양</strong>: InitialBinding에는 정말 필요한 것만 등록</li>
<li><strong>타입 안전성</strong>: <code>Get.put&lt;Type&gt;()</code>에서 타입을 명시적으로 선언</li>
<li><strong>메모리 관리</strong>: <code>permanent</code>와 <code>fenix</code> 옵션을 적절히 활용</li>
<li><strong>의존성 순서</strong>: 의존성 간의 순서를 고려하여 등록</li>
</ol>
<h4 id="권장-패턴">권장 패턴</h4>
<pre><code class="language-dart">// ✅ 좋은 예
class WellStructuredBinding extends Bindings {
  @override
  void dependencies() {
    // 1. 필수 서비스부터
    Get.put&lt;CoreService&gt;(CoreService());

    // 2. ViewModel 등록
    Get.put&lt;PageViewModel&gt;(PageViewModel(
      coreService: Get.find&lt;CoreService&gt;(),
    ));

    // 3. 옵셔널 서비스는 lazy로
    Get.lazyPut&lt;OptionalService&gt;(() =&gt; OptionalService());
  }
}</code></pre>
<h4 id="비-권장-패턴">비 권장 패턴</h4>
<pre><code class="language-dart">// ❌ 피해야 할 예
class PoorBinding extends Bindings {
  @override
  void dependencies() {
    // 타입 명시 없음
    Get.put(SomeController());

    // 불필요한 permanent 사용
    Get.put(TemporaryService(), permanent: true);

    // 의존성 순서 무시
    Get.put(DependentService(Get.find&lt;NotYetRegistered&gt;()));
  }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter]Firebase Apple 로그인 시 "Invalid OAuth response from apple.com" 에러]]></title>
            <link>https://velog.io/@s_soo100/Firebase-Apple-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%8B%9C-Invalid-OAuth-response-from-apple.com-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@s_soo100/Firebase-Apple-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%8B%9C-Invalid-OAuth-response-from-apple.com-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Fri, 27 Jun 2025 01:58:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Unhandled Exception: [firebase_auth/invalid-credential] Invalid OAuth response from apple.com</p>
</blockquote>
<p><strong>Firebase Authentication</strong> 관련해서, 특히 <strong>Apple 소셜로그인</strong> 기능에서 해당 에러가 발생하는데 원인과 해결책을 공유하고자 합니다.</p>
<p>이것은.. Firebase 인증을 사용하여 Flutter 앱에 Apple로 로그인 기능을 통합할 때 발생하는 &quot;[firebase_auth/invalid-credential] Invalid OAuth response from apple.com&quot; 오류입니다. </p>
<p>이 오류는 Firebase에 제공된 OAuth 자격 증명이 불완전하거나 유효하지 않을 때 발생합니다. </p>
<pre><code>[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: [firebase_auth/invalid-credential] Invalid OAuth response from apple.com
#0      FirebaseAuthHostApi.signInWithCredential (package:firebase_auth_platform_interface/src/pigeon/messages.pigeon.dart:1098:7)
&lt;asynchronous suspension&gt;
#1      MethodChannelFirebaseAuth.signInWithCredential (package:firebase_auth_platform_interface/src/method_channel/method_channel_firebase_auth.dart:306:22)
&lt;asynchronous suspension&gt;
#2      FirebaseAuth.signInWithCredential (package:firebase_auth/src/firebase_auth.dart:515:9)
&lt;asynchronous suspension&gt;
#3      AuthService.signInWithApple (package:parkbestapp/services/auth_service.dart:73:9)
&lt;asynchronous suspension&gt;</code></pre><p>위와 같이 에러가 나오더라도, 처음 직면했다면 원인을 어디부터 찾아야 할 지 애먹을 수 있습니다.</p>
<hr>
<h3 id="원인-분석">원인 분석</h3>
<p>Firebase Auth, 특히 애플쪽 에러는 4가지를 체크 해야 합니다.</p>
<p><strong>1단계: Apple Developer Console 설정
2단계: Firebase Console 설정
3단계: 내 프로젝트 코드 확인</strong></p>
<p>현재 문제는, <strong>결론부터 말씀 드리면 3단계, 내 프로젝트 코드부터 체크 해야 하는 문제로</strong>, 이전까지 OAuthCredential을 생성하기 위해 넣던 데이터에 한 가지를 추가로 넣어주어야 합니다.
FirebaseApple 로그인 프로세스에서 검색된 accessToken이 누락되거나 유효하지 않기 때문입니다. </p>
<p>특히 내 패키지들 버전(혹은 플러터 버전도 같이)을 훅 올렸을 때 직면하게 되는 문제인데, firebase_auth 패키지 버전 4.3.0부터 웹 Firebase SDK v9의 모듈식 접근 방식과 관련된 변경 사항이 적용되었고 이로 인해 Apple로 로그인 시 accessToken 또는 authorizationCode를 전달해주도록 변경되었습니다.</p>
<p>그러므로 <strong>3단계 코드 수정</strong>부터 들어가고, 이후 1, 2단계 부분을 어떻게 확인해야 하는지도 같이 살짝 들여다보겠습니다.
(필요 없으신 분들은 바로 아래에 있는 &quot;3단계: 코드 수정하기&quot;까지만 봐주세요!)</p>
<hr>
<h4 id="3단계-코드-수정하기">3단계: 코드 수정하기</h4>
<p>코드 &#39;수정하기&#39; 인 만큼, 이전에 애플 소셜로그인 기능을 이미 다 구현했다고 생각하고 진행 하겠습니다.
처음 구현해야 한다면 <a href="https://velog.io/@s_soo100/Flutter-Firebase%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%95%A0%ED%94%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-Apple-Sign-in-With-Firebase">이 블로그 글</a>로 가주세요!</p>
<p>아마 대부분의 플러터 개발자 분들이 <a href="https://firebase.flutter.dev/docs/auth/social#apple">FlutterFire 공식 docs</a>를 보고 애플 소셜 로그인 기능을 구현 하실텐데요, 아직 이 공식문서에도 문제 해결 방법은 적용이 안 되어 있습니다.</p>
<p>위에서 말씀드린 OAuthCredential 생성 부분입니다.</p>
<pre><code class="language-dart">  // Create an `OAuthCredential` from the credential returned by Apple.
  final oauthCredential = OAuthProvider(&quot;apple.com&quot;).credential(
    idToken: appleCredential.identityToken,
    rawNonce: rawNonce,
  );
</code></pre>
<p>이 부분에서 accessToken이라는 한 가지 매개변수를 추가해주면 됩니다.</p>
<pre><code class="language-dart">final oauthCredential = OAuthProvider(&quot;apple.com&quot;).credential(
  idToken: appleCredential.identityToken,
  rawNonce: rawNonce,
  accessToken: appleCredential.authorizationCode, // 이제 appleCredential의 인증 코드를 넣어주세요
);</code></pre>
<p>끝이냐구요? 네, 끝입니다.
accessToken을 넣어준 것으로 생성되는 OAuthCredential객체가(위 코드에는 타입이 안 써있지만, oauthCredential의 명시적 타입입니다.) 필요한 데이터를 잘 받게 되었으니 이제 다른 firebase나 apple developer쪽에 문제가 없으면 로그인이 정상적으로 잘 될 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/9ff19783-7b26-40f2-8231-cf2da63a2f09/image.png" alt="">그래도 혹시 모르니 print를 촘촘하게 찍어서 모두 체크해주세요.
이런식으로 보면 서로 알기 쉽고 좋습니다.</p>
<p>여기 말고도 FirebaseAuth의 소셜 로그인 Flow마다 응답 객체와 승인 여부를 함께 확인해주세요.</p>
<p>이후에도 로그인이 안 된다면, 아래 1단계, 2단계 설정 확인 방법을 진행해주세요.</p>
<hr>
<h4 id="1단계-apple-developer-console-설정">1단계: Apple Developer Console 설정</h4>
<p><a href="https://developer.apple.com/account">https://developer.apple.com/account</a> 로 접속후 로그인,
이후 <strong>&#39;인증서, ID 및 프로파일(Certificates, Identifiers &amp; Profiles)&#39;</strong>에서 다음 설정을 확인해주세요:</p>
<hr>
<p><strong>1-1. 식별자(Identifiers) 확인하기</strong>
<img src="https://velog.velcdn.com/images/s_soo100/post/f080b939-decb-40b7-ad2c-2d25474282c7/image.png" alt=""></p>
<p>위 스크린샷 처럼 Service ID를 찾아서 들어간 후, 소셜로그인에 사용하고 있는 Service ID를 누릅니다.</p>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/149ebae6-af4d-432b-8359-f7d90ccbaf72/image.png" alt=""></p>
<p>그리고 위와 같이 primary app id, domain, returl url이 잘 적혀있는지 체크해주세요. </p>
<p>primary app id는 &#39;Identifiers&#39;의 &#39;App IDs&#39;에서 확인할 수 있는데, 프로젝트를 등록할 때 필수로 쓰는 &#39;com.myCompany.projectName&#39;같은 값을 말합니다.</p>
<p><strong>Return URL</strong>의 경우는 Firebase Console의 Authentication-로그인 방법-로그인 제공업체 중 Apple로 들어가시면 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/s_soo100/post/39fa9696-f5ff-4e32-a914-ab876851534e/image.png" alt=""></p>
<p><strong>Domain</strong>은 Return URL에서 맨 앞의 &#39;https(혹은 http)://&#39;와 맨 뒤의 &#39;/__/auth/handler&#39;를 뺀 부분입니다.</p>
<hr>
<p><strong>1-2. Keys확인하기</strong></p>
<p>다시 Certificates, Identifiers &amp; Profiles에서 이제는 Keys로 들어갑니다. 그러면 내 소셜로그인에 사용하고 있는 Key가 보일텐데, 들어가서 &#39;Sign In with Apple&#39;서비스가 Enable되어 있는지, &#39;Primary App ID&#39;가 &#39;Service ID&#39;에 설정한 &#39;Primary App ID&#39;랑 동일한지를 확인해주세요.
<img src="https://velog.velcdn.com/images/s_soo100/post/d58f466d-064c-4ad1-a27b-5ec1f6c3dafb/image.png" alt=""></p>
<p>이미 만들어놓은 key의 경우 .p8파일 다운로드가 안되니, 어디에 잘 보관해두신 파일이 있는지 찾아보거나 새로 만들어주세요!</p>
<p>이 Service ID와 key는 Firebase Console에 그대로 넣어야 합니다. </p>
<hr>
<h4 id="2단계-firebase-console-설정">2단계: Firebase Console 설정</h4>
<p>내 프로젝트의 Authentication-로그인 방법-로그인 제공업체 중 Apple로 들어가서, 위의 Apple Developer Console에서 확인한 정보들이 잘 기입되어 있는지 체크합니다.</p>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/e365e525-2935-46f3-ace6-d3fce874323b/image.png" alt=""></p>
<p>특히 비공개 키의 경우, 위에서 말씀드린 대로 사용하시는 키의 .p8파일을 코드에디터로 열어서 그 안에 있는 내용을 전체 다 복붙 해줘야 합니다.</p>
<hr>
<p>자 이제 Apple Developer Console 점검 방법, Firebase Console 점검 방법, 내 코드 점검 방법까지 확인해봤습니다.
에러가 해결되셨기를 바라며 마치겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Provider라이브러리 파헤치기 4 - Element 트리와 역할 그리고 Provider의 데이터 변경 흐름]]></title>
            <link>https://velog.io/@s_soo100/Flutter-Provider%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-4-Element%ED%8A%B8%EB%A6%AC%EC%99%80-Provider%EC%9D%98%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@s_soo100/Flutter-Provider%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-4-Element%ED%8A%B8%EB%A6%AC%EC%99%80-Provider%EC%9D%98%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Mon, 28 Apr 2025 06:19:48 GMT</pubDate>
            <description><![CDATA[<p>이번 시간에는 Flutter에서 Provider가 Element 트리를 어떻게 활용하고, 왜 화면을 다시 그려주는지, 값은 어디다 저장하는건지 조금 더 깊게 파헤쳐보고자 합니다.</p>
<p>그러기 앞서 먼저 Flutter의 기본 골자에 대해서 다시 복습해 보죠.</p>
<hr>
<h2 id="flutter의-기본-구조-widget-element-renderobject-트리">Flutter의 기본 구조: Widget, Element, RenderObject 트리</h2>
<p>Widget: UI를 설명하는 불변(immutable) 객체입니다. 단순한 설계도 역할을 합니다.</p>
<p>Element: Widget의 런타임 표현으로, 실제 사용 중에 상태를 관리하고 연결을 유지합니다.</p>
<p>RenderObject: Element와 연결되어 실제 화면을 구성하고 그림을 그리는 객체입니다.</p>
<p><strong>즉, Flutter는 Widget → Element → RenderObject로 이어지는 구조를 통해 화면을 구성합니다.</strong>
provider또한 이 구조를 사용해서 상태를 관리하고 리렌더링을 하게 되는데, 특히 데이터를 효율적으로 전달하기 위해 element 트리를 활용합니다.</p>
<hr>
<h2 id="provider의-element활용">Provider의 Element활용</h2>
<h3 id="1-provider의-기본-inheritedwidget-기능-확장">1. Provider의 기본: InheritedWidget 기능 확장</h3>
<ul>
<li><p>(1장 내용)Provider는 Flutter의 InheritedWidget을 기반으로 하여, 데이터 전파와 상위-하위 위젯 간 데이터 공유를 가능하게 합니다.</p>
</li>
<li><p>하지만 정확히 말하면, Provider는 InheritedWidget을 확장(extend)하여 InheritedElement를 통해 상태 관리와 화면 리렌더링을 제어하고 있어요. 여기에 상태관리 기능(DelegateState)을 추가한거죠.</p>
</li>
<li><p>InheritedWidget인은 InheritedElement를 만들어서 관리하는데, Provider는 내부적으로 InheritedWidget의 메커니즘을 extend하고 있기 때문에 마찬가지로 InheritedElement를 사용합니다.</p>
</li>
</ul>
<h3 id="2-buildcontext와-element의-관계">2. BuildContext와 Element의 관계</h3>
<ul>
<li><p>Flutter에서 BuildContext는 사실상 하나의 Element입니다. build메서드는 보통 <code>build(BuildContext context)</code>이렇게 선언되잖아요?</p>
</li>
<li><p>모든 위젯이 빌드 될 때에 이(BuildContext context)가 포인터 개념이 되어서 자신의 위치를 알려주게 됩니다.</p>
</li>
<li><p>이것을 응용해서 Provider는, Provider.of<T>(context), context.watch<T>() 등을 호출하면, context를 시작으로 가장 가까운 InheritedElement를 탐색하도록 구성되어 있습니다.
마찬가지로 1장에서 다뤘듯, .of메서드는 <code>dependOnInheritedWidgetOfExactType</code>을 래핑해놓은 문법적 설탕이죠</p>
</li>
<li><p>자 이제 탐색 과정에서 내가 찾고있는 데이터가 있으면 의존성 등록을 하여, 이후 데이터 변경을 감지할 수 있게 됩니다.</p>
</li>
</ul>
<pre><code class="language-dart">InheritedWidget dependOnInheritedWidgetOfExactType&lt;T extends InheritedWidget&gt;() {
  registerDependency(this, inheritedElement);
  return inheritedElement.widget as T;
}</code></pre>
<ul>
<li>registerDependency(의존성 등록)에 inheritedElement를 넣잖아요? 
해당 Element의 _dependencies 목록에 등록되어, Provider가 데이터 변경(notify) 시 관련 위젯들이 리빌드됩니다.</li>
</ul>
<h3 id="3-데이터-변경-통지-과정">3. 데이터 변경 통지 과정</h3>
<p>Provider 내부 데이터가 변경되면, 
  (1)의존성을 등록했던 Consumer 위젯들이 다시 알림을 받고,
  (2)didChangeDependencies()가 호출되며,
(3) 마침내 해당 위젯들이 다시 build()됩니다.</p>
<p>이렇게 Element 구조를 이용해 Provider는 효율적으로 화면을 리렌더링합니다.</p>
<hr>
<h2 id="delegatestate의-동작-방식">DelegateState의 동작 방식</h2>
<p>  다음은 Provider 내부 구조의 핵심 중 하나인 DelegateState에 대해서 보겠습니다.</p>
<h3 id="1-delegatestate란">1. DelegateState란?</h3>
<ul>
<li>Provider 패키지에서 상태 관리와 생명주기를 처리하기 위한 내부 클래스입니다. 이 클래스는 실제 상태 객체(실제 관리하는 값)을 관리하고, 상태 생명주기를 제어합니다.</li>
<li>또한 실제로 필요할 때까지 상태 객체 생성을 지연시키는 등 리소스 효율성을 향상시키도록 설계되어 있습니다.</li>
</ul>
<h3 id="2-내부-구조">2. 내부 구조</h3>
<ul>
<li><p>이번에도   실제 코드를 보면서 공부해봅시다.</p>
<pre><code class="language-dart">// Provider 패키지 내부에서 꺼내옴 (간략히)
class _DelegateState&lt;T&gt; {
// 상태 값 저장
T? _value;
bool _hasValue = false;

// 값에 접근하는 방법, getter사용
bool get hasValue =&gt; _hasValue;
T get value {
  if (!_hasValue) throw StateError(&#39;대충 값 없다는 내용&#39;);
  return _value as T;
}

// 상태 업데이트, 값을 저장해줌!!
void update(T newValue) {
  _value = newValue;
  _hasValue = true;
}

// 상태 폐기
void dispose() {
  // 리소스 정리 로직
  _hasValue = false;
  _value = null;
}
}
</code></pre>
</li>
</ul>
<pre><code>- 구성은 생각보다 단순합니다. 그저 상태 객체의 생성 여부를 추적하고, dispose하면 리소스를 정리해주죠
- 즉, 정리하면 &#39;값을 가지게 되는 컨테이너&#39;이면서,  생명주기(lifecycle)를 관리해주는 놈입니다.


### 3. DelegateState의 작동 흐름

```dart
Provider&lt;Counter&gt;(
  create: (_) =&gt; Counter(),
  child: MyWidget(),
)
</code></pre><ul>
<li>이런식으로 Provider를 등록하면, create메서드가 내가 원하는 값 객체(지금은 카운터죠)를 만듭니다.</li>
<li>그 후 DelegateState가 update()로 이 Counter 객체를 저장하고, 값을 보관해줍니다.</li>
<li>이후에 값을 찾는 메서드, 예를 들어서 <code>context.read&lt;Counter&gt;()</code> 같은 코드가 호출되면 이제서야 DelegateState가 내부에 저장된 Counter 객체를 value로 꺼내서 우리에게 보여주는거죠.</li>
<li>바로 이 DelegateState가 Element안에 붙어있게 되는데, _InheritedProviderScopeElement 내부를 보면 해당 코드가 있습니다.</li>
</ul>
<pre><code class="language-dart">class _InheritedProviderScopeElement&lt;T&gt; extends InheritedElement {
  final _DelegateState&lt;T&gt; _delegateState = _DelegateState&lt;T&gt;();
}</code></pre>
<p>정리해보면, 
  <strong>(1)엘리멘트 트리에 올라간 하나의 Provider마다 하나의 엘리멘트, <code>_InheritedProviderScopeElement</code>가 만들어지는데</strong>, 
**  (2) 여기에 하나의 DelegateState가 생성되고(내부 변수로), **
**  (3)이 친구가 Element와 값을 분리해서 element를 새로 짜더라도 값을 유지해줍니다.**</p>
<hr>
<h2 id="최종-정리-provider의-데이터-변경-흐름">최종 정리 Provider의 데이터 변경 흐름</h2>
<p>Provider가 어디에 배치되고, 어떻게 동작하는지를 알아봤고, 값이 어디에 저장되는지 까지 알아봤으니 이제 전체적인 흐름을 다시 보겠습니다.</p>
<h3 id="값의-변경-흐름-추적하기">값의 변경 흐름 추적하기</h3>
<p>(1)  사용자 액션 발생 (값 입력이나 버튼 클릭 등등)</p>
<p>(2) Provider가 관리하는 객체(T value)가 notifyListeners() 호출</p>
<p>(3) _InheritedProviderScopeElement가 감지</p>
<p>(4) 필요하면 DelegateState에 새로운 값을 update()
(DelegateState.update(newValue)를 통해 값만 갱신)</p>
<p>(5) 그러고 나서 notifyClients() 호출</p>
<p>(6) 의존성 걸린 Consumer들이 didChangeDependencies() 호출</p>
<p> (7) Consumer.build()를 통해 리빌드 발생</p>
<h3 id="결론">결론</h3>
<p>  <strong>Provider는 Element 트리를 기반으로 DelegateState에 상태를 저장하고, 변경을 감지하여 필요한 부분만 효율적으로 리렌더링하는 구조를 가지고 있습니다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Provider라이브러리 파헤치기 3 - Consumer와 Selector, 위젯의 리빌드와 최적화 전략]]></title>
            <link>https://velog.io/@s_soo100/Flutter-Provider-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-3-Consumer%EC%99%80-Selector-%EC%9C%84%EC%A0%AF%EC%9D%98-%EB%A6%AC%EB%B9%8C%EB%93%9C%EC%99%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@s_soo100/Flutter-Provider-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-3-Consumer%EC%99%80-Selector-%EC%9C%84%EC%A0%AF%EC%9D%98-%EB%A6%AC%EB%B9%8C%EB%93%9C%EC%99%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Thu, 20 Mar 2025 07:32:24 GMT</pubDate>
            <description><![CDATA[<p>이번 시간에는 위젯을 리빌드 하는 전략에 대해서 몇가지 파헤쳐보고자 합니다.
Consumer, Selector, .read, .watch메서드 등의 주요 컨셉과 사용에 대해서 같이 보겠습니다.</p>
<hr>
<h3 id="consumer-위젯의-이해">Consumer 위젯의 이해</h3>
<ul>
<li>Consumer 위젯은 Provider 패키지에서 제공하는 위젯으로, Provider를 통해 제공되는 상태 변화를 감지하고 해당 상태를 사용하는 위젯을 다시 빌드하는 역할을 합니다. </li>
<li>Consumer로 감싸진 위젯은 자신이 리슨(listen)하고 있는 데이터가 변경될 때마다 자동으로 다시 빌드됩니다. 이 위젯은 위젯 트리의 상위에 있는 Provider로부터 필요한 데이터를 얻어옵니다.   <pre><code class="language-dart">Consumer&lt;MyModel&gt;(
builder: (context, model, child) {
  return Text(model.someValue);
},
)</code></pre>
</li>
<li>컨슈머는 위와 같이 기본적으로 구성 되는데요, <code>&lt;MyModel&gt;</code>처럼 제네릭으로 어떤 타입의 데이터를 listen할지 구체적으로 명시 해야 합니다.
그러면 우리가 이전에 공부했던 위젯 트리에서 해당 제네릭의 프로바이더를 찾아서 필요한 데이터를 긁어오고, notifyListeners()의 알림도 들어줍니다.</li>
</ul>
<h4 id="그러면-똑같은-타입의-프로바이더가-여러개면요">그러면, 똑같은 타입의 프로바이더가 여러개면요?</h4>
<ul>
<li>기본적으로는 위젯 트리 내에서 더 가까운 프로바이더를 우선해서 가져옵니다.<pre><code class="language-dart">ChangeNotifierProvider&lt;MyModel&gt;( // 첫 번째 MyModel Provider
create: (context) =&gt; MyModel(data: &#39;Data from Provider 1&#39;),
child: Builder(
  builder: (context) {
    return ChangeNotifierProvider&lt;MyModel&gt;( // 두 번째 MyModel Provider (더 가까움)
      create: (context) =&gt; MyModel(data: &#39;Data from Provider 2&#39;),
      child: Consumer&lt;MyModel&gt;(
        builder: (context, myModel, child) {
          return Text(myModel.data); // Data from Provider 2가 표시
        },
      ),
    );
  }
),
);</code></pre>
하지만 우리가 알고싶은건 이런게 아니죠, MultiProvider로 같이 꽂아버리면 어떻게 될까요?</li>
</ul>
<pre><code class="language-dart">MultiProvider(
  providers: [
    Provider&lt;MyModel&gt;(create: (_) =&gt; MyModel(data: &#39;1번타자&#39;)),
    Provider&lt;MyModel&gt;(create: (_) =&gt; MyModel(data: &#39;2번타자&#39;)),
  ],
  child: MyWidget(),
);</code></pre>
<ul>
<li><p>이런 상상을 하셨다면, 아래같은 에러 부터 보게 됩니다.
<code>Error: Multiple providers of the same type found at the same level in the widget tree.</code>
예외는 프로바이더를 만드신 선배님들이 처리했으니 안심하라구!</p>
</li>
<li><p>즉, 같은 타입의 프로바이더는, 위젯트리의 같은 위치에 중복 배치가 불가능하며 필수적으로 레벨을 분리해야 하기 때문에, &#39;더 가까운 놈&#39;을 찾는다고만 생각하시면 됩니다.</p>
</li>
</ul>
<hr>
<h3 id="consumer-위젯의-구성">Consumer 위젯의 구성</h3>
<ul>
<li><p>다음은 provider패키지의 src내에 있는 Consumer.dart를 일부 발췌한 것 입니다.
Consumer2~6도 있지만 이건 나중에 ProxyProvider랑 같이 보는게 좋을 거 같아요.</p>
<pre><code>class Consumer&lt;T&gt; extends SingleChildStatelessWidget {
/// {@template provider.consumer.constructor}
/// Consumes a [Provider&lt;T&gt;]
/// {@endtemplate}
Consumer({
  Key? key,
  required this.builder,
  Widget? child,
}) : super(key: key, child: child);

/// {@template provider.consumer.builder}
/// Build a widget tree based on the value from a [Provider&lt;T&gt;].
///
/// Must not be `null`.
/// {@endtemplate}
final Widget Function(
  BuildContext context,
  T value,
  Widget? child,
) builder;

@override
Widget buildWithChild(BuildContext context, Widget? child) {
  return builder(
    context,
    Provider.of&lt;T&gt;(context),
    child,
  );
}
}</code></pre></li>
<li><p>생성자 부터 볼까요?
builder함수를 필수적으로 전달 받아야 하며 builder 함수는 세 개의 파라미터를 받습니다. 우리가 제시할 프로바이더의 타입<code>&lt;T&gt;</code>을 명시해주고, ChangeNotifier에서 notifyListeners()가 호출되면, 해당 Provider를 리슨하는 모든 Consumer 위젯의 builder 함수가 실행됩니다.</p>
</li>
<li><p>child와 buildWithChild라는, 위에서 설명하지 않은 파라미터와 메서드가 보입니다. 
child는 Provider의 데이터 변경에 영향을 받지 않고 리빌드 되지 않는 위젯이며, 아래같이 전달 하는 것도 가능합니다.</p>
<pre><code>Consumer&lt;MyModel&gt;(
builder: ((context, value, child) {
  return Column(
    children: [child!, Text(&quot;child를 이렇게 사용할 수 있어요.&quot;)],
  );
}),
child: Text(&quot;Consumer의 child 파라미터&quot;),
),</code></pre></li>
<li><p>마찬가지로 buildWithChild는 부모인 <code>SingleChildStatelessWidget</code>의 추상 메서드를 구현한 것이며, 개발자는 대부분 생성자를 사용하고 이 메서드를 직접 호출하지는 않습니다. 
Provider와의 상호작용, 데이터 가져오기, child 위젯 처리 등의 복잡한 로직을 buildWithChild 내부에 캡슐화 한 것입니다.</p>
</li>
</ul>
<hr>
<h3 id="consumer-위젯이-부분적으로-리빌드-되는-이유">Consumer 위젯이 부분적으로 리빌드 되는 이유</h3>
<ul>
<li>부분적인 리빌드란 전체 화면이나 위젯 트리를 다시 빌드하는 대신, 상태가 변경된 특정 부분의 UI만 업데이트하는 것을 말 합니다.
이것은 데이터 변경이 잦은 서비스를 만들 때,UI성능 최적화에 매우 중요한 역할을 수행합니다. </li>
<li>예를 들어서 <code>StatefulWidget</code>에서 <code>setState</code>메서드를 호출하면 일반적으로 해당 위젯 자체와 그 자식 위젯들이 모두 다시 빌드되는 반면, <code>Consumer</code> 위젯을 사용하면 상태 변화에 의존하는 특정 위젯만 선택적으로 다시 빌드할 수 있습니다.
즉, Consumer 위젯으로 특정 위젯을 감싸면, 해당 위젯만이 Provider의 상태 변화에 반응하여 다시 빌드됩니다.</li>
<li>그러면 왜 리빌드 되는걸까요? Consumer의 builder메서드 자체가 직접 Provider의 데이터 변경을 감지하는 것은 아닙니다. Consumer 위젯 내부의 buildWithChild 메서드, 더 정확히는 그 안의 Provider.of<T>(context)가 데이터 변경 감지와 builder 함수 호출을 담당합니다.</li>
<li>of메서드는 context.dependOnInheritedWidgetOfExactType을 캡슐화 한 것이니 넘어가고,
중요한 것은 윗 부분에 언급한 buildWithChild 메서드 내에서 Provider.of<T>(context)가 호출된다는 것 입니다.
이 다음은 Provider의 기본 동작과도 같죠? 위젯 트리에서 가장 가까운 Provider<T>를 찾으며, 그것이 ChangeNotifierProvider면 ChangeNotifier의 addListener를 호출하여, ChangeNotifier의 데이터 변경을 구독(subscribe) 합니다.</li>
<li>Provider.of<T>(context)는 ChangeNotifier의 리스너 중 하나이므로, 데이터가 바뀌어서 notifyListeners() 호출을 받으면 Consumer 위젯을 rebuild시킵니다.
그러면 마지막으로 buildWithChild는 이 새로운 데이터와 함께 builder 함수를 호출합니다.</li>
</ul>
<hr>
<h3 id="실제로-부분만-리빌드가-되는가">실제로 부분만 리빌드가 되는가?</h3>
<ul>
<li><p>stateful위젯으로 리빌드마다 카운트를 해주는 기능을 붙여서, Provider의 동작과 전체 스크린의 rebuild가 별도로 일어나는지 확인해보겠습니다.</p>
</li>
<li><p>아래 위젯은 두 가지 기능을 합니다.
** 1. 전체 위젯의 리빌드 추적:**</p>
<ul>
<li>rebuildCount 변수를 통해 전체 MyHomePage 위젯이 리빌드되는 횟수를 추적합니다.</li>
</ul>
<p><strong>2. Consumer를 사용한 부분 리빌드:</strong></p>
<ul>
<li>카운터 값 표시를 Consumer<CounterProvider>로 감싸놨으며, CounterProvider의 상태가 변경될 때만 해당 부분이 리빌드됩니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-dart">
class MyHomePage extends StatefulWidget {
  MyHomePage({super.key});

  @override
  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();
}

class _MyHomePageState extends State&lt;MyHomePage&gt; {
  int rebuildCount = 0;

  @override
  Widget build(BuildContext context) {
    rebuildCount++;
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(&quot;current rebuild count is ${rebuildCount}&quot;),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &lt;Widget&gt;[
            Text(&quot;current rebuild count is ${rebuildCount}&quot;),
            ElevatedButton(
              onPressed: () {
                setState(() {});
              },
              child: const Text(&#39;rebuild screen&#39;),
            ),
            const Text(
              &#39;현재 카운트:&#39;,
              style: TextStyle(fontSize: 20),
            ),
            Consumer&lt;CounterProvider&gt;(
              builder: (context, counter, child) {
                return Text(
                  &#39;${counter.count}&#39;,
                  style: const TextStyle(
                      fontSize: 40, fontWeight: FontWeight.bold),
                );
              },
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    context.read&lt;CounterProvider&gt;().decrement();
                  },
                  child: const Text(&#39;-&#39;),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () {
                    context.read&lt;CounterProvider&gt;().increment();
                  },
                  child: const Text(&#39;+&#39;),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<pre><code>  &lt;img src=&quot;https://velog.velcdn.com/images/s_soo100/post/fd31f47f-6b03-448e-acfa-8398fc80dc1d/image.gif&quot; width=&quot;50%&quot;/&gt;</code></pre><ul>
<li>rebuild screen 버튼을 누르면 전체 위젯이 리빌드되어 rebuildCount가 증가합니다.
하지만 + 또는 - 버튼을 눌러 카운터 값을 변경할 때는 Consumer 내부의 Text 위젯만 리빌드되고, 전체 위젯은 리빌드되지 않습니다.</li>
</ul>
<hr>
<h3 id="selector-위젯의-개요-강점">Selector 위젯의 개요, 강점</h3>
<ul>
<li>Selector 위젯은 Provider 패키지에서 제공하는 또 다른 위젯으로, Provider의 데이터 중 특정 부분만 리슨하고 해당 부분이 변경될 때만 위젯을 리빌드하는 데 사용됩니다 </li>
<li>Selector는 Consumer에 비해 더욱 세밀한 리빌드 제어를 제공하여 성능 최적화에 유용합니다
위젯이 Provider의 전체 데이터가 아닌 특정 부분에만 의존하는 경우, Selector를 사용하면 불필요한 리빌드를 방지할 수 있습니다.
엄청 테크니션한 느낌이 들죠? <ul>
<li>예를 들어 햄버거, 감자튀김, 콜라를 모두 한 모델에서 관리하고 있다고 가정하고, 남은 콜라의 양을 표시하는 위젯이 하나 있다고 하겠습니다.
Consumer를 사용하면 감자튀김을 먹어서 감자튀김 양은 변했지만 콜라 양은 전혀 변하지 않았는데 리빌드를 강제한다면,
Selector는 콜라를 마셨을 때만 위젯을 리빌드 해 줍니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-dart">Selector&lt;MyModel, String&gt;(
  selector: (context, model) =&gt; model.specificValue,
  builder: (context, value, child) {
    return Text(value);
  },
)</code></pre>
<ul>
<li>생성자를 보며 설명하자면, MyModel까지는 Consumer와 동일합니다.
그 뒤에 붙는 String이 내가 MyModel안에서 주시할 데이터 타입 입니다. 
그리고 그 타입의 값을 <code>selector: (context, model) =&gt; model.specificValue</code> 형태로 콕 찝어서 골라놓고, 이 값이 변하는지만 예의주시 합니다.</li>
</ul>
<hr>
<h3 id="selector의-심화-사용">Selector의 심화 사용</h3>
<ul>
<li><p>Selector 위젯은 shouldRebuild라는 콜백 함수를 쥐어 줄 수 있습니다. 이것은 Selector의 핵심 기능 중 하나로, rebuild 에 조건을 추가할 수 있는 기능입니다.</p>
</li>
<li><p>selector 함수가 반환하는 값이 변경되었을 때, 실제로 UI를 rebuild 할 필요가 있는지를 추가적으로 판단하는 역할을 합니다.</p>
<p>예를 들어서, 감자튀김의 갯수를 표시하고 있다고 해볼까요?
중요한 것은 갯수이지, 감자튀김 각각의 사이즈나 눅눅해진 정도가 아니겠죠? 이 경우 아래처럼 구현 할 수 있습니다.</p>
</li>
</ul>
<pre><code class="language-dart">Selector&lt;MyModel, List&lt;int&gt;&gt;(
  selector: (context, model) =&gt; model.potato,
  shouldRebuild: (previous, next) =&gt; previous.length != next.length, 
  // 리스트의 길이가 변경된 경우에만 rebuild
  builder: (context, numbers, child) {
    return Text(&#39;감자튀김 ${numbers.length}개 남음&#39;);
  },
)</code></pre>
<ul>
<li>위의 Selector는 배열의 길이가 변경될 때만 리빌드 되며, 내부요소가 변경되더라도 길이가 같다면 리빌드 되지 않습니다.</li>
<li>요소가 변경된 건 어떻게 아냐구요? Selector는 DeepCollectionEquality를 사용해서 요소 변경도 반응하게 설계 되었습니다.</li>
</ul>
<hr>
<h3 id="read와-watch등-다른-상태-소비consume방식과의-비교">Read와 Watch등 다른 상태 소비(consume)방식과의 비교</h3>
<ul>
<li>Provider 패키지에서는 Consumer와 Selector 외에도 다양한 방식으로 상태에 접근하고 사용할 수 있습니다. 
&#39;소비(consume)&#39;한다고 표현하기도 합니다.
그리고 프로바이더는 read, watch, Consumer, Selector라는 네 가지 방식을 제공합니다. 
각 방식은 상태를 접근하고 UI를 업데이트하는 방식과 성능에 미치는 영향이 다른데, 쉽게 한번 표로 보겠습니다.
(올려놓고 보니 잘 안보이는거 같네요..)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/f3810148-f1db-4fac-829c-dbedde681d0a/image.png" alt=""></p>
<p>  간단히 정리하면 아래와 같으며, 내가 지금 사용하고자 하는 데이터의 특성에 맞춰서 전략적으로 사용해 보면 좋겠습니다 :)</p>
<ul>
<li>context.read<T>(): 
상태를 한 번만 읽고, 변경 사항을 감지하지 않습니다.</li>
<li>context.watch<T>(): 
상태를 읽고, 변경 사항을 감지하여 UI를 업데이트합니다.</li>
<li>Consumer<T>: 
상태를 읽고, 변경 사항을 감지하여, 
builder 함수를 통해 UI의 일부분만 업데이트합니다.</li>
<li>Selector&lt;T, S&gt;: 
상태를 읽고, 특정 부분의 변경 사항만 감지하여, 
builder 함수를 통해 UI를 업데이트합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Provider라이브러리 파헤치기 2 - notifyListeners()]]></title>
            <link>https://velog.io/@s_soo100/Flutter-Provider%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-2-notifyListeners</link>
            <guid>https://velog.io/@s_soo100/Flutter-Provider%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-2-notifyListeners</guid>
            <pubDate>Wed, 26 Feb 2025 04:23:53 GMT</pubDate>
            <description><![CDATA[<h3 id="개요">개요</h3>
<ul>
<li><p>이전 시간에는 Provider를 이루고 있는 Inherited Widget의 구성과 해당 위젯으로 상태관리를 하는 방법에 대해 알아보았습니다.</p>
</li>
<li><p>이번 시간에는 Provider가 Inherited Widget에 추가한 기능, 자동 화면 리빌드를 도와주는 notifyListeners() 메서드에 대해서 알아보려고 합니다.</p>
</li>
<li><p>그러기 위해서는 ChangeNotifierProvider와 ChangeNotifier라는 두 가지 클래스를 살펴보려고 합니다.</p>
</li>
</ul>
<h3 id="changenotifierprovider">ChangeNotifierProvider</h3>
<pre><code class="language-dart">class ChangeNotifierProvider&lt;T extends ChangeNotifier?&gt;
    extends ListenableProvider&lt;T&gt; {

  ChangeNotifierProvider({
    Key? key,
    required Create&lt;T&gt; create,
    bool? lazy,
    TransitionBuilder? builder,
    Widget? child,
  }) : super(
          key: key,
          create: create,
          dispose: _dispose,
          lazy: lazy,
          builder: builder,
          child: child,
        );

// ...생략
}</code></pre>
<ul>
<li><p><code>ChangeNotifierProvider</code>는 가장 흔하게 쓰는 Provider의 위젯입니다. 
아마 대부분 사용해 보셨을거에요. Provider의 상태 관리 기능과 ChangeNotifier의 자동 화면 리빌드가 함께 붙어있죠.
이는 아래 구조로 상속되어 있습니다.</p>
<blockquote>
<p>InheritedProvider
-&gt; ListenableProvider
---&gt; ChangeNotifierProvider</p>
</blockquote>
<p>그리고 <code>ChangeNotifierProvider</code> 클래스는 <code>ChangeNotifier</code>의 하위 타입인<code>&lt;T extends ChangeNotifier?&gt;</code>를 반환합니다.</p>
<p><code>InheritedProvider</code> 기반으로 상태 관리를 해주며,  <code>ChangeNotifier.notifyListeners</code>가 호출 될 때 마다 의존성이 있는 위젯들을 모두 다시 빌드합니다.</p>
<p>** 즉, 값을 감시하다가 값의 변동이 감지되면 build()함수를 호출합니다.**</p>
</li>
</ul>
<h3 id="notifylisteners">notifyListeners()?</h3>
<ul>
<li><p>Provider라이브러리의 notifyListeners()는 ChangeNotifier클래스를 기반으로 하는 상태 관리에서 핵심적인 역할을 합니다. </p>
</li>
<li><p>ChangeNotifier는 Flutter의 Foundation 패키지에 포함된 클래스이며, 상태가 변경되었음을 구독자(Listeners)에게 알리는 역할을 수행합니다.</p>
<p>그럼 우선 Flutter내부의 ChangeNotifier의 내부 구현 코드를 보겠습니다.
(주석은 제외함)</p>
<pre><code class="language-dart">// flutter/lib/src/foundation/change_notifier.dart
class ChangeNotifier {
  ObserverList&lt;VoidCallback&gt;? _listeners = ObserverList&lt;VoidCallback&gt;();

  void addListener(VoidCallback listener) {
    _listeners?.add(listener);
  }

  void removeListener(VoidCallback listener) {
    _listeners?.remove(listener);
  }

  @protected
  void notifyListeners() {
    if (_listeners != null) {
      for (final VoidCallback listener in List&lt;VoidCallback&gt;.from(_listeners!)) {
        listener();
      }
    }
  }
}
</code></pre>
<p>위의 코드에서 보면 _listeners 리스트에 리빌드가 필요한 위젯의 콜백 함수(listener)를 저장합니다.
<code>notifyListeners()</code>가 호출되면, _listeners에 저장된 모든 리스너가 실행되면서 여기에 등록된 build()<code>함수가 호출되며,</code>addListener()<code>를 호출한 순서대로(FIFO, First-In-First-Out) 실행됩니다.
이런 식으로</code>notifyListeners()<code>는 Flutter안에서 우리의 친구인</code>setState()`처럼 동작하여 화면을 다시 그리도록 하는 핵심 메커니즘 입니다.</p>
</li>
</ul>
<h3 id="changenotifier를-활용한-상태-관리">ChangeNotifier를 활용한 상태 관리</h3>
<p><em>(이해하셨다면 여기는 예시 부분이니 안 보셔도 괜찮습니다)</em></p>
<ul>
<li>이제 ChangeNotifier를 활용하여 실제로 상태를 관리하는 방법을 살펴보겠습니다.
ChangeNotifierProvider를 이용하기 위해 우리는 ChangeNotifier를 상속하는 클래스를 만들어 &quot;상태&quot;를 관리하고, <code>notifyListeners()</code>를 호출하여 UI가 자동으로 갱신되도록 할 수 있습니다.</li>
<li>간결하게 봐야 하니 다시 counter 위젯으로 해볼게요.</li>
</ul>
<p><strong>1. ChangeNotifier를 상속한 모델 클래스 생성</strong></p>
<p><code>이전 시간에 만든 counter모델 클래스와 동일합니다.</code></p>
<pre><code class="language-dart">import &#39;package:flutter/material.dart&#39;;

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count =&gt; _count;

  void increment() {
    _count++;
    notifyListeners(); // 값 변경 알림~~
  }
}
</code></pre>
<ul>
<li>counter가 지겨워도 이만한 예시가 없습니다.
위 코드에서 CounterModel은 ChangeNotifier를 상속받고 있으며, 내부적으로 <strong>_count</strong> 상태를 관리합니다.
<code>increment()</code> 메서드를 호출하면 <strong>_count</strong> 값을 증가시키고, <code>notifyListeners()</code>를 호출하여 UI가 변경되었음을 알립니다.</li>
</ul>
<p><strong>2. ChangeNotifierProvider로 모델을 제공(Provide)</strong></p>
<ul>
<li>자 인제 ChangeNotifierProvider를 사용하여 CounterModel을 위젯 트리에 등록하고, UI에서 이를 사용해 보겠습니다.<pre><code class="language-dart">// main.dart
import &#39;package:flutter/material.dart&#39;;
import &#39;package:provider/provider.dart&#39;;
import &#39;counter_model.dart&#39;; // 방금 만든 모델 클래스
</code></pre>
</li>
</ul>
<p>void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) =&gt; CounterModel(),
      child: MyApp(),
    ),
  );
}</p>
<p>class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterScreen(),
    );
  }
}</p>
<p>class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(&quot;Provider 상태 관리&quot;)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Consumer<CounterModel>(
              builder: (context, counter, child) {
                return Text(
                  &quot;현재 카운트: ${counter.count}&quot;,
                );
              },
            ),
            ElevatedButton(
              onPressed: () {
                context.read<CounterModel>().increment();
              },
              child: Text(&quot;카운트 증가&quot;),
            ),
          ],
        ),
      ),
    );
  }
}</p>
<pre><code>
- 위 코드를 보면, ChangeNotifierProvider가 CounterModel을 생성하여 앱의 위젯 트리에 제공하고 있습니다.
그리고 `Consumer&lt;CounterModel&gt;`을 사용하여 count 값이 변경될 때마다 해당 부분만 다시 빌드됩니다.
- increment호출에 `.read()`메서드를 사용하고 있는데요, `context.watch&lt;T&gt;()`는 상태 변경 시 UI를 리빌드하고, `context.read&lt;T&gt;()`는 상태 변경을 감지하지 않고 값을 가져옵니다.
- 함수 호출시 onPressed 내부에서 `.watch()`메서드를 사용하면버튼 자체가 상태 변경을 감지하여 불필요하게 다시 빌드될 가능성이 있습니다. 이 메서드는 값(상태)을 감시해야 할 때만 써주세요!


### notifyListeners()가 없으면 어떻게 될까?
- 그렇다면 만약 `notifyListeners()`를 호출하지 않는다면 어떤 일이 일어날까요?
CountModel에서 `notifyListeners()`를 주석 처리 해보겠습니다.
```dart
class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count =&gt; _count;

  void increment() {
    _count++;
    // notifyListeners(); 호출하지 않음
  }
}
</code></pre><ul>
<li>이렇게 하면 <code>increment()</code>가 호출되어 _count 값이 증가해도 UI는 업데이트되지 않습니다.
즉, Provider를 사용해도 상태가 변경되었음을 알리지 않으면 화면이 리빌드되지 않습니다.</li>
</ul>
<h3 id="정리">정리</h3>
<blockquote>
<ol>
<li><code>notifyListeners()</code>는 ChangeNotifier에서 상태가 변경되었음을 위젯 트리에 알리는 역할을 합니다.</li>
<li>ChangeNotifierProvider를 사용하면 ChangeNotifier 객체를 위젯 트리에 제공할 수 있습니다.</li>
<li><code>notifyListeners()</code>는 상태 변경시 ChangeNotifier안의 listener 콜백들을 순차적으로 실행합니다.
(이 때에 화면도 리빌드 됩니다)</li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Provider라이브러리 파헤치기 1 - InheritedWidget]]></title>
            <link>https://velog.io/@s_soo100/Flutter-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-Provider1</link>
            <guid>https://velog.io/@s_soo100/Flutter-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-Provider1</guid>
            <pubDate>Wed, 19 Feb 2025 06:13:18 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>Flutter를 처음 배우기 시작하면 보통 상태관리의 중요성과 프로바이더 라이브러리 부터 배웁니다.</p>
</li>
<li><p>하지만 원리를 이해하고 싱글톤 패턴과 뭐가 다르구나 느껴보고 프로젝트에 잘 적용했다고 해도 그것이 어떻게 만들어졌는지 직접 열어본 사람은 적을 것 같습니다.</p>
</li>
<li><p>더 깊은 이해를 위해 Provider 라이브러리의 lib 폴더를 분석하여 주요 구성 요소와 그 작동 원리를 상세하게 살펴보려 합니다.</p>
</li>
</ul>
<p><a href="https://pub.dev/packages/provider">🚀Provider pub dev 링크</a>
<a href="https://github.com/rrousselGit/provider">🚀Provider github repo 링크</a>
<a href="https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html">🚀InheritedWidget Flutter 공식문서 링크</a></p>
<h2 id="뿌리를-찾아서-inheritedwidget">뿌리를 찾아서, InheritedWidget</h2>
<p><img src="https://velog.velcdn.com/images/s_soo100/post/4082b818-f01a-4c3a-8f1b-c7bf475358b1/image.png" alt=""></p>
<ul>
<li><p>프로바이더를 맨 처음 공부하며 이런 이미지를 많이 봤을 것입니다. 
위젯 트리에서 각자 멀리 떨어진 위젯들이 값을 공유하고, update되면 알려줄 수 있는 상태관리 기능이 프로바이더의 핵심이기 때문입니다.</p>
</li>
<li><p>이런 WidgetTree 의 상위 위젯들에 접근하여 데이터를 가져오게 할 수 있도록 하는 근간이 바로 Inherited Widget입니다.</p>
</li>
</ul>
<ul>
<li><p>Provider 라이브러리의 다양한 Provider위젯들은 모두
InheritedWidget를 베이스로 제작 되었습니다.
그리고 InheritedWidget은 ProxyWidget을 상속하고 있는 추상 클래스인데, 몇몇 중요한 특징을 가지고 있습니다.</p>
<blockquote>
<ol>
<li>상태를 하위 위젯 트리에 전달</li>
<li>BuildContext의 dependOnInheritedWidgetOfExactType() 메서드를 사용해 하위 위젯에서 해당 위젯을 찾고 상태를 구독</li>
<li>updateShouldNotify를 오버라이드하여 상태가 변경될 때 하위 위젯들에게 알릴지 여부를 결정</li>
</ol>
</blockquote>
</li>
<li><p>자동으로 화면을 리빌드하는 부분만 제외하면 <strong>Provider의 기본 기능과 동일합니다.</strong> <br/>
InheritedWidget도 마찬가지로 위젯 트리의 상단에 배치해서 그 하위 위젯들에게 상태를 공유하는 역할을 하기 때문입니다.</p>
<p>이제 Flutter를 열어 InheritedWidget의 코드를 보자.
(보기 좋게 주석은 모두 제외하였음)</p>
<pre><code class="language-dart">// flutter/lib/src/widgets/framework.dart
abstract class InheritedWidget extends ProxyWidget {
const InheritedWidget({ super.key, required super.child });

@override
InheritedElement createElement() =&gt; InheritedElement(this);

@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}</code></pre>
</li>
<li><p>override해 온 updateShouldNotify 메서드는 InheritedWidget이 가진 데이터가 변경되었을 때 그것이 기존의 데이터와 같은지 비교하고, 다를 경우에만 notify를 해줍니다.</p>
<p>그리고 InheritedWidget의 실제 인스턴스이자 렌더링과 업데이트를 담당하는, InheritedElement를 만들어주는 createElement()메서드가 있습니다.</p>
<p>그렇다면 부모를 확인했으니 우리도 Provider를 만들 수 있지 않을까요?</p>
</li>
</ul>
<h2 id="inheritedwidget으로-상태-관리하기">InheritedWidget으로 상태 관리하기</h2>
<ul>
<li><p>provider에 대해서 초보자들이 질문하면 가장 많이 받는 답변이 하나 있습니다. 
바로 <em><strong>&quot;카운터 위젯을 Provider로 짜신 다음에 Flutter 기본 프로젝트랑 뭐가 다른지 비교해보세요!&quot;</strong></em> 라는 말인데, (나 때는 그랬다)
우리도 Provider 라이브러리를 열어 보기 전에 InheritedWidget을 가지고 똑같은 일을 해보겠습니다.</p>
</li>
<li><p>우선 InheritedWidget으로 카운터를 만들겠습니다.</p>
<p>숫자를 담는 count 변수와, count를 1씩 증가시키는 increment 함수만 가지고 있도록 단순하게 구성했다.
그리고 InheritedWidget은 하나의 자식 Widget을 받는습니다.</p>
</li>
</ul>
<pre><code class="language-dart">// MyInheritedWidget.dart
class MyInheritedWidget extends InheritedWidget {
  final int count;
  final VoidCallback increment;

  MyInheritedWidget({
    Key? key,
    required this.count,
    required this.increment,
    required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(MyInheritedWidget oldWidget) {
    return oldWidget.count != count;
  }
}
</code></pre>
<ul>
<li><p>이제 이 MyInheritedWidget을 담은 Provider를 만듭니다.</p>
<pre><code class="language-dart">// InheritedCounterProvider.dart
class InheritedCounterProvider extends StatefulWidget {
final Widget child;

const InheritedCounterProvider({Key? key, required this.child})
    : super(key: key);

@override
_InheritedCounterProviderState createState() =&gt; _InheritedCounterProviderState();
}
</code></pre>
</li>
</ul>
<p>class _InheritedCounterProviderState extends State<InheritedCounterProvider> {
  int count = 0;</p>
<p>  void increment() {
    setState(() {
      count++;
    });
  }</p>
<p>  @override
  Widget build(BuildContext context) {
    return MyInheritedWidget(
      count: count,
      increment: increment,
      child: widget.child,
    );
  }
}</p>
<pre><code>
- 자, 다 왔다. 이제 카운터를 보여줄 메인 페이지만 구성하면 됩니다.
```dart

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return InheritedCounterProvider(
      child: MaterialApp(
        title: &#39;Flutter Demo&#39;,
        theme: ThemeData(
          useMaterial3: false, 
        ), 
        home: InheritedHomePage(),
      ),
    );
  }
}

class InheritedHomePage extends StatelessWidget {
  const InheritedHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final counterWidget =
        context.dependOnInheritedWidgetOfExactType&lt;MyInheritedWidget&gt;();

    return Scaffold(
      appBar: AppBar(title: Text(&#39;InheritedWidget Example&#39;)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(&#39;Counter: ${counterWidget?.count ?? 0}&#39;,
                style: TextStyle(fontSize: 24)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: counterWidget?.increment,
              child: Text(&#39;Increment&#39;),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre><p><img src="https://velog.velcdn.com/images/s_soo100/post/3d786459-7c12-40e1-b22e-bbf69f86c24c/image.png" alt=""></p>
<h2 id="inheritedwidget-구현-분석">InheritedWidget 구현 분석</h2>
<ul>
<li>이 구현의 포인트는 바로 <code>BuildContext.dependOnInheritedWidgetOfExactType&lt;T&gt;()</code> 메서드에 있으며, 이 메서드는 한 눈에 봐도 단어가 엄청 긴데, &#39;depend on inherited widget of &quot;EXACT TYPE&quot;&#39;, 즉, 상속된 위젯의 정확한 타입에 따라 달라진다. 라는 의미이며, 주석에는 이렇게 써있습니다.<blockquote>
<p>Returns the nearest widget of the given type T and creates a dependency on it, or null if no appropriate widget is found.</p>
</blockquote>
</li>
</ul>
<ul>
<li>즉, 위젯트리에서 T 타입의 위젯 중 가장 가까운 것을 반환 해줍니다. 
위의 코드에서는 <code>MyInheritedWidget</code> 타입을 찾아서 그 counter값과 increment함수를 사용했습니다.
이것은 Provider에서 <code>.of()</code>메서드의 동작과 동일합니다.
실제로 라이브러리를 열어보면 동일한 메서드를 사용하고 있습니다.</li>
</ul>
<pre><code class="language-dart">// provider/packages/provider/lib/src/provider.dart

...
static T of&lt;T&gt;(BuildContext context, {bool listen = true}) {
    ...
    final inheritedElement = _inheritedElementOf&lt;T&gt;(context);
    ...

    if (listen) {
      context.dependOnInheritedWidgetOfExactType&lt;_InheritedProviderScope&lt;T?&gt;&gt;();
    }
    ...
    final value = inheritedElement?.value;
    ...
    return value as T;
}</code></pre>
<ul>
<li><p>하지만 Provider와는 다르게 <code>InheritedCounterProvider</code> 위젯 코드 내에서 <code>setState()</code>를 호출해서 화면을 리빌드 하고 있습니다.</p>
<p>또한 Provider는 Multi Provider, Proxy Provider등 다양하고 중요한 기능들을 미리 개발해서 제공하고 있습니다.</p>
</li>
<li><p>그러므로 위 코드는 단순히 Provider가 어떻게 만들어졌는지 파악하는 용도로 공부하면 좋을 것 같습니다.</p>
<p>여기서 꼭 알아가야 할 포인트가 있다면 다음과 같습니다.</p>
</li>
</ul>
<blockquote>
<h4 id="1-provider의-부모인-inheritedwidget의-구성">1. Provider의 부모인 InheritedWidget의 구성</h4>
</blockquote>
<ul>
<li><p>Flutter에서 상태를 하위 위젯에 전달하는 기본적인 위젯</p>
</li>
<li><p>updateShouldNotify 메서드를 오버라이드하여 상태가 변경될 때 하위 위젯을 다시 빌드할지 결정</p>
</li>
<li><p><code>context.dependOnInheritedWidgetOfExactType&lt;T&gt;()</code>를 사용해 하위 위젯에서 접근 가능</p>
<blockquote>
<h4 id="2-widget-tree에서-inheritedwidget의-위치">2. Widget Tree에서 InheritedWidget의 위치</h4>
</blockquote>
</li>
<li><p>보통 앱의 최상단 또는 특정 상태를 공유해야 하는 범위에 배치</p>
</li>
<li><p>그 하위 위젯들이 InheritedWidget의 데이터를 참조</p>
<blockquote>
<h4 id="3-inheritedwidget을-통한-상태관리">3. InheritedWidget을 통한 상태관리</h4>
</blockquote>
<ul>
<li>상태를 전달만 하는 역할이므로 직접적인 상태 변경 기능은 없음</li>
<li>StatefulWidget과 조합하여 상태 변경이 필요할 때 setState와 함께 사용</li>
<li>Provider는 InheritedWidget을 상속하여 이 부분을 보강하였음!</li>
</ul>
<p>다음 시간에는 본격적으로 Provider 라이브러리의 코드들을 열어보고 구성을 살펴보기로 하겠습니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dart언어의 Fragile Base Class Problem이란?]]></title>
            <link>https://velog.io/@s_soo100/Dart%EC%96%B8%EC%96%B4%EC%9D%98-Fragile-Base-Class-Problem%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@s_soo100/Dart%EC%96%B8%EC%96%B4%EC%9D%98-Fragile-Base-Class-Problem%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Tue, 22 Oct 2024 11:07:52 GMT</pubDate>
            <description><![CDATA[<h1 id="fragile-base-class-problem">Fragile Base Class Problem??</h1>
<ul>
<li>확장(extends)기능은 코드를 재사용할 수 있는 좋은 기능이나, 클래스 간의 의존 관계를 만들기 때문에 가끔 문제를 일으키고는 합니다.<br/></li>
<li><strong>Fragile Base Class Problem</strong>은 객체 지향 프로그래밍에서 발생하는 객체 지향 프로그래밍 시스템의 근본적인 아키텍처 문제로,<br/><strong><code>부모 클래스</code>의 변경이 <code>자식 클래스</code>에 side-effect를 발생시키는 현상을 말합니다.</strong></li>
</ul>
<h3 id="예시-코드-1">예시 코드 1</h3>
<pre><code class="language-dart">class Parent {
  void methodA() {
    print(&quot;A&quot;);
    methodB();  // methodB 호출
  }

  void methodB() { 
    print(&quot;B&quot;);
  }
}

class Child extends Parent {
  @override
  void methodB() {
    super.methodB();  // 부모의 methodB도 호출
    print(&quot;Child&#39;s B&quot;);
  }
}</code></pre>
<p>가장 쉬운 예시로 위 코드를 보겠습니다.<br/>
이 예시 코드에서 만약 부모 클래스의 methodA가 수정되어 methodB를 두 번 호출하도록 변경된다면 자식 클래스의 동작도 예상치 못하게 변경됩니다.<br/>
예를 들어 Child.methodA();를 호출한다면 Child는 알지 못하는 변경에 의해 본인의 methodB를 두 번 부르게 됩니다.
<br/>
또는 이렇게 생각할 수 있습니다. <br/>
&quot;Child의 methodB를 override 할 때, super.methodB를 호출하지 않고 새롭게 정의해서 쓰면 되는게 아닌가?&quot;<br/>
이 경우 만약 Parent 클래스의 methodB를()가 중요한 검증 로직을 포함하고 있었다면 이를 호출하지 않으므로써 필수적인 알고리즘 단계를 건너뛰는 오류를 야기할 수 있으며, 이는 &quot;계약 위반(Contract Violation)&quot;이라는 새로운 문제가 발생하는 것 입니다.<br/>
<br/></p>
<h3 id="예시-코드-2">예시 코드 2</h3>
<pre><code class="language-dart">class Parent {
  void validateBalance() {
    originMethod();
    checkNetwork();  // 나중에 추가된 필수 네트워크 체크
    print(&quot;checkNetwork후에 잔액 검증 하는 메서드&quot;);
  }

  void checkNetwork() { // 새로 추가된 필수 검증 메서드
    print(&quot;네트워크 상태 확인하는 메서드&quot;);
  }
}</code></pre>
<p>조금 더 구체적인 예시는 &quot;부모 클래스에 새로운 의존성이 추가되었을 때&quot; 발생하는 문제 입니다.<br/>
Parent 클래스에 checkNetwork라는 새로운 의존성이 생겨버리면 Parent를 상속하는 많은 클래스들은 이를 알 수 없습니다.<br/>
이에 따라서 Child 클래스에서 side-effect가 발생하기 쉽습니다.<br/></p>
<h2 id="해결-방안">해결 방안</h2>
<h3 id="1-템플릿-메서드-패턴-사용">1. 템플릿 메서드 패턴 사용</h3>
<pre><code class="language-dart">abstract class Parent {
  // 템플릿 메서드 - final로 선언하여 오버라이드 방지
  final void methodA() {
    step1(); // 하위 클래스에서 구현
    if (shouldExecuteStep2()) {  // 선택적 기능을 제공하는 hook메서드
      step2();  // 하위 클래스에서 구현
    }
    _commonStep(); // 공통 기능은 접근 불가능하도록 
  }

  void step1(); // 추상 메서드 - 반드시 구현해야 함
  void step2() { // 선택적으로 구현 가능한 메서드 (훅 메서드)
    print(&quot;기본 step2 실행&quot;); // 기본 구현을 제공하고 사용시 추가 구현
  }

  bool shouldExecuteStep2() { // 훅 메서드 - 실행 여부 결정
    return false;  // 기본값
  }

  void _commonStep() {  // private 메서드 - 변경 불가능한 공통 기능
    print(&quot;변경 불가능한 공통 기능 실행&quot;);
  }
}

// 구현 클래스 1
class Child1 extends Parent {
  @override
  void step1() {
    print(&quot;Child1의 step1 구현&quot;);
  }
}

// 구현 클래스 2
class Child2 extends Parent {
  @override
  void step1() {
    print(&quot;Child2의 step1 구현&quot;);
  }

  @override
  void step2() {
    print(&quot;Child2의 커스텀 step2 구현&quot;);
  }

  @override
  bool shouldExecuteStep2() {
    return true;  // step2 실행하도록 구현
  }
}</code></pre>
<p>템플릿 메서드 패턴은 알고리즘의 구조를 메서드에 정의하고, 일부 단계를 하위 클래스에서 구현할 수 있도록 하는 패턴입니다.<br/> 
알고리즘의 뼈대는 상위 클래스에서 정의하고, 일부 단계의 구현은 하위 클래스에 위임하는 형태입니다.<br/>
&quot;템플릿&quot;을 통해 알고리즘의 구조를 명확하게 하고 하위 클래스에서 수정 가능한 부분이 명확히 정의하기 때문에<br/>
공통 기능의 무결성이 보장되며 확장은 용이하되 기존 구조는 깨지지 않습니다.
(패턴 자체에 대해서는 다른 글에서 더 자세히 다뤄보겠습니다)</p>
<h3 id="2-컴포지션-사용">2. 컴포지션 사용</h3>
<pre><code class="language-dart">class WithdrawValidator {
  void validate() {
    print(&quot;잔액 검증&quot;);
  }
}

class BankAccount {
  final WithdrawValidator validator; // 컴포지션으로, 상속이 아니라 내부변

  BankAccount(this.validator);

  void withdrawMoney() {
    validator.validate();
    processingWithdraw();
  }
}</code></pre>
<p>컴포지션은 &quot;has-a&quot; 관계를 만드는 것으로, 상속(&quot;is-a&quot; 관계) 대신 인스턴스를 내부에 포함시켜 사용하는 방식입니다.<br/>
상속과 달리 결합도가 매우 낮아 내부 구현 변경에 &#39;덜&#39;취약하며 테스트가 용이한 장점이 있습니다. </p>
<h3 id="3-인터페이스-정의">3. 인터페이스 정의</h3>
<pre><code class="language-dart">abstract class BalanceValidator {
  void validate();
}

class StandardValidator implements BalanceValidator {
  @override
  void validate() {
    print(&quot;표준 잔액 검증&quot;);
  }
}

class CustomValidator implements BalanceValidator {
  @override
  void validate() {
    print(&quot;커스텀 잔액 검증&quot;);
  }
}</code></pre>
<p>BalanceValidator라는 부모 클래스의 구현을 포기하고 &quot;인터페이스로 사용&quot;하여 상세 구현을 모두 자식 클래스에서 한다면 해당 문제는 해결됩니다.</p>
<h4 id="정리">정리</h4>
<ul>
<li>상속은 강력한 기능이나 약점이 명확하며,Fragile Base Class Problem은 여러 명이 공동작업을 해야 하는 프로젝트 일수록 발생되기 쉽습니다.</li>
<li>상속에서 오는 고질적인 문제인 Fragile Base Class Problem를 피하기 위해서는 대신 위와 같은 설계 패턴을 사용하여 더 안전하고 유연한 구조를 만드는 것이 좋습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dart 언어의 class modifier 훑어보기]]></title>
            <link>https://velog.io/@s_soo100/Dart-%EC%96%B8%EC%96%B4%EC%9D%98-class-modifier-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@s_soo100/Dart-%EC%96%B8%EC%96%B4%EC%9D%98-class-modifier-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 20 Oct 2024 09:15:23 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>클래스 모디파이어(클래스 수정자 혹은 제어자, Class Modifiers)란, class 혹은 mixin이 라이브러리 내/외부로 사용되는 방식을 결정합니다.</p>
</li>
<li><p>모디파이어 키워드는 class 혹은 mixin 바로 앞에 배치하며 mixin은 그 자체로 제어자 키워드중 하나이기도 하며, 클래스의 사용 방식에(프로젝트 내에서 그 클래스가 사용되기를 &#39;원하는&#39;방식에) 직접적으로 관여합니다.</p>
</br>
🍀 공식 문서 : https://dart.dev/language/class-modifiers
</br>
</br>

</li>
</ul>
<hr>
<h1 id="빠르게-보는-modifier의-사용과-조합">빠르게 보는 modifier의 사용과 조합</h1>
<ul>
<li>Dart의 주요 class modifier에는 abstract, final, sealed, base, mixin, interface 등이 있습니다.
에기에 class까지 해서 7개 키워드의 각 기능을 정리하면 아래와 같습니다.
생성자, 상속, 구현, 완전성 검사등의 유용한 기능들을 modifier를 사용해서 제한하거나 특정 상황에서만 동작하게 만들 수 있습니다.
enum, typedef, extension, extension type 등에는 사용이 불가능합니다.</li>
</ul>
<table>
<thead>
<tr>
<th>Modifier</th>
<th align="center">construct</th>
<th align="center">extend</th>
<th align="center">implement</th>
<th align="center">mixin</th>
<th align="center">exhaustive</th>
</tr>
</thead>
<tbody><tr>
<td><strong>class</strong></td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td><strong>abstract</strong></td>
<td align="center"></td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td><strong>final</strong></td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td><strong>base</strong></td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td><strong>interface</strong></td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center">✔</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td><strong>sealed</strong></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">✔</td>
</tr>
<tr>
<td><strong>mixin</strong></td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center">✔</td>
<td align="center"></td>
</tr>
</tbody></table>
<p>(<a href="https://dart.dev/language/modifier-reference">공식문서의 Valid combinations</a>)</p>
<h3 id="기능의-한줄-설명">기능의 한줄 설명</h3>
<ul>
<li>construct: 클래스 인스턴스화</li>
<li>extend: 다른 클래스를 부모로 상속</li>
<li>implement: 인터페이스 메서드 구현</li>
<li>mixin: 여러 클래스에 공통사용 가능한 기능을 추가</li>
<li>exhaustive: 본인을 상속한 모든 하위 클래스를 처리하는 <code>완전성 검사</code></li>
</ul>
<h4 id="각-기능들이-왜-중요한가요">각 기능들이 왜 중요한가요?</h4>
<ul>
<li>클래스 간의 관계와 동작을 정의하기 때문입니다.
객체 지향 프로그래밍(OOP) 에서 예를들어, 클래스의 확장과 구현은 OOP의 핵심 원리인 <code>재사용성</code>과 <code>유연성</code>을 실현하게 해줍니다.<br/><br/></li>
<li>확장(extends)은 기존 클래스를 상속하는 과정을 통해 이루어지는데,<br/> 매번 기본 기능을 추가할 필요가 없어 코드의 재사용성을 올려주며, 기본 기능의 수정을 상위클래스에서 하면 상속자들에게 모두 반영되기 때문에 유지보수성이 좋다고 할 수 있습니다.
(물론 여기서 발생되는 다른 문제들 또한 존재합니다)
<br/><br/></li>
<li>구현(implements)은 구현자에게 인터페이스에 정의된 메서드 구현을 강제합니다. <br/>이는 OOP에서 <strong>다형성(polymorphism)</strong>을 지원하고, 일관된 동작을 제공하는 데 매우 유용합니다.
<br/>이로 인해 클래스의 구성이 달라도 인터페이스가 같기 때문에 에러 없이 상호작용 할 수 있습니다. <br/>이는 상속과는 다른 방식으로 유연한 설계를 가능하게 해줍니다.</li>
</ul>
<h1 id="각-modifier의-원리와-사용구조">각 modifier의 원리와 사용구조</h1>
<h2 id="1-abstract추상-클래스">1. abstract(추상 클래스)</h2>
<ul>
<li>abstract는 공통된 동작이나 기능을 여러 클래스에 제공하는 목적으로 사용됩니다.<br/>또한 클래스가 직접 인스턴스화될 수 없도록 만들며, 반드시 상속을 통해 구현해야 하는 메서드들을 정의하는 데 사용되며 각 하위 클래스는 필요에 따라 이를 확장하거나 수정할 수 있습니다.</li>
<li><strong>하지만 모든 메서드를 추상화 해야 하는것은 아니며</strong>, 구현 메서드나 값 또한 포함할 수 있습니다.</li>
<li>interface와 함께 사용할 수도 있습니다. 이 경우 굉장히 <code>pure</code>한 인터페이스를 만들 수 있습니다.</li>
</ul>
<pre><code class="language-dart">abstract class Animal {
  void makeSound(); // 추상 메서드
  void move() { // 구현 메서드
    print(&quot;move&quot;);
  }
}

class Dog extends Animal {
  @override
  void makeSound() {
    print(&#39;Woof!&#39;);
  }
}
</code></pre>
<h2 id="2-final">2. final</h2>
<ul>
<li>변수에 사용하는 final 키워드와 달리 이는 <strong>상속을 방지</strong>하는 용도로 사용합니다.<br/>
더이상 mixin이나 class가 상속되지 않도록 고정하며, abstract가 아니고 class로 만들었다면 인스턴스화는 가능합니다.<pre><code class="language-dart">final class Robot {
void turnOn() {...}
}
</code></pre>
</li>
</ul>
<p>// class Gundam extends Robot {} // ERROR!! : Robot은 상속이 불가능합니다.
Robot myRobot = Robot(); // 인스턴스화는 가능합니다.</p>
<pre><code>
## 3. sealed
- sealed 는 Dart 3.0에서 도입된 키워드로, 내 클래스를 같은 파일 안에서만 상속이 가능하도록 합니다.&lt;br/&gt;
즉, sealed 클래스를 상속하는 모든 하위 클래스는 같은 파일에 있어야 합니다.&lt;br/&gt;이를 통해 상속 계층을 더 엄격하게 관리할 수 있습니다.
- sealed 클래스를 상속받는 클래스는 enum과도 비슷하다고 생각 할 수 있습니다. 모든 하위 클래스를 포괄적으로 처리할 수 있는 `완전성(exhaustive)`을 제공하기 때문입니다.
  &lt;br/&gt;하위 클래스의 범위가 특정 파일 내(현재 파일 내)로 제한되기 때문에, 모든 확장 가능성을 알고 이를 처리할 수 있게 됩니다. 이를 통해 switch 구문이나 패턴 매칭에서 모든 하위 클래스를 반드시 처리할 수 있습니다.
- 정리하면 sealed는 코드의 안전성과 유지보수성을 극대화하는 기능입니다.
- 외부에서 직접 상속할 수 없는 클래스를 만들 때, sealed와 final이라는 두 가지 선택지가 주어집니다.&lt;br/&gt;이에 대한 비교는 별도의 글에서 다루도록 하겠습니다.

(sealed 예시코드는 역시 도형이 맛있다.)
```dart
sealed class Shape {}

class Circle extends Shape {}
class Square extends Shape {}

Shape shape = getShape();

switch (shape) {
  case Circle:
    // Circle 처리
    break;
  case Square:
    // Square 처리
    break;
  // 모든 하위 클래스를 처리하므로 오류 방지 가능
}
</code></pre><h2 id="4-base">4. base</h2>
<ul>
<li>클래스 또는 믹스인 구현의 상속을 강제하는 모디파이어 입니다.</li>
<li>base 클래스는 상속받을 수는 있지만 구현(implement)하거나 mixin사용은 불가능합니다.(명시적인 상속 제한이라 부릅니다.)</li>
<li>파일 경계를 넘어 상속이 가능하며, 상속받는 모든 클래스는 base, final, sealed중 하나의 키워드를 표시해야 합니다.
```dart</li>
<li>----a.dart
base class Vehicle {
 void moveForward(int meters) {
   // ...
 }
}</li>
<li>----b.dart
import &#39;a.dart&#39;;</li>
</ul>
<p>// Can be constructed.
Vehicle myVehicle = Vehicle();</p>
<p>// Can be extended.
base class Car extends Vehicle {
  int passengers = 4;
  // ...
}</p>
<p>// ERROR: Can&#39;t be implemented.
base class MockVehicle implements Vehicle {
  @override
  void moveForward() {
    // ...
  }
}</p>
<pre><code>
## 5. interface
- 본래 Dart에서는 Java등과 다르게 interface라는 별도의 키워드가 없었으며 모든 클래스는 자동으로 인터페이스 역할을 했습니다.&lt;br/&gt;interface 키워드는 Dart 3.0 업데이트와 함께 추가되어 이를 통해 명시적으로 인터페이스로 사용할 클래스를 정의할 수 있습니다. 
- 이 키워드는 클래스 자체를 인터페이스로 사용하도록 구성하여 implements를 통해 내부 메서드들을 구현하도록 강제합니다.
- 상속을 방지하여 내부 로직이 의도치 않게 변경되는 것을 방지하고 `fragile base class problem`를 줄일 수 있습니다.(이 또한 다른 글에서 다루겠습니다)
- 추상 메서드가 아니라 구현 메서드도 내부에 포함할 수 있습니다.
```dart
interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}

// 생성자 사용 가능
Vehicle myVehicle = Vehicle();

// ERROR!!: 상속 불가
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// 구현 가능
class MockVehicle implements Vehicle {
  @override
  void moveForward(int meters) {
    // ...
  }
}</code></pre><h2 id="6-mixin믹스인">6. mixin(믹스인)</h2>
<ul>
<li>상속, 구현과는 별개로 여러 클래스 위계에서 재사용 될 수 있는 코드를 만듭니다. <br/>특정 클래스 상속이 불가능하며 생성자 선언도 불가능합니다. 마찬가지로 자체 인스턴스 화도 불가능합니다.</li>
<li>기능을 재사용하고 코드 중복을 줄이는데 큰 도움을 주며,<br/>여러 클래스에서 사용할 내용을 하나의 mixin에서 한 번에 제공하도록 설계되어 있습니다.</li>
<li>with 키워드로 클래스 명 혹은 부모 클래스 명 뒤에 기재하며, 복수 사용이 가능합니다.</li>
<li>추상 메서드/추상 변수를 가질 수 있으며, 믹스인 사용시 꼭 이를 구현해야 합니다.</li>
<li>mixin class로 구성해서 상속과 믹스인 두 가지 용도로 사용하는 것 또한 가능합니다.<pre><code class="language-dart">mixin class Both {}
</code></pre>
</li>
</ul>
<p>class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}</p>
<pre><code>
# modifier combinations
- modifier는 여러개를 조합해서 붙일 수 있습니다. 그중 아래 표의 조합만이 가능하며, 이는 클래스의 기능을 더 명확하게 제한해줍니다.
- 아까보다 조금 더 긴 표를 보겠습니다. 아래 조합만 valid하며, 표기되지 않은 조합(mixin abstract등)은 사용이 불가능 합니다.
- 이 조합을 통해 우리는 각각의 클래스가 상속, 구현, 믹스인이 가능한지 불가능한지를 정의합니다.&lt;br/&gt;
abstract interface class등을 통해 구현만 가능한 `pure`한 인터페이스를 정의하는 것도 가능합니다.
  &lt;br/&gt;




| Class Type                    |Construct | Extend | Implement | Mixin | Exhausive |
|-------------------------------|------------|---------------|------------------|--------------|--------|
| `class`                       | ✔          | ✔             | ✔                |              |        |
| `base class`                  | ✔          | ✔             |                  |              |        |
| `interface class`             | ✔          |               | ✔                |              |        |
| `final class`                 | ✔          |               |                  |              |        |
| `sealed class`                |            |               |                  |              | ✔      |
| `abstract class`              |            | ✔             | ✔                |              |        |
| `abstract base class`         |            | ✔             |                  |              |        |
| `abstract interface class`    |            |               | ✔                |              |        |
| `abstract final class`        |            |               |                  |              |        |
| `mixin class`                 | ✔          | ✔             | ✔                | ✔            |        |
| `base mixin class`            | ✔          | ✔             |                  | ✔            |        |
| `abstract mixin class`        |            | ✔             | ✔                | ✔            |        |
| `abstract base mixin class`   |            | ✔             |                  | ✔            |        |
| `mixin`                       |            |               | ✔                | ✔            |        |
| `base mixin`                  |            |               |                  | ✔            |        |
</code></pre>]]></description>
        </item>
    </channel>
</rss>