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

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

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를 쓸 계획이라면, 지금 숫자가 하한이라는 것을 예산에 반영했는가.

마지막 항목이 실무에서 제일 자주 빠집니다. 저도 이번에 재보기 전까지는 컨텍스트 창을 글자 수로 어림하고 있었고, 그 어림이 두 배 틀려 있었습니다.


댓글

이 블로그의 인기 게시물

옵시디언 3기기 동기화, 유료 결제 없이 구글 드라이브로 끝내기

최저가 주유소, 얼마나 돌아가면 손해일까?

회의시간 줄이는 법, 개수보다 배치 먼저

유류세 인하 2개월 연장, 주유소 가격 실제 인하폭 계산

국민연금 추납 조회, 미루면 얼마나 더 낼까?