Skip to Content

16장. String.length가 거짓말할 때 — 이모지는 왜 2인가

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

자바·JS의 length는 글자 수가 아니라 UTF-16 코드 유닛을 세므로, 목적에 따라 코드포인트나 자소 묶음 도구를 선택해야 한다.

면접 실전 질문: ① JS length가 이모지를 2로 세는 이유는? ② slice로 이모지를 자를 때 생기는 문제는? ③ 사용자 글자 수에는 어떤 단위를 써야 하나?


배경 — 매일 쓰는 length의 배신

15장에서 “글자 수”는 층위마다 다르다고 했죠. 이건 교과서 얘기가 아니에요. 우리가 매일 쓰는 str.length, substring, charAt이 유니코드 앞에서 조용히 거짓말을 합니다. 그리고 그 거짓말은 트위터 글자 수 세기부터 DB 저장, 문자열 자르기까지 실무 곳곳에서 버그로 터져요. 이 장은 그 현장입니다.

스토리 — 이모지는 왜 2인가

자바스크립트나 자바는 12장에서 봤듯 문자열을 UTF-16 코드 유닛 단위로 다뤄요. 그래서 length가 세는 건 글자 수가 아니라 코드 유닛 개수입니다. BMP 문자는 1코드 유닛이라 티가 안 나지만, 이모지처럼 BMP 밖 문자는 서로게이트 페어라 2코드 유닛이에요.

"😀".length // 2 ← 서로게이트 두 조각을 센다 "👨‍👩‍👧‍👦".length // 11 ← 사람4(각 2) + ZWJ 3 "a😀b".length // 4 ← a(1) + 😀(2) + b(1)

😀 하나가 길이 2로 잡혀요. 별거 아닌 것 같죠? 그런데 이게 실무에서 진짜 사고를 냅니다.

① 글자 수 제한이 어긋난다. “280자 제한” 같은 걸 length로 재면, 이모지 많은 글은 실제보다 빨리 한도에 걸려요. 입력창의 “몇 자 남음” 카운터도 이모지 하나를 2로 세서 사용자 눈엔 숫자가 이상하게 줄어들죠.

② 문자열을 자르면 글자가 깨진다. 이게 제일 무섭습니다. length나 인덱스를 믿고 이모지 한가운데를 자르면, 서로게이트 페어의 반쪽만 남아요.

"😀".slice(0, 1) // "\ud83d" — 상위 서로게이트 반쪽! 화면엔 깨진 � "😀".charAt(0) // "\ud83d" — 역시 반쪽

“제목이 너무 길면 100자에서 자르고 …붙이기” 같은 흔한 코드가, 하필 이모지 위치에서 잘리면 마지막 글자를 로 깨뜨립니다. 6장에서 EUC-KR 한글을 바이트 단위로 자르다 깨졌던 그 사고가, 유니코드 시대엔 코드 유닛 단위 자르기로 되풀이되는 거예요.

핵심 — 층위에 맞는 도구를 써라

length가 틀린 게 아니에요. 당신이 원하는 층위와 다른 층위를 세고 있을 뿐입니다. 그러니 해법은 “무엇을 세고 싶은지” 정하고, 그 층위에 맞는 도구를 쓰는 거예요.

코드포인트로 세고 싶다면 — 서로게이트를 하나로 묶어 순회하는 도구를 씁니다. 자바스크립트 에선 스프레드([...])나 Array.from, for...of가 문자열을 코드포인트 단위로 순회해요.

[..."😀"].length // 1 ← 코드포인트로 세니 제대로 1 [..."a😀b"].length // 3 ← a, 😀, b "😀".slice(0, 1) // "\ud83d" 깨짐 ❌ [..."😀"][0] // "😀" 안전 ✅

사용자가 보는 글자(자소 묶음)로 세고 싶다면 — 코드포인트로도 부족해요. 가족 이모지는 코드포인트가 7개지만 사람 눈엔 1글자잖아요. 이땐 자소 묶음(grapheme) 단위로 잘라주는 도구가 필요합니다. 자바스크립트라면 Intl.Segmenter예요.

const seg = new Intl.Segmenter("ko", { granularity: "grapheme" }); [...seg.segment("👨‍👩‍👧‍👦")].length // 1 ← 사람이 보는 그대로 1글자

언어마다 기본 태도가 다르다는 것도 알아두면 좋아요. 파이썬 3len은 코드포인트를 세서 가족 이모지가 7, 자바·JSlength는 코드 유닛이라 11. 스위프트는 아예 String.count자소 묶음 기준이라 1이 나와요(유니코드를 가장 사람 눈높이로 다루는 편이죠). Go는 문자열이 바이트열이라 len이 바이트 수고, 코드포인트는 rune으로 따로 셉니다. 결국 “이 언어의 length는 어느 층위를 세는가” 를 아는 게 핵심이에요.

정리하면, 유니코드 문자열을 다룰 땐 세 가지 질문을 먼저 던지세요. 바이트가 궁금한가 (저장·전송), 코드포인트가 궁금한가(유니코드 처리), 아니면 사람이 보는 글자가 궁금한가 (UI·글자 수 제한)? 질문이 정해지면 도구도 정해집니다.

정리

  • 자바·JS의 lengthUTF-16 코드 유닛 개수라 이모지가 2(😀), 11(가족)로 잡힌다. 글자 수가 아니다.
  • 대표 사고: ① 글자 수 제한 어긋남(이모지가 2로 계산) ② 인덱스·slice·charAt로 이모지 한가운데를 자르면 서로게이트 반쪽만 남아 로 깨짐(6장 “바이트 단위 자르기”의 코드 유닛 판).
  • 해법은 층위에 맞는 도구: 코드포인트는 [...str]·Array.from·for...of, 사용자 글자(자소 묶음)는 Intl.Segmenter. length가 틀린 게 아니라 원하는 층위와 다른 층위를 센 것.
  • 언어별 기본값: 파이썬 len=코드포인트, 자바·JS length=코드 유닛, 스위프트 count=자소 묶음, Go len=바이트. “이 length는 뭘 세나” 를 아는 게 관건.

생각해볼 질문: 지금까지는 애플리케이션 코드 얘기였어요. 그런데 데이터를 최종적으로 담는 곳은 데이터베이스죠. 자, MySQL에 이모지가 든 문자열을 저장하면 어떻게 될까요? 분명히 문자셋을 utf8로 설정했는데도, Incorrect string value 에러가 터지면서 저장이 거부 됩니다. UTF-8이라며 왜 UTF-8 문자를 못 받죠? 충격적이게도, MySQL의 utf8은 진짜 UTF-8이 아니거든요. 이 악명 높은 함정으로 갑니다.

17장 · utf8은 진짜 UTF-8이 아니다 — MySQL utf8 vs utf8mb4

Last updated on