코드 유닛, 코드 포인트, 그래핌 — 한 문자열의 세 가지 길이
"이 문자열은 얼마나 긴가?"라고 물으면 JavaScript, Unicode, 그리고 사용자가 각각 다른 숫자를 내놓습니다. String.prototype.length는 UTF-16 코드 유닛 — JavaScript 내부 인코딩의 16비트 조각 — 을 셉니다. Basic Multilingual Plane 밖의 모든 문자는 서로게이트 쌍(surrogate pair)으로 저장되므로, 눈에는 기호 하나여도 '👍'.length는 2입니다.
문자열을 스프레드([...s])하면 코드 포인트 단위로 순회합니다. 서로게이트 쌍 문제는 해결되지만 시퀀스 문제는 남습니다. 가족 이모지 👨👩👧👦는 사람 코드 포인트 4개를 zero-width joiner(U+200D) 3개로 꿰맨 것입니다: length 11, 코드 포인트 7, 그러나 보이는 글리프는 하나. 국기는 regional indicator 기호 2개입니다 — 🇰🇷의 length는 4. 피부톤 수정자는 기본 이모지 위에 코드 포인트를 하나 더 얹습니다.
사용자가 인지하는 단위는 그래핌 클러스터(grapheme cluster)이고, 2021년부터 플랫폼이 이를 네이티브로 셀 수 있습니다: granularity를 'grapheme'으로 준 Intl.Segmenter는 텍스트 커서가 움직이는 방식 그대로 문자열을 분할합니다. 글자 카운터가 "사용자가 보는 글자 수"로 보여줘야 할 숫자는 바로 이것입니다.
백엔드는 네 번째 잣대를 더합니다. MySQL utf8mb4의 VARCHAR(10)은 코드 포인트를 세고, Postgres의 varchar(10)도 그렇습니다. 하지만 바이트로 제한하는 API는 CJK 문자 하나를 UTF-8 3바이트, 이모지를 4바이트로 봅니다. 같은 일본어 문자열이 JS length 검사에서는 제한 이내이면서 바이트를 세는 백엔드에서는 초과일 수 있습니다. 항상 제한을 집행하는 시스템과 같은 잣대로 검증하세요.
const fam = '👨👩👧👦';
fam.length // 11 UTF-16 code units
[...fam].length // 7 code points (4 people + 3 ZWJ)
const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
[...seg.segment(fam)].length // 1 — what the user sees
'👍'.length // 2 — surrogate pair
'🇰🇷'.length // 4 — two regional indicators플랫폼 제한 해부 — 트위터의 280, SMS의 160, 구글의 600픽셀
현실의 글자 수 제한은 단순 문자 개수인 경우가 드뭅니다. Twitter/X는 280 가중 유닛을 허용합니다: 대부분의 라틴 문자, 숫자, 문장부호는 가중치 1이지만 CJK 문자, 이모지 등 넓은 범위는 가중치 2입니다 — 그래서 일본어 트윗은 최대 140자입니다. URL은 실제 길이와 무관하게 정확히 23 유닛으로 계산됩니다. t.co 래핑이 길이를 정규화하기 때문입니다.
SMS는 더 기묘합니다. 전체 텍스트가 GSM-7 알파벳 안에 들어가면 한 통에 160자입니다. 그 밖의 문자가 하나라도 있으면 — 이모지, 워드프로세서에서 붙여넣은 둥근 아포스트로피, 일부 악센트 문자 — 메시지 전체가 조용히 UCS-2로 전환되고 예산이 70자로 떨어집니다. 긴 메시지는 연결(concatenated) 세그먼트로 쪼개지는데 각 세그먼트가 헤더 바이트를 희생해 GSM-7이면 153자, UCS-2면 67자가 남습니다. 스마트 따옴표 하나가 과금 1통을 3통으로 만들 수 있습니다 — 대량 SMS 청구서에서 끊임없이 드러나는 문제입니다.
그 외: Instagram 캡션은 2,200자와 해시태그 30개가 상한입니다. Google은 타이틀 태그를 렌더링된 픽셀 폭으로 자릅니다 — 약 600px, 글자 폭에 따라 대략 50~60자(WWW는 iii보다 훨씬 넓습니다) — 메타 디스크립션은 155~160자 부근입니다. 실무 결론: 대상 시스템이 코드 유닛을 세는지, 가중 유닛인지, GSM-7 septet인지, 픽셀인지 파악하고, lorem ipsum이 아니라 실제를 대표하는 텍스트로 테스트하세요.
단어 세기는 언어 의존적이다 — CJK는 공백 규칙을 깨뜨린다
공백으로 나누는 방식은 영어, 독일어, 스페인어에서는 견실한 단어 카운터입니다. 하지만 중국어나 일본어 문장에는 정확히 1을 반환합니다 — 이 문자 체계들은 띄어쓰기 없이 씁니다. CJK의 진짜 단어 분할에는 사전이나 통계 모델이 필요합니다: 일본어는 MeCab이 표준 형태소 분석기이고, 중국어는 jieba, 브라우저에서는 granularity를 'word'로 준 Intl.Segmenter가 의존성 없이 ICU의 사전 기반 분할을 제공합니다.
한국어는 중간에 있습니다. 띄어쓰기를 쓰지만 그 단위는 어절 — 단어+조사 덩어리 — 이어서 공백 카운트는 사전적 단어가 아니라 어절 수를 셉니다. 실무에서 한국어 글은 어차피 글자 수로 측정됩니다. 학교와 출판계에서 여전히 언급되는 200자 원고지 전통에서 물려받은 관례입니다.
이 구분에는 돈이 걸려 있습니다. 전문 번역은 영어류 언어에서는 원문 단어당, 일본어·중국어에서는 글자당 과금되며, 에이전시들은 경계를 넘어 견적을 낼 때 공표된 환산 계수 — 영어 단어 1개당 CJK 두 글자 안팎 수준 — 를 적용합니다. 학술·법률 분량 제한도 같은 식으로 갈립니다: 일본 저널은 글자 수(字数)를, 영어 저널은 단어 수를 명시합니다.
여러 언어 사용자에게 단어 수를 보여주는 제품이라면 Intl.Segmenter의 isWordLike 세그먼트를 세세요. 같은 코드 경로로 영어와 CJK 모두에 언어학적으로 타당한 숫자를 돌려줍니다.
function countWords(text, locale = 'en') {
const seg = new Intl.Segmenter(locale, { granularity: 'word' });
return [...seg.segment(text)].filter(s => s.isWordLike).length;
}
countWords('The quick brown fox'); // 4
countWords('日本語は分かち書きをしない', 'ja'); // dictionary-segmented
'日本語は分かち書きをしない'.split(/\s+/).length; // 1 — naive split failsNFC vs NFD — 픽셀은 그대로인데 글자 수가 달라지는 정규화
Unicode에는 같은 가시 텍스트에 대한 인코딩이 여러 개인 경우가 흔합니다. 한국어 음절 '한'은 미리 조합된 코드 포인트 하나(U+D55C)로도, 조합형 자모 셋(초성 ᄒ, 중성 ᅡ, 종성 ᆫ)으로도 존재합니다. é는 단일 코드 포인트 U+00E9이거나 e 뒤에 결합 양음 부호 U+0301이 붙은 형태입니다. NFC가 조합형, NFD가 분해형입니다. 둘은 픽셀 단위로 동일하게 렌더링되지만 비교하면 다릅니다.
Mac에서 이건 이론이 아닙니다. HFS+는 파일명을 분해형 변종으로 저장했고, macOS의 여러 표면은 지금도 NFD 텍스트를 건네줍니다 — Finder에서 한글 파일명을 복사해 카운터에 붙여넣으면 한글.txt가 6자가 아니라 10자로 보고됩니다. macOS에서 업로드된 파일명도 마찬가지입니다: 백엔드 maxlength 검사가 직접 타이핑한 텍스트는 통과시키고, 똑같아 보이는 붙여넣은 텍스트는 거부할 수 있습니다.
두 번째 증상은 동등성입니다. 한쪽이 NFC이고 다른 쪽이 NFD면 'é' === 'é'가 false일 수 있고, 이는 중복 제거, 검색 매칭, unique 제약을 조용히 망가뜨립니다. 해결책은 한 줄입니다 — 세거나 비교하거나 해싱하기 전에 str.normalize('NFC')를 호출하세요. 정규화는 멱등이고 빠릅니다. Intl.Segmenter를 통한 그래핌 클러스터 카운트는 대체로 면역입니다. '한'의 두 인코딩 모두 클러스터 하나를 이루기 때문이며, 사람에게 보여줄 숫자라면 그래핌을 세라는 또 하나의 근거입니다.
const nfc = '한'.normalize('NFC'); // U+D55C
const nfd = '한'.normalize('NFD'); // U+1112 U+1161 U+11AB
nfc.length // 1
nfd.length // 3 — same pixels, three code units
nfc === nfd // false
'한글.txt'.normalize('NFD').length // 10
'한글.txt'.normalize('NFC').length // 6읽기 시간의 수학 — 분당 238단어와 근거 있는 숫자들
읽기 시간 추정은 어림짐작처럼 보이지만 측정된 기준점들이 있습니다. Brysbaert의 2019년 메타분석(약 190개 연구)은 영어 논픽션 묵독 속도를 분당 약 238단어로 추정하며, 픽션은 약간 빠른 260 부근입니다. Medium의 읽기 시간 알고리즘은 265wpm을 쓰고, 첫 이미지에 12초를 더한 뒤 이후 이미지마다 1초씩 줄여 최저 3초까지 내려갑니다. 일본어와 중국어는 대신 글자 수 기준으로 계획합니다 — 편안한 묵독 기준 분당 대략 400~600자입니다.
평균 단어 길이가 환산을 가능하게 합니다. 영어는 단어당 약 4.7자, 뒤따르는 공백을 포함하면 약 5.7자입니다. 그래서 1,000단어 기사는 약 5,700자이고 238wpm으로 4분 남짓 걸립니다. 같은 산수로 제한도 번역됩니다: 155자 메타 디스크립션에는 영어 단어 약 25개, 160자 GSM-7 SMS에는 약 28개가 들어갑니다.
말하기는 묵독보다 훨씬 느립니다 — 내레이션과 컨퍼런스 발표는 분당 130~150단어 수준입니다. '5분 읽기' 분량이 9분짜리 보이스오버 대본이 되는 이유입니다. 밀도는 유용한 편집 신호이기도 합니다: 평균 단어 길이가 6자를 훌쩍 넘어가면 전문용어가 많은 텍스트라서, 어떤 단어당 공식이 예측하는 것보다도 느리게 읽힙니다.