이번 시간에는 JSON 데이터를 TOON이라는 형식으로 바꿔 Claude Code에 넣으면 입력 토큰이 실제로 얼마나 줄어드는지, claude -p(대화 없이 한 번 실행하고 끝나는 모드)로 측정하며 배워가는 시간입니다.
예를 들어서 우리가 주문 목록을 프롬프트에 붙여 넣고 "환불된 주문이 몇 건이야?"라고 묻는다고 해볼게요. 이때 입력 토큰의 대부분은 질문이 아니라 데이터예요. 그리고 JSON은 주문마다 "customer": 같은 키 이름과 따옴표, 괄호를 되풀이해서 씁니다.
TOON은 이 되풀이를 줄이는 형식이에요. 같은 데이터를 들여쓴 JSON, 공백 없는 JSON, TOON 세 가지로 넣고, 같은 질문 네 개를 섞은 순서로 반복해서 보내 볼게요.
claude -p에는 temperature나 seed를 정하는 옵션이 없어서, 다시 돌리면 숫자가 달라질 수 있어요. 이번 측정에서 입력 토큰은 같은 조건의 회차끼리 모두 같았고, 출력 토큰과 비용은 회차마다 달랐습니다.
TOON이 줄이는 것과 공개 수치
TOON은 JSON과 같은 데이터를 표처럼 적는 형식이에요. 키가 같고 값이 단순한 객체가 이어지는 배열이면 키 이름을 머리글에 한 번만 쓰고, 그 아래에 값만 쉼표로 이어 적습니다. 그래서 줄어드는 것은 프롬프트에 넣는 데이터, 즉 입력 쪽뿐이에요.
TOON README는 TOON이 토큰을 42.6% 적게 썼다고 적었어요. 42.6%는 들여쓴 JSON과 비교한 값이고, 토큰은 OpenAI 계열 토크나이저(o200k_base)로 셌어요. 저자는 깊게 중첩되거나 모양이 들쭉날쭉한 데이터는 공백 없는 JSON이 나은 경우가 많다고도 적었습니다.
들여쓴 JSON과 비교하느냐, 공백 없는 JSON과 비교하느냐에 따라 절감 수치가 크게 달라져요.
실습 준비
이 글의 명령은 모두 실습 코드의 toon-format-claude-tokens/ 디렉터리에서 실행합니다.
gen_data.mjs: 고정 시드로 주문 데이터 두 벌과 정답을 만드는 스크립트questions-A.txt: 데이터 A에 보내는 질문 네 개questions-B.txt: 데이터 B에 보내는 질문 네 개run_measure.sh: 조건을 섞은 순서로claude -p를 17번 부르는 측정 스크립트summarize.mjs: 결과 JSON을 모아 조건별로 비교하는 집계 스크립트data/: 만든 데이터와 TOON 변환 결과README.md: 파일 목록
따라 하시려면 Claude Code 2.1.309(claude --version)와 Claude 구독 로그인, Docker가 필요해요. Node와 TOON CLI는 Mac에 설치하지 않고 node:24-alpine 컨테이너 안에서만 써요.
먼저 셸 함수 두 개를 만들게요.
node24: 현재 디렉터리를 컨테이너의/w에 연결하고node:24-alpine에서 명령을 실행해요.--rm을 붙였으니 끝나면 컨테이너는 지워집니다.toon: TOON CLI 4.4.0을npx로 받아 실행해요.npx가 찍는npm notice줄은 걸러 냅니다.
아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.
node24() { docker run --rm -v "$PWD":/w -w /w node:24-alpine "$@"; }
toon() { node24 npx -y @toon-format/cli@4.4.0 "$@" 2>&1 | grep -v "npm notice"; }
gen_data.mjs는 데이터 두 벌을 만들어요.
- 데이터 A: 주문 150건이고, 모든 주문이 같은 키 8개(
id,customer,region,sku,qty,price,status,date)를 가져요. TOON이 가장 유리한 균일한 표 모양입니다. - 데이터 B: 주문 40건이고,
customer가 객체이고items배열의 길이가 1~4로 제각각이에요.coupon과note는 일부 주문에만 있습니다. 대조용 중첩·불균일 데이터예요.
각 데이터는 들여쓴 JSON(data/A.json)과 공백 없는 JSON(data/A.min.json)으로 저장되고, 질문 네 개의 정답은 같은 스크립트가 계산해서 data/truth.json에 남겨요.
node24 node gen_data.mjs
{"A":{"q1":37,"q2":5670.5,"q3":"Chen","q4":44},"B":{"q1":92,"q2":10,"q3":"Bob","q4":44}}
q1~q4는 데이터마다 보낼 질문 네 개의 정답이에요(개수, 합계, 특정 주문의 고객, 가장 큰 주문).
실습 1: TOON으로 바꾸고 도구의 추정치 보기
이제 두 데이터를 TOON으로 바꿔 볼게요. --stats를 붙이면 TOON CLI가 바꾸기 전과 후의 토큰 추정치를 함께 보여 줘요. 여기서는 공백 없는 JSON을 입력으로 줘서 비교 기준도 공백 없는 JSON이 됩니다.
toon data/A.min.json -o data/A.toon --stats
toon data/B.min.json -o data/B.toon --stats
✔ Encoded `data/A.min.json` → `data/A.toon`
● Token estimates: ~6151 (JSON) → ~3768 (TOON)
✔ Saved ~2383 tokens (-38.7%)
✔ Encoded `data/B.min.json` → `data/B.toon`
● Token estimates: ~1712 (JSON) → ~1705 (TOON)
✔ Saved ~7 tokens (-0.4%)
데이터 A의 TOON은 이렇게 생겼어요.
sed -n '1,4p' data/A.toon
orders[150]{id,customer,region,sku,qty,price,status,date}:
1,Eli,busan,MN-505,3,75.36,refunded,2026-09-01
2,Bob,incheon,MN-505,3,26.52,paid,2026-09-18
3,Bob,incheon,CB-404,5,66.3,paid,2026-09-28
첫 줄이 머리글이에요. orders[150]은 주문이 150건이라는 뜻이고, 중괄호 안이 키 이름입니다. 그 아래로 주문 한 건이 한 줄씩 값만 들어가요. 데이터 B는 주문마다 customer 객체와 items 배열이 들어 있고 키도 달라서, 키가 같은 items 안쪽만 표 모양으로 바뀌어요.
도구의 추정치로는 데이터 B에서 TOON과 공백 없는 JSON이 거의 같아요(-0.4%). 다만 이 숫자는 토크나이저를 돌린 값이 아니라, OpenAI 토크나이저(o200k_base)에 맞춰 글자 수로 어림한 추정치예요. Claude가 실제로 세는 토큰은 다를 수 있으니, 이제 Claude에 직접 넣어 볼게요.
실습 2: 세 형식을 같은 질문으로 섞어 보내기
데이터 A에 보낼 질문 파일 questions-A.txt예요.
아래 주문 데이터만 보고 네 질문에 답해 줘. 설명 없이 JSON 한 줄로만 답해: {"q1": 정수, "q2": 숫자, "q3": "문자열", "q4": 정수}
1. status가 refunded인 주문은 몇 건인가?
2. region이 busan인 주문의 qty × price 합계는? 소수 둘째 자리까지.
3. id가 137인 주문의 customer는?
4. qty × price가 가장 큰 주문의 id는?
questions-B.txt도 같은 형식으로 items 항목 수 합계, coupon이 있는 주문 수, id 1027 주문의 customer 이름, HD-303 상품의 qty 합계를 물어요.
측정 스크립트 run_measure.sh는 조건 이름이 적힌 schedule 순서대로 claude -p를 17번 부르고, 회차마다 보낸 프롬프트(prompt.txt)와 결과 JSON(claude.json)을 out/<조건>/run-001/ 같은 디렉터리에 남겨요. 프롬프트를 만들고 claude -p를 부르는 부분만 볼게요(발췌).
case "$format" in
pretty) data="data/$dataset.json"; fence=json ;;
compact) data="data/$dataset.min.json"; fence=json ;;
toon) data="data/$dataset.toon"; fence=toon ;;
*) data="" ;;
esac
if [ -z "$data" ]; then
cat questions-A.txt > "$run_dir/prompt.txt"
else
{ cat "questions-$dataset.txt"; echo; echo '```'"$fence"; cat "$data"; echo '```'; } > "$run_dir/prompt.txt"
fi
(cd work && claude -p --model opus --effort low \
--setting-sources project --strict-mcp-config \
--tools "" --no-session-persistence --output-format json \
< "../$run_dir/prompt.txt" > "../$run_dir/claude.json" 2> "../$run_dir/stderr.log")
조건 이름은 <데이터>-<형식>이에요.
pretty: 들여쓴 JSON(data/A.json)compact: 공백 없는 JSON(data/A.min.json)toon: TOON(data/A.toon)empty: 데이터 없이 질문만 보내요. Claude Code가 매번 넣는 고정 부분과 질문만의 크기를 재서, 다른 조건에서 빼면 데이터(감싼 코드 블록 표시 포함)가 차지한 토큰이 나옵니다.
데이터 A는 형식마다 3번, 데이터 B는 2번, empty는 처음과 끝에 한 번씩이에요. 데이터는 질문 아래에 코드 블록으로 넣고, 블록 언어를 json 또는 toon으로 적었어요. README가 TOON을 넣을 때 코드 블록으로 감싸라고 권하거든요.
claude -p에 준 옵션 중 이 둘만 짚을게요.
--setting-sources project --strict-mcp-config: 사용자 설정, 플러그인, 계정에 연결한 claude.ai 커넥터가 섞이지 않게 해요. 이게 없으면 사람마다 고정 부분의 크기가 달라져요.--tools "": 내장 도구를 모두 빼요. 데이터가 프롬프트 안에 다 있으니 파일을 읽을 일이 없고, 도구 정의도 입력에 들어가지 않습니다.
실행해 볼게요. 맥이 잠들면 호출이 끊기니 caffeinate로 감싸서 돌립니다.
chmod +x run_measure.sh
caffeinate -dims ./run_measure.sh
empty out/empty/run-001
A-toon out/A-toon/run-001
A-pretty out/A-pretty/run-001
A-compact out/A-compact/run-001
B-pretty out/B-pretty/run-001
B-toon out/B-toon/run-001
B-compact out/B-compact/run-001
A-compact out/A-compact/run-002
A-toon out/A-toon/run-002
A-pretty out/A-pretty/run-002
B-toon out/B-toon/run-002
B-compact out/B-compact/run-002
B-pretty out/B-pretty/run-002
A-pretty out/A-pretty/run-003
A-compact out/A-compact/run-003
A-toon out/A-toon/run-003
empty out/empty/run-002
실습 3: 집계하고 비교하기
summarize.mjs는 조건마다 결과 JSON을 읽어서 입력 토큰과 출력 토큰의 중앙값을 내고, 답을 truth.json과 비교해 맞힌 수를 세요. 입력 토큰은 input_tokens, cache_creation_input_tokens, cache_read_input_tokens 세 필드에 나뉘어 있는데, 캐시에서 읽었든 새로 썼든 모델이 읽은 양은 같으니 셋을 더해서 봅니다.
비용은 회차를 두 묶음으로 나눠서 보여 줘요. cost_write는 데이터를 처음 보내 캐시에 쓴 회차, cost_read는 같은 프롬프트를 다시 보내 캐시에서 읽은 회차예요.
마지막 두 줄은 데이터가 차지한 토큰(입력 토큰에서 empty를 뺀 값)을 들여쓴 JSON과 비교한 비율, 전체 입력이 줄어든 비율, TOON 조건의 입력에서 고정 부분이 차지하는 비율이에요.
node24 node summarize.mjs
empty runs=2 input=3066 output=87 cost_write=0.0085 cost_read=0.0020 correct=- model=claude-opus-5-5
A-pretty runs=3 input=16325 output=1031 cost_write=0.1300 cost_read=0.0293,0.0239 correct=12/12 model=claude-opus-5-5
A-compact runs=3 input=12269 output=1046 cost_write=0.1093 cost_read=0.0234,0.0224 correct=12/12 model=claude-opus-5-5
A-toon runs=3 input=7938 output=1024 cost_write=- cost_read=0.0220,0.0221,0.0255 correct=12/12 model=claude-opus-5-5
B-pretty runs=2 input=7957 output=466 cost_write=0.0547 cost_read=0.0111 correct=8/8 model=claude-opus-5-5
B-compact runs=2 input=6146 output=446 cost_write=0.0399 cost_read=0.0102 correct=8/8 model=claude-opus-5-5
B-toon runs=2 input=5668 output=482.5 cost_write=0.0369 cost_read=0.0108 correct=8/8 model=claude-opus-5-5
data tokens = input - empty
A: pretty=13259 compact=9203(-30.6%) toon=4872(-63.3%) toon_vs_compact=-47.1% total_toon_vs_pretty=-51.4% fixed_share_in_toon=38.6%
B: pretty=4891 compact=3080(-37.0%) toon=2602(-46.8%) toon_vs_compact=-15.5% total_toon_vs_pretty=-28.8% fixed_share_in_toon=54.1%
모든 실행의 모델이 claude-opus-5-5였고, 입력 토큰은 같은 조건의 회차끼리 모두 같았어요. 그래서 입력 토큰 비교는 회차 수가 적어도 흔들리지 않습니다.
데이터가 차지한 토큰
데이터 A에서는 들여쓴 JSON 13,259, 공백 없는 JSON 9,203, TOON 4,872였어요. TOON은 들여쓴 JSON보다 63.3%, 공백 없는 JSON보다 47.1% 적었습니다.
데이터 B에서는 4,891, 3,080, 2,602였어요. TOON이 들여쓴 JSON보다 46.8%, 공백 없는 JSON보다 15.5% 적었습니다. 다만 B는 한 단계만 중첩됐고 items 안쪽은 표로 바뀌는 모양이라, README가 공백 없는 JSON이 낫다고 한 깊게 중첩된 데이터는 이번에 재지 않았어요.
데이터 없는 호출은 questions-A.txt로 쟀으니, B의 값에는 두 질문 파일의 길이 차이가 들어 있습니다. B의 세 형식은 같은 질문을 썼으니 형식 사이의 토큰 차이에는 영향이 없어요.
실습 1에서 TOON CLI의 추정치는 B에서 공백 없는 JSON 대비 -0.4%로 거의 같았는데, Claude가 실제로 센 토큰으로는 15.5% 적었어요. 도구 추정치는 어림 계산이고 Claude는 토크나이저도 달라서, 형식 사이의 차이도 달라질 수 있습니다.
전체 입력에서는 고정 부분이 남아요
데이터만 보면 63.3%가 줄었지만, Claude Code가 매번 넣는 고정 부분 3,066토큰은 그대로예요. 그래서 전체 입력은 A에서 51.4%, B에서 28.8% 줄었습니다.
TOON으로 바꾼 A에서는 입력의 38.6%가, B에서는 54.1%가 이 고정 부분이에요. 이번 측정은 도구를 모두 뺀 상태라 고정 부분이 작은 편입니다. 도구를 켠 평소 세션이라면 고정 부분이 더 커서, 같은 데이터를 바꿔도 전체에서 줄어드는 비율은 더 작아져요.
정답과 출력은 그대로였어요
정답은 세 형식 모두 A 12개 중 12개, B 8개 중 8개였어요. 이 질문들로는 형식 사이의 정확도 차이를 가를 수 없습니다.
출력 토큰 중앙값은 A에서 1,031, 1,046, 1,024로 거의 같았어요. TOON은 입력만 줄이고, 모델이 생각하고 답하는 양은 바꾸지 않았어요.
비용은 캐시가 먼저 줄여요
데이터를 처음 보낸 회차(cost_write)는 A에서 들여쓴 JSON 0.1300달러, 공백 없는 JSON 0.1093달러였어요. 같은 프롬프트를 다시 보내 입력을 캐시에서 읽은 들여쓴 JSON 회차는 0.0293달러로 크게 낮아졌습니다.
캐시에서 읽은 회차끼리 비교하면 A의 비용은 들여쓴 JSON 0.0239~0.0293, 공백 없는 JSON 0.0224~0.0234, TOON 0.0220~0.0255달러로 범위가 서로 겹쳐요. 입력이 싸진 뒤에는 회차마다 달라지는 출력이 비용을 주로 정하기 때문이에요.
A-toon에는 캐시에 쓴 회차가 없어요(cost_write=-). 이 측정 직전에 같은 프롬프트로 확인 호출을 한 번 보냈는데, 그때 캐시에 쓴 것을 첫 회차부터 읽었거든요. 여러분이 처음 돌리면 A-toon도 첫 회차는 캐시에 쓴 회차로 나와요. 프롬프트 캐시가 언제 깨지는지는 프롬프트 캐시를 다룬 글을 참고하세요.
정리
범위: Claude Code 2.1.309의 claude -p, claude-opus-5-5, effort low, 도구 없음, 고정 시드로 만든 주문 데이터 두 벌(균일 150건, 중첩·불균일 40건), 질문 네 개, 17번 호출.
- 균일한 주문 150건이 차지한 입력 토큰은 들여쓴 JSON 13,259, 공백 없는 JSON 9,203, TOON 4,872였어요. 비교 기준을 공백 없는 JSON으로 잡아도 47.1%가 줄었으니, 표 모양 데이터를 넣는다면 TOON을 써 볼 만합니다.
- 한 단계 중첩된 주문 40건에서도 TOON이 공백 없는 JSON보다 15.5% 적었지만, TOON CLI의
--stats추정치는 -0.4%였어요. 도구 추정치는 OpenAI 토크나이저에 맞춘 어림값이고 README 수치도 OpenAI 토크나이저 기준이니, Claude에서의 효과는 결과 JSON의usage로 직접 재세요. - 도구를 모두 뺀 상태에서도 Claude Code의 고정 부분과 질문이 3,066토큰이라, 전체 입력은 A에서 51.4%, B에서 28.8%만 줄었어요. 도구를 켠 세션에서는 전체 대비 절감 비율이 더 작아진다고 보셔야 합니다.
- 정답은 세 형식 모두 20개 중 20개(A 12개, B 8개)였고, 출력 토큰 중앙값도 거의 같았어요. 이 질문으로는 정확도 차이를 가를 수 없으니 "정확도가 오른다"고 기대하지는 마세요.
- 같은 프롬프트를 다시 보내면 입력을 캐시에서 읽어, 들여쓴 JSON의 비용이 0.1300달러에서 0.0293달러로 내려갔어요. 같은 데이터를 반복해서 묻는 흐름이라면 캐시가 먼저 비용을 줄여요.