유니코드 기본

코드 포인트, BMP·보조 평면, UTF-16 서로게이트 쌍, JavaScript·에디터에서의 처리

에디터와 IME가 다루는 텍스트는 유니코드로 표현된다. 코드 포인트·인코딩·서로게이트 쌍을 구분해야 문자열 길이·커서·선택을 올바르게 처리할 수 있다.


1. 코드 포인트와 스칼라 값

**코드 포인트(code point)**는 유니코드에서 한 문자에 부여한 번호다. 범위는 U+0000 ~ U+10FFFF(십진 0 ~ 1,114,111). 표기할 때는 U+ 뒤에 16진수 4~6자리를 쓴다(예: U+0041 = ‘A’, U+AC00 = ‘가’, U+1F600 = ’😀’).

**스칼라 값(scalar value)**은 유니코드 명세에서 “유효한” 코드 포인트를 이르는 말이다. U+D800U+DFFF 구간은 서로게이트 전용으로 예약되어 있어 문자에 할당되지 않으며, 이 구간을 제외한 나머지가 스칼라 값이다. 실무에서는 “한 코드 포인트 = 한 스칼라 값”으로 보면 된다(U+D800U+DFFF는 단독으로 문자를 나타내지 않음).


2. BMP와 보조 평면

**기본 다국어 평면(Basic Multilingual Plane, BMP)**은 U+0000 ~ U+FFFF 구간이다. 한 코드 포인트가 16비트 하나로 표현 가능하다.

**보조 평면(Supplementary Planes)**은 U+10000 ~ U+10FFFF 구간이다. 한글 완성형(U+AC00~U+D7A3), 라틴·히라가나·한자 대부분은 BMP 안에 있고, 많은 이모지·일부 한자·기호는 보조 평면에 있다. 보조 평면 문자는 UTF-16에서 16비트 코드 유닛 두 개(서로게이트 쌍)로 인코딩된다.


3. 서로게이트 쌍 (Surrogate pairs)

UTF-16은 16비트 단위(코드 유닛)로 텍스트를 저장한다. BMP(U+0000~U+FFFF)는 코드 유닛 1개로 표현하고, U+10000 ~ U+10FFFF는 코드 유닛 2개로 표현한다. 이 두 개의 코드 유닛을 서로게이트 쌍이라 한다.

  • High surrogate: U+D800 ~ U+DBFF (1,024개)
  • Low surrogate: U+DC00 ~ U+DFFF (1,024개)

코드 포인트 U( U+10000 ≤ U ≤ U+10FFFF )를 UTF-16으로 바꾸는 공식은 다음과 같다.

  1. U' = U - 0x10000 (0 ~ 0xFFFFF)
  2. High surrogate = 0xD800 + (U' >> 10)
  3. Low surrogate = 0xDC00 + (U' & 0x3FF)

역으로, high surrogate H, low surrogate L이 연달아 오면 코드 포인트는
0x10000 + ((H - 0xD800) << 10) + (L - 0xDC00) 이다.

서로게이트 구간(U+D800~U+DFFF)은 단독으로는 유니코드 문자를 나타내지 않는다. High만 있거나 low만 있거나, 순서가 바뀌면 잘못된 바이트 시퀀스로 간주된다.


4. JavaScript/웹에서의 영향

ECMAScript의 문자열은 UTF-16 코드 유닛 단위다. 따라서 보조 평면 문자(이모지 등)는 길이 2로 잡힌다.

"😀".length === 2;   // true
"가".length === 1;    // true (U+AC00은 BMP)

str[i]처럼 인덱스로 접근하면 코드 유닛 한 개만 반환한다. 서로게이트 쌍 한 글자 중간 인덱스면 high 또는 low surrogate만 나오므로, 그대로 출력하면 깨진 문자()가 나올 수 있다.

코드 포인트 단위로 다루려면 다음을 사용한다.

  • for...of: 문자열을 코드 포인트(확장 그래핀은 아님) 단위로 순회한다.
  • Array.from(str): 코드 포인트 단위로 분할한 배열을 만든다.
  • String.prototype.codePointAt(i): 인덱스 i에서 시작하는 코드 포인트를 반환한다(서로게이트 쌍이면 21비트 값).
  • String.fromCodePoint(cp): 코드 포인트 cp를 문자열로 만든다.

5. IME·에디터 관점

커서 위치·선택·한 글자 삭제·substring을 구현할 때 “한 글자”를 무엇으로 볼지 정해야 한다.

  • 코드 유닛: length, str[i], substring(start, end)가 기준으로 하는 단위. 서로게이트 쌍을 반으로 쪼개면 잘못된 문자가 나온다.
  • 코드 포인트: 이모지(단일 코드 포인트)나 BMP 문자를 “한 단위”로 셀 수 있다. 서로게이트 쌍을 쪼개지 않게 할 수 있다.
  • 그래핀 클러스터(grapheme cluster): 사용자가 “한 글자”로 인식하는 단위. 예: é(e + combining acute), 이모지 + variation selector, ZWJ 시퀀스(👨‍👩‍👧‍👦 등). 여러 코드 포인트가 묶여 한 개로 보일 수 있다.

에디터에서는 최소한 서로게이트 쌍을 쪼개지 않도록 해야 한다. Backspace/Delete로 “한 글자” 지울 때 코드 유닛 1개만 지우면 이모지가 반만 지워져 깨진 문자가 남는다. 커서를 “한 글자” 단위로 이동하려면 코드 포인트 단위(또는 그래핀 단위)로 경계를 계산해야 한다. 그래핀 단위 분할이 필요하면 Intl.Segmenter (granularity: 'grapheme')를 사용할 수 있다. 여러 코드 포인트로 이루어진 사용자 인식 문자(이모지 조합 등)는 이모지 문서를 참고하면 된다.


6. 인코딩 요약

인코딩단위BMP 문자보조 평면 문자(U+10000~)
UTF-81~4바이트1~3바이트4바이트
UTF-1616비트 코드 유닛1개2개(서로게이트 쌍)

유니코드 정규화(NFC, NFD)는 같은 문자를 다른 바이트/코드 포인트 시퀀스로 표현할 수 있게 한다. 한글·유럽어에서의 NFC/NFD 동작은 한글 조합 원리, 유럽권 조합을 참고하면 된다.