Claude Code는 앱이다 — Agentic Loop, 토큰 흐름, Hook의 동작 원리
Claude Code가 "AI"인 줄 알았는데, 까보니 API를 반복 호출하는 Node.js 앱이었다.
Claude Code는 Claude가 아니다
많은 사람이 Claude Code를 "터미널에서 돌아가는 Claude"라고 생각한다. 틀렸다.
Claude Code ≠ Claude
Claude Code = Claude API를 반복 호출하는 루프 프로그램Claude(LLM)는 원격 서버에 있는 언어 모델이다. 파일을 읽지 못하고, 터미널 명령어를 실행하지 못한다. "Read 도구를 쓰겠습니다"라고 말은 하지만, 실제로 파일을 여는 건 내 맥에서 돌아가는 Claude Code 앱이다.
┌─────────────────────────────────────────────┐
│ 터미널 (내 맥) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Claude Code (Node.js 앱) │ │
│ │ │ │
│ │ - 사용자 입력 수집 │ │
│ │ - API 호출 조립 │ │
│ │ - 도구 실행 (파일 읽기/쓰기/bash) │ │
│ │ - 결과를 다시 API에 전달 │ │
│ │ - Hook 실행 │ │
│ └──────────────┬──────────────────────┘ │
│ │ HTTPS │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Anthropic API (원격 서버) │ │
│ │ │ │
│ │ - 텍스트 생성 (LLM 추론) │ │
│ │ - "다음에 뭘 할지" 판단 │ │
│ │ - 도구 호출 요청 반환 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘Claude가 "파일을 수정하겠습니다"라고 응답하면, Claude Code 앱이 로컬에서 fs.writeFile()을 실행한다. Claude는 그 결과를 다음 API 호출에서 텍스트로 전달받을 뿐이다.
Agentic Loop: "파일 수정해줘" 한 마디의 여정
사용자가 "CLAUDE.md 파일 수정해줘"라고 입력하면 어떤 일이 벌어지는가.
턴 1: 일단 읽자
Claude Code가 API에 보내는 것:
┌──────────────────────────────────────────────┐
│ system: "You are Claude Code..." │ ← 시스템 프롬프트
│ + CLAUDE.md 내용 │
│ + MEMORY.md 내용 │
│ │
│ messages: [ │
│ { role: "user", │
│ content: "CLAUDE.md 수정해줘" } │
│ ] │
│ │
│ tools: [Read, Edit, Write, Bash, Glob, ...] │ ← 사용 가능한 도구 목록
└──────────────────────────────────────────────┘API 응답:
{
"role": "assistant",
"content": [{
"type": "tool_use",
"name": "Read",
"input": { "file_path": "/path/CLAUDE.md" }
}]
}Claude Code 앱이 로컬에서 fs.readFile("/path/CLAUDE.md")을 실행하고, 파일 내용을 확보한다.
턴 2: 읽었으니 수정하자
Claude Code가 API에 보내는 것:
┌──────────────────────────────────────────────┐
│ system: (동일한 시스템 프롬프트 + CLAUDE.md) │ ← 또 보냄
│ │
│ messages: [ │
│ { role: "user", content: "수정해줘" }, │
│ { role: "assistant", [Read 도구 호출] }, │
│ { role: "user", [파일 내용 결과] }, │ ← 턴 1 결과
│ ] │
└──────────────────────────────────────────────┘API가 Edit 도구 호출을 응답하면, Claude Code가 로컬에서 파일을 수정한다.
턴 3: 수정 결과 확인 후 종료
Edit 결과가 messages에 추가되고, API가 텍스트만 응답하면(도구 호출 없음) 루프가 끝난다.
핵심은 이거다:
- 시스템 프롬프트(CLAUDE.md 포함)는 매 턴마다 다시 보낸다
- 대화 히스토리는 턴마다 누적된다
- input 토큰은 턴이 쌓일수록 기하급수적으로 증가한다
매 API 호출마다 보내는 것들
Claude Code가 API 한 번 호출할 때 보내는 payload 구조다.
┌─────────────────────────────────────────────┐
│ SYSTEM PROMPT ~5,000 토큰 │ ← Claude Code 내장 지시문
│ ├─ "You are Claude Code..." │
│ ├─ 도구 사용법, 커밋 규칙, PR 규칙 등 │
│ │ │
│ ├─ CLAUDE.md (프로젝트) ~1,800 토큰 │ ← 매번 포함
│ ├─ CLAUDE.md (글로벌) ~900 토큰 │ ← 매번 포함
│ ├─ @참조 파일 (RTK.md 등) ~320 토큰 │ ← 매번 포함
│ └─ MEMORY.md ~400 토큰 │ ← 매번 포함
├─────────────────────────────────────────────┤
│ MESSAGES (대화 히스토리) 누적 증가 │
│ ├─ user: "수정해줘" │
│ ├─ assistant: [Read 호출] │
│ ├─ user: [Read 결과] │
│ ├─ assistant: [Edit 호출] │
│ ├─ user: [Edit 결과] │
│ └─ ... (턴마다 계속 누적) │
├─────────────────────────────────────────────┤
│ TOOLS (도구 스키마) ~3,000 토큰 │ ← 매번 포함
│ ├─ Read, Edit, Write, Bash, Glob, Grep... │
│ └─ MCP 도구들 (Figma, Slack 등) │
└─────────────────────────────────────────────┘CLAUDE.md의 진짜 비용
실측값 기준이다.
프로젝트 CLAUDE.md ~1,800 토큰
글로벌 CLAUDE.md ~900 토큰
RTK.md (@참조) ~320 토큰
MEMORY.md ~400 토큰
─────────────────────────────────
합계 ~3,420 토큰이 ~3,420 토큰이 매 API 호출(매 턴)마다 포함된다.
간단한 파일 수정 (5턴):
3,420 × 5 = ~17,100 토큰 ← CLAUDE.md 반복 비용만복잡한 작업 (30턴):
3,420 × 30 = ~102,600 토큰 ← CLAUDE.md 반복 비용만물론 Anthropic의 prompt caching이 실제 과금을 줄여준다. 한 세션에서 실측한 API 호출 데이터를 보면:
{
"input_tokens": 3,
"cache_creation_input_tokens": 32420,
"cache_read_input_tokens": 10410,
"output_tokens": 733
}시스템 프롬프트 부분은 캐시되어 매번 완전히 재처리되지 않는다. 하지만 context window 한도에는 그대로 잡힌다.
턴별 총 input 토큰 증가 추이:
턴 1: ~10,000 토큰
턴 5: ~25,000 토큰
턴 15: ~60,000 토큰
턴 30: ~120,000 토큰 ← context window 한계 접근Hook이 개입하는 지점
Hook은 Claude Code 앱 안에서, 로컬에서, API 호출 전후로 실행된다. LLM과는 무관한 로컬 코드 실행이다.
전체 생명주기:
사용자 입력: "파일 수정해줘"
│
▼
┌─────────────────────────┐
│ ① UserPromptSubmit │ ← 사용자 입력 직후, API 호출 전
│ (로컬 실행) │
│ │
│ exit 0 + 응답 없음 │ → 그냥 통과 (토큰 0)
│ suppressOutput: true │ → 아무것도 주입 안 함 (토큰 0)
│ additionalContext │ → API input에 텍스트 추가 (토큰 = 텍스트 길이)
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ ② API 호출 조립 │ ← system + messages + tools
│ + Hook이 주입한 텍스트 │ (있다면)
└────────────┬────────────┘
│
▼ HTTPS → Anthropic API (토큰 과금)
│
▼
API 응답: "Edit 도구를 쓰겠다"
│
▼
┌─────────────────────────┐
│ ③ PreToolUse │ ← 도구 실행 직전
│ (로컬 실행) │
│ │
│ exit 0 → 통과 │ 도구 실행 진행
│ exit 2 → 차단 │ 도구 실행 안 함, "차단됨" 전달
│ updatedInput → 수정 │ 수정된 입력으로 도구 실행
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ ④ 도구 실행 (로컬) │ ← 실제 파일 수정
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ ⑤ PostToolUse │ ← 도구 실행 직후
│ (로컬 실행) │
│ │
│ 통과 → 다음 턴 │
│ 차단 → Claude에게 실패 │ 피드백 전달
└────────────┬────────────┘
│
▼
다음 턴 (②로 돌아감)이 외에도 Stop, PreCompact, SubagentStart 등 다양한 lifecycle event가 있다. Hook handler 유형도 4가지:
| 유형 | 용도 |
|---|---|
| command | 셸 스크립트 실행 (가장 강력) |
| http | 원격 서버 검증 (팀/CI 연동) |
| prompt | 다른 LLM에게 판단 위임 |
| agent | 서브에이전트로 복잡한 검증 |
Hook 결과별 토큰 영향
Hook이 언제 토큰을 아끼고, 언제 못 아끼는지.
로컬 (내 맥) │ 원격 (Anthropic API)
토큰 비용: 0 │ 토큰 비용: 발생
│
┌─────────────┐ │
│ Hook 실행 │ ~100ms │
│ (Node.js) │ 토큰: 0 │
└──────┬──────┘ │
│ │
├─ suppressOutput ─────────────────│──→ 토큰 0
│ │
├─ exit 0 (통과, 응답 없음) ────────│──→ 토큰 0
│ │
├─ exit 2 (차단) ──────────────────│──→ ~20 토큰 ("차단됨" 메시지)
│ │
└─ additionalContext ──────────────│──→ 주입한 텍스트 길이만큼정리하면:
| Hook 결과 | Claude가 보는 것 | 토큰 비용 |
|---|---|---|
suppressOutput: true | 아무것도 안 봄 | 0 |
exit 0 (응답 없음) | 아무것도 안 봄 | 0 |
exit 2 (차단) | "Hook이 차단했습니다" 짧은 메시지 | ~20 |
additionalContext | 주입된 전체 텍스트 | 텍스트 길이 |
토큰을 절약하는 건 "아무것도 주입하지 않는" Hook뿐이다. 텍스트를 주입하는 순간, CLAUDE.md에 쓰는 것과 토큰 비용은 동일하다.
CLAUDE.md vs Hook: 언제 뭘 쓸까
둘 다 Claude의 행동을 제어한다. 하지만 메커니즘이 완전히 다르다.
| CLAUDE.md | Hook | |
|---|---|---|
| 강제력 | 권고 (무시 가능) | 물리적 차단 |
| 토큰 비용 | 매 턴 소비 | 0 (차단/통과 시) |
| 외부 연동 | 불가 | HTTP/셸 |
| 입력 수정 | 불가 | updatedInput |
| 자동 승인 | 불가 | PermissionRequest |
판단 기준은 단순하다.
기계적 규칙 → Hook
"rm -rf 차단", "main 브랜치 push 차단", "특정 패턴 자동 승인" 같은 건 Claude가 "이해"할 필요 없다. 조건 매칭 → 차단/통과. Hook이 토큰 0으로 물리적으로 강제한다.
CLAUDE.md 방식:
"rm -rf" 금지 규칙 ~30 토큰 × 30턴 = ~900 토큰
+ Claude가 무시할 수 있음
Hook 방식:
PreToolUse → "rm -rf" 감지 → exit 2
토큰: ~20 (차단 메시지 1회)
+ 물리적으로 차단됨맥락/가이드 → CLAUDE.md
"이 프로젝트는 Next.js 15 + Tailwind 사용", "에러 처리는 이 패턴으로" 같은 건 Claude가 읽고 이해해야 한다. Hook의 additionalContext로 주입해도 토큰 비용은 동일하다.
CLAUDE.md에 "Next.js 15 + Tailwind 사용" 작성
→ 매 턴 input에 포함 → 토큰 소비
Hook additionalContext로 동일 내용 주입
→ 매 턴 input에 포함 → 토큰 소비 (동일)Hook으로 옮긴다고 절약되는 게 아니다. Claude가 텍스트를 읽어야 하는 한, 토큰은 소비된다.
배운 것
-
Claude Code는 "영리한 while 루프"다. API 호출 → 도구 실행 → 결과 전달 → 다시 API 호출. 이 루프가 돌 때마다 전체 대화 히스토리 + 시스템 프롬프트가 재전송된다.
-
CLAUDE.md는 "매 턴 과금되는 README"다. 한 번 쓰면 끝이 아니라, 모든 API 호출의 system prompt에 포함된다. 30턴짜리 작업이면 30번 반복 전송된다. 필요한 내용만 간결하게 쓰는 게 토큰 절약의 기본이다.
-
Hook은 토큰 시스템 바깥에서 작동한다. 로컬 Node.js 프로세스가 도구 호출을 가로채고, 조건에 따라 차단하거나 통과시킨다. LLM을 거치지 않으니 토큰이 0이다. 단, 텍스트를 주입하는 순간 CLAUDE.md와 동일한 비용이 발생한다.
-
prompt caching이 실제 비용을 줄여준다. 시스템 프롬프트가 매번 재전송되지만, Anthropic의 캐싱 덕분에 캐시 히트 시 실제 처리 비용은 낮다. 다만 context window 한도에는 그대로 잡힌다.
-
"왜 Hook으로 안 옮기나요?"에 대한 답. 기계적 규칙(차단/통과)은 Hook이 낫다. 하지만 Claude가 읽고 이해해야 하는 맥락 정보는 어디에 쓰든 토큰 비용이 같다. 만능 해법은 없다.
남은 궁금증
- context window 한계에 도달하면 자동 압축(compaction)이 일어나는데, 정확히 어떤 기준으로 어떤 메시지가 잘리는가?
- MCP 서버 도구가 추가될수록 TOOLS 스키마 토큰이 늘어나는데, 현실적인 상한은?
- prompt caching의 cache hit rate는 실제로 얼마나 되는가? 세션 길이에 따라 어떻게 변하는가?
- Sub-agent(Task 도구)는 별도 context window를 가지는가, 아니면 부모의 것을 공유하는가?