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에 "세션·토큰 사용량 관리" 섹션을 추가했다. 전역 문서라 모든 프로젝트에 자동 적용된다. 내용은 여섯 가지다.

  1. 작업 단위가 끝나면 새 세션을 열거나 /compact로 정리한다. 턴 수가 누적 토큰을 좌우한다.
  2. 자동 루프는 2~4시간마다 세션을 리셋한다. 24시간 넘는 단일 세션은 금지.
  3. 스크린샷은 확인이 끝나면 컨텍스트에 남기지 않는다. 전체 캡처 대신 zoom으로 바뀐 영역만 찍고, 반복적인 UI 확인·클릭 루프는 서브에이전트에 위임한다.
  4. 같은 탐색(ls/grep/find)이 3번 넘게 반복되면 Explore 서브에이전트에 넘긴다. 파일이 수천 개인 디렉토리는 쿼리 범위를 좁힌다.
  5. 끝난 워크트리와 브랜치는 바로 정리한다(prune-branch 스킬). 이날 워크트리 12개와 원격 브랜치 4개를 실제로 치웠다.
  6. 사용량이 평소보다 튀면 ~/.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. 정리: 나라면 이 순서로 한다

  1. 먼저 측정한다. ~/.claude/projects/*.jsonl의 usage를 합산하는 스크립트 하나면 충분하다. 어디가 새는지 모르고 아끼면 엉뚱한 곳만 조인다.
  2. 세션을 끊는다. 가장 큰 레버다. 작업 단위가 끝나면 새 세션, 자동 루프는 몇 시간 단위로 리셋.
  3. 스크린샷은 영역을 좁히고 위임한다. 이미지 한 장이 이후 모든 턴에 따라붙는다.
  4. 읽지 않아도 되는 건 deny로 막는다. node_modules, dist, .git.
  5. 탐색은 도구에 맡긴다. 코드 그래프(graft 같은 것)와 서브에이전트.
  6. 중요한 습관은 훅으로 강제한다. 부탁은 지켜지지 않는다.
  7. 설정은 적용 후 직접 열어서 확인한다. 나처럼 같은 규칙을 세 번 쓰고도 모르고 지나가지 않도록.

토큰 절약은 한 방에 끝나는 일이 아니라, 측정 → 규칙화 → 강제의 반복이었다. 다음 목표는 주간 집계 스크립트를 자동으로 돌려서, 튀는 주가 생기면 바로 알려주게 만드는 것이다.