한 줄 요약 — 토큰을 많이 먹은 것은 어려운 작업이 아니라 오래 이어진 세션이었습니다. 세션 로그를 직접 집계해 원인을 찾았고, /compact·/clear와 서브에이전트 위임으로 같은 작업량에서 누적 입력을 크게 줄였습니다.

들어가며: 한도가 예상보다 빨리 닳았다

클로드코드로 제품을 여러 개 동시에 굴리다 보니, 어떤 날은 오후도 되기 전에 사용 한도 경고가 떴습니다. 그날 한 일을 돌아보면 특별히 무거운 작업은 없었습니다. 큰 리팩터링도, 대량 코드 생성도 아니었습니다.

공통점은 하나였습니다. 아침에 연 세션 하나를 닫지 않고 하루 종일 이어 썼다는 것입니다. 버그를 고치고, 브라우저로 화면을 확인하고, 다시 고치고, 다른 기능으로 넘어가도 같은 세션이었습니다. "느낌상 그런 것 같다"로 끝내지 않고 로그로 확인해 보기로 했습니다.

구조: 매 턴 대화 전체를 다시 보낸다

모델은 이전 대화를 기억하지 않습니다. 클로드코드는 턴마다 시스템 프롬프트, CLAUDE.md, 지금까지의 대화, 도구 출력 전부를 다시 모델에 보냅니다. 그래서 n번째 턴의 입력은 그때까지 쌓인 컨텍스트 전체입니다.

턴마다 컨텍스트가 일정하게 늘면, 세션 전체의 누적 입력은 턴 수의 제곱에 비례해 커집니다. 같은 500턴이라도 한 세션으로 이어 쓰느냐, 100턴마다 새로 시작하느냐에 따라 총량이 크게 갈립니다.

[embed: node/71f427a5-b343]

프롬프트 캐시 덕분에 반복되는 앞부분은 싸게 처리됩니다. 그래도 매번 읽는 양 자체는 줄지 않습니다. 그리고 컨텍스트 창이 차면 성능과 속도가 함께 떨어집니다.

로그 분석: 세션 파일에서 usage를 뽑는다

클로드코드는 세션마다 대화 기록을 ~/.claude/projects/<프로젝트 경로>/<세션 ID>.jsonl에 한 줄씩 남깁니다. 어시스턴트 응답 줄에는 message.usage가 들어 있습니다. 여기서 세 값을 더하면 그 호출 때 모델이 받은 컨텍스트 크기가 됩니다.

  • input_tokens: 캐시되지 않은 새 입력
  • cache_creation_input_tokens: 이번에 캐시에 새로 쓴 입력
  • cache_read_input_tokens: 캐시에서 다시 읽은 입력

세션별로 이 합을 모두 더하면 "그 세션이 모델에 보낸 입력의 총량"이 나옵니다. 사용자 줄에 들어 있는 image 블록 수도 함께 셌습니다. 스크린샷이 의심스러웠기 때문입니다.

#!/usr/bin/env python3
"""Claude Code 세션 로그(jsonl)에서 세션별 누적 입력과 이미지 수를 뽑는다."""
import json, sys
from pathlib import Path

root = Path(sys.argv[1] if len(sys.argv) > 1 else Path.home() / ".claude/projects")

def count_images(content):
    n = 0
    if isinstance(content, list):
        for b in content:
            if not isinstance(b, dict):
                continue
            if b.get("type") == "image":
                n += 1
            n += count_images(b.get("content"))   # tool_result 안의 스크린샷
    return n

rows = []
for f in root.rglob("*.jsonl"):
    turns = ctx_sum = ctx_max = images = 0
    for line in f.open(encoding="utf-8", errors="ignore"):
        try:
            e = json.loads(line)
        except json.JSONDecodeError:
            continue
        msg = e.get("message") or {}
        if e.get("type") == "user":
            images += count_images(msg.get("content"))
        u = msg.get("usage")
        if e.get("type") == "assistant" and u:
            ctx = (u.get("input_tokens", 0)
                   + u.get("cache_creation_input_tokens", 0)
                   + u.get("cache_read_input_tokens", 0))
            turns += 1
            ctx_sum += ctx
            ctx_max = max(ctx_max, ctx)
    if turns:
        rows.append((ctx_sum, turns, ctx_max, images, f.parent.name, f.stem[:8]))

rows.sort(reverse=True)
total = sum(r[0] for r in rows) or 1
print(f"{'누적입력':>12} {'비중':>6} {'호출':>6} {'최대컨텍스트':>12} {'이미지':>6}  프로젝트/세션")
for s, t, m, img, proj, sid in rows[:15]:
    print(f"{s:>12,} {s/total:>6.1%} {t:>6} {m:>12,} {img:>6}  {proj}/{sid}")

결과는 한눈에 보였습니다. 상위 몇 개 세션이 전체 누적 입력의 대부분을 차지했고, 그 세션들은 하나같이 호출 수가 수백에서 천 단위였습니다. 같은 세션들에 이미지 수도 몰려 있었습니다. 반대로 30~50턴 안에 끝난 세션은 개수는 많아도 합계로는 존재감이 거의 없었습니다.

한 가지 주의할 점이 있습니다. 캐시 읽기는 요금 단가가 낮습니다. 그래서 이 합계는 "청구 금액"이 아니라 "모델이 매번 읽어야 했던 양"으로 보는 편이 맞습니다. 다만 구독 사용 한도와 응답 속도, 그리고 모델이 긴 컨텍스트에서 앞부분 지시를 놓치는 문제는 이 양을 따라 움직였습니다.

범인 1: 닫지 않은 세션

긴 세션이 비싼 이유는 세션 길이 자체보다, 그 안에 쌓이는 것들 때문이었습니다. 로그를 열어 보니 세션 뒷부분의 컨텍스트는 대부분 지금 하는 일과 상관없는 내용이었습니다.

  • 이미 끝난 작업의 흔적: 오전에 고친 버그의 파일 내용, 테스트 출력, 오류 메시지가 오후 작업 내내 함께 실려 갔습니다.
  • 도구 출력: composer test, PHPStan 결과, grep·파일 읽기 결과는 한 번에 수천 토큰씩 들어옵니다. 실패와 재시도를 몇 번 반복하면 같은 출력이 여러 벌 남습니다.
  • 다른 제품의 문맥: 제품을 바꿀 때 세션을 새로 열지 않으면, 한 제품의 규칙과 코드가 다른 제품 작업에 계속 끼어듭니다.

품질 문제도 같이 왔습니다. 세션이 길어지면 CLAUDE.md에 적은 규칙을 앞부분에서는 잘 지키다가 뒤로 갈수록 놓치는 일이 잦았습니다. 비용이 늘어나는 구간과 실수가 늘어나는 구간이 거의 겹쳤습니다.

범인 2: 스크린샷은 지나가지 않고 남는다

화면 작업을 할 때는 Claude in Chrome으로 페이지를 열고 스크린샷을 찍어 확인하게 했습니다. jblove.net 속도 진단처럼 브라우저 안에서 측정까지 돌리는 작업도 있었습니다. 편리했지만, 로그상으로는 이미지가 가장 무거운 짐이었습니다.

이미지는 대략 가로 × 세로 ÷ 750 토큰으로 계산됩니다. 1280×800 화면 한 장이면 약 1,400토큰입니다. 한 장은 별것 아니지만 문제는 두 가지입니다.

  1. 한 번 들어온 이미지는 세션이 끝날 때까지 매 턴 다시 전송됩니다. 텍스트처럼 "읽고 지나가는" 것이 아닙니다. CSS를 10번 고치며 10장을 찍으면, 그 뒤의 모든 턴이 10장을 함께 싣고 갑니다.
  2. 대부분 확인용이었습니다. "버튼 색이 바뀌었나" 정도를 보려고 찍은 화면이 많았습니다. 이런 확인은 DOM 텍스트나 계산된 스타일 값을 읽어도 충분합니다.

그래서 규칙을 바꿨습니다. 값으로 확인할 수 있는 것은 read_page나 JavaScript로 값을 읽게 하고, 스크린샷은 레이아웃처럼 눈으로만 판단할 수 있는 순간에만 찍습니다. 스크린샷을 많이 찍는 작업은 아예 서브에이전트에게 넘겼습니다(아래 처방 2).

처방 1: /clear와 /compact를 쓰는 시점

두 명령은 하는 일이 다릅니다. 이 차이를 알고 나서 언제 무엇을 쓸지가 분명해졌습니다.

명령

하는 일

쓰는 때

/clear

대화를 비우고 새로 시작

작업이 끝났을 때, 제품을 바꿀 때

/compact

지금까지를 요약으로 바꾸고 이어감

같은 작업이 길어지는데 맥락은 이어 가야 할 때

/context

컨텍스트를 무엇이 차지하는지 보여 줌

무거워졌다고 느낄 때 먼저 확인

/clear가 기본값입니다. 하나의 작업이 끝나면 커밋하고 /clear합니다. 다음 작업에 필요한 맥락은 대화가 아니라 저장소에 있습니다. CLAUDE.md, 커밋 로그, 명세 문서가 그 역할을 합니다. 새 세션은 이것들만 읽고도 충분히 이어 갑니다.

/compact에는 지시를 붙입니다. 그냥 실행하면 요약이 무엇을 남길지 모델이 정합니다. 남길 것을 직접 말해 주면 요약 뒤에도 흐름이 덜 끊깁니다.

/compact 결정된 설계, 수정한 파일 목록, 남은 할 일만 남기고 테스트 출력과 스크린샷 설명은 버려

자동 압축을 기다리지 않습니다. 컨텍스트가 거의 찼을 때 자동으로 압축되기도 하지만, 그 시점이면 이미 무거운 턴을 많이 지나온 뒤입니다. 작업 단계가 바뀌는 지점, 예를 들어 원인 분석이 끝나고 수정으로 넘어갈 때 직접 압축하는 편이 결과가 좋았습니다.

긴 작업은 시작할 때부터 끊을 자리를 정해 둡니다. 원인 분석 결과를 docs/ 아래 메모로 남기게 하고 세션을 닫은 뒤, 새 세션에서 그 메모를 읽고 수정만 하게 합니다. 대화 기록 대신 파일 하나가 다리 역할을 합니다.

처방 2: 무거운 일은 서브에이전트에게

서브에이전트는 자기 컨텍스트에서 일하고, 메인 세션에는 결과 요약만 돌려줍니다. 수십 개 파일을 읽든 스크린샷을 스무 장 찍든, 메인 세션에 남는 것은 몇백 토큰짜리 보고서 하나입니다. 일을 줄이는 것이 아니라, 무거운 일의 찌꺼기가 메인 세션에 쌓이지 않게 하는 것입니다.

로그에서 무거웠던 작업을 기준으로 세 가지를 .claude/agents/에 만들어 두었습니다.

서브에이전트

맡기는 일

돌려받는 것

권한

code-auditor

코드베이스 전체 탐색, PHPStan 경고 전수 조사

파일·줄 번호가 붙은 우선순위 목록

읽기 전용

ui-checker

브라우저로 화면 열기, 스크린샷 확인, 콘솔 오류 수집

문제 있는 화면과 원인 몇 줄

브라우저 + 읽기

test-runner

전체 테스트 실행과 실패 원인 분석

실패 테스트 이름과 원인 요약

실행 + 읽기

가장 효과가 컸던 것은 ui-checker입니다. 스크린샷이 메인 세션에 하나도 들어오지 않게 되었기 때문입니다.

---
name: ui-checker
description: 브라우저로 화면을 열어 레이아웃·콘솔 오류를 확인한다. 화면 확인이 필요할 때 사용.
tools: Read, Grep, Glob, mcp__claude-in-chrome__*
---
주어진 URL과 확인 항목을 점검한다.
- 값으로 확인할 수 있는 것은 DOM이나 계산된 스타일로 먼저 확인한다.
- 스크린샷은 레이아웃 판단에만 쓴다.
- 보고는 10줄 이내: 통과/실패, 실패한 항목, 의심되는 파일과 선택자.
- 스크린샷이나 원본 출력은 보고에 넣지 않는다.

마지막 두 줄이 핵심입니다. 보고 형식을 정하지 않으면 서브에이전트가 긴 원본 출력을 그대로 돌려줘서, 메인 세션이 다시 무거워집니다.

반대로 맡기지 않는 일도 정했습니다. 한두 파일만 보면 되는 수정은 메인 세션에서 직접 합니다. 서브에이전트는 매번 맥락 없이 시작하므로, 작은 일을 넘기면 오히려 설명하는 비용이 더 듭니다.

결과와 운영 규칙

바꾼 뒤 같은 스크립트로 다시 집계했습니다. 하루 작업량은 비슷했지만 세션 수는 늘고, 세션 하나의 최대 컨텍스트와 누적 입력은 눈에 띄게 줄었습니다. 메인 세션의 이미지 수는 거의 0이 되었습니다. 한도 경고 없이 하루를 마치는 날이 많아졌고, 세션 뒷부분에서 규칙을 놓치는 일도 줄었습니다.

지금은 아래 규칙을 CLAUDE.md와 습관으로 지키고 있습니다.

  • 작업 하나가 끝나면 커밋하고 /clear
  • 제품을 바꿀 때는 무조건 새 세션
  • 같은 작업이 길어지면 단계가 바뀌는 지점에서 지시를 붙여 /compact
  • 무거워졌다고 느끼면 /context로 무엇이 차지하는지 먼저 확인
  • 값으로 확인할 수 있는 화면 검사는 스크린샷 대신 DOM·스타일 값으로
  • 파일 수십 개 탐색, 스크린샷 다수, 전체 테스트는 서브에이전트에게
  • 서브에이전트 보고는 형식과 길이를 정해 두기
  • 일주일에 한 번 집계 스크립트를 돌려 상위 세션 확인

돌아보며

  1. 비용은 작업이 아니라 구조에서 나온다. 무거운 작업 하나보다 닫지 않은 세션 하나가 훨씬 비쌌습니다. 매 턴 전체를 다시 보내는 구조를 이해하자 원인이 바로 보였습니다.
  2. 느낌 대신 로그를 본다. 스크립트 한 장으로 "어느 세션이 얼마나"가 숫자로 나왔습니다. 짐작으로는 스크린샷이 이 정도로 무겁다는 것을 알 수 없었습니다.
  3. 맥락은 대화가 아니라 저장소에 둔다. /clear를 겁내지 않게 된 것은 CLAUDE.md와 커밋, 메모 파일이 맥락을 들고 있기 때문입니다. 세션을 버려도 잃는 것이 없으면 세션은 짧아집니다.
  4. 위임은 결과의 크기를 정하는 일이다. 서브에이전트의 가치는 일을 대신하는 데보다, 메인 세션으로 돌아오는 양을 작게 만드는 데 있었습니다. 보고 형식을 정하지 않은 위임은 효과가 반쯤 사라졌습니다.
  5. 토큰을 줄이면 품질도 오른다. 짧고 깨끗한 컨텍스트에서 모델은 규칙을 더 잘 지켰습니다. 절약과 품질이 같은 방향이었습니다.

혼자 여러 제품을 운영하는 입장에서 토큰은 시간과 같은 자원입니다. 오늘 쓰는 세션이 몇 턴째인지 한 번 확인해 보세요. 수백 턴을 넘겼다면, 지금이 /clear 할 때일 가능성이 큽니다.