똑같아 보이는 한글이 토큰을 11배 쓴다 — 자모 분리(NFD) 실측
화면에 똑같이 “안녕하세요”라고 보이는 두 텍스트가 있습니다. 복사해 붙여 넣으면 구별이 안 됩니다. 그런데 하나는 2토큰이고 다른 하나는 36토큰입니다. 18배 차이입니다.
한글 유니코드 정규화 문제입니다. 알고 나면 간단하지만 모르면 원인을 찾을 수 없습니다. 직접 측정한 결과를 정리했습니다.
먼저 결론
| 텍스트 | NFC (정상) | NFD (분리) | 배수 |
|---|---|---|---|
| 안녕하세요 | 2 토큰 | 36 토큰 | 18.0배 |
| 인공지능 기술이 빠르게 발전하고 있습니다 | 10 토큰 | 136 토큰 | 13.6배 |
| 계약서 내용을 다시 한번 확인해 주시기 바랍니다 | 12 토큰 | 150 토큰 | 12.5배 |
| 한국어 자연어 처리 연구 논문 초록 | 10 토큰 | 110 토큰 | 11.0배 |
| 2026년 9월 프로젝트 최종 보고서.pdf | 12 토큰 | 86 토큰 | 7.2배 |
| 합계 | 46 | 518 | 11.3배 |
평균 11배입니다. 문서 하나를 넣었는데 요금이 열 배로 나온다면 이것을 의심해야 합니다.
왜 이런 일이 생기나
한글 “한”은 유니코드에서 두 가지 방식으로 표현할 수 있습니다.
- NFC (완성형): “한” 글자 하나 = 코드 1개
- NFD (조합형): ㅎ + ㅏ + ㄴ = 코드 3개
화면에 그려질 때는 둘 다 “한”으로 보입니다. 사람 눈으로는 구별할 수 없습니다. 하지만 컴퓨터가 보는 데이터는 완전히 다릅니다.
실제로 “안녕하세요” 다섯 글자를 NFD로 풀면 이렇게 됩니다.
NFC: '안녕하세요' → 5글자
NFD: '안녕하세요' → 12글자 (자모가 분리됨)
U+110B U+1161 U+11AB ← '안' = ㅇ+ㅏ+ㄴ
U+1102 U+1167 U+11BC ← '녕' = ㄴ+ㅕ+ㅇ
U+1112 U+1161 ← '하'
U+1109 U+1166 ← '세'
U+110B U+116D ← '요'그런데 왜 토큰은 18배나 되나
글자 수는 5개에서 12개로 2.4배만 늘었는데 토큰은 18배가 됐습니다. 이유는 토크나이저가 학습한 방식에 있습니다.
토크나이저는 완성형 한글로 학습했습니다. “한국” 같은 덩어리가 하나의 토큰으로 등록되어 있습니다. 그런데 NFD로 분리된 자모는 학습 데이터에 거의 없어서, 토크나이저가 처리할 방법을 찾지 못하고 바이트 단위까지 쪼갭니다.
| 입력 | 토큰 수 | 쪼개진 모양 |
|---|---|---|
| NFC ‘한국어’ | 2 | ‘한국’ / ‘어’ |
| NFD ‘한국어’ | 24 | 의미 없는 바이트 조각 24개 |
자모 하나가 3바이트씩 쪼개지면서 토큰이 폭발합니다.
어디서 NFD가 섞여 들어오나
가장 잘 알려진 경로는 맥에서 파일명을 다룰 때입니다. macOS의 파일 시스템은 한글 파일명을 자모 분리 형태로 저장하는 특성이 있어, 파일명을 복사하거나 파일 목록을 프로그램으로 읽어오면 NFD가 따라옵니다.
그 외에 의심해볼 만한 경우입니다.
- 맥에서 만든 파일이나 압축 파일을 윈도우에서 풀었을 때
- 파일명을 그대로 텍스트로 옮겨 적은 목록
- 서로 다른 시스템을 거쳐 온 데이터베이스 내용
- 여러 도구를 거친 자동화 파이프라인의 중간 산출물
직접 타이핑한 글은 대부분 NFC라 문제가 없습니다. 문제는 기계를 거쳐 온 텍스트, 특히 파일명이 섞인 목록입니다.
확인하는 법 — 3초
눈으로는 구별할 수 없으니 확인 방법을 알아두는 것이 중요합니다. 가장 간단한 것은 글자 수를 세보는 것입니다.
# 파이썬
import unicodedata
s = 의심스러운_텍스트
print(len(s), len(unicodedata.normalize("NFC", s)))
# 두 값이 다르면 NFD가 섞여 있다코딩 없이 확인하려면, 해당 텍스트를 글자 수 세기 도구에 넣어보세요. 한글 다섯 글자가 열두 글자로 나온다면 NFD입니다.
브라우저 콘솔로도 가능합니다
// 브라우저 개발자도구 콘솔 (F12)
const s = "붙여넣은 텍스트";
console.log(s.length, s.normalize("NFC").length);고치는 법 — 한 줄
해결은 간단합니다. NFC로 정규화하면 끝입니다.
# 파이썬
import unicodedata
fixed = unicodedata.normalize("NFC", text)
# 자바스크립트
const fixed = text.normalize("NFC");코딩을 하지 않는 경우라면 다음 방법이 있습니다.
- 텍스트를 메모장 같은 단순 편집기에 붙였다가 다시 복사해보세요. 도구에 따라 정규화되기도 합니다.
- 파일명 목록이라면, 파일명을 직접 타이핑해 옮겨 적는 편이 확실합니다.
- 가장 확실한 방법은 위 한 줄짜리 코드를 돌리는 것입니다.
데이터를 대량으로 다루는 자동화 파이프라인이라면, 입력 단계에서 무조건 NFC로 정규화하는 것을 기본값으로 두세요. 비용뿐 아니라 검색·정렬·중복 제거가 전부 어긋나는 것을 막아줍니다.
비용으로 환산하면
앞선 측정에서 한국어 1,000자는 약 536토큰이었습니다. NFD가 섞이면 이 문서가 약 6,000토큰이 됩니다.
| 분량 | NFC (정상) | NFD (분리) |
|---|---|---|
| 한국어 1,000자 | 약 536 토큰 | 약 6,000 토큰 |
| A4 10장 | 약 5,100 토큰 | 약 57,000 토큰 |
| 128K 컨텍스트에 들어갈 분량 | A4 약 250장 | A4 약 22장 |
컨텍스트 한도가 실질적으로 11분의 1로 줄어듭니다. “분명 넉넉한 분량인데 자꾸 한도를 넘는다”면 이것을 의심해보세요.
덤 — 문체를 바꿔도 절약은 안 된다
“반말로 쓰면 토큰이 줄지 않을까” 하는 생각이 들 수 있어 함께 재봤습니다.
| 문체 | 예시 | 토큰 | 글자당 |
|---|---|---|---|
| 정중한 존댓말 | 회의 자료를 검토해 주시기 바랍니다 | 10 | 0.53 |
| 일반 존댓말 | 회의 자료를 검토해 주세요 | 7 | 0.50 |
| 평서문 | 회의 자료를 검토한다 | 6 | 0.55 |
| 반말 | 회의 자료 검토해 | 5 | 0.56 |
| 명사형 | 회의 자료 검토 요망 | 6 | 0.55 |
글자당 토큰은 0.50~0.56으로 거의 차이가 없습니다. 짧게 쓰면 총 토큰은 줄지만 그건 내용을 줄인 것이지 문체 덕이 아닙니다. 오히려 반말이 글자당으로는 약간 비쌉니다. 문체는 읽는 사람 기준으로 정하세요.
재현 코드
import tiktoken, unicodedata
enc = tiktoken.get_encoding("o200k_base")
n = lambda t: len(enc.encode(t))
s = "안녕하세요"
nfc = unicodedata.normalize("NFC", s)
nfd = unicodedata.normalize("NFD", s)
print(len(nfc), n(nfc)) # 5 2
print(len(nfd), n(nfd)) # 12 36
print(n(nfd) / n(nfc)) # 18.0
# 어떻게 쪼개지는지
ids = enc.encode(unicodedata.normalize("NFD", "한국어"))
print(len(ids)) # 24정리
- 화면에 똑같아 보여도 NFD 한글은 토큰을 평균 11배 씁니다. 최대 18배까지 측정됐습니다.
- 맥에서 온 파일명이 가장 흔한 경로입니다. 직접 친 글은 대개 안전합니다.
- 확인:
len(s)와len(normalize("NFC", s))가 다르면 NFD입니다. - 해결: NFC로 정규화하는 한 줄이면 끝입니다.
- 문체(존댓말/반말)는 토큰 효율에 거의 영향이 없습니다.
측정 조건이나 결과에 이견이 있으시면 문의 페이지로 알려주세요. 재측정해서 반영하겠습니다.
측정일: 2026년 9월 21일 / tiktoken 0.14.0 / 인코딩 o200k_base / Python unicodedata