한글 토큰 계산, 글자수로 어림하면 안 되는 이유

3줄 요약
- 영어 기준 어림셈인 "1토큰 = 4자"를 한글 원고에 쓰면 토큰을 절반 이하로 잡게 됩니다.
- 원고 55편 624,261자 실측 결과 o200k_base 329,961토큰(토큰당 1.892자), cl100k_base 461,817토큰(1.352자)으로 인코딩만 바꿔도 1.4배가 갈립니다. 글자수÷4 추정치는 실측 대비 52.7% 모자랐습니다.
- 자기 원고 폴더에
gpt-tokenizer를 붙여 한 번 세어 보고, 글자수가 아니라 바이트 수 기준 계수로 예산표를 다시 만드시면 됩니다.
원고 한 편이 평균 11,350자. 컨텍스트 창 200,000. 4로 나누면 토큰 2,838. 그러면 70편은 들어가겠구나. 지난달에 아카이브 전체를 한 번에 모델에 물려 볼 생각을 하면서 제가 머릿속으로 한 계산입니다.
실제로 세어 보니 33편이었습니다. 절반이 안 됐습니다. 어림셈이 틀린 게 아니라, 애초에 한국어에 쓸 수 있는 어림셈이 아니었습니다.
1. 같은 원고를 인코딩 세 개로 세면 2.8배가 갈립니다
측정 대상은 2026-08-25부터 09-11까지 쓴 블로그 원고 55편입니다. YAML front matter를 걷어낸 본문만 썼고, 공백을 포함해 624,261자, UTF-8로 1,169,977바이트입니다.
토크나이저는 npm의 gpt-tokenizer 4.0.0을 썼습니다. 파이썬 tiktoken을 먼저 시도했는데 BPE 사전을 실행 시점에 내려받는 구조라 격리된 환경에서는 동작하지 않았습니다. gpt-tokenizer는 사전을 패키지 안에 담고 있어 네트워크 없이 돕니다.
# 사전이 패키지에 동봉돼 있어 설치만 하면 오프라인에서 바로 돕니다
npm install gpt-tokenizer
node -e "import('gpt-tokenizer/encoding/o200k_base').then(m=>console.log(m.encode('한글 토큰 세기').length))"
| 인코딩 | 대표 모델군 | 토큰 수 | 문자당 토큰 | 토큰당 문자 |
|---|---|---|---|---|
p50k_base |
GPT-3 계열 | 915,488 | 1.467 | 0.682자 |
cl100k_base |
GPT-4 · 임베딩 3 계열 | 461,817 | 0.740 | 1.352자 |
o200k_base |
GPT-4o 계열 | 329,961 | 0.529 | 1.892자 |
같은 글자입니다. 인코딩만 바꿨는데 p50k_base와 o200k_base 사이가 2.775배, cl100k_base와 o200k_base 사이가 1.400배 벌어집니다.
p50k_base는 지금 새로 쓸 일이 거의 없지만 표에 남겨 두었습니다. 오래된 예제 코드나 블로그 글을 그대로 가져다 쓰면 이 사전이 딸려 오는 경우가 있고, 그때 한국어 문서를 재면 세 배 가까이 부풀어 보입니다. 예산이 과다 추정되는 방향이라 사고로 이어지지는 않지만, "한글은 토큰을 세 배 먹는다"는 잘못된 상식이 여기서 나옵니다.
편차는 원고마다도 생깁니다
55편을 한 편씩 따로 재면 문자당 토큰이 cl100k_base에서 0.572~0.862(중앙값 0.760), o200k_base에서 0.436~0.610(중앙값 0.528)으로 흩어집니다. 최소와 최대가 1.4~1.5배 차이입니다.
가장 무거운 원고는 15,706자에 7,903토큰, 가장 가벼운 원고는 7,694자에 4,016토큰이었습니다. 같은 사람이 같은 형식으로 쓴 글인데도 이만큼 벌어집니다. 그래서 평균 계수 하나로 전체를 추정하면 개별 요청에서는 계속 빗나갑니다.
어림셈을 실측에 대 봤습니다
| 추정 방식 | 추정 토큰 | o200k 실측 대비 |
|---|---|---|
| 글자수 ÷ 4 (영어 통설) | 156,065 | −52.7% |
| 글자수 ÷ 2 | 312,131 | −5.4% |
| 글자수 × 1.0 | 624,261 | +89.2% |
"1토큰은 영어 기준 약 4자 또는 0.75단어"라는 문장은 Anthropic 공식 가격 문서의 FAQ에 그대로 있습니다. 같은 문장이 "정확한 수는 언어와 콘텐츠 유형에 따라 달라진다"고 덧붙이고 있는데, 저를 포함해 대부분은 앞 절만 외우고 뒷 절을 흘려듣습니다.
2. 한글이 무거운 이유는 글자가 아니라 바이트에 있습니다
왜 이렇게 되는지 어절 단위로 내려가 보면 사정이 분명해집니다.
| 문자열 | 글자 수 | cl100k_base |
o200k_base |
|---|---|---|---|
| 품질 | 2 | 5 | 2 |
| 회귀 | 2 | 3 | 2 |
| 테스트 | 3 | 3 | 2 |
| 파이프라인 | 5 | 7 | 4 |
| 합니다 | 3 | 1 | 1 |
| 습니다 | 3 | 1 | 1 |
품질은 두 글자인데 cl100k_base에서 5토큰입니다. 글자보다 토큰이 많습니다. 사전에 없는 음절은 UTF-8 바이트로 쪼개져 들어가는데, 한글 음절 하나가 3바이트이기 때문입니다.
반대로 합니다, 습니다, 입니다는 양쪽 모두 1토큰입니다. 경어체 종결어미가 통째로 사전에 올라 있습니다. 경어체로 쓰는 한국어 문서는 이 점에서 뜻밖의 이득을 봅니다.
여기서 규칙성을 찾으려 해도 잘 안 잡힙니다. 테스트와 회귀는 둘 다 cl100k_base에서 3토큰인데 글자 수는 다르고, 품질은 두 글자인데 5토큰입니다. 사전에 어떤 조각이 올라 있는지는 학습 데이터에 그 조각이 얼마나 자주 나왔는지에 달려 있어서, 사람이 보기에 흔한 단어인지와는 별개입니다. 그래서 단어 목록을 보고 미리 무게를 가늠하려는 시도는 접는 편이 낫습니다. 실제로 인코딩해 보는 것 말고는 방법이 없습니다.
문장 단위로 보면 영어와의 격차가 드러납니다
// 같은 뜻의 두 문장을 인코딩별로 세어 비교합니다
const ko = '테스트 자동화 파이프라인을 설계했습니다'; // 21자
const en = 'I designed a test automation pipeline'; // 37자
// p50k / cl100k / o200k
// ko → 50 / 19 / 12
// en → 6 / 6 / 6
영어 문장이 글자로는 1.8배 긴데 토큰은 절반입니다. p50k_base에서는 8.3배까지 벌어집니다.
그런데 바이트로 재면 격차가 확 줄어듭니다
같은 내용을 10kB씩 채워서 넣어 봤습니다.
| 입력 | 바이트 | 글자 수 | cl100k_base |
o200k_base |
|---|---|---|---|---|
| 한국어 10kB | 10,282 | 4,094 | 3,458 | 2,003 |
| 영어 10kB | 10,259 | 10,259 | 1,710 | 1,710 |
글자 기준으로는 2.5배 차이였는데, 바이트 기준으로는 o200k_base에서 1.17배로 좁혀집니다. 한글이 특별히 홀대받는 게 아니라, 한 글자가 담는 정보량이 커서 바이트를 많이 쓰고 그만큼 토큰도 쓰는 것입니다.
이게 실무에서 쓸모가 있습니다. 제 아카이브 전체는 o200k_base 기준 3.55바이트당 1토큰이었습니다. 웹 페이지를 가져와 컨텍스트에 넣는 작업에서 상한을 걸 때, 글자 수로 거는 것보다 바이트 수로 거는 편이 언어가 섞여도 덜 흔들립니다. 공식 문서가 웹 페이지 10kB를 약 2,500토큰으로 예시하는 것도 바이트 기준이라 언어에 덜 민감합니다.
3. 같은 문서 안에서도 유형마다 계수가 다릅니다
원고를 코드블록, 표, 산문 세 덩어리로 갈라서 따로 쟀습니다.
// 펜스 코드블록 안팎을 상태 변수로 가르고, 파이프로 시작하는 줄을 표로 봅니다
let inCode = false;
for (const ln of text.split('\n')) {
if (/^\s*```/.test(ln)) { inCode = !inCode; code.push(ln); continue; }
if (inCode) { code.push(ln); continue; }
if (/^\s*\|/.test(ln)) { table.push(ln); continue; }
prose.push(ln);
}
| 유형 | 문자 수 | o200k_base 토큰 |
문자당 토큰 | 토큰당 문자 |
|---|---|---|---|---|
| 코드블록 | 122,502 | 54,290 | 0.443 | 2.256자 |
| 표 | 90,551 | 50,198 | 0.554 | 1.804자 |
| 산문 | 411,260 | 225,424 | 0.548 | 1.824자 |
코드블록이 가장 가볍습니다. 문자당 0.443으로 산문보다 19% 효율적입니다. 코드는 대부분 ASCII이기 때문입니다. 다만 제 코드블록에도 한글 음절이 23,996자(19.6%) 섞여 있습니다. 주석을 한국어로 쓰기 때문인데, 그래서 순수 영문 코드만큼 가볍지는 않습니다.
표가 산문과 비슷한 것은 조금 뜻밖이었습니다. 파이프와 하이픈이 많아 가벼울 줄 알았는데, 셀 안이 대부분 한글이라 상쇄됩니다. 표를 늘려도 토큰 부담은 산문을 늘리는 것과 거의 같다는 뜻이라, 읽기 좋게 표로 정리하는 선택에 비용상의 불이익은 없었습니다.
유형별로 나눠 재는 것이 왜 필요한지는 문서군을 바꿔 보면 드러납니다. 코드가 8할인 저장소 문서와 산문이 9할인 회의록은 문자당 계수가 1.2배 넘게 차이 납니다. 두 종류를 한 파이프라인에 태우면서 평균 계수 하나로 예산을 잡으면, 한쪽은 늘 남고 다른 쪽은 늘 모자랍니다.
컨텍스트 창에 몇 편이 들어가는가
원고 한 편 평균이 11,350자, o200k_base 5,999토큰, cl100k_base 8,397토큰입니다.
| 컨텍스트 | o200k_base |
cl100k_base |
"글자수÷4" 착각 |
|---|---|---|---|
| 8,000 | 1.3편 | 1.0편 | 2.8편 |
| 32,000 | 5.3편 | 3.8편 | 11.3편 |
| 128,000 | 21.3편 | 15.2편 | 45.1편 |
| 200,000 | 33.3편 | 23.8편 | 70.5편 |
도입부에서 제가 70편이라고 계산했던 자리에 실제로는 33편이 들어갑니다. RAG 파이프라인에서 청크 크기를 정할 때도 같은 오차가 그대로 옮겨붙습니다. 마크다운 문서를 청킹해 본 기록에서 청크 경계가 코드블록을 자르는 문제를 다뤘는데, 거기서 쓴 chunk_size 값도 문자 기준인지 토큰 기준인지에 따라 실제 크기가 두 배 달라집니다.
4. 이 숫자를 Claude 예산으로 그대로 옮기면 안 됩니다
여기까지가 실측이고, 여기서부터는 조심해야 하는 구간입니다.
지금까지 잰 것은 전부 OpenAI 계열 토크나이저입니다. Anthropic은 공식 문서에서 tiktoken을 쓰지 말라고 명시하고 있습니다. Claude 토큰을 일반 텍스트에서 15~20% 과소 추정하며, 코드나 비영어 입력에서는 그보다 훨씬 크게 벗어난다는 것입니다. 정확한 값이 필요하면 토큰 카운팅 엔드포인트를 쓰라고 안내합니다.
여기에 변수가 하나 더 있습니다. Claude 4.7 이후 모델은 새 토크나이저를 쓰며 같은 텍스트에서 약 30% 더 많은 토큰이 나옵니다. Sonnet 4.6 이하는 이전 토크나이저입니다. 같은 원고, 같은 회사 모델인데도 어느 모델을 부르느냐에 따라 청구되는 토큰이 달라집니다.
제가 확인하지 못한 것
제 원고에서 Claude가 실제로 몇 토큰을 세는지는 확인된 근거가 없습니다. 측정 환경에 API 키가 없어 count_tokens를 호출하지 못했습니다. 위 표의 o200k_base 값은 Claude 기준으로는 하한으로만 봐야 합니다.
아래 비용표도 그런 뜻에서 읽어 주십시오. 요금은 2026-09-11 기준 공식 가격 페이지 값입니다.
| 모델 | 입력 요금 | o200k 기준(하한) |
공식 "+15~20%" 반영 |
|---|---|---|---|
| Haiku 4.5 | $1 / MTok | $0.3300 | $0.3795~$0.3960 |
| Sonnet 5 | $2 / MTok | $0.6599 | $0.7589~$0.7919 |
| Opus 5 | $5 / MTok | $1.6498 | $1.8973~$1.9798 |
원고 55편 전체를 한 번 입력으로 넣는 경우입니다. 절대액은 크지 않지만, 같은 파이프라인이 하루 세 번 도는 예약 작업이라면 배수가 붙습니다. 제 발행 파이프라인이 하루 세 슬롯으로 돌고 있고 슬롯마다 아카이브를 훑기 때문에, 여기서 계수가 두 배 틀리면 월 단위로는 눈에 띄는 차이가 됩니다.
15~20%와 30%를 곱해서 쓰고 싶은 유혹이 있는데, 두 숫자의 기준 모델이 문서에 명시돼 있지 않아 곱하면 근거 없는 값이 됩니다. 저는 곱하지 않고 하한과 공식 보정치까지만 표에 남겼습니다.
토큰 카운팅 엔드포인트를 쓸 수 있다면
count_tokens는 무료이고 분당 요청 수 제한만 있습니다. 사용 등급별로 Start 5,000 RPM, Build 10,000 RPM, Scale 20,000 RPM입니다. 메시지 생성과 별도 한도라 서로 잡아먹지 않습니다. 정확한 값이 필요하면 추정하지 말고 부르는 편이 낫습니다.
다만 이 엔드포인트가 돌려주는 값도 문서상 추정치이며, 실제 생성 시점의 입력 토큰과 소폭 다를 수 있다고 명시돼 있습니다. 시스템이 자동으로 붙이는 토큰이 포함될 수 있고 그 몫은 과금되지 않습니다. 그러니 이 값을 회계 장부처럼 다루기보다, 한도에 걸리는지와 창에 들어가는지를 판단하는 용도로 쓰는 편이 맞습니다.
5. 도입 난이도로 따져 보면
| 방법 | 도입 난이도 | 유지보수 비용 | 팀 학습곡선 | 정확도 |
|---|---|---|---|---|
| 바이트÷3.6 어림셈 | 매우 낮음 (계산기면 됨) | 없음 | 없음 | 자기 문서군에 한해 ±10%대 |
gpt-tokenizer 로컬 측정 |
낮음 (npm 설치 한 줄) | 낮음 (사전 동봉, 버전만 고정) | 낮음 | OpenAI 계열 정확, Claude는 하한 |
count_tokens API |
중간 (키·네트워크 필요) | 중간 (모델 ID 바뀌면 재측정) | 낮음 | Claude 기준 정확 |
QA 4~6명 규모에서 AI 도구를 붙이는 초기 단계라면 두 번째 줄부터 시작하시길 권합니다. 설치 한 줄이고 오프라인에서 돌기 때문에 CI에 넣어도 외부 의존이 생기지 않습니다. 정확한 청구액이 걸린 판단에 이르렀을 때 세 번째 줄로 올라가면 됩니다.
첫 번째 줄은 예산 회의에서 즉석으로 답해야 할 때만 쓰십시오. 제 문서군에서 나온 3.55라는 값이지 보편 상수가 아닙니다.
내일 자기 원고에서 재보는 순서
// 자기 문서 폴더의 바이트당 토큰 계수를 구합니다.
// 이 계수는 문서군마다 다르므로 반드시 자기 데이터로 뽑으십시오.
import { encode } from 'gpt-tokenizer/encoding/o200k_base';
import fs from 'fs';
const files = process.argv.slice(2); // 파일 경로들을 인자로 받습니다
let chars = 0, bytes = 0, tokens = 0;
for (const f of files) {
const t = fs.readFileSync(f, 'utf8');
chars += t.length;
bytes += Buffer.byteLength(t, 'utf8');
tokens += encode(t).length;
}
console.log(`문자당 ${(tokens / chars).toFixed(3)} · 바이트당 ${(tokens / bytes).toFixed(4)}`);
console.log(`→ 예산 어림셈: 바이트 ÷ ${(bytes / tokens).toFixed(2)}`);
확인할 항목은 네 개입니다.
- 자기 문서군의 바이트당 계수를 뽑았는가. 남의 계수를 가져다 쓰지 마십시오.
- 코드·표·산문 비중이 문서군마다 다르므로, 섞여 있다면 유형별로 나눠 쟀는가.
- 쓰려는 모델의 인코딩을 맞췄는가.
cl100k_base와o200k_base는 1.4배 차이입니다. - Claude를 쓸 계획이라면, 지금 숫자가 하한이라는 것을 예산에 반영했는가.
마지막 항목이 실무에서 제일 자주 빠집니다. 저도 이번에 재보기 전까지는 컨텍스트 창을 글자 수로 어림하고 있었고, 그 어림이 두 배 틀려 있었습니다.
댓글
댓글 쓰기