UTF-16 과 서로게이트 페어 — 문자 수의 함정
UTF-16 은 16 비트 단위의 인코딩. BMP 밖의 문자는 서로게이트 페어로 2 단위가 되고, C# 이나 JavaScript 의 length 는 눈에 보이는 문자 수와 일치하지 않게 된다.
「3 문자의 이름이 들어가지 않는다」— 문자 수 세는 법의 사건
「성명은 4 문자까지」일 터인 폼에 일본어 「𠮷野家」(요시노야)의 3 문자가 들어가지 않는다. 이모지 2 개의 게시물이 「4 문자」로 카운트된다. C# 에서 Length 를 보고 잘라냈더니 문자의 잔해가 섞였다. — 이 장은 「바이트 수」에 이은 또 하나의 수의 어긋남, 「문자 수」를 세는 방법의 이야기입니다.
주역은 UTF-16. Windows 의 내부, Java·C#·JavaScript 의 문자열 타입이 채용하고 있는 인코딩으로, 16 비트(2 바이트)의 코드 단위를 기본으로 합니다. 일본어 문자 「あ」 U+3042 라면, 그대로 16 비트에 들어가 1 단위 — 여기까지는 순조롭습니다.
그러나 제 2 장에서 본 대로, Unicode 의 번호는 U+10FFFF 까지 있습니다. 16 비트로 나타낼 수 있는 U+FFFF 까지의 범위를 BMP(기본 다국어 평면)라고 부르는데, 이모지(U+1F600 대)나 「𠮷」(U+20BB7) 같은 문자는 BMP 의 바깥쪽. 16 비트 1 개에는 들어가지 않습니다.
서로게이트 페어 — 2 단위로 1 문자를 나타낸다
UTF-16 의 답은, 2 개의 코드 단위를 페어로 만들어 1 문자를 나타내는 것이었습니다. 절차는 UTF-8 의 템플릿과 많이 닮았습니다.
D842 DFB7. D800〜DFFF 는 단독으로는 문자가 되지 않는 예약 영역이므로, 읽는 쪽은 페어임을 즉시 알 수 있다이 2 단위 1 조를 서로게이트 페어라고 부릅니다. 이모지도 같은 구조로, 「😀」 U+1F600 은 D83D DE00 의 2 단위입니다.
BMP 내의 「あ」는 1 단위, BMP 밖의 「𠮷」는 2 단위. length 가 세고 있는 것은 이 「상자의 수」이지, 눈에 보이는 문자 수가 아닙니다.
소문제 5-1 — UTF-16 과 서로게이트 페어
16 비트 단위의 세계에서, 큰 번호의 문자가 어떻게 표현되는지를 확인합니다.
Q1. UTF-16 의 설명으로 올바른 것은 어느 것입니까.
UTF-16 의 기본 단위는 16 비트입니다. Unicode 초기의 「전 문자 2 바이트로 충분」이라는 예상이 무너지고, U+FFFF 를 넘는 문자를 2 단위로 나타내는 서로게이트 페어가 나중에 도입되었습니다. 「고정 길이」는 BMP 내 한정의 가까운 과거의 모습이며, 현재는 가변 길이(1 또는 2 단위)입니다. 1 바이트 단위로 ASCII 호환인 것은 UTF-8 쪽입니다.
Q2. 일본어 한자 「𠮷」(쓰치요시)의 코드 포인트는 U+20BB7 로, U+FFFF 를 넘습니다. UTF-16 에서는 어떻게 표현됩니까.
U+20BB7 에서 0x10000 을 뺀 0x10BB7 을 20 비트로 나타내고, 상위 10 비트를 고위 서로게이트(0xD800〜), 하위 10 비트를 저위 서로게이트(0xDC00〜)에 실으면 D842 DFB7 이 됩니다. D800〜DFFF 는 이 용도를 위해 예약된 「단독으로는 문자가 되지 않는」 영역으로, 2 개가 1 조가 되어 비로소 1 문자를 나타냅니다. 이모지(U+1F600 등)도 모두 같은 구조입니다.
Q3. C# 의 string.Length 나 JavaScript 의 .length 는 UTF-16 의 코드 단위 수를 돌려줍니다. 일본어 「𠮷野家」(𠮷 = U+20BB7, 野·家는 BMP 내)의 length 는 얼마입니까.
「𠮷」는 서로게이트 페어로 2 단위, 「野」 「家」는 1 단위씩이므로 2 + 1 + 1 = 4 입니다. 눈에는 3 문자인데 4 가 돌아온다 — 「length = 문자 수」라는 착각이 무너지는 순간입니다. 문자 수 제한 체크나 선두 N 문자 잘라내기를 코드 단위로 하면, 페어의 한가운데가 잘려 깨진 문자가 생깁니다.
length 가 돌려주는 것은 「상자의 수」
여기서 서두의 사건이 풀립니다. C# 의 string.Length 도 JavaScript 의 .length 도, 돌려주고 있는 것은 UTF-16 의 코드 단위 수 — 즉 서로게이트 페어의 「상자」를 2 로 센 값입니다.
| 문자열 | 눈에 보이는 문자 수 | length(코드 단위) | 왜 |
|---|---|---|---|
| あいう | 3 문자 | 3 | 모두 BMP 내에서 1 단위씩 |
| 𠮷野家 | 3 문자 | 4 | 𠮷 가 2 단위 |
| 😀😀😀 | 3 문자 | 6 | 이모지는 각 2 단위 |
이것은 처리계의 버그가 아니라 사양대로입니다. 위험한 것은, length 를 「문자 수」로 바꿔 읽어 문자 수 제한 체크나 선두 N 문자 잘라내기에 사용할 때. 잘라내기가 페어의 한가운데에 떨어지면, 고위 서로게이트만 남은 「깨진 문자」가 생겨, 후단의 처리나 데이터베이스에서 오류의 씨앗이 됩니다.
한 걸음 더 — 결합 문자와 「눈에 보이는 1 문자」
실은 「코드 포인트 단위로 세면 문자 수가 된다」도, 아직 정답이 아닙니다. 가족 이모지 「👨👩👧」는, 「남성」 「여성」 「여아」의 3 개의 이모지를 제로 폭 접합자(ZWJ)라는 보이지 않는 코드 포인트로 이은 것으로, 코드 포인트는 5 개, UTF-16 의 코드 단위로는 8 개. 그런데도 화면에 보이는 것은 1 문자입니다. 일본어 「が」를 「か + 탁점」의 2 코드 포인트로 나타내는 결합 문자도 같은 부류입니다(한글이 자모의 결합으로 표현되는 경우도 마찬가지입니다).
사람의 눈에 비치는 1 문자에 대응하는 단위는 그래핌 클러스터(grapheme cluster)라고 불리며, 세려면 전용 API(JavaScript 의 Intl.Segmenter 등)를 사용합니다. 정리하면, 「문자 수」에는 적어도 4 가지 세는 방법 — 바이트·코드 단위·코드 포인트·그래핌 — 이 있고, 어느 것을 세고 있는지를 말할 수 있는 것이 이 장의 목표입니다. 제 3 장의 시뮬레이터에 「𠮷野家」나 가족 이모지를 넣어, 4 가지 수가 전부 어긋나는 것을 확인해 보세요.
소문제 5-2 — length 의 덫과 결합 문자
「문자 수를 센다」는 것의 의외의 어려움을, 이모지로 마무리합니다.
Q4. 이모지 「😀」는 U+1F600 으로, UTF-16 에서는 2 코드 단위입니다. JavaScript 에서 「😀😀😀」(이모지 3 개)의 .length 는 얼마입니까.
2 단위 × 3 개 = 6 입니다. 「이모지 3 문자까지 OK」라는 생각으로 length <= 3 이라고 쓴 입력 체크는, 이모지 2 개에서 튕겨 버립니다. SNS 의 문자 수 카운트나 DB 의 문자 수 제한에서 빈출하는, 현대다운 함정입니다.
Q5. length(Length)가 「눈에 보이는 문자 수」와 어긋나는 장면에서의 올바른 이해는 어느 것입니까.
length 는 「UTF-16 의 코드 단위 수」라는 명확한 사양대로 움직이고 있습니다. 문제는 사용하는 쪽이 「문자 수」로 바꿔 읽어 버리는 것입니다. 그리고 이모지를 피해도 「𠮷」 같은 한자나, 다음 문제의 결합 문자에서 같은 어긋남은 일어납니다. 세고 싶은 단위(바이트·코드 단위·코드 포인트·그래핌)를 의식해서 고른다 — 이것이 이 장의 결론입니다.
Q6. 가족 이모지 「👨👩👧」는, 복수의 이모지를 제로 폭 접합자(ZWJ)로 이은 것으로, JavaScript 의 .length 는 8 이 됩니다. 「눈에 보이는 1 문자」를 올바르게 세고 싶을 때의 적절한 단위는 어느 것입니까.
👨👩👧 는 「남성 + ZWJ + 여성 + ZWJ + 여아」의 5 코드 포인트(그중 이모지 3 개는 각 2 단위)로, 코드 단위로는 8, 코드 포인트로도 5 입니다. 사람의 눈에 비치는 「1 문자」에 대응하는 단위는 그래핌 클러스터이며, 이것을 세어야 비로소 1 이 됩니다. 많은 언어에 전용 API(JavaScript 의 Intl.Segmenter 등)가 있고, 제 3 장 시뮬레이터의 「눈에 보이는 문자 수」 란도 이 단위로 세고 있습니다.
이 장에서 가져갈 것
- UTF-16 은 16 비트의 코드 단위가 기본. BMP(U+FFFF 까지)는 1 단위, BMP 밖은 서로게이트 페어로 2 단위
- 「𠮷」 U+20BB7 = D842 DFB7, 「😀」 U+1F600 = D83D DE00
- C#·JavaScript 의 length 는 코드 단위 수. 「𠮷野家」는 4, 「😀😀😀」는 6
- 눈에 보이는 1 문자 = 그래핌 클러스터. 바이트·코드 단위·코드 포인트·그래핌의 4 가지 세는 방법을 구별한다
인코딩의 구조는 여기까지로 모두 나왔습니다. 다음 장부터는 실무편 — 「선언과 실체의 어긋남」이 낳는 현장의 트러블을 정리해 갑니다.