</>DevTools

U+유니코드 변환

유니코드 이스케이프와 텍스트 간 변환

유니코드 이스케이프와 코드포인트 다루기

\uD55C 같은 표기는 유니코드 코드포인트를 ASCII 문자만으로 적는 방법입니다. 소스 코드가 특정 인코딩에 의존하지 않게 하거나, JSON·프로퍼티 파일처럼 ASCII만 안전한 곳에 비ASCII 문자를 넣을 때 씁니다. 다루면서 가장 자주 부딪히는 지점은 이모지처럼 코드포인트가 큰 문자를 만났을 때 \u 하나로 표현되지 않는다는 사실입니다.

코드포인트, 코드 유닛, 서로게이트 페어

유니코드는 문자마다 고유 번호(코드포인트)를 부여합니다. '한'은 U+D55C, 이모지 😀는 U+1F600입니다. 문제는 JavaScript와 Java의 문자열이 UTF-16 기반이고, UTF-16의 기본 단위(코드 유닛)가 16비트라는 점입니다. 16비트로는 U+FFFF까지만 표현할 수 있습니다.

그래서 U+FFFF를 넘는 문자는 16비트 두 개를 짝지어 표현합니다. 이 짝을 서로게이트 페어라고 하며, 앞쪽은 U+D800~U+DBFF, 뒤쪽은 U+DC00~U+DFFF 범위를 씁니다. 😀는 \uD83D\uDE00 두 조각으로 적힙니다. ES6부터는 \u{1F600} 처럼 중괄호로 코드포인트를 직접 쓸 수 있어 훨씬 명확합니다.

문자코드포인트\u 표기 (UTF-16)ES6 표기UTF-8 바이트
AU+0041\u0041\u{41}41
éU+00E9\u00E9\u{E9}C3 A9
U+D55C\uD55C\u{D55C}ED 95 9C
😀U+1F600\uD83D\uDE00\u{1F600}F0 9F 98 80

사용 방법

  1. 변환할 텍스트를 입력하면 유니코드 이스케이프 표기를 얻습니다.
  2. 반대로 \uXXXX 형태의 문자열을 넣으면 원래 문자로 되돌립니다.
  3. 결과를 소스 코드나 JSON 값에 그대로 붙여 넣어 사용합니다.
  4. 이모지가 두 조각으로 나뉘어 나온다면 그것이 서로게이트 페어이며 정상입니다.

문자열 길이가 예상과 다른 이유

'😀'.length 는 1이 아니라 2입니다. length는 문자 개수가 아니라 UTF-16 코드 유닛 개수를 세기 때문입니다. 이 때문에 이모지가 포함된 입력에 글자 수 제한을 적용하면 사용자가 체감하는 것과 다르게 동작합니다.

더 나아가 사람이 하나로 인식하는 글자가 코드포인트 여러 개로 이루어진 경우도 많습니다. 가족 이모지 👨‍👩‍👧 는 사람 이모지 세 개를 ZWJ(U+200D)로 이어 붙인 것이고, 국기 이모지는 지역 표시 문자 두 개의 조합입니다. 한글도 조합형으로 표현하면 자모 여러 개가 한 글자를 이룹니다.

세는 방법에 따라 달라지는 길이
const s = "👨‍👩‍👧";

s.length                              // 8  (UTF-16 코드 유닛)
[...s].length                         // 5  (코드포인트)
new TextEncoder().encode(s).length    // 18 (UTF-8 바이트)

// 사용자가 체감하는 '글자' 단위로 세기
const seg = new Intl.Segmenter("ko", { granularity: "grapheme" });
[...seg.segment(s)].length            // 1

정규화(NFC/NFD)를 알아야 하는 이유

같은 글자를 유니코드로 표현하는 방법이 둘 이상인 경우가 있습니다. 'é'는 하나의 코드포인트 U+00E9로 쓸 수도 있고, 'e'(U+0065)와 결합 악센트(U+0301) 두 개로 쓸 수도 있습니다. 화면에는 똑같이 보이지만 === 비교는 false가 됩니다.

한국어 사용자에게 특히 중요한 사례가 macOS입니다. macOS 파일 시스템은 파일명을 NFD(분해형)로 저장하므로, 한글 파일명을 압축해 Windows에서 풀면 자모가 분리되어 보이는 현상이 생깁니다. 문자열을 비교하거나 저장할 때는 String.prototype.normalize("NFC")로 한 형태로 맞춰 두는 것이 안전합니다.

겉보기에 같지만 다른 문자열
const a = "가";                    // NFC: U+AC00
const b = "\u1100\u1161";          // NFD: ㄱ + ㅏ

a === b                            // false
a.normalize("NFC") === b.normalize("NFC")  // true

이런 작업에 씁니다

  • Java .properties 파일처럼 ASCII만 허용되는 설정 파일에 한글 넣기
  • 소스 코드 인코딩에 의존하지 않도록 비ASCII 리터럴을 이스케이프
  • API 응답에 \uXXXX로 인코딩되어 온 한글을 사람이 읽을 수 있게 복원
  • 제로 폭 문자나 특수 공백 등 눈에 보이지 않는 문자를 찾아내 확인
  • 이모지의 실제 코드포인트를 확인해 데이터베이스 저장 방식 결정

데이터베이스에서 이모지가 사라지는 문제

MySQL의 utf8 문자셋은 이름과 달리 코드포인트당 최대 3바이트만 저장하므로, 4바이트가 필요한 이모지를 넣으면 잘리거나 오류가 납니다. 이모지를 저장해야 한다면 문자셋을 utf8mb4로, 콜레이션을 utf8mb4_unicode_ci 또는 utf8mb4_0900_ai_ci로 지정해야 합니다. 커넥션 문자셋까지 함께 맞춰야 하며, 테이블만 바꾸고 커넥션을 두면 여전히 깨집니다.

자주 묻는 질문

\uXXXX와 \u{XXXXX}는 어떻게 다른가요?
\uXXXX는 16비트 코드 유닛 하나를 지정하므로 U+FFFF까지만 표현할 수 있고, 그보다 큰 문자는 서로게이트 페어 두 개로 써야 합니다. ES6에서 도입된 \u{...}는 코드포인트를 직접 지정하므로 \u{1F600} 한 번으로 이모지를 표현할 수 있습니다. 최신 JavaScript 환경이라면 후자가 읽기 쉽습니다.
JSON에 한글을 그대로 써도 되나요?
됩니다. JSON 규격은 UTF-8을 기본 인코딩으로 정하고 있어 한글을 직접 넣어도 유효합니다. JSON.stringify가 이스케이프하지 않는 이유입니다. 다만 일부 서버나 레거시 라이브러리가 ASCII만 안전하게 처리하는 경우가 있어, 그런 환경 대응용으로 \uXXXX 이스케이프를 쓰기도 합니다. 두 형태는 파싱 후 완전히 동일한 값입니다.
제로 폭 문자가 뭔가요?
폭이 없어 화면에 보이지 않는 문자입니다. U+200B(zero width space), U+200D(ZWJ), U+FEFF(BOM) 등이 있습니다. 웹에서 복사한 텍스트에 섞여 들어와 문자열 비교가 실패하거나 정규식이 매칭되지 않는 원인이 되곤 합니다. 이 도구로 이스케이프해 보면 눈에 보이지 않던 문자가 드러납니다.
이모지 하나가 코드포인트 여러 개인 이유는?
유니코드는 조합으로 새 표현을 만드는 방식을 씁니다. 피부색 변형은 기본 이모지에 피부색 수정자를 붙인 것이고, 가족 이모지는 사람 이모지들을 ZWJ로 연결한 것이며, 국기는 지역 표시 문자 두 개의 쌍입니다. 그래서 이모지 하나를 지우려면 백스페이스를 여러 번 눌러야 하는 앱이 있는 것입니다.
BOM은 지워도 되나요?
UTF-8에서는 대개 지우는 것이 좋습니다. UTF-8은 바이트 순서가 정해져 있어 BOM이 필요 없는데, 파일 맨 앞에 남아 있으면 JSON 파싱 오류, 셸 스크립트 실행 실패, PHP 출력 앞 공백 문제 등을 일으킵니다. UTF-16이라면 BOM이 바이트 순서를 알리는 실질적 역할을 하므로 함부로 지우면 안 됩니다.
왜 length로 글자 수를 세면 안 되나요?
length는 UTF-16 코드 유닛 개수라서 이모지 하나가 2 이상으로 계산됩니다. 사용자가 인식하는 글자 단위로 세려면 Intl.Segmenter의 grapheme 단위를 쓰는 것이 가장 정확합니다. 코드포인트 단위면 [...str].length 로 충분합니다.

💡 참고: 문자열 비교가 이유 없이 실패한다면 정규화 형태(NFC/NFD)가 다르거나 제로 폭 문자가 섞였을 가능성이 높습니다. 양쪽을 이스케이프해서 코드포인트를 직접 비교해 보세요.

🔗관련 도구🔄 텍스트/데이터 변환