Intl.Segmenter는 문자열을 사람이 인식하는 단위 — 글자(grapheme), 단어, 문장 — 로 정확히 쪼개주는 자바스크립트 내장 API입니다. 이모지나 한글 조합, CJK 텍스트를 str.length나 split('')로 세면 틀리는데, 그 문제를 로케일 규칙에 맞춰 바로잡습니다.
이걸 알기 전엔 이모지 하나가 왜 length로 2, 심하면 7로 세어지는지 몰라서 한참 헤맸습니다. 답은 간단합니다. 자바스크립트 문자열은 UTF-16 코드 유닛의 나열일 뿐, '사람이 보는 글자'가 아니거든요.
왜 length가 거짓말을 하나
예를 봅시다. 겉보기엔 한 글자인데 세는 방법마다 값이 다릅니다.
const s = '👨👩👧';
s.length; // 8 (UTF-16 코드 유닛)
[...s].length; // 5 (코드 포인트)
// 사람 눈엔? 1글자.
가족 이모지는 여러 이모지를 결합 문자로 이어붙인 것이라 코드 포인트로 세도 여러 개로 나옵니다. 스프레드 연산자([...s])는 코드 포인트 단위라 이모지 낱개는 잘 세지만, 이런 결합 문자(grapheme cluster) 앞에선 여전히 틀립니다.
grapheme 단위로 정확히 세기
const seg = new Intl.Segmenter('ko', { granularity: 'grapheme' });
const chars = [...seg.segment('👨👩👧éa')];
chars.length; // 3 (가족·é·a)
chars[0].segment; // '👨👩👧'
드디어 '사람이 보는 글자 수'가 나옵니다. 문자열 길이 제한(트위터식 카운터), 커서 이동, 한 글자씩 자르는 타이핑 애니메이션 같은 데서 이게 정답입니다. granularity만 바꾸면 단위가 달라집니다.
단어 세기 — 띄어쓰기 없는 언어까지
split(' ')로 단어를 세면 한국어·일본어·중국어처럼 띄어쓰기 규칙이 다른 언어에서 엉망이 됩니다. word 단위 분할은 로케일의 단어 경계 규칙을 씁니다. 구두점·공백은 isWordLike로 걸러냅니다.
const seg = new Intl.Segmenter('ja', { granularity: 'word' });
let count = 0;
for (const { isWordLike } of seg.segment('今日は良い天気です')) {
if (isWordLike) count++;
}
// 공백 없이도 단어 경계를 인식
성능 팁
Intl.Segmenter 인스턴스 생성은 공짜가 아닙니다. 로케일 데이터를 준비하기 때문이죠. 루프 안에서 매번 new 하지 말고, 한 번 만들어 재사용하세요. 나는 모듈 상단에서 한 번 생성해 export 해두고 씁니다.
자주 묻는 질문
[...str]로 세는 거랑 뭐가 다른가요?
스프레드는 코드 포인트 단위라 기본 이모지는 맞지만, 피부색·가족·국기처럼 여러 코드 포인트가 결합된 글자를 여전히 여러 개로 셉니다. Intl.Segmenter의 grapheme 단위는 이런 결합 문자까지 하나로 묶어 '사람이 보는 글자'와 일치시킵니다.
브라우저 지원은 넉넉한가요?
넉넉합니다. 2024년을 지나며 주요 브라우저와 Node.js에 자리 잡아 2026년 현재 폭넓게 쓸 수 있습니다. 외부 라이브러리 없이 표준으로 처리할 수 있다는 게 큰 장점입니다.
정규식으로 단어를 나누면 안 되나요?
\b 같은 정규식 단어 경계는 유니코드·다국어에 약합니다. 한중일처럼 띄어쓰기 규칙이 다른 언어나 이모지가 섞이면 어긋나요. 로케일 규칙을 따르는 Intl.Segmenter가 훨씬 정확합니다.

댓글 0