Claude Code 해부 · 사용자 가이드

Claude Code를
잘 쓰는 법

설계 의도를 알고 나면 프롬프트가 달라진다. 8만 줄 소스에서 뽑아낸 15가지 단서와 그로부터 파생된 사용 전략.

분석 대상~80K LOC TypeScript
도구20+
커맨드100+
방법서브에이전트 4개 병렬
날짜2026-04-05

Claude Code를 오래 쓴 사람과 막 쓰기 시작한 사람의 차이는 프롬프트 실력이 아니다. "Claude Code가 어떤 모양의 입력을 소화하게 만들어졌는가"를 아는가다. 이 모양에 맞춰 프롬프트·메모리·세션을 짜면 같은 돈으로 두세 배의 일이 나온다.

이 책은 코드를 파는 책이 아니다. 코드 중심 버전은 같은 레포의 DEEP-DIVE.md에 따로 두었다. 이 책은 내부 구조에서 "왜 이렇게 만들었는가"를 역추적한 뒤 사용자로서 그 의도에 맞춰 쓰는 방법을 정리한 책이다.

예를 들어 이런 것들이 설명된다. 왜 긴 탐색을 서브에이전트에 넘기면 메인 대화가 훨씬 오래 버티는지. 왜 CLAUDE.md를 자주 바꾸면 비용이 늘어나는지. 왜 "이거 좀 도와줄래?"보다 "X를 고쳐라"가 결과가 좋은지. 왜 /compact를 자동보다 수동으로 치는 편이 더 안정적인지. 왜 훅이 block을 반환해도 다음 턴이 그냥 가는 경우가 있는지. 이 모두가 설계 의도의 결과다.

Claude Code 전체 지도 6개 계층으로 나뉜 아키텍처 UX 레이어 ink · components · REPL.tsx · keybindings · vim · voice 확장성 레이어 commands · skills · plugins · hooks · memdir · mcp 도구 레이어 Tool.ts · 20개+ (Bash · Edit · Read · Grep · Agent · Web…) 코어 런타임 · 심장 QueryEngine · query.ts · tokenBudget · stopHooks compact · toolOrchestration 부가 서비스 api · oauth · lsp · analytics · SessionMemory · cost-tracker entrypoints · bootstrap · setup · main.tsx
노란색 두 레이어가 이 책에서 집중적으로 다루는 영역. 확장성 레이어는 사용자가 직접 건드리는 곳이고, 코어 런타임은 "왜 그렇게 동작하는가"의 답이 있는 곳이다.
Part One
원작자의
머릿속
Chapter 01

모든 것이 스트리밍이다

"사용자가 언제든 끼어들 수 있어야 한다"는 전제가 아키텍처 전체를 지배한다.

설계 의도

Claude Code의 한 턴은 함수 호출이 아니라 비동기 제너레이터다. 사용자 입력이 들어오면 LLM 응답 토큰 하나하나, 도구 실행 진행률, 파일 변경 알림, 스톱 훅 결과까지 모두 한 줄기 스트림으로 흐른다.

QueryEngine.ts · line 209export class QueryEngine {
  async *submitMessage(
    prompt: string | ContentBlockParam[],
    options?: { uuid?: string; isMeta?: boolean },
  ): AsyncGenerator<SDKMessage, void, unknown>
}

원작자들이 이 구조를 선택한 이유는 하나로 요약된다. LLM 응답이 길수록 사용자는 가다가 방향을 바꾸고 싶어진다. 기존 CLI들은 완성 후 표시 모델이라 끊으면 지금까지 한 일이 다 날아간다. Claude Code는 반대다. 끊어도 이미 쓴 파일은 남고, 도구 실행이 절반 된 상태면 그 절반은 디스크에 기록되어 있다.

한 턴의 흐름 AsyncGenerator 파이프 · Ctrl+C 어디서든 가능 사용자 입력 submitMessage queryLoop while(true) 1. 컨텍스트 압축 2. LLM 스트리밍 호출 3. Tool 병렬 실행 4. Stop Hooks 5. 계속? 종료? 터미널 UI Ink 렌더 모든 단계에서 yield · 어디서든 interrupt
한 턴이 제너레이터 한 개다. 소비자가 for await로 당기는 만큼만 생산되므로 취소, 스트리밍, 백프레셔가 공짜로 따라온다.

무엇이 달라지는가

01
관찰 우선 · 프롬프트 나중
첫 5초 규칙

긴 작업을 돌릴 때 첫 5~10초는 반드시 지켜봐라. 모델이 예상과 다른 파일을 읽기 시작하면 그 자리에서 Ctrl+C로 끊는 편이 낫다. 나중에 끊을수록 "절반 맞고 절반 틀린" 상태가 되어 컨텍스트가 오염된다. 오염된 컨텍스트는 다음 턴에 전염된다.

스트리밍이기 때문에 중간 개입이 공짜다. 기존 CLI에서는 한 번 실행하면 끝이라 신중하게 프롬프트를 짜야 했다. Claude Code는 반대다. 완벽한 프롬프트를 만드느라 30초 고민하느니 일단 던져놓고 첫 tool_use 두세 개를 본 뒤 판단하는 편이 빠르다. 프롬프트 엔지니어링 시간을 줄이고 관찰 시간을 늘려라.

안 좋음
"파일을 먼저 읽고 구조를 파악한 뒤 조심스럽게 수정해줘. 기존 스타일을 유지하고, 혹시 관련된 다른 파일이 있으면 그것도 확인해주고…"
좋음
auth.ts:42의 validation을 RFC 5322로 고쳐라. 첫 Read가 맞는 파일이면 계속, 아니면 끊고 다시 지시.
Takeaway 관찰이 먼저다. 프롬프트 완벽주의는 스트리밍 아키텍처의 의미를 반감시킨다.
Chapter 02

안전 기본값

"모르면 멈춰서 물어라." LLM 에이전트의 가장 큰 리스크는 실수가 아니라 조용한 실수다.

설계 의도

도구의 모든 기본값이 보수적 쪽으로 세팅되어 있다. 새 도구를 만들면 자동으로 "읽기 전용 아님", "동시 실행 안전 아님", "파괴적 아님"으로 판정된다. 파일 편집은 반드시 Read가 선행되어야 하고, bash는 네 개의 독립된 권한 계층을 통과해야 한다.

Tool.ts · TOOL_DEFAULTSconst TOOL_DEFAULTS = {
  isEnabled:         () => true,
  // 모르면 직렬, 모르면 쓰기 가능, 모르면 비파괴 — 권한 검사가 별도로 잡는다
  isConcurrencySafe: () => false,
  isReadOnly:        () => false,
  isDestructive:     () => false,
  checkPermissions:  async () => ({ behavior: 'allow' }),
  toAutoClassifierInput: () => '',
}

BashTool은 특히 방어가 두텁다. 한 파일에 다 몰아넣지 않고 독립 파일 7개로 쪼갰다. 각 계층이 다른 계층을 몰라도 되게 해서, 하나가 뚫려도 나머지가 막는다.

BashTool 4계층 권한 검사 defense in depth · 하나가 뚫려도 나머지가 막는다 ① 읽기 전용 검증 find · grep · ls 화이트리스트 · find -exec 바이패스 차단 ② 경로 제약 CWD 이스케이프 금지 · .git / .ssh 보호 · 심볼릭 링크 순회 차단 ③ AST 의미 검사 tree-sitter 파싱 · zsh 위험 명령 · 인코딩 공격 감지 ④ 규칙 기반 매칭 정규식 allow/deny/ask · 분류기 통합 · git:* 와일드카드
bash 하나를 실행하려면 네 개의 독립된 검사를 통과해야 한다. 노란색이 사용자가 직접 건드릴 수 있는 계층이다.
LLM 에이전트의 가장 큰 리스크는 잘못하는 것이 아니라 조용히 잘못하는 것이다. 성능 손실을 감수하고서라도 의심스러우면 멈추고 묻는다. 원작자의 판단

무엇이 달라지는가

기본 권한 시스템을 신뢰하되, 훅으로 더 조여라. 많은 사용자가 --dangerously-skip-permissions를 기본값으로 쓰거나 acceptEdits 모드를 세션 내내 켜둔다. 이건 원작자의 의도를 역행하는 선택이다. 대신 PreToolUse 훅을 써라.

반복해서 프롬프트에 "git push --force는 하지 마"라고 쓸 필요가 없다. settings.json의 훅 한 줄로 막을 수 있다.

원작자들이 훅을 만든 이유가 바로 이것이다. 예상 못한 위험에 사용자가 직접 선을 그을 수 있게.

Takeaway 안전 장치를 끄지 말고 위에 더 쌓아라. 프롬프트로 반복해서 주의를 주는 건 낭비다.
Chapter 03

토큰은
희소 자원이다

컨텍스트 경제학. 토큰을 아끼는 네 가지 장치와 그 활용법.

설계 의도

Claude Code 내부에는 토큰을 아끼기 위한 장치가 네 층이나 된다.

첫째, Deferred Tool Loading. 수십 개의 커맨드와 20개가 넘는 빌트인 도구 중 핵심만 시스템 프롬프트에 올라간다. 나머지는 모델이 ToolSearch로 검색할 때에만 스키마가 로드된다. 이 한 가지로 초기 프롬프트가 수천 토큰 가벼워진다.

둘째, 4단계 자동 압축. 가벼운 정리에서 시작해 읽기 전용 구간 축약을 거쳐 실제 LLM 요약까지, 필요한 만큼만 공격적으로 돈다. 그리고 압축 직후에는 "최근 변경 파일 5개"를 다시 밀어 넣어 방금 한 작업의 맥락을 보존한다.

4단계 컨텍스트 압축 가벼운 것부터 공격적인 것까지 순차 적용 01 snipCompact 오래된 스니펫 제거 · feature flag로 on/off 가벼움 02 microcompact 캐시된 변환 · tombstone 정리 가벼움 03 contextCollapse 읽기 전용 구간 병렬 축약 중간 04 autocompact LLM 요약 호출 · 3회 실패 시 회로차단기 무거움 postCompactCleanup 최근 변경 파일 5개 · 현재 스킬 5KB · 플랜 파일 재주입
네 단계가 순서대로 돌고 마지막에 postCompactCleanup이 방금 한 작업의 맥락을 되살린다. 세 번 연속 실패하면 autocompact가 회로차단기로 정지.

셋째, Prompt Caching 할인. 같은 접두사가 반복되면 캐시 읽기는 원가의 10%, 캐시 쓰기(첫 적재)는 125%다. 첫 호출만 약간 더 비쌀 뿐, 이후부터는 매 호출이 90% 할인이라 대화가 길어질수록 실질 비용이 떨어진다.

넷째, 서브에이전트 독립 버짓. 병렬로 4개를 돌리면 비용은 4배가 되지만, 각자 독립 예산이라 메인 대화가 서브 조사 때문에 일찍 끊기는 사고가 나지 않는다.

토큰은 무한하지 않고, 사용자는 토큰 관리에 인지 부담을 쓰면 안 된다. 시스템이 알아서 짜내 준다. 원작자의 판단

무엇이 달라지는가

02
가장 큰 실수
CLAUDE.md를 자주 바꾸지 마라

CLAUDE.md는 시스템 프롬프트의 안정된 접두사로 들어가 prompt caching의 본체가 된다. 한 문장을 고치면 캐시가 깨지고, 캐시가 깨지면 다음 호출부터 풀 가격이다. 글로벌은 일주일에 한 번 이하로, 자주 바꾸는 건 프로젝트별 .claude/CLAUDE.md에 분리해라.

03
캐시 적중 전략
맥락은 대화 초반에 몰아 넣어라

첫 메시지에 "지금 이런 작업을 할 건데, 제약은 이거고, 완료 기준은 이거다"를 다 박아놓으면 그 전체가 캐시에 올라간다. 나중에 조각조각 "아 그리고 이것도"라고 추가하면 캐시 바깥의 새 접두사라 할인이 안 된다.

04
모델 역할 분담
판단은 Opus, 구현은 Sonnet, 반복은 Haiku

단일 모델로 전부 돌리는 건 돈을 버리는 짓이다. 서브에이전트 단위로 모델을 바꿀 수 있으니, 조사 에이전트는 Sonnet, 최종 결정만 Opus 같은 식으로 짜라.

CLAUDE.md는 어떻게 짜야 하는가

캐시 효율과 실용성을 동시에 잡는 구조가 있다.

글로벌 (~/.claude/CLAUDE.md): 나에 대한 고정 사실만. 코딩 스타일, 커밋 규칙, 금지 사항. 한 달에 한 번 이하로 수정. 20줄 이내.

프로젝트 (.claude/CLAUDE.md): 이 레포에 특화된 규칙. 빌드 명령, 테스트 방법, 아키텍처 요약. 자주 바뀌어도 괜찮다 — 프로젝트 단위 캐시라서 글로벌 캐시에 영향을 주지 않는다.

안 좋음
글로벌에 프로젝트별 빌드 명령까지 몰아넣기 → 프로젝트 바꿀 때마다 캐시 깨짐
좋음
글로벌은 스타일만, 프로젝트는 .claude/CLAUDE.md에 분리 → 캐시 독립
~/.claude/CLAUDE.md · 글로벌 예시# 코딩 스타일
TypeScript 우선. React + Next.js + Tailwind.
함수형 컴포넌트 + hooks. class 금지.
camelCase 변수, PascalCase 컴포넌트.
주석은 "왜"만 쓴다.

# 커밋
Conventional Commits (feat/fix/refactor/docs/test/chore).
console.log 디버깅 코드를 커밋에 남기지 않는다.

# 금지
내가 물어보지 않은 것을 설명하지 마라.
package.json에 불필요한 의존성 추가하지 마라.
.claude/CLAUDE.md · 프로젝트 예시# 이 프로젝트
Next.js 15 + App Router. /src/app 기반.
DB: Supabase (Postgres). ORM: Drizzle.

# 빌드
pnpm dev → localhost:3000
pnpm test → vitest
pnpm lint → eslint + prettier

# 주의
/src/lib/auth.ts는 건드리지 마라 (인증 로직 안정화 중).
API 라우트는 /src/app/api/ 아래에만.
Takeaway 토큰을 아끼는 건 시스템의 몫이 아니라 협업이다. 시스템이 네 겹으로 준비해둔 장치를 사용자가 깨지 않게 하는 것이 전부다.
Chapter 04

서브에이전트는
격리된 뇌

메인 뇌를 보호하라. 긴 조사는 격리된 뇌에서, 메인은 판단만.

설계 의도

AgentTool이 서브에이전트를 만들 때 가장 흥미로운 점은 격리 방식이다. 서브에이전트는 부모의 권한과 도구는 모두 상속하지만, 메인 AppState에는 쓸 수 없다. 태스크 레지스트리 같은 인프라만 예외로 공유하고, 나머지는 "부모는 읽기 전용, 결과는 메시지로만" 원칙이다.

서브에이전트 격리 모델 메인은 판단만 · 힘든 일은 격리된 서브들이 Main Thread 판단 · 조율 · 최종 결정 AppState · 메시지 · 비용 사용자 대화 컨텍스트 핵심 판단 프롬프트 ↑ 여기에 탐색 결과를 쌓으면 품질이 떨어진다 fork fork fork Subagent A · Explore 파일 10개 읽기 → 요약 한 단락 Subagent B · Implementer services/auth 구현 · 메인 못 건드림 Subagent C · Reviewer B의 결과 검토 · 읽기 전용 권한
서브의 결과는 요약으로만 돌아와서 메인 컨텍스트를 깨끗하게 유지한다. setAppState는 서브에이전트에서 no-op이다.

이게 왜 중요한가. LLM은 컨텍스트가 길어질수록 품질이 떨어진다. 모델 벤더들이 "100만 토큰 지원"을 자랑하지만, 실무에서 50만 토큰을 채운 대화는 20만 토큰일 때보다 답이 흐릿하다. 원작자들은 이걸 알고 있었다. 그래서 메인 대화를 최대한 깨끗하게 유지하는 것을 최우선으로 두고, 탐색·조사·검증 같은 "토큰을 많이 먹지만 결과는 짧은" 작업을 전부 격리된 서브에이전트로 밀어냈다.

무엇이 달라지는가

05
핵심 규칙
메인 스레드 3파일 룰

메인 스레드에서 직접 파일을 세 개 이상 읽고 있으면 멈춰라. 그 순간 이미 잘못 가고 있다. Task 도구나 Explore 에이전트를 불러서 "이 세 파일을 읽고 X에 대해 요약해달라"고 넘겨라. 메인 컨텍스트에는 요약 한 단락만 들어온다. 같은 작업을 메인이 하면 수천 토큰이 쌓인다.

병렬로 서브에이전트를 돌리는 것을 두려워하지 마라. 격리되어 있으니 상태 충돌이 없다. 단, 파일 쓰기 범위가 겹치지 않게 에이전트별 스코프를 명시해라. "Agent A는 /services만, Agent B는 /components만." 이걸 프롬프트에 박아놓지 않으면 둘이 같은 파일을 동시에 건드려 덮어쓰는 사고가 난다.

검증은 반드시 별도 에이전트로. 구현한 에이전트에게 "네가 한 거 맞아?"를 물으면 답은 항상 "맞다"다. code-reviewer 에이전트를 별도로 띄워서 똑같은 결과물을 처음 보는 시선으로 읽게 해라. 원작자들이 AgentTool을 격리한 이유가 바로 이 새 시선이다.

Takeaway 메인 스레드는 오케스트레이터다. 판단, 조율, 최종 결정만 한다. 실제 힘든 일은 서브가 한다.
Chapter 05

훅은 탈출구

"우리 예상을 넘어서라." 원작자들이 사용자에게 열어둔 문.

설계 의도

훅 시스템은 PreToolUse, PostToolUse, SessionStart, UserPromptSubmit, Stop 같은 이벤트에 사용자가 스크립트를 걸 수 있게 한다. 가장 강력한 기능은 PreToolUse의 updatedInput이다. 모델이 호출한 도구 입력을 훅이 다시 써서 돌려줄 수 있다.

Hook 응답 스키마{
  "continue": true,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    // 모델이 호출한 입력을 훅이 고쳐서 돌려준다
    "updatedInput": {
      "command": "git commit -m 'fix' --signoff"
    },
    "additionalContext": "자동 signoff 추가됨"
  }
}

예를 들어 모델이 git commit -m "fix"를 호출하면 훅이 그걸 받아서 git commit -m "fix" --signoff로 바꿔서 실제 실행으로 넘긴다. 모델은 자기가 쓴 명령이 통과된 줄 안다. 사용자는 자기 정책을 조용히 얹었다.

우리는 사용자가 뭘 원할지 다 알 수 없다. "우리가 예상 못한 요구는 여기 끼워 넣어라"라는 탈출구를 팠다. 겸손함의 아키텍처

무엇이 달라지는가

프롬프트로 반복해서 주의를 주지 말고 훅으로 박아라. "git push --force 하지 마", "rm -rf 하지 마", "console.log 남기지 마" 같은 지시를 매번 프롬프트에 넣는 건 낭비다. 한 번 훅으로 만들면 영원히 작동한다. 토큰도 아끼고 실수도 막는다.

훅과 스킬·커맨드의 구분은 Chapter 10에서 다룬다. 여기서는 훅 내부 메커니즘만 집중한다.

06
주의
훅이 조용히 실패할 수 있다

permissionDecision: 'deny'만 반환하면 도구 호출은 차단되지만 모델이 다음 도구를 바로 시도할 수 있다. additionalContext에 "이 명령은 금지됨"을 명시해야 모델이 같은 시도를 반복하지 않는다. 그리고 훅 스크립트에는 반드시 로깅을 넣어라.

Takeaway 반복되는 규칙은 프롬프트가 아니라 훅에 넣어라. 한 번 설정하면 매 턴 자동으로 작동한다.
Chapter 06

플랜과 실행은
다른 상태다

탐색과 변경은 뇌가 다르게 쓴다. 원작자들은 그걸 물리적으로 분리했다.

EnterPlanModeTool은 아무 입력도 받지 않는다. 단지 세션 상태를 "plan 모드"로 바꿀 뿐이다. 이 모드에서는 파일 편집 도구가 모두 잠긴다. Read, Grep, Glob 같은 읽기 전용만 살아남는다.

왜 이걸 별도 상태로 만들었는가. 탐색 중에는 "가능한 방법을 넓게 보는" 사고가 필요하고, 변경 중에는 "정확히 한 가지를 수행하는" 사고가 필요하다. LLM도 사람도 이 둘을 동시에 하면 품질이 떨어진다.

진입 기준

다섯 개 이상 파일에 걸친 변경. 요구사항이 불명확한 상태. 비가역 작업 포함 — DB 마이그레이션, 파일 삭제, git reset 같은 것들. 프레임워크 업그레이드 같은 환경 변경. 처음 보는 코드베이스 탐색. 이 다섯 중 하나라도 해당하면 플랜 모드로 먼저 들어가는 편이 안전하다.

진입 후 흐름

넓게 읽고, 질문을 정리하고, 계획을 글로 쓰고, 계획을 비판하고, 확정되면 ExitPlanMode로 나와서 실행한다. 다섯 단계 중 어느 하나라도 건너뛰면 플랜의 의미가 없다. 특히 네 번째 "계획을 비판한다"가 핵심이다. "이 계획의 약점은?" "빠진 케이스는?"을 모델에게 한 번 더 묻는 것만으로 놓친 부분이 드러난다.

07
드리프트 신호
"이것도 고쳐야겠네"가 두 번

리팩터링 중에 "이것도 고쳐야겠네"가 두 번 이상 나오면 지금 하고 있는 일을 멈추고 플랜 모드로 돌아가라. 스코프가 넓어지는 순간이 이미 계획이 어긋나고 있다는 신호다.

Takeaway Plan 모드는 실행을 막는 안전장치가 아니라 사고의 질을 높이는 분리 장치다. 복잡한 작업 전에 진입하고, 계획이 확정되면 빠져나와라.
Chapter 07

메모리는
파일이다

DB가 아니라 마크다운. 사용자가 직접 열어보고 고칠 수 있어야 한다는 신뢰의 아키텍처.

설계 의도

Claude Code의 메모리는 데이터베이스가 아니다. ~/.claude/projects/<프로젝트>/memory/ 아래에 마크다운 파일로 저장된다. MEMORY.md가 인덱스고, 상세 내용은 topics/ 아래 개별 파일이다. 인덱스는 25KB / 200줄 제한이 있고, 초과하면 경고와 함께 잘린다.

memdir 2-tier 메모리 구조 인덱스 한 장 + 개별 토픽 파일 MEMORY.md 인덱스 · 항상 컨텍스트에 로드 ≤ 25KB · ≤ 200줄 - [user role](user_role.md) - [feedback/git](git_rules.md) - [project stack](stack.md) - [refs/deploy](deploy.md) 한 줄 엔트리만 topics/ 필요할 때만 로드 · 크기 제한 없음 user_role.md type: user · 장기 사실 git_rules.md type: feedback · 반복 실수 교정 stack.md type: project · 프로젝트 맥락 deploy.md type: reference · 외부 링크
인덱스는 한 줄 엔트리만, 상세는 별도 파일로. user / feedback / project / reference 네 가지 타입으로 분류된다.
DB에 저장된 AI의 기억은 믿기 어렵다. 마크다운 파일은 열어보고, 고치고, git으로 버전 관리할 수 있다. 신뢰는 투명성에서 나온다. 원작자의 판단

무엇이 달라지는가

08
가장 흔한 실수
MEMORY.md에 본문을 쓰지 마라

이건 인덱스 파일이다. 한 줄 엔트리로만 채워라. 상세는 topics/ 아래 개별 파일로. 이걸 지키지 않으면 200줄 넘어서 잘릴 때 중요한 정보가 날아간다.

타입별 기본 사용법. user 타입은 나에 대한 장기 사실 — 한 번 넣고 거의 안 고친다. feedback은 반복 실수 교정 — 실수할 때마다 늘어난다. project는 프로젝트 구조 — 자주 바뀌는 건 주의. reference는 외부 링크 모음.

git으로 버전 관리하라. ~/.claude/projects/<slug>/memory/를 git 리포로 만들어두면 "언제 어떤 메모리가 추가됐는지" 추적할 수 있다. 메모리 오염 사고(잘못된 사실 기억)가 났을 때 롤백 가능하다.

Takeaway 메모리는 DB가 아니라 파일이다. 인덱스는 가볍게, 상세는 개별 파일로, 전체를 git으로 버전 관리해라.
Part Two
그래서 어떻게
써야 하는가
Chapter 08

프롬프트의 모양

Claude Code가 잘 삼키는 문장은 명령형 단문이고, 3단 구조이고, 파일과 라인을 명시한다.

Claude Code의 시스템 프롬프트는 이미 "도구를 써서 해결해라"를 깔아놓았다. 그래서 사용자 프롬프트는 설명하는 글이 아니라 지시하는 문장이어야 한다. 대답을 구하면 도구 쓰기를 포기하고, 긴 탐구를 구하면 서브에이전트로 위임한다.

안 좋음
"혹시 login form의 validation을 좀 개선해줄 수 있을까? 가능하면 RFC 규격에 맞게 해주면 좋을 것 같은데, 어떻게 생각해?"
좋음
LoginForm.tsx:42의 이메일 validation을 RFC 5322로 고쳐라. UI는 건드리지 마라. 테스트 3개 추가. package.json 금지.

3단 구조 · 목표 + 제약 + 검증

[목표] login form의 이메일 validation을 RFC 5322로 고쳐라.
[파일] src/components/LoginForm.tsx, src/utils/validation.ts
[제약] UI 마크업은 건드리지 마라. 기존 props 시그니처 유지.
[검증] npm test가 모두 통과해야 한다. 새 케이스 3개 추가해라.
[금지] 다른 폼 컴포넌트는 건드리지 마라.

이걸 복붙 템플릿으로 만들어두면 생각하는 시간이 줄어든다. 목표만 있으면 제약을 임의로 깬다. 제약만 있으면 목표가 흐려진다. 검증이 없으면 "대충 끝났다"로 종료한다.

안 먹히는 모양

"생각해보니…" 뒤에 나오는 요청. 결정이 안 끝난 사고과정을 프롬프트에 넣으면 모델도 흐려진다. 결정을 먼저 내리고 요청해라.

여러 작업을 한 프롬프트에. "A 하고 B 하고 C 해줘"는 거의 항상 B가 대충 된다. 한 턴 한 작업.

"가능하면…". 조건부 요청은 LLM이 조건 판단을 피하는 쪽으로 움직인다. 하라면 하라, 말라면 말라.

Takeaway 좋은 프롬프트는 설명이 아니라 지시다. 목표·파일·제약·검증·금지, 다섯 줄이면 된다.
Chapter 09

작업 단위 — 턴 · 세션 · 워크트리

세 가지 단위를 구분하지 못하면 고생한다.

단위크기기준
submitMessage 1회한 가지 변경
세션resume 가능한 대화 전체한 기능 · 한 주제
워크트리격리된 git 디렉터리리스크 큰 실험

기능 완료는 새 세션 시작. 실험적 변경은 워크트리 진입. 작은 고침 여러 개는 한 세션 여러 턴. 하루의 시작에는 어제 세션을 resume할지 새로 시작할지 먼저 결정한다.

워크트리는 언제 쓰나

기존 브랜치를 건드리지 않고 실험하고 싶을 때다. EnterWorktree를 쓰면 현재 레포의 복사본이 임시 디렉토리에 생기고, 그 안에서 자유롭게 작업한다. 결과가 마음에 들면 머지, 아니면 그냥 버린다. 메인 코드가 실험 잔해로 더러워지지 않는다.

안 좋음
메인 브랜치에서 “한번 이거 해볼까” → 망함 → git stash / reset 반복
좋음
워크트리 진입 → 실험 → 성공하면 머지, 실패하면 삭제. 메인은 깨끗.

세션을 이어갈까, 새로 시작할까

이어가는 게 나은 경우. 같은 기능의 후속 작업. 버그 수정 후 테스트 추가. 리뷰 피드백 반영. 컨텍스트가 아직 유효하다.

새로 시작하는 게 나은 경우. 다른 도메인으로 넘어갈 때. 세션이 길어져서 모델이 초반 지시를 잊기 시작할 때. /compact를 두 번 이상 쳤는데도 맥락이 흐려질 때. 이 때는 끊고 새 세션에서 “방금 X를 완료했다. 다음은 Y다”로 시작하는 편이 낫다.

판단 기준은 단순하다. “지금 이 세션이 내가 원하는 걸 알고 있나?” 아니면 새로 시작.

01
복구 팁
NDJSON은 일부러 그런 것이다

망가진 세션은 session-<id>.jsonl을 직접 열 수 있다. NDJSON이니 한 줄이 한 메시지다. 마지막 몇 줄만 잘라내고 다시 resume하면 사고 직전 상태로 복구 가능하다. 이 파일 포맷을 일부러 JSON이 아니라 NDJSON으로 만든 것도 원작자의 배려다 — 문제가 생기면 끝부분만 잘라서 구할 수 있게.

Takeaway 기능 완료는 새 세션, 실험은 워크트리, 작은 고침은 같은 세션. 단위를 의식적으로 쪼개라.
Chapter 10

Skill · Command · Hook

확장 방식이 여러 개라 헷갈린다. 간단한 결정 트리.

확장 방식 결정 트리 무엇을 원하는가? 이벤트 자동 내가 칠 때 모델이 자율 외부 연결 Hook PreToolUse · PostToolUse SessionStart · Stop settings.json Command /status · /deploy 순수 로컬 로직 commands/ Skill whenToUse 기반 호출 모델 자율 판단 ~/.claude/skills/ MCP Notion · Gmail · Linear stdio · SSE · HTTP .claude/mcp.json Plugin 세 가지를 묶어서 배포
한 가지 확장 방식이 딱 맞는 경우는 드물다. 묶음이 필요하면 Plugin.

예시로 보는 구분

"블로그 글을 3개 플랫폼에 동시 발행해줘." 자주 쓰는 자연어 트리거다. Skill로 만든다. whenToUse에 "블로그 발행"이라고 써두면 모델이 알아서 호출한다.

"git push 전에 반드시 lint를 돌려라." 매번 자동으로 끼어들어야 한다. Hook으로 만든다. PreToolUse 이벤트에 matcher를 Bash로 걸고, 명령이 git push면 lint 먼저. 프롬프트에 매번 넣지 마라.

훅의 내부 동작(updatedInput, blocking 분기)은 Chapter 05에서 다뤘다.

"특정 프로젝트 상태 요약이 필요하다." 내가 원할 때 치는 단축이다. Command. /status를 치면 요약이 나오게.

"Notion에서 할 일을 가져와야 한다." 외부 시스템 연결이다. MCP Server. Notion의 API를 감싸는 MCP 서버를 연결한다. 스킬이나 커맨드 안에서 MCP 서버의 도구를 호출.

MCP 서버 연결하기

MCP(Model Context Protocol)는 Claude Code를 외부 시스템과 연결하는 표준이다. Notion, Gmail, Linear, Slack 같은 서비스를 도구로 쓸 수 있게 해준다.

설정은 .claude/mcp.json에 한다.

.claude/mcp.json{
  "mcpServers": {
    "github": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_..."
      }
    },
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
    }
  }
}

stdio vs SSE. 로컬 프로세스를 띄우는 건 stdio. 원격 서버에 연결하는 건 sse로 URL을 넣는다. 대부분의 커뮤니티 서버는 stdio다.

주의. MCP 서버가 등록되면 해당 도구들이 ToolSearch에 deferred tool로 올라간다. 모델이 필요할 때 검색해서 호출한다. 매 턴 시스템 프롬프트에 올라가지 않으므로 토큰 비용은 크지 않다.

Takeaway 매번 자동이면 Hook, 내가 칠 때면 Command, 모델이 알아서면 Skill. 묶으면 Plugin.
Chapter 11

위임 타이밍

메인이 할 일과 서브에이전트가 할 일의 경계.

위임 신호

파일을 3개 이상 읽어야 한다. 작업 결과가 파일 수정이 아니라 요약이나 리포트다. 같은 패턴의 작업을 독립적으로 여러 번 해야 한다. 결과물이 메인 컨텍스트에 남지 않아도 된다. 현재 컨텍스트를 오염시키고 싶지 않다. 이 중 하나라도 해당하면 서브에이전트에 넘겨라.

위임하지 말 신호

반대로 다음은 메인에서 해야 한다. 이 작업 결과가 다음 턴의 전제가 된다. 사용자와 대화하며 조정해야 한다. 인터랙티브한 결정이 필요하다. 도메인이 불명확해서 서브에이전트에 스펙을 쓸 수 없다.

병렬 위임 시 파일 스코프 분리와 에이전트 수 제한은 Chapter 04를 참고하라. 여기서는 언제 위임을 결정하는가에 집중한다.

01
타이밍 판단
위임 비용 vs 설명 비용

서브에이전트에 프롬프트를 쓰는 데 드는 시간이 직접 하는 시간보다 길면 위임하지 마라. 3줄 고치는 일에 10줄짜리 프롬프트를 쓰는 건 역전이다. 위임은 탐색 범위가 넓거나 독립 도메인이 2개 이상일 때만 이득이다.

Takeaway 위임 프롬프트 쓰는 시간이 직접 하는 시간보다 길면 위임하지 마라. 탐색 범위가 넓을 때만 이득이다.
Part Three
효율 극대화
Chapter 12

비용을 반으로 줄이는
습관 10가지

설계 의도를 지키면 자연스럽게 돈이 줄어든다.

01
시스템 프롬프트에 예제를 넣지 마라

CLAUDE.md에 “이렇게 해줘” 예시를 길게 넣으면 매 턴마다 그만큼 입력 토큰이 늘어난다. 예시는 첫 프롬프트에 한 번 넣고, CLAUDE.md에는 규칙만 적어라.

02
Read 결과가 길면 범위를 좁혀라

offsetlimit 파라미터를 쓰면 필요한 줄만 읽는다. 2000줄짜리 파일을 통째로 읽으면 그게 다 컨텍스트에 쌓인다.

03
Grep > Bash grep

빌트인 Grep 도구가 bash의 grep이나 rg보다 토큰 효율이 높다. 전용 도구는 결과를 구조화해서 반환하고, bash 출력은 raw text로 컨텍스트를 잡아먹는다.

04
/compact를 수동으로

자동보다 예측 가능하다. 기능 하나를 끝낸 직후가 가장 좋은 타이밍.

05
TodoWrite 대신 TaskCreate

할 일 관리를 프롬프트에 쓰면 매 턴 토큰이 든다. TaskCreate/TaskUpdate는 별도 저장소에 상태를 관리하므로 컨텍스트를 아낀다.

06
훅으로 불필요한 도구 호출 차단

모델이 실수로 큰 파일을 읽으려 할 때 훅이 막으면 토큰 절약.

07
에러 로그 전체를 붙여넣지 마라

500줄짜리 스택 트레이스를 통째로 넣으면 모델이 핵심을 놓친다. 첫 에러 메시지와 관련 줄 5~10개만 붙여넣어라. 나머지는 “전체 로그는 /tmp/error.log에 있다”로 충분.

08
자동 continuation을 제어하라

토큰 버짓을 명시적으로 낮춰두면 "계속 계속" 달리다 예산 날리는 사고가 준다.

09
실패한 세션은 빨리 끊어라

한 세션에서 두 번 이상 모델이 헛소리를 하면 /clear. 오염된 컨텍스트로 뭘 더 해도 돈만 나간다.

10
/cost를 루틴으로

하루에 몇 번 본다. "오늘 얼마 썼나"를 모르면 새어나가는 곳을 못 잡는다.

Takeaway 토큰을 아끼는 건 습관이다. 도구를 바꾸는 게 아니라 쓰는 패턴을 바꿔라.
Chapter 13

이상할 때 보는 것

증상 → 원인 → 처방 치트시트.

증상원인처방
방금 수정한 파일을 모르는 척 autocompact 후 복원 한계 (5개/50KB) 해당 파일 다시 Read 지시
같은 실수 반복 feedback 메모리 없음 명시적 저장 요청 후 파일 확인
5분 전 말을 잊음 컨텍스트 90% 근처 /compact 또는 /clear
훅이 안 먹음 preventContinuation 누락 훅 응답 둘 다 세팅
서브에이전트가 헛소리 맥락 부족 위임 프롬프트 두껍게
비용 급증 캐시 미적중 CLAUDE.md 안정화
병렬 작업 덮어쓰기 쓰기 범위 미분할 에이전트별 스코프 명시
플랜이 계속 커짐 스코프 드리프트 플랜 모드 재진입
세션이 엉킴 여러 주제 혼재 /clear 후 분리
git push 실수 방어선 없음 PreToolUse 훅에서 deny

직접 열어볼 파일들

문제가 생기면 위에서부터 차례로 연다. ~/.claude/sessions/<proj>/session-<id>.jsonl에 현재 세션 원본이 있다. ~/.claude/history.jsonl에 전역 히스토리가 있다. ~/.claude/projects/<slug>/memory/MEMORY.md가 인덱스 메모리, 그 아래 topics/에 상세 메모리. 훅·MCP·LSP 로그는 ~/.claude/logs/ 아래에 있다(있는 경우).

Takeaway 이상하면 세 가지를 확인해라 — 컨텍스트 오염, 권한 차단, 토큰 초과. 대부분 이 셋 중 하나다.
Bonus

처음 보는 레포에서
첫 30분

설계 의도를 아는 사람이 새 프로젝트를 받았을 때 실제로 하는 순서.

0~5분: 세션 세팅

새 터미널을 열고 프로젝트 루트에서 Claude Code를 시작한다. 첫 프롬프트에 목표·제약·검증 기준을 한 번에 넣는다. 이게 캐시의 접두사가 된다.

[목표] 이 레포의 아키텍처를 파악하고 README를 보강해라.
[제약] 코드를 수정하지 마라. 읽기만.
[검증] 주요 진입점, 데이터 흐름, 의존성 구조를 설명할 수 있어야 한다.

5~15분: Explore 에이전트 병렬 투입

"프로젝트 구조를 파악해라"라고 직접 시키지 않는다. 대신 3개 질문을 서브에이전트에 던진다.

  1. "진입점은 어디인가? main, index, app 파일을 찾아라."
  2. "핵심 데이터 모델은? 타입 정의 파일들을 찾아라."
  3. "외부 의존성은 뭔가? package.json / go.mod / requirements.txt를 읽어라."

메인 스레드는 이 결과를 기다리면서 README.mdCLAUDE.md가 있으면 먼저 읽는다. 3파일 룰 — 메인은 읽기만, 무거운 탐색은 서브가.

15~25분: 지도 그리기

서브에이전트 결과가 돌아오면 메인에서 합성한다. "진입점은 X, 데이터는 Y를 거쳐 Z로 간다, 외부 의존성은 A·B·C." 이 한 단락이 이 레포에 대한 멘탈 모델이 된다.

여기서 /compact를 한 번 친다. 탐색 결과가 컨텍스트에 쌓여 있으니 정리해야 다음 작업이 효율적이다.

25~30분: 첫 작업 착수

멘탈 모델이 잡혔으면 실제 작업(README 작성, 버그 수정, 기능 추가)을 시작한다. 이때부터는 구현 에이전트(Sonnet)를 위임하고 메인은 판단만 한다.

핵심은 첫 15분을 탐색에 쓰는 것이다. 바로 코드를 고치기 시작하면 컨텍스트가 탐색 잔해로 오염된다. 탐색을 서브에이전트에 격리하면 메인은 깨끗한 상태에서 구현에 집중할 수 있다.

Takeaway 새 레포에서 첫 15분은 서브에이전트로 탐색, 다음 15분은 메인에서 합성과 착수. 바로 고치기 시작하지 마라.
Part Four
원작자가
남긴 단서
Chapter 14

15가지 설계 의도

소스 분석에서 뽑아낸 "왜 이렇게 했을까"의 답들.

01
AsyncGenerator 전면화

"사용자가 언제든 끼어들 수 있어야 한다." LLM 응답은 길다. 길면 방향을 바꾸고 싶어진다. 완성 후 표시 모델로는 끼어들 수 없다.

queryLoop 안의 yield* 체인이 5단계다. 어디서 끊어도 이전 yield까지는 보존된다.

02
Tool 기본값이 모두 보수적

"조용히 부수지 말라." isReadOnly, isConcurrencySafe, isDestructive 모두 false가 기본. LLM 에이전트의 가장 큰 리스크는 조용한 파괴다.

BashTool은 예외적으로 isConcurrencySafe: false를 명시 — 셸 명령은 병렬로 실행하면 경쟁 조건이 생긴다.

03
ToolSearch로 스키마 lazy load

"스키마도 토큰이다." 100개 도구를 시스템 프롬프트에 다 넣으면 수천 토큰. 쓸 것만 싣고 나머지는 검색하게.

→ deferred tool 목록은 system-reminder 블록에 이름만 노출되고, ToolSearch 호출 후에야 전체 JSON Schema가 컨텍스트에 합류한다.

04
AgentTool로 서브에이전트 격리

"메인 뇌를 보호하라." 컨텍스트가 길면 품질이 떨어진다. 긴 조사는 격리된 뇌에서, 결과는 요약으로만.

→ 서브에이전트는 부모의 허용 목록(allowedTools)을 상속받지 않는다 — 스폰할 때 별도로 지정하지 않으면 기본 도구만 쓴다.

05
memdir를 파일 시스템에

"사용자가 열어볼 수 있게 하라." DB에 저장된 AI의 기억은 믿기 어렵다. 신뢰는 투명성에서 나온다.

~/.claude/memory/ 아래 파일명은 세션 ID 기반이라 어떤 세션이 쓴 메모인지 역추적 가능하다.

06
Compact 4단계 + 3회 회로차단기

"무한 루프 금지." 실패가 반복되면 포기한다. 조용한 파괴만큼 무서운 게 소리 없는 무한 루프.

→ 3회 실패 후 CompactionError를 던지고 세션을 읽기 전용으로 전환한다 — 망가진 상태에서 추가 쓰기를 막는다.

07
Hook의 updatedInput

"우리 예상을 넘어서라." block/allow만으로는 부족하다. 모델이 쓴 입력을 훅이 다시 쓸 수 있게.

updatedInputPreToolUse 훅에서만 유효하다 — PostToolUse에서 반환해도 무시된다.

08
Plan 모드를 별도 상태로

"탐색과 실행은 다른 뇌다." 사용자가 물리적으로 모드 전환을 인식하게.

→ Plan 모드에서는 isDestructive: true인 도구 호출이 자동으로 차단되어 실수로 파일을 바꾸는 사고를 원천 방지한다.

09
PKCE OAuth + Keychain 저장

"API 키를 텍스트 파일에 두지 말라." .env에 키를 박는 문화에 대한 거부.

→ 토큰 갱신은 refreshToken 로직이 처리하며, Keychain 쓰기 실패 시 in-memory fallback으로 세션이 유지된다.

10
VCR 픽스처 시스템

"LLM도 테스트 가능해야 한다." 메시지 해시로 응답을 캐시하는 테스트 인프라.

→ 픽스처 파일은 __fixtures__/ 아래 SHA-256 해시 이름으로 저장되며, 해시 충돌 시 순번 접미사를 붙여 분리한다.

11
Ink 기반 React 터미널 UI

"터미널도 앱이다." ncurses 대신 React. 터미널 프로그램을 modern web dev 경험으로.

useInput 훅이 키 입력을 구독하는 방식이라 Ctrl+C 같은 시그널도 React 이벤트 사이클 안에서 처리된다.

12
세션을 NDJSON으로 저장

"복구 가능해야 한다." 단일 JSON이면 파일 끝이 손상되면 전체가 못 읽힌다. NDJSON은 끝부분만 잘라내고 구할 수 있다.

→ 각 줄에 {"type":"message","uuid":"...","timestamp":...} 구조가 들어가 jq 한 줄로 특정 턴만 추출할 수 있다.

세션 복구 방법(마지막 몇 줄 잘라내고 재시작)은 Chapter 09에서 다뤘다.

13
Stop hook이 blocking/non-blocking 분리

"응답 속도가 요약보다 중요하다." 메모리 추출 같은 백그라운드 작업은 fire-and-forget.

type: "stop" 훅의 blocking 여부는 settings.jsonblocking 필드로 제어하며 기본값은 false다.

14
Feature flag로 빌드 타임 제거

"외부 배포판은 가볍게." 내부 실험 기능은 프로덕션 빌드에서 dead code로 제거.

→ 플래그는 IS_INTERNAL_BUILD 환경변수 하나로 분기되며, esbuild의 define 옵션이 false 브랜치를 트리쉐이킹한다.

15
Permission이 의심스러우면 묻기

"실수보다 느린 게 낫다." LLM 에이전트가 실수하는 비용이 사용자를 한 번 더 귀찮게 하는 비용보다 크다는 판단.

needsPermission이 true인 도구는 세션당 한 번 승인하면 이후 같은 경로 패턴에 대해 자동 허용으로 전환된다.

Takeaway 설계 의도를 읽으면 설명서가 필요 없다. 원작자가 왜 이렇게 만들었는지 알면 응용은 자연히 따라온다.
Epilogue

마치며

이 책의 핵심 주장은 한 문장이다. Claude Code를 잘 쓰는 방법은 내부 구조를 따라가는 것이다.

프롬프트 엔지니어링 팁, 치트 시트, 매직 워드. 그런 것들은 표면이다. 표면은 버전이 올라가면 바뀐다. 반면 설계 의도는 쉽게 바뀌지 않는다. 원작자들이 "스트리밍 제너레이터 하나로 모든 것을 흘리기로" 결정한 순간부터 지금까지, 그리고 앞으로도, Claude Code를 잘 쓰는 사람의 습관은 이 결정과 맞닿아 있을 것이다.

일곱 줄 요약 끼어들 수 있음을 믿고 관찰 먼저 하라. 모르면 멈추는 시스템 위에 규칙을 더 쌓아라. 토큰을 희소 자원으로 대하라. 메인 뇌를 보호하고 나머지는 위임해라. 탈출구를 만들고 프롬프트로 반복하지 마라. 탐색과 실행을 분리해라. 메모리는 열어볼 수 있는 파일에 둬라.

나머지는 응용이다.

전체 ebook을 PDF로 받아가세요. CLAUDE.md 템플릿, MCP 가이드 포함. 메이저 업데이트마다 무료 갱신.

Get the PDF$9+

Pay what you want · English PDF · Instant download