유니코드 정규화(NFC/NFD)는 눈에 똑같이 보이는 문자가 내부적으로 여러 코드포인트 조합으로 표현될 수 있다는 문제를 해결하기 위한 표준 변환이다. NFC는 결합해서 한 글자로, NFD는 낱자로 분해해 저장한다. 한글과 악센트 문자에서 특히 자주 사고를 낸다.
이 개념을 모르면 정말 귀신에 홀린다. 나는 "분명 같은 파일명인데 ===가 false"인 버그를 반나절 붙잡았다. 로그를 아무리 봐도 café와 café가 다를 이유가 없어 보였다. 원인은 화면 뒤편, 바이트에 있었다.
같아 보이지만 다른 바이트
악센트가 붙은 é를 예로 들자. 두 가지 표현이 있다.
- NFC(합성):
U+00E9한 개 — "é라는 완성된 글자" - NFD(분해):
U+0065(e) +U+0301(결합 악센트) 두 개 — "e에 악센트를 얹은 것"
화면엔 똑같이 é로 렌더된다. 하지만 문자열 길이도, 바이트 배열도, 해시도 다르다. 그래서 단순 비교가 어긋난다.
const nfc = "café"; // é 하나
const nfd = "café"; // e + ́
nfc === nfd // false !
nfc.length // 4
nfd.length // 5
nfc.normalize("NFC") === nfd.normalize("NFC") // true
해법은 명확하다. 비교하거나 저장하기 전에 한쪽 형태로 정규화하면 된다. 웹과 대부분의 텍스트 저장은 NFC가 사실상의 표준이다.
한글이 특히 위험한 이유
한글은 이 문제의 단골이다. 한이라는 글자는 완성형 U+D55C(NFC) 한 개일 수도, 초·중·종성 자모 ㅎ+ㅏ+ㄴ(NFD) 세 개로 분해될 수도 있다. 문제는 macOS 파일 시스템이다. macOS는 전통적으로 파일명을 NFD에 가까운 형태로 저장한다. 반면 Linux, Windows, 그리고 대부분의 웹 서버는 NFC를 쓴다.
그래서 이런 일이 벌어진다. Mac에서 만든 보고서.pdf를 zip으로 묶어 Linux 서버에 올렸더니, 다운로드 링크가 죽는다. DB엔 NFC로 저장된 파일명이, 실제 파일은 NFD로 존재하니 경로가 안 맞는 것이다.
나는 이걸 파일 업로드 기능에서 제대로 데였다. 대응은 업로드를 받는 즉시 파일명을 NFC로 정규화해 저장하는 것이었다. 입력 경계에서 한 번 통일해두면 이후로는 평화롭다.
정규화 형태 네 가지
| 형태 | 방식 | 쓰임 |
|---|---|---|
| NFC | 정준 합성 | 저장·전송 기본값 |
| NFD | 정준 분해 | macOS 내부, 자모 처리 |
| NFKC | 호환 합성 | 검색 색인·식별자 정규화 |
| NFKD | 호환 분해 | 느슨한 비교 |
K가 붙은 호환(compatibility) 형태는 더 공격적이다. 전각 A를 반각 A로, ①을 1로, 합자 fi를 fi로 바꾼다. 검색이나 아이디 중복 검사엔 유용하지만, 원문을 훼손하므로 표시용 데이터엔 함부로 쓰면 안 된다. 나는 검색 색인에만 NFKC를 쓰고, 보여줄 원문은 NFC로 따로 보관한다.
실무 원칙
- 입력 경계에서 정규화: 사용자 입력, 파일명, 외부 API 응답은 받는 즉시 NFC로 통일한다.
- 비교 전 정규화: 문자열 동등 비교, 정렬 키, DB 유니크 제약 대상은 정규화된 값을 쓴다.
- 표시용과 검색용을 분리: 원문은 NFC로 보존하고, 검색 매칭용으로 NFKC 사본을 따로 둔다.
데이터베이스도 이 함정을 그대로 물려받는다. 유니크 제약이 걸린 컬럼에 한쪽은 NFC, 다른 쪽은 NFD로 들어오면 DB는 둘을 다른 값으로 보고 중복을 허용해버린다. 사람 눈엔 같은 아이디인데 계정이 두 개 생기는 식이다. 그래서 나는 애플리케이션 계층에서 저장 직전에 NFC로 통일한 값만 DB로 내려보낸다. DB 콜레이션에 기대기보다 입력 경계에서 한 번 정규화하는 편이 훨씬 예측 가능했다. Git도 마찬가지라, macOS와 Linux 개발자가 섞인 팀에서는 파일명에 한글·악센트를 쓰면 같은 파일이 두 경로로 커밋되는 사고가 나기도 한다.
자주 묻는 질문
NFC와 NFD 중 뭘 기본으로 써야 하나요?
저장과 전송의 사실상 표준은 NFC입니다. 웹, 데이터베이스, 대부분의 시스템이 NFC를 기대하므로 입력을 받는 즉시 NFC로 정규화해 저장하면 대부분의 호환성 문제가 사라집니다. NFD는 macOS 파일 시스템이나 자모 단위 처리가 필요할 때 마주치는 형태입니다.
macOS에서 만든 파일이 Linux에서 깨지는 이유는요?
macOS는 파일명을 분해형(NFD에 가까운 형태)으로 저장하는 반면 Linux와 웹은 합성형(NFC)을 기대하기 때문입니다. 파일명은 같아 보여도 바이트가 달라 경로 매칭이 실패합니다. 업로드 시점에 파일명을 NFC로 정규화해 저장하면 해결됩니다.
NFKC는 언제 쓰면 안 되나요?
원문을 그대로 보존해야 하는 표시용 데이터에는 쓰면 안 됩니다. NFKC는 전각을 반각으로, 특수 기호를 일반 문자로 바꿔 원래 의미나 서식을 훼손할 수 있습니다. 검색 색인이나 식별자 중복 검사처럼 "느슨하게 같게 취급"하고 싶을 때만 별도 사본에 적용하세요.
문자열 길이가 이상하게 나올 때가 있어요.
분해형(NFD)으로 저장된 문자열은 결합 문자가 별도 코드포인트로 세어져 길이가 더 길게 나옵니다. 길이나 자릿수 검증을 하기 전에 NFC로 정규화하면 사람이 세는 글자 수와 대체로 일치하게 됩니다. 다만 이모지 등은 여전히 예외라 그래핌 단위 계산이 필요할 수 있습니다.

댓글 0