PDF에서 복사한 텍스트가 토큰을 3배 먹는다 — 숨은 문자 실측
보고서 PDF를 열어 내용을 복사하고 AI에 붙여 넣습니다. 누구나 하는 일입니다. 그런데 그 순간 보이지 않는 비용이 따라 들어옵니다.
복사 과정에서 섞여 들어오는 것들이 토큰을 얼마나 늘리는지 직접 측정했습니다. 가장 심한 경우 3.26배였고, 원인은 화면에 전혀 보이지 않는 문자였습니다.
측정 결과
같은 내용(한국어 218자)에 복사 과정에서 흔히 생기는 오염을 하나씩 적용해 측정했습니다.
| 상태 | 글자 수 | 토큰 | 원문 대비 |
|---|---|---|---|
| 깨끗한 원문 | 218자 | 116 | 1.00배 |
| PDF 줄바꿈 (줄 끝마다) | 224자 | 123 | 1.06배 |
| 탭 들여쓰기 + 줄바꿈 | 238자 | 135 | 1.16배 |
| 이중 공백 | 272자 | 170 | 1.47배 |
| 머리말·꼬리말·쪽번호 포함 | 364자 | 181 | 1.56배 |
| non-breaking space | 218자 | 201 | 1.73배 |
| zero-width space | 435자 | 378 | 3.26배 |
주목할 것은 아래 두 줄입니다. non-breaking space는 글자 수가 218자로 원문과 똑같은데 토큰은 1.73배입니다. 겉으로는 아무 차이가 없습니다.
보이지 않는 문자들
non-breaking space (U+00A0)
줄바꿈을 막는 특수 공백입니다. 웹 페이지에서 복사하면 일반 공백 대신 이것이 딸려오는 경우가 많습니다. 화면에서는 일반 공백과 완전히 똑같이 보입니다.
그런데 토크나이저 입장에서는 전혀 다른 문자입니다. “공백 + 단어”라는 흔한 패턴으로 학습된 덩어리를 쓰지 못하고 매번 따로 처리해야 합니다. 그 결과가 1.73배입니다.
zero-width space (U+200B)
너비가 0인 문자입니다. 화면에 아무것도 그려지지 않습니다. 웹 페이지에서 줄바꿈 위치를 조절하려고 넣어두는 경우가 있고, 일부 편집기나 변환 도구가 삽입하기도 합니다.
글자 사이사이에 이것이 끼어 있으면 토크나이저가 단어를 인식하지 못하고 글자 단위로 쪼갭니다.
| 입력 | 토큰 |
|---|---|
| 인공지능 (정상) | 3 |
| 인공지능 (글자 사이 zero-width) | 7 |
눈에 보이지 않는 문자 3개가 토큰 4개를 추가했습니다. 보이지 않으니 원인을 찾을 수도 없습니다.
PDF 특유의 문제 — 머리말과 꼬리말
PDF를 통째로 복사하면 페이지마다 반복되는 요소가 전부 따라옵니다. 보고서 제목, 쪽번호, 저작권 문구 같은 것들입니다.
측정 결과 전체 토큰의 32%가 머리말·꼬리말이었습니다. 페이지가 많을수록 이 비중은 더 커집니다.
| 구성 | 토큰 | 비중 |
|---|---|---|
| 실제 본문 | 123 | 68% |
| 머리말·꼬리말·쪽번호 | 58 | 32% |
50페이지 보고서라면 같은 제목과 저작권 문구를 50번 전송하는 셈입니다.
정리하면 얼마나 돌아오나
간단한 정리만 해도 상당 부분이 회수됩니다.
| 오염 유형 | 정리 전 | 정리 후 | 절감 |
|---|---|---|---|
| zero-width space | 378 | 116 | 69% |
| non-breaking space | 201 | 116 | 42% |
| 이중 공백 | 170 | 116 | 32% |
| 탭 들여쓰기 | 135 | 118 | 13% |
| PDF 줄바꿈 | 123 | 118 | 4% |
보이지 않는 문자일수록 정리 효과가 큽니다. 반대로 줄바꿈처럼 눈에 보이는 문제는 생각보다 비용이 크지 않습니다 (4%). 줄바꿈을 일일이 지우느라 시간 쓸 필요는 없다는 뜻이기도 합니다.
실전 — 붙여 넣기 전에 한 번
코딩이 가능하다면
import re, unicodedata
def clean(t):
t = unicodedata.normalize("NFC", t) # 한글 자모 분리 복구
t = t.replace("\u200b", "") # zero-width space 제거
t = t.replace("\u00a0", " ") # non-breaking space -> 일반 공백
t = re.sub(r"\n(?!\n)", " ", t) # 단일 줄바꿈 -> 공백
t = re.sub(r"[ \t]+", " ", t) # 연속 공백/탭 -> 하나
t = re.sub(r"\n{3,}", "\n\n", t) # 과도한 빈 줄 정리
return t.strip()첫 줄의 NFC 정규화도 함께 넣었습니다. 한글 자모가 분리되어 있으면 토큰이 11배까지 늘어나기 때문입니다.
코딩 없이 한다면
- 메모장(윈도우) 같은 순수 텍스트 편집기에 한 번 붙였다가 다시 복사하세요. 서식과 특수 문자가 상당 부분 떨어져 나갑니다.
- 편집기의 찾기·바꾸기로 이중 공백(스페이스 2칸)을 한 칸으로 바꾸세요. 몇 번 반복하면 됩니다.
- PDF는 필요한 페이지만 복사하세요. 머리말·꼬리말 반복이 가장 큰 낭비입니다.
- 표가 포함된 자료라면 표만 따로 떼어 CSV 형태로 정리해 넣으세요.
의심될 때 확인하는 법
# 파이썬 — 보이지 않는 문자가 있는지
s = 붙여넣은_텍스트
print("zero-width:", s.count("\u200b"))
print("nbsp:", s.count("\u00a0"))
print("글자수:", len(s), "/ 공백 제외:", len(s.replace(" ", "")))브라우저 개발자도구 콘솔에서도 같은 확인이 가능합니다.
const s = "붙여넣은 텍스트";
console.log([...s].filter(c => c === "\u200b").length);
console.log([...s].filter(c => c === "\u00a0").length);우선순위 — 무엇부터 할까
전부 다 할 필요는 없습니다. 효과가 큰 순서입니다.
| 순위 | 할 일 | 효과 |
|---|---|---|
| 1 | 필요한 부분만 복사 (머리말·꼬리말 제외) | 가장 큼 |
| 2 | 보이지 않는 문자 제거 (zero-width, nbsp) | 최대 69% |
| 3 | 한글 NFC 정규화 | 최대 11배 |
| 4 | 연속 공백 정리 | 약 32% |
| 5 | 줄바꿈 정리 | 약 4% (후순위) |
측정 방법과 한계
한국어 218자 문단에 각 오염을 프로그램으로 적용해 tiktoken 0.14.0의 o200k_base 인코딩으로 측정했습니다. PDF 머리말·꼬리말은 5줄마다 한 페이지로 가정했습니다.
한계: 실제 PDF와 웹 페이지의 오염 정도는 문서와 도구에 따라 크게 다릅니다. 위 수치는 각 오염이 전면적으로 적용됐을 때의 값이므로, 실제로는 더 작을 수 있습니다. 정확한 값은 본인 텍스트를 위 확인 코드로 직접 재보시는 것이 확실합니다.
정리
- 복사한 텍스트는 최대 3.26배까지 토큰을 더 씁니다.
- 가장 비싼 원인은 화면에 보이지 않는 문자입니다 (zero-width 3.26배, nbsp 1.73배).
- PDF는 머리말·꼬리말이 전체의 32%를 차지합니다.
- 정리하면 최대 69%가 회수됩니다.
- 줄바꿈 정리는 효과가 4%뿐이니 후순위로 두세요.
측정일: 2026년 9월 21일 / tiktoken 0.14.0 / 인코딩 o200k_base