유니코드와 UTF-8은 어떻게 다르고 한글은 왜 가끔 깨질까?

유니코드와 UTF-8은 어떻게 다르고 한글은 왜 가끔 깨질까?

웹 페이지나 데이터베이스에서 멀쩡하던 한글이 다른 사람에게서는 검색이 안 된다거나, macOS에서 만든 파일을 윈도우에 올렸더니 같은 이름인데도 다른 파일로 인식되는 경험이 있으실 텐데요. 이런 문제 대부분의 뿌리에는 유니코드(Unicode)와 정규화(normalization)라는 개념이 자리잡고 있습니다.

이번 포스팅에서는 UTF-8과 자주 헷갈리는 유니코드의 정체부터 짚어보겠습니다. 한글이 유니코드에서 어떻게 표현되는지, 같은 글자가 왜 두 가지 방식으로 저장될 수 있는지도 함께 살펴보겠습니다.

유니코드는 인코딩이 아닙니다

유니코드와 UTF-8을 같은 것으로 알고 계셨다면 사실 둘은 다른 차원의 개념인데요.

유니코드는 문자 집합(character set) 표준입니다. “한”이라는 글자에 U+D55C라는 고유 번호(코드 포인트)를 부여하는 약속이지, 이를 컴퓨터에 저장할 때 어떻게 바이트로 풀어내는지는 정의하지 않습니다.

반면 UTF-8은 인코딩(encoding) 방식입니다. 유니코드가 정한 코드 포인트를 실제 바이트 시퀀스로 변환하는 규칙인데요. UTF-16, UTF-32 같은 다른 인코딩 방식도 있고, 모두 같은 유니코드 표준을 다른 방식으로 표현하는 것입니다.

유니코드 (표준)  : "한" → U+D55C

UTF-8  (인코딩)  : U+D55C → 0xED 0x95 0x9C       (3바이트)
UTF-16 (인코딩)  : U+D55C → 0xD5 0x5C            (2바이트)
UTF-32 (인코딩)  : U+D55C → 0x00 0x00 0xD5 0x5C  (4바이트)

같은 글자라도 어떤 인코딩을 쓰느냐에 따라 다른 바이트로 저장되지만 코드 포인트는 모두 U+D55C로 동일합니다. “유니코드는 글자에 번호를 매기는 약속, UTF-8은 그 번호를 바이트로 풀어내는 방식”이라고 정리해두면 헷갈리지 않습니다.

유니코드 코드 포인트의 구조

유니코드는 U+0000부터 U+10FFFF까지 약 110만 개의 코드 포인트를 정의하고 있는데요. 이를 17개의 평면(plane)으로 나누어 관리합니다.

가장 중요한 평면은 BMP(Basic Multilingual Plane)입니다. U+0000부터 U+FFFF까지 6만 5천여 개의 코드 포인트를 담고 있고, 한글, 한자, 가나, 키릴 문자, 아랍 문자 등 거의 모든 일상적인 문자가 이 영역에 들어 있습니다.

평면 0  (BMP):     U+0000   - U+FFFF      (대부분의 일상 문자)
평면 1  (SMP):     U+10000  - U+1FFFF     (이모지, 고대 문자)
평면 2  (SIP):     U+20000  - U+2FFFF     (확장 한자)
...
평면 16:           U+100000 - U+10FFFF    (사용자 정의 영역)

🚀(U+1F680)처럼 BMP 바깥의 보조 평면에 위치하는 문자는 자바스크립트에서 길이가 이상하게 잡히곤 하는데, 이는 자바스크립트의 문자열이 UTF-16 기반이라 BMP 바깥 문자를 두 개의 코드 단위(서로게이트 페어)로 표현하기 때문입니다.

한글이 유니코드에서 자리잡은 방법

한글은 유니코드에서 두 가지 방식으로 표현됩니다.

첫 번째는 한글 음절(Hangul Syllables) 영역인데요. U+AC00부터 U+D7A3까지 11,172개의 코드 포인트가 한글 음절 하나하나에 직접 할당되어 있습니다.

가 = U+AC00
각 = U+AC01
간 = U+AC04
...
한 = U+D55C
힣 = U+D7A3

두 번째는 한글 자모(Hangul Jamo) 영역입니다. 초성(, , …), 중성(, , …), 종성(, , …)이 각각 별도의 코드 포인트로 정의되어 있고, 자모를 조합해서도 한글 음절을 만들 수 있습니다.

ㅎ = U+1112  (초성)
ㅏ = U+1161  (중성)
ㄴ = U+11AB  (종성)

여기서 한 가지 재미있는 점이 있는데요. “한”이라는 같은 글자가 두 가지 방식으로 표현될 수 있습니다.

방식 A (조합형 음절):  "한" = U+D55C                    (1개 코드 포인트)
방식 B (자모 분리):    "한" = U+1112 + U+1161 + U+11AB  (3개 코드 포인트)

화면에 그려지는 모양은 같지만 컴퓨터에는 완전히 다른 데이터입니다.

정규화: NFC와 NFD

같은 글자를 다르게 표현할 수 있다는 점이 정규화(normalization)가 필요한 이유인데요. 유니코드는 이런 표현을 일관되게 통일하기 위해 네 가지 정규화 형식을 정의하고 있습니다.

가장 널리 쓰이는 두 가지를 살펴보면 NFC(Normalization Form Composed)는 가능한 한 합쳐진 형태로 표현합니다. “한”을 U+D55C 한 코드 포인트로 표현하는 방식입니다.

반대로 NFD(Normalization Form Decomposed)는 자모 단위로 분해된 형태로 표현합니다. “한”을 U+1112 + U+1161 + U+11AB 세 코드 포인트로 풀어 표현합니다.

원본:        "한" (사용자 입력)
NFC (합성):  U+D55C
NFD (분해):  U+1112 U+1161 U+11AB

문자열을 비교하거나 검색할 때, 한쪽은 NFC고 다른 한쪽은 NFD라면 같은 글자라도 다른 문자열로 판단됩니다. 이것이 바로 “검색이 안 된다”거나 “같은 이름인데 다른 파일”로 인식되는 문제의 정체입니다.

macOS의 한글 파일명 함정

macOS는 파일 시스템에서 한글 파일명을 NFD 형태로 분해해서 다루는 경향이 있는데요. 윈도우나 리눅스는 NFC 합성형으로 다루기 때문에 macOS에서 만든 파일을 다른 OS에 올리면 정규화 불일치가 일어납니다.

macOS에서 만든 파일:  "한글.txt" → NFD: U+1112 U+1161 U+11AB U+...
다른 OS에 업로드한 뒤: "한글.txt" → NFC: U+D55C U+AE00

→ 시각적으로 같지만 바이트 시퀀스는 완전히 다름
→ Git이 두 파일로 인식하거나, 압축을 풀 때 깨진 글자로 보임

특히 macOS에서 작업한 파일을 GitHub 같은 원격 저장소에 올리거나 압축해서 윈도우, 리눅스로 보낼 때 파일명에 한글이 있으면 이런 문제가 자주 터집니다. 해결책은 두 가지인데요. 파일명을 영문으로 짓거나, NFC로 정규화한 뒤 다루는 것입니다.

자바스크립트의 String.normalize()

자바스크립트에는 문자열을 특정 정규화 형식으로 변환하는 String.prototype.normalize() 메서드가 내장되어 있습니다.

const composed = "한"; // 기본 입력은 보통 NFC
const decomposed = composed.normalize("NFD");

console.log(composed.length); // 1 (음절 1개)
console.log(decomposed.length); // 3 (자모 3개)

console.log(composed === decomposed); // false (같은 글자지만 다른 문자열)
console.log(composed === decomposed.normalize("NFC")); // true (NFC로 통일하면 같음)

normalize()는 인자로 "NFC", "NFD", "NFKC", "NFKD" 네 가지를 받을 수 있는데요. 사용자 입력을 비교하거나 검색할 때는 양쪽을 같은 형식(보통 NFC)으로 정규화한 뒤 비교하는 것이 안전합니다.

function sameText(a, b) {
  return a.normalize("NFC") === b.normalize("NFC");
}

sameText("한", "한"); // true (어떤 형식이든 동일하게 비교)

이 패턴은 한글이 들어가는 검색, 정렬, 중복 제거 같은 모든 문자열 작업에서 유용하게 쓰입니다. 특히 macOS에서 업로드된 파일명이나 사용자 입력을 처리하는 서버에서는 NFC로 한 번 정규화하는 단계가 거의 필수입니다.

마치며

지금까지 유니코드가 UTF-8 같은 인코딩과 어떻게 다른지부터 한글의 두 가지 표현 방식과 정규화의 필요성까지 살펴보았습니다.

유니코드와 인코딩의 구분, 그리고 NFC와 NFD 정규화는 한글을 다루는 한국 개발자가 한 번쯤은 부딪히는 주제인데요. 이 개념을 한 번 정리해두면 macOS와 다른 OS 사이에서 파일명이 어긋나거나, 데이터베이스에서 똑같이 보이는 한글이 매칭되지 않는 문제를 빠르게 진단할 수 있습니다.

더 자세한 명세는 Unicode Standard를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord