7월부터 10월까지 Claude Code로 PHP/CodeIgniter 4 프로젝트 여러 개를 돌리면서 토큰이 눈에 띄게 불어났다. 감으로 아끼려 하지 않고 세션 로그부터 집계해서 원인을 찾았고, 그 뒤로 시도한 것들과 실제로 효과가 있었던 것, 없었던 것을 정리한다.
1. 계기: "토큰이 왜 이렇게 늘었지?"
8월 24일, Claude Code에 이렇게 물었다.
토큰 사용량이 많이 늘었는데 각 디렉토리 로그 확인해서 분석해주고 토큰 사용량 줄일 수 있는 방법 알려줘
Claude Code는 ~/.claude/projects/ 아래에 세션마다 .jsonl 로그를 남긴다. 메시지마다 usage(입력·출력·캐시 읽기 토큰)가 들어 있어서, 이걸 합치면 어느 프로젝트와 어느 세션이 토큰을 먹는지 바로 나온다. 당시 98개 프로젝트 디렉토리를 훑은 결과는 의외였다.
- 누적 토큰의 **약 99%가
cache_read**였다. 프롬프트 캐싱은 멀쩡히 동작하고 있었다. 문제는 캐시가 안 되는 게 아니라, 매 턴 읽어야 하는 컨텍스트 자체가 너무 크고 너무 오래 유지된다는 점이었다. - 사용량은 "평소 작업이 조금씩 늘어서"가 아니라 초장기 세션 몇 개가 만들었다. 한 세션은 약 27시간 동안 2,700여 턴을 이어가며 최대 컨텍스트가 99만 토큰까지 커졌고, 그 프로젝트 전체 사용량의 18%를 혼자 차지했다. 상위 5개 세션이 60% 이상이었다.
- 거대한 tool 결과 118건을 뽑아보니 거의 전부 브라우저 스크린샷(건당 2~3만 토큰)이었다. 한 번 찍은 이미지는 컨텍스트에서 빠지지 않고, 이후 모든 턴마다 다시 전송된다.
- 부수 원인으로
ScheduleWakeup//loop자동 워크플로가 끊기지 않고 수십 시간 돌았고, 다 쓴 워크트리 수십 개가 방치돼 로그와 디스크가 쌓였다.
정리하면 토큰 = 턴 수 × 컨텍스트 크기다. 캐시 덕에 단가는 싸지만 곱셈 자체는 그대로 커진다. 줄이려면 곱하는 두 숫자 중 하나를 건드려야 한다.
2. 1단계: 습관을 문서로 박제 (8/24)
분석 직후 "절감방법을 최상위 클로드문서에 넣어줘"라고 해서 전역 ~/.claude/CLAUDE.md에 "세션·토큰 사용량 관리" 섹션을 추가했다. 전역 문서라 모든 프로젝트에 자동 적용된다. 내용은 여섯 가지다.
- 작업 단위가 끝나면 새 세션을 열거나
/compact로 정리한다. 턴 수가 누적 토큰을 좌우한다. - 자동 루프는 2~4시간마다 세션을 리셋한다. 24시간 넘는 단일 세션은 금지.
- 스크린샷은 확인이 끝나면 컨텍스트에 남기지 않는다. 전체 캡처 대신
zoom으로 바뀐 영역만 찍고, 반복적인 UI 확인·클릭 루프는 서브에이전트에 위임한다. - 같은 탐색(
ls/grep/find)이 3번 넘게 반복되면 Explore 서브에이전트에 넘긴다. 파일이 수천 개인 디렉토리는 쿼리 범위를 좁힌다. - 끝난 워크트리와 브랜치는 바로 정리한다(
prune-branch스킬). 이날 워크트리 12개와 원격 브랜치 4개를 실제로 치웠다. - 사용량이 평소보다 튀면
~/.claude/projects/로그부터 확인한다.
규칙을 문서로 남긴 이유는 단순하다. 나는 어제의 결심을 오늘 잊는다. 세션이 시작될 때 자동으로 로드되는 문서에 적어두면 Claude가 대신 기억해 준다.
3. 2단계: 읽을 필요 없는 걸 못 읽게 막기 (9/9)
두 번째는 컨텍스트에 들어가는 입력을 줄이는 쪽이다. settings.json의 permissions.deny에 node_modules, dist, .git 하위 경로의 Read를 막았다.
"permissions": {
"deny": [
"Read(**/node_modules/**)",
"Read(**/dist/**)",
"Read(**/.git/**)"
]
}
Claude가 빌드 산출물이나 의존성 폴더의 파일을 통째로 읽어 컨텍스트를 낭비하는 일을 원천 차단한다. 한 번 설정하면 신경 쓸 일이 없다는 점이 가장 큰 장점이다.
(덧붙임: 나중에 설정 파일을 다시 열어보니 같은 규칙이 세 번씩 중복 기록돼 있었고, 막은 건 Read뿐이었다. Grep/Glob까지 막았다고 생각했는데 실제로는 아니었다. 설정을 바꾼 뒤에는 결과 파일을 직접 열어 확인하는 편이 안전하다.)
4. 3단계: 코드 탐색을 도구로 대체 — graft (8/25~)
토큰의 상당 부분은 결국 "코드를 찾느라 읽는 파일들"이다. 이슈 하나를 시작할 때마다 grep → cat → sed를 수십 번 반복하면 그 출력이 전부 컨텍스트에 쌓인다.
그래서 graft를 도입했다. 저장소의 모든 심볼과 호출 관계를 미리 그래프로 만들어 두고, "X는 어디 있나", "Y를 누가 부르나", "이 파일의 API가 뭔가"를 한 번의 호출로 답하게 하는 도구다. 처음 켰을 때 저장소 하나는 파일 671개, 심볼 5,308개, 엣지 9,682개로 인덱싱됐고, graft는 응답에 이런 한 줄을 붙였다.
graft saved ~694,538 tokens this turn.
다만 이 숫자는 graft가 스스로 계산한 추정치이고, 내가 독립적으로 검증한 값은 아니다. 그래도 방향은 분명하다. 파일을 줄줄이 읽는 대신 필요한 심볼과 호출 관계만 받아오면 컨텍스트가 훨씬 작아진다. 이후 /dev-issue, /start-issue 스킬이 탐색 단계에서 graft를 먼저 쓰도록 연결했다.
5. 4단계: 말수를 줄이기 — caveman (8/25~)
출력 토큰도 줄이고 싶어서 같은 날 caveman 플러그인을 설치했다. 인사말, 군더더기, 반복 요약을 걷어내고 기술 용어와 코드는 그대로 두는 간결 응답 모드다. 코드 블록, 커밋 메시지, 문서처럼 다른 사람이 읽는 글은 정상 문장으로 쓰도록 예외를 둔다. 긴 세션에서 답변이 점점 장황해지는 현상을 눌러 주는 효과가 있다.
6. 5단계: 약속은 안 지켜진다, 그래서 훅으로 강제 (10월)
여기서부터가 이 글에서 가장 솔직하게 적고 싶은 부분이다.
graft와 caveman을 도입했는데도 Claude가 자꾸 쓰지 않았다. 내 기록에는 이런 메시지가 줄줄이 남아 있다.
- "graft 사용했냐고?"
- "caveman 안써?" → "caveman 진짜 안쓸꺼야?" → "caveman 안쓰냐? 어제부터 몇번째야?"
한 프로젝트의 메모리에는 같은 위반이 세 번 기록됐다. 메모리 파일과 시작 시 안내가 컨텍스트에 분명히 있었는데도, "읽을 파일이 이미 정해져 있다"는 이유로 습관적으로 ls/grep/cat부터 잡았다. 세 번째에는 "다음부터 쓰겠다"고 말로 약속까지 했지만 또 어겼다. 그때 메모리에 적힌 결론은 이렇다.
메모리만으로는 재발이 막히지 않았다는 증거라 훅 같은 기계적 강제가 필요하다.
그래서 PreToolUse(Bash) 훅을 만들었다. 해당 저장소에서 세션 안에 graft 호출 이력이 없는 상태로 grep/cat/sed/ls/find 같은 탐색 명령을 내리면, 훅이 exit 2와 함께 "graft를 먼저 호출하라"는 메시지를 돌려주며 차단한다. 트랜스크립트에서 mcp__graft__ 또는 graft ask|grep|skeleton|callers 호출 이력을 찾는 방식이라, 한 번 호출하고 나면 그 뒤로는 자유롭게 쓸 수 있다.
교훈은 두 가지다.
- "기억해 둬"는 규칙이 아니다. 모델에게 부탁하는 규칙은 확률적이고, 확률은 언젠가 빗나간다. 반드시 지켜져야 하는 절약 습관이라면 훅이나 deny 규칙처럼 도구 수준에서 강제한다.
- 반대로 메모리와 CLAUDE.md는 "왜 그렇게 하는지"를 전달하는 데 좋다. 강제는 훅이, 맥락은 문서가 맡는 식으로 역할을 나눴다.
7. 그 밖에 효과를 본 것들
- 작업 흐름 표준화:
dev-issue스킬에 "구현 전에 탐색부터 위임한다 / 푸시 전에 전체 검증을 통과시킨다"를 못 박았다. 실측해 보니 시간(과 토큰)을 가장 많이 새게 한 건 코딩이 아니라 불확실성(무엇을 어디에 손댈지 모른 채 시작)과 CI 재실패 왕복이었다. 이미 구현된 이슈를 다시 구현하는 낭비만 막아도 30~60분이 절약된다. - CI 왕복 줄이기:
pre-push훅에서composer check를 돌려 실패를 로컬에서 먼저 잡는다.push → CI 실패 → 재커밋한 바퀴마다 컨텍스트에는 로그가 한 덩어리씩 쌓인다. - 서브에이전트 위임: 구현 계획을 태스크 단위로 쪼개서 서브에이전트가 각자 깨끗한 컨텍스트에서 처리하게 했다. 메인 세션에는 결과 요약만 돌아온다.
- 지연 로딩 도구:
ENABLE_TOOL_SEARCH=auto로 두면 수백 개 MCP 도구의 스키마를 처음부터 다 싣지 않고 필요할 때만 불러온다. - 워크트리 즉시 정리: 머지가 끝나면 워크트리와 브랜치를 바로 치운다.
8. 결과: 숫자로 보면
세션 로그를 중복 제거해서 주 단위로 다시 집계했다. (아래 숫자는 같은 요청 ID를 한 번만 센 값이라, 8/24에 처음 집계한 총량과는 다르다. 대신 두 시기를 같은 기준으로 비교할 수 있다.)
| 구간 | 주당 cache_read | 턴당 평균 읽기 | 주당 세션 수 |
|---|---|---|---|
| 7월 말8월 중순 (W30W33) | 1.2~2.5B | 약 220~395k | 13~55 |
| 10월 (W40~W43) | 0.14~0.33B | 약 100~145k | 121~138 |
- 턴당 평균 컨텍스트가 약 30만 → 10만 토큰대로 내려갔다. 곱셈의 한쪽 숫자가 실제로 줄었다는 뜻이다.
- 세션은 훨씬 잘게 쪼개졌다. 주당 세션 수가 13~55개에서 120개 이상으로 늘었고, 길게 이어지던 마라톤 세션이 사라졌다.
- 주당 총 cache_read도 W30~W33 평균 약 1.8B에서 10월 평균 약 0.25B 수준으로 내려왔다.
주의할 점도 있다. 이 비교는 엄밀한 통제 실험이 아니다. 10월은 이슈의 크기, 작업량, 프로젝트 구성이 7~8월과 다르고, W43은 아직 진행 중인 주다. 그래서 총량 감소의 몇 퍼센트가 어떤 대책 덕분인지는 나눠서 말할 수 없다. graft의 절감 토큰도 추정치다. 확실히 말할 수 있는 건 "마라톤 세션이 사라졌고 턴당 컨텍스트가 줄었다"는 정도다.
9월 30일에는 한 프로젝트에서 사용량 한도에 도달해 작업이 끊기기도 했다. 아직 갈 길이 남았다는 신호로 받아들인다.
9. 정리: 나라면 이 순서로 한다
- 먼저 측정한다.
~/.claude/projects/*.jsonl의usage를 합산하는 스크립트 하나면 충분하다. 어디가 새는지 모르고 아끼면 엉뚱한 곳만 조인다. - 세션을 끊는다. 가장 큰 레버다. 작업 단위가 끝나면 새 세션, 자동 루프는 몇 시간 단위로 리셋.
- 스크린샷은 영역을 좁히고 위임한다. 이미지 한 장이 이후 모든 턴에 따라붙는다.
- 읽지 않아도 되는 건 deny로 막는다.
node_modules,dist,.git. - 탐색은 도구에 맡긴다. 코드 그래프(graft 같은 것)와 서브에이전트.
- 중요한 습관은 훅으로 강제한다. 부탁은 지켜지지 않는다.
- 설정은 적용 후 직접 열어서 확인한다. 나처럼 같은 규칙을 세 번 쓰고도 모르고 지나가지 않도록.
토큰 절약은 한 방에 끝나는 일이 아니라, 측정 → 규칙화 → 강제의 반복이었다. 다음 목표는 주간 집계 스크립트를 자동으로 돌려서, 튀는 주가 생기면 바로 알려주게 만드는 것이다.
댓글 0개