사전 Unicode
표준

Unicode

gabury1고친 사람 github-actions[bot]

Unicode 는 세상의 글자마다 번호를 하나씩 매겨 둡니다. 어느 나라 글자든 그 번호로 가리키게 해서, 서로 다른 프로그램이 같은 글자를 같은 것으로 읽게 합니다. 번호를 바이트로 적는 방법은 따로 정합니다. 그래서 한 문서 안에 한글과 아랍 문자와 이모지가 같이 들어갈 수 있습니다.

쉽고 빠른 이해

Unicode 는 글자 하나하나에 번호를 붙여 둔 표입니다. 영어 A 는 65번이고, 한글 한 도 자기 번호를 갖습니다.

이 표가 없으면 나라마다 다른 번호표를 씁니다. 그러면 같은 바이트가 한쪽에서는 한글로, 다른 쪽에서는 알 수 없는 기호로 읽힙니다. 화면에 글자가 깨져 뜨는 것이 그 결과입니다.

어떻게 도나:

  1. 글자 하나에 번호 하나를 정합니다. 한 번 정한 번호는 안 바뀝니다
  2. 그 번호를 바이트로 적는 방법을 따로 고릅니다
  3. 읽는 쪽은 바이트를 번호로 되돌리고, 번호로 글자를 찾습니다

대가도 있습니다. 번호 하나가 곧 글자 하나가 아닙니다. 악센트나 이모지처럼 번호 여럿이 모여 한 글자로 보이는 것이 있어서, 글자 수를 세는 일이 생각보다 까다롭습니다.

상세

이 절은 Unicode 가 무엇을 정하고 무엇은 안 정하는지를 한글 한 한 글자로 따라갑니다. 먼저 글자에 번호를 붙이는 일을 보고, 그 번호를 바이트로 적는 일이 왜 다른 일인지 봅니다.

이어서 번호 공간이 어떻게 생겼는지, 그리고 번호 하나와 눈에 보이는 글자 하나가 왜 어긋나는지를 봅니다. 마지막으로 백엔드에서 이 어긋남이 어디서 문제가 되는지를 짚습니다.

글자마다 붙는 번호

Unicode 이전에는 번호표가 나라마다 따로 있었습니다. 같은 바이트 값이 한국어 번호표에서는 한글로, 일본어 번호표에서는 다른 글자로 읽혔습니다.

그래서 문서를 주고받으려면 상대가 어느 번호표를 쓰는지 먼저 알아야 했습니다. 한 문서 안에 두 나라 글자를 같이 넣는 일은 아예 안 되기도 했습니다. Unicode 는 번호표를 하나로 합쳐 이 문제를 없앴습니다.

Unicode 가 하는 일은 하나입니다. 글자 하나에 번호 하나를 정해 주고, 한 번 정한 번호는 다시 안 바꿉니다. 이 번호를 코드 포인트라고 부릅니다.

적을 때는 U+ 를 앞에 붙이고 16진수로 씁니다. A 는 U+0041, 한 은 U+D55C 입니다. 파이썬에서는 ord 가 이 번호를 돌려줍니다.

Python
ord("A")        # 65
hex(ord("한"))   # '0xd55c'
chr(0xd55c)     # '한'

번호가 곧 그 글자의 정체입니다. 글꼴이 바뀌어도, 파일에 몇 바이트로 적히든 U+D55C 는 언제나 한 입니다.

번호와 함께 그 글자의 성질도 같이 정해집니다. 이 글자가 숫자인지, 짝이 되는 대문자가 있는지, 왼쪽에서 오른쪽으로 읽는지 같은 것들입니다. 프로그램이 「이 글자가 공백인가」를 물을 때 답을 주는 것이 이 성질 목록입니다.

번호를 정하는 일과 바이트로 적는 일

번호만 정해서는 파일에 못 씁니다. D55C 라는 수를 바이트 두 개로 적을지 세 개로 적을지, 어떤 순서로 적을지를 따로 약속해야 합니다. 이 두 번째 약속이 문자 인코딩입니다.

Unicode 는 첫 번째 약속만 맡습니다. 글자 목록을 정하고 번호를 붙이는 문자 집합 쪽입니다. 그 번호를 바이트로 옮기는 일은 UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트) 같은 인코딩이 맡습니다.

flowchart TD
    subgraph U["Unicode 가 정하는 것"]
        A["글자 · 한"] --> B["코드 포인트 · U+D55C"]
    end
    subgraph E["인코딩이 정하는 것"]
        C["바이트 · ED 95 9C"]
    end
    B --> C

그림의 아래 칸은 방법이 여럿입니다. UTF-16 은 두 바이트를 한 단위로 적고, UTF-32 는 모든 글자를 네 바이트로 적습니다. 셋 다 같은 번호를 씁니다.

그래서 「이 파일은 유니코드다」라는 말만으로는 파일을 못 읽습니다. 어느 인코딩으로 적혔는지까지 알아야 바이트를 번호로 되돌릴 수 있습니다. 인코딩을 잘못 짚어 글자가 깨지는 것을 모지바케라고 부릅니다.

번호 공간과 평면

번호는 U+0000 부터 U+10FFFF 까지입니다. 백십만 칸이 조금 넘습니다. 이 번호 공간을 65,536칸씩 끊은 토막을 평면이라고 부르고, 평면은 모두 열일곱입니다.

번호 범위 부르는 이름 주로 들어 있는 글자
U+0000 ~ U+FFFF 기본 다국어 평면 오늘날 쓰이는 문자 대부분
U+10000 ~ U+10FFFF 보충 평면 이모지 · 드문 한자 · 옛 문자

표의 위 줄이 먼저 찼고, 이모지처럼 나중에 들어온 글자가 아래 줄로 갔습니다. 한글과 한자, 라틴 문자는 모두 위 줄에 있습니다.

두 줄의 차이는 인코딩에서 드러납니다. UTF-16 은 위 줄의 글자를 두 바이트로 적고, 아래 줄의 글자는 두 단위를 짝지어 적습니다. 이 짝을 대리 쌍이라고 부릅니다.

번호가 U+10FFFF 에서 멈추는 까닭도 거기 있습니다. 대리 쌍으로 가리킬 수 있는 마지막 번호가 그것입니다. 번호 공간을 더 넓히면 UTF-16 이 못 따라옵니다.

글자 하나가 번호 하나가 아니다

눈에 보이는 글자 하나가 번호 하나라고 생각하기 쉽지만 그렇지 않습니다. é 를 적는 방법이 둘입니다. é 자체에 붙은 번호를 쓸 수도 있고, e 를 적은 뒤 악센트 기호만 따로 붙일 수도 있습니다. 뒤에 덧붙이는 이런 기호를 결합 문자라고 부릅니다.

Python
a = "é"   # é 한 글자
b = "é"  # e + 악센트 기호
a == b         # False
len(b)         # 2

두 문자열은 화면에 똑같이 보이는데 같지 않습니다. 글자 수마저 다릅니다. 이 어긋남을 없애려고 한쪽 꼴로 맞추는 일이 유니코드 정규화입니다.

미리 합쳐 두는 꼴을 NFC(Normalization Form C), 쪼개 두는 꼴을 NFD(Normalization Form D)라고 부릅니다. 비교하기 전에 양쪽을 같은 꼴로 옮겨 놓으면 됩니다.

Python
from unicodedata import normalize
normalize("NFC", b) == a  # True

번호 여럿이 모여 한 글자로 보이는 묶음에는 따로 이름이 있습니다. 그래핌 클러스터입니다. 사람이 「한 글자」라고 말할 때 가리키는 단위가 이것이고, 번호 개수와는 다릅니다. 가족 이모지 하나가 번호 일곱 개로 이루어진 경우도 있습니다.

ASCII 를 물려받은 앞쪽 128칸

ASCII(American Standard Code for Information Interchange)는 영어 글자와 숫자, 기호에 0번부터 127번까지 번호를 매긴 옛 약속입니다. Unicode 는 이 128칸을 번호까지 바꾸지 않고 물려받았습니다.

덕분에 옛 영어 문서는 번호를 하나도 안 고치고 유니코드 문서가 됩니다. UTF-8 이 이 칸을 한 바이트로 적기 때문에 파일 안의 바이트도 안 바뀝니다. Unicode 가 빠르게 퍼진 까닭 중 하나입니다.

백엔드에서 만나는 곳

문자열 길이를 셀 때 답이 셋입니다. 바이트 수, 번호 개수, 사람이 세는 글자 수가 서로 다릅니다. 데이터베이스 컬럼 길이가 어느 쪽을 재는지 안 맞으면 저장이 거절되거나 글자가 잘립니다.

정규화 꼴이 갈리면 눈에 같은 두 문자열이 안 맞습니다. 파일 이름을 합친 꼴로 저장하는 운영체제와 쪼갠 꼴로 저장하는 운영체제 사이에서 한글 파일이 안 찾아지는 일이 이래서 납니다. 검색 색인이나 로그인 아이디 비교도 같은 함정을 밟습니다.

대소문자 바꾸기와 정렬은 언어를 알아야 답이 나옵니다. 터키어에서 i 의 대문자는 I 가 아닙니다. 언어를 안 정하고 대소문자를 바꾸면 어떤 언어에서는 틀린 글자가 나옵니다.

마지막은 보안입니다. 키릴 문자 а 와 라틴 문자 a 는 번호가 다른데 화면에서는 구별이 안 갑니다. 도메인 이름이나 사용자 이름을 이렇게 흉내 내는 공격이 있어서, 겉보기가 같은 글자를 한 꼴로 맞춘 뒤 비교하는 검사를 따로 둡니다.

관련 항목

Unicode 가 정한 번호를 바이트로 적는 인코딩

UTF-8 · UTF-16 · UTF-32 · 문자 인코딩 · 대리 쌍 · 바이트 순서 표시 · 엔디언

Unicode 이전에 쓰이던 글자별 번호표

ASCII · EUC-KR · CP949 · ISO-8859-1 · Shift_JIS · windows-1252 · 코드 페이지

Unicode 에서 글자 하나를 이루는 구성 요소

코드 포인트 · 결합 문자 · 그래핌 클러스터 · 이모지 · 한중일 통합 한자

Unicode 가 번호를 나누어 담는 구획

기본 다국어 평면 · 보충 평면 · 사용자 지정 영역 · 제어 문자

Unicode 텍스트를 다룰 때 거치는 처리 단계

유니코드 정규화 · 인코딩 · 디코딩 · 콜레이션 · 대소문자 접기

Unicode 를 다룰 때 자주 나는 오류

모지바케 · 대체 문자 · 문자열 자르기 · 동형 문자 공격

Unicode 를 정의하고 관리하는 표준·단체

유니코드 컨소시엄 · ISO/IEC 10646 · 문자 집합 · 표준

Unicode 문자열을 기본으로 삼는 언어와 저장소

Java · Python · JavaScript · PostgreSQL · MySQL

Unicode 텍스트를 실어 나르는 포맷과 프로토콜

JSON · XML · HTML · HTTP · URI · 퍼센트 인코딩

다른 이름: 유니코드 · 유니코드 표준 · The Unicode Standard