본문으로 건너뛰기

Claude Code는 앱이다 — Agentic Loop, 토큰 흐름, Hook의 동작 원리

·14 min read

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가 텍스트만 응답하면(도구 호출 없음) 루프가 끝난다.

핵심은 이거다:

  1. 시스템 프롬프트(CLAUDE.md 포함)는 매 턴마다 다시 보낸다
  2. 대화 히스토리는 턴마다 누적된다
  3. 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.mdHook
강제력권고 (무시 가능)물리적 차단
토큰 비용매 턴 소비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가 텍스트를 읽어야 하는 한, 토큰은 소비된다.

배운 것

  1. Claude Code는 "영리한 while 루프"다. API 호출 → 도구 실행 → 결과 전달 → 다시 API 호출. 이 루프가 돌 때마다 전체 대화 히스토리 + 시스템 프롬프트가 재전송된다.

  2. CLAUDE.md는 "매 턴 과금되는 README"다. 한 번 쓰면 끝이 아니라, 모든 API 호출의 system prompt에 포함된다. 30턴짜리 작업이면 30번 반복 전송된다. 필요한 내용만 간결하게 쓰는 게 토큰 절약의 기본이다.

  3. Hook은 토큰 시스템 바깥에서 작동한다. 로컬 Node.js 프로세스가 도구 호출을 가로채고, 조건에 따라 차단하거나 통과시킨다. LLM을 거치지 않으니 토큰이 0이다. 단, 텍스트를 주입하는 순간 CLAUDE.md와 동일한 비용이 발생한다.

  4. prompt caching이 실제 비용을 줄여준다. 시스템 프롬프트가 매번 재전송되지만, Anthropic의 캐싱 덕분에 캐시 히트 시 실제 처리 비용은 낮다. 다만 context window 한도에는 그대로 잡힌다.

  5. "왜 Hook으로 안 옮기나요?"에 대한 답. 기계적 규칙(차단/통과)은 Hook이 낫다. 하지만 Claude가 읽고 이해해야 하는 맥락 정보는 어디에 쓰든 토큰 비용이 같다. 만능 해법은 없다.

남은 궁금증

  • context window 한계에 도달하면 자동 압축(compaction)이 일어나는데, 정확히 어떤 기준으로 어떤 메시지가 잘리는가?
  • MCP 서버 도구가 추가될수록 TOOLS 스키마 토큰이 늘어나는데, 현실적인 상한은?
  • prompt caching의 cache hit rate는 실제로 얼마나 되는가? 세션 길이에 따라 어떻게 변하는가?
  • Sub-agent(Task 도구)는 별도 context window를 가지는가, 아니면 부모의 것을 공유하는가?