Skip to Content

11장. 모든 문자에 번호를 — 유니코드와 코드포인트

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

유니코드는 모든 문자에 하나의 코드포인트를 주어 서로 다른 코드페이지의 혼선을 끝내고, UTF-8·16·32는 그 번호를 바이트로 옮기는 서로 다른 방식으로 분리했다.

면접 실전 질문: ① 유니코드와 UTF의 차이는? ② 코드포인트란 무엇인가? ③ 유니코드가 ASCII와 호환되는 이유는?


배경 — 표가 다른 게 문제라면

10장에서 모든 깨짐의 원인을 한 문장으로 못 박았죠. 인코딩 표가 서로 다르다. 그렇다면 해결책도 한 문장이에요. 모두가 똑같은 표 하나만 쓰면 된다. 나라마다 회사마다 다른 표를 만들어서 0xE9éИ도 됐던 거니까, 세상 모든 문자를 담은 단 하나의 표에 다 같이 합의하면 이 전쟁은 끝납니다.

말은 쉬운데, 정말 가능할까요? 지구상 문자를 다 합치면 십수만 개, 이걸 모순 없이 하나의 표에 담는 건 오랫동안 불가능해 보였습니다. 그런데 1980년대 말, 몇몇 엔지니어가 진짜로 이 일에 뛰어들어요.

스토리 — 유니코드의 탄생

1987년 무렵, 제록스와 애플의 엔지니어들이 이 발상을 정리하기 시작합니다. 제록스의 조 베커(Joe Becker)가 이 프로젝트에 유니코드(Unicode) 라는 이름을 붙였어요. “unique(고유), universal(보편), uniform(균일)“을 담은 이름이었죠. 1991년 유니코드 컨소시엄이 결성되고, 첫 표준이 세상에 나옵니다.

핵심 아이디어는 단순합니다. 세상 모든 문자에, 고유한 번호를 하나씩 부여하자. 이 번호를 코드포인트(code point) 라고 부르고, U+ 뒤에 16진수로 씁니다.

for c in ["A", "한", "가", "😀"]: print(f"{c} = U+{ord(c):04X}") # A=U+0041, 한=U+D55C, 가=U+AC00, 😀=U+1F600

AU+0041, 한국어 U+D55C, 웃는 이모지는 U+1F600. 전 세계가 이 번호 하나에 합의하면, U+D55C는 지구 어디서든 이에요. 러시아에서도 브라질에서도. 봉화 시대부터 이어진 “같은 신호 다른 약속표”의 저주가, 마침내 단 하나의 약속표로 풀리는 순간입니다.

처음엔 16비트, 그러니까 65,536개면 세상 문자를 다 담을 줄 알았어요(그래서 초기 유니코드는 16비트 기반이었죠). 하지만 한자, 특히 희귀·확장 한자까지 담으려니 그걸 넘어섰습니다. 그래서 코드 공간은 U+0000부터 U+10FFFF까지, 약 111만 개로 확장돼요. 이걸 65,536개씩 17개 평면(plane) 으로 나누는데, 가장 앞의 평면(U+0000~U+FFFF)을 기본 다국어 평면(BMP) 이라 부르고 대부분의 흔한 문자가 여기 삽니다. 이모지처럼 나중에 들어온 문자들은 그 바깥 평면에 있고요. (이 “16비트를 넘는 문자”가 다음 장의 핵심 사건이 됩니다.)

핵심 — 번호와 바이트는 다르다

여기서 유니코드의 진짜 천재성이 나옵니다. 이 책에서 제일 중요한 구분이니 밑줄 긋고 가세요.

유니코드는 “문자 ↔ 번호”를 확정하고, “번호 ↔ 바이트”는 별도의 층에 맡깁니다.

무슨 말이냐면, 유니코드는 “U+D55C번이다”라는 번호만 못 박아요. 이 U+D55C메모리나 파일에 실제로 몇 바이트로, 어떻게 저장할지는 코드포인트 자체가 정하지 않습니다. 코드포인트는 그냥 추상적인 번호예요. 이 번호를 진짜 바이트로 바꾸는 건 별도의 층, 곧 UTF-8, UTF-16, UTF-32 같은 인코딩이 맡습니다(다음 두 장의 주제죠). 같은 번호를 인코딩마다 다른 바이트로 담아요.

for enc in ["utf-8", "utf-16-be", "utf-32-be"]: print(enc, "한".encode(enc).hex()) # ed959c / d55c / 0000d55c # 같은 U+D55C인데 UTF-8은 3바이트, UTF-16은 2바이트, UTF-32는 4바이트. 문자는 똑같다.

이 분리가 왜 결정적일까요? 6~9장에서 EUC-KR이나 Shift-JIS는 “어떤 문자냐”와 “몇 바이트로 담냐”가 한 덩어리였어요. 그래서 문자를 추가하려면 바이트 구조까지 흔들렸고, 0x5C 충돌 같은 사고가 났죠. 유니코드는 이 둘을 깔끔하게 쪼갭니다.

  • 무엇을 담을까 (코드포인트): 유니코드가 정한다. =U+D55C. 전 세계 공통, 영원히.
  • 어떻게 담을까 (인코딩): UTF-8이든 UTF-16이든 골라 쓴다. 위에서 봤듯 바이트 수는 다르지만 가리키는 문자는 똑같다.

덕분에 “문자셋”은 유니코드 하나로 통일하면서, “바이트로 옮기는 방식”은 상황에 맞게 고를 수 있게 됐어요. 문자셋과 인코딩의 분리, 이게 인코딩 역사의 결정적 전환점입니다.

그리고 유니코드는 과거와 싸우지 않았어요. 첫 128개 코드포인트를 ASCII와 똑같이 맞췄거든요. A는 유니코드에서도 U+0041, 즉 65번. 첫 256개는 Latin-1과 일치하고요. 4장의 ASCII가 유니코드 안에 그대로 흡수된 거예요. 한글은 어떨까요? 7·8장에서 그렇게 애먹던 한글 음절 11,172자가, 유니코드에선 U+AC00(가)부터 U+D7A3(힣)까지 하나도 빠짐없이 연속으로 들어가 있습니다.

print(chr(0x41), chr(0xE9)) # A é — 첫 128=ASCII, 첫 256=Latin-1 print(chr(0xAC00), chr(0xD7A3)) # 가 힣 — 한글 블록의 시작과 끝 print(0xD7A3 - 0xAC00 + 1) # 11172 — 빠짐없이 연속

완성형의 2,350자 감옥도, 조합형과의 다툼도, 유니코드 앞에선 그냥 한 블록이에요.

정리

  • 모지바케의 원인이 “표가 다르다”라면, 해법은 모두가 쓰는 단 하나의 표 — 그게 유니코드(1987 명명, 1991 컨소시엄)다.
  • 유니코드는 세상 모든 문자에 고유 번호 코드포인트(U+XXXX)를 부여한다. A=U+0041, =U+D55C. 코드 공간은 U+0000U+10FFFF(약 111만), 17개 평면, 흔한 문자는 BMP (U+0000U+FFFF)에.
  • 가장 중요한 구분: 유니코드 = 문자↔번호 표(코드포인트)일 뿐, 번호↔바이트(인코딩)는 별개다. 문자셋과 인코딩을 분리한 게 유니코드의 결정적 도약. “무엇을 담을까”는 유니코드, “어떻게 담을까”는 UTF-8/16/32.
  • 하위 호환: 첫 128 코드포인트 = ASCII, 첫 256 = Latin-1. 한글 11,172자는 U+AC00~U+D7A3 연속 배치 — 7·8장의 완성형·조합형 문제가 한 블록으로 해소.

생각해볼 질문: 자, 번호는 정해졌어요. U+D55C. 그런데 이 번호를 컴퓨터에 저장 하려면 결국 바이트로 바꿔야 하잖아요? U+D55C는 2바이트에 들어가니 간단해 보여요. 그런데 아까 그 웃는 이모지 U+1F600은요? 이건 16비트(2바이트)를 넘어섭니다. “16비트면 충분할 줄 알았다”던 그 예상이 깨지는 순간, 번호를 바이트로 옮기는 일이 갑자기 까다로워져요. 표만 만들어서는 부족했던 겁니다.

12장 · 표만으로는 부족하다 — UTF-16과 서로게이트

Last updated on