코드 포인트
고친 사람 github-actions[bot]
코드 포인트는 세상의 글자마다 하나씩 붙여 둔 번호로, 프로그램이 글자를 모양 대신 번호로 주고받게 해 줍니다. 어느 프로그램이든 같은 번호를 받으면 같은 글자로 읽습니다. 이 번호를 파일에 어떤 바이트로 적을지는 인코딩이 따로 정합니다.
쉽고 빠른 이해
글자 하나에 번호 하나를 붙이는 약속입니다. 영문 A 는 65번, 한글 한 은 54,620번입니다.
번호표가 나라마다 따로 있으면 같은 번호를 두고 나라마다 다른 글자로 읽습니다. 온 세상이 번호표를 한 벌로 맞춰 두면 받은 쪽이 글자를 잘못 읽을 일이 줄어듭니다.
어떻게 도는가:
- 글자마다 번호를 하나씩 정해 둡니다
- 프로그램은 글자를 그 번호로 다룹니다
- 저장하거나 보낼 때는 번호를 바이트 몇 개로 바꿔 적습니다
대가도 있습니다. 번호 하나를 적는 바이트 수가 글자마다 다릅니다. 눈에 보이는 글자 하나가 번호 여러 개로 이루어지기도 합니다. 그래서 문자열 길이를 무엇으로 세느냐에 따라 답이 갈립니다.
문자열을 자를 때는 바이트가 아니라 번호의 경계, 할 수 있으면 눈에 보이는 글자의 경계에 맞춥니다. 길이를 제한할 때는 화면과 저장하는 쪽이 같은 단위로 세게 맞춥니다.
상세
식당 메뉴판에서 음식마다 번호가 붙어 있다고 해 보겠습니다. 손님이 「12번 주세요」라고 하면 메뉴 이름이 어떤 글씨체로 적혀 있든 주방은 같은 음식을 만듭니다. 주문서에 12를 숫자로 적든 「십이」로 적든 가리키는 음식은 하나입니다.
이 절은 코드 포인트를 A · 한 · 웃는 얼굴 이모지 세 글자로 따라갑니다. 번호가 어떻게
생겼는지, 어디까지 있는지, 바이트와 어떻게 다른지를 차례로 봅니다. 마지막 두 소절은 번호와
바이트, 눈에 보이는 글자가 서로 다르다는 점이 백엔드 코드에서 어떤 고장을 내는지 봅니다.
글자에 붙은 번호
Unicode(유니코드)는 세상의 문자를 한 표에 모아 글자마다 번호를 매긴 표준입니다. 그 번호 하나하나를 코드 포인트라고 부릅니다. 메뉴판의 번호에 해당합니다.
코드 포인트는 U+ 뒤에 십육진수를 붙여 적습니다. 네 자리보다 짧으면 앞을 0으로 채웁니다.
A 는 U+0041, 한 은 U+D55C, 웃는 얼굴 이모지는 U+1F600 입니다. U+0041 을 십진수로
바꾸면 65, U+D55C 는 54,620입니다.
이 번호는 글자의 모양과 상관이 없습니다. A 를 어떤 글꼴로 그리든 번호는 U+0041 하나입니다.
모양은 글꼴이 정합니다. 번호는 유니코드가 정합니다.
번호가 한 벌로 통일된 데는 까닭이 있습니다. 예전에는 나라마다 따로 만든 문자 집합이 같은 번호에 서로 다른 글자를 두었습니다. 보낸 쪽과 받은 쪽이 다른 표를 쓰면 글자가 깨졌습니다. 유니코드는 모든 문자를 한 번호 공간에 넣어 이 어긋남을 없앴습니다.
코드 포인트라는 말은 번호를 매긴 문자 집합이면 어디서나 씁니다. 실무에서는 대개 유니코드의 번호를 가리킵니다. 이 문서도 그 뜻으로 씁니다.
번호가 있는 범위
코드 포인트는 U+0000 부터 U+10FFFF 까지 있습니다. 십진수로 111만 개가 조금 넘습니다. 이
범위 전체를 번호 공간이라고 부릅니다.
번호 공간은 65,536개씩 열일곱 토막으로 나뉩니다. 한 토막을 평면이라고 합니다. 맨 앞 평면
U+0000 부터 U+FFFF 까지가 기본 다국어 평면입니다. 한글 음절과 라틴 문자, 자주 쓰는 한자가
이 평면에 들어 있습니다.
그 뒤의 열여섯 평면을 보충 평면이라고 부릅니다. 이모지와 옛 문자, 드물게 쓰는 한자가
여기 있습니다. 웃는 얼굴 이모지 U+1F600 도 보충 평면의 글자입니다.
번호 공간을 펴 보면 이렇습니다.
block-beta columns 1 a["U+0000 ~ U+D7FF · 기본 다국어 평면 · 글자가 있다"] b["U+D800 ~ U+DFFF · 기본 다국어 평면 · 글자를 두지 않는 구간"] c["U+E000 ~ U+FFFF · 기본 다국어 평면 · 글자가 있다"] d["U+10000 ~ U+10FFFF · 보충 평면 열여섯 개"]
가운데 U+D800 부터 U+DFFF 까지는 어떤 글자에도 주지 않는 번호입니다. 이 구간을 어디에
쓰는지는 다음 소절 「번호와 바이트」에서 봅니다.
번호 공간에 있다고 모두 글자가 정해진 것은 아닙니다. 아직 비어 있는 번호가 훨씬 많습니다. 새 글자가 들어오면 빈 번호를 받습니다. 한 번 준 번호는 다른 글자에게 넘기지 않습니다.
번호와 바이트
코드 포인트는 수일 뿐입니다. 파일이나 네트워크에 담으려면 이 수를 바이트로 적는 규칙이 따로 필요합니다. 그 규칙이 문자 인코딩입니다.
인코딩은 번호를 일정한 크기의 단위로 끊어 적습니다. 그 단위 하나가 코드 유닛입니다. 코드 유닛의 크기는 인코딩마다 다릅니다. 번호 하나를 코드 유닛 몇 개로 적느냐가 인코딩마다 갈리기 때문에 이 단위를 따로 셉니다.
UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트)은 한 바이트를 코드 유닛 하나로 씁니다. 번호가 작으면 한 바이트, 크면 네 바이트까지 씁니다.
UTF-16 은 이름의 숫자대로 16비트, 곧 두 바이트를 코드 유닛 하나로 씁니다. 기본 다국어 평면의 글자는 코드 유닛 하나로 적습니다. 보충 평면의 글자는 코드 유닛 둘로 나눠 적습니다.
세 글자를 두 인코딩으로 적은 값입니다. 값은 모두 십육진수입니다.
| 글자 | 코드 포인트 | UTF-8 바이트 | UTF-16 코드 유닛 |
|---|---|---|---|
A |
U+0041 |
41 |
0041 |
한 |
U+D55C |
ED 95 9C |
D55C |
| 웃는 얼굴 | U+1F600 |
F0 9F 98 80 |
D83D DE00 |
코드 포인트 열은 세 줄 모두 번호 하나입니다. 오른쪽 두 열은 인코딩마다 개수가 달라집니다. 같은 글자라도 인코딩이 다르면 바이트가 다릅니다. 번호는 그대로입니다.
웃는 얼굴의 UTF-16 값 D83D 와 DE00 은 둘 다 앞에서 비워 둔 U+D800~U+DFFF 구간에 듭니다.
보충 평면의 번호를 둘로 나눠 이 구간의 값 두 개로 적은 것입니다. 이 코드 유닛 두 개 묶음을
대리 쌍이라고 합니다. 이 구간을 글자에 주지 않는 것은 대리 쌍의 반쪽을 글자와 헷갈리지 않게
하려는 것입니다.
번호 공간이 U+10FFFF 에서 끝나는 것도 대리 쌍 때문입니다. 대리 쌍으로 가리킬 수 있는 마지막
번호가 U+10FFFF 입니다. 번호 공간의 끝을 여기에 맞췄습니다.
글자 하나가 번호 여럿일 때
눈에 글자 하나로 보여도 코드 포인트는 여럿일 수 있습니다. 앞 글자에 얹혀 모양을 바꾸는 코드 포인트가 있기 때문입니다. 이런 것을 결합 문자라고 부릅니다.
é 가 그 예입니다. 번호 하나 U+00E9 로 적을 수 있습니다. e(U+0065) 뒤에 위에 얹는 강세
부호 U+0301 을 붙여 둘로 적을 수도 있습니다. 화면에는 똑같이 나옵니다.
한글도 마찬가지입니다. 한 은 완성된 음절 하나 U+D55C 로 적을 수 있습니다. 첫소리 ᄒ
(U+1112), 가운뎃소리 ᅡ(U+1161), 끝소리 ᆫ(U+11AB) 세 번호를 이어 적을 수도 있습니다.
눈에는 같아도 번호 열이 다르면 프로그램은 다른 문자열로 봅니다. 로그인 아이디나 검색어를 견줄 때 같은 글자가 안 맞는 일이 여기서 생깁니다. 비교 전에 한쪽 모양으로 번호 열을 맞추는 일을 유니코드 정규화라고 합니다.
이모지도 번호를 여럿 이어 붙입니다. 엄지를 든 손 U+1F44D 뒤에 피부색 번호 U+1F3FD 를
붙이면 피부색이 바뀐 손 하나로 그려집니다.
이렇게 눈에 글자 하나로 보이는 묶음을 그래핌 클러스터라고 합니다. 사람이 「몇 글자」라고 셀 때 세는 것이 이 묶음입니다. 아래에서는 이것을 「보이는 글자」라고 줄여 씁니다.
문자열 길이를 세는 단위
지금까지 나온 단위가 넷입니다. 바이트, 코드 유닛, 코드 포인트, 보이는 글자입니다. 문자열 길이를 물으면 이 넷 중 무엇으로 셌는지에 따라 답이 달라집니다.
Java 의 String 은 문자열을 UTF-16 코드 유닛의 열로 봅니다. 그래서 length() 는
코드 유닛 수를 돌려줍니다. 코드 포인트 수가 필요하면 codePointCount() 를 따로 부릅니다.
웃는 얼굴 이모지 하나를 세 가지로 세 보겠습니다.
String s = "😀";
s.length() // 2
s.codePointCount(0, s.length()) // 1
s.getBytes(UTF_8).length // 4
첫 줄은 UTF-16 코드 유닛 둘을 셌습니다. 둘째 줄은 코드 포인트 하나를 셌습니다. 셋째 줄은 UTF-8 로 적었을 때의 바이트 넷을 셌습니다.
글자를 몇 개 더 넣으면 네 단위가 이렇게 갈립니다.
| 문자열 | UTF-8 바이트 | UTF-16 코드 유닛 | 코드 포인트 | 보이는 글자 |
|---|---|---|---|---|
A |
1 | 1 | 1 | 1 |
한 |
3 | 1 | 1 | 1 |
| 웃는 얼굴 | 4 | 2 | 1 | 1 |
é 를 둘로 적은 것 |
3 | 2 | 2 | 1 |
| 피부색이 바뀐 엄지 | 8 | 4 | 2 | 1 |
맨 오른쪽 열은 모든 줄이 1입니다. 네 단위가 모두 1로 맞는 것은 A 한 줄뿐입니다. 한 부터는
어느 단위로 세느냐에 따라 값이 벌어집니다.
이 차이는 길이 제한에서 드러납니다. 화면의 입력 칸은 보이는 글자로 열 개까지 받는다고 해 보겠습니다. 데이터베이스 컬럼은 20바이트까지 받는다고 해 보겠습니다.
한 열 개는 입력 칸을 통과합니다. 하지만 UTF-8 로 적으면 30바이트라 컬럼에 들어가지 않습니다.
화면과 컬럼이 같은 단위로 세야 이런 일이 없습니다.
자를 때도 조심해야 합니다. 앞의 s 에 s.substring(0, 1) 을 부르면 코드 유닛 위치 1에서
자릅니다. 대리 쌍의 앞쪽 코드 유닛 하나만 남습니다. 반쪽만 남은 코드 유닛은 어떤 글자도 아니어서
화면에 깨진 글자가 나옵니다.
그래서 자르는 위치는 코드 포인트 경계, 가능하면 보이는 글자의 경계에 맞춥니다.
관련 항목
코드 포인트를 매기는 표준과 문자 집합
Unicode · ISO/IEC 10646 · 문자 집합 · ASCII · 유니코드 블록 · EUC-KR
코드 포인트를 바이트로 적는 인코딩
문자 인코딩 · UTF-8 · UTF-16 · UTF-32 · UCS-2 · 바이트 순서 표시
번호 공간을 나누는 구간
기본 다국어 평면 · 보충 평면 · 대리 쌍 · 사용자 정의 영역 · 비문자
코드 포인트와 헷갈리는 세는 단위
코드 유닛 · 바이트 · 옥텟 · 그래핌 클러스터 · 글리프
코드 포인트 여럿으로 글자 하나를 이루는 방식
결합 문자 · 유니코드 정규화 · 한글 자모 · 제로 폭 결합자 · 이모지
코드 포인트를 잘못 다뤄 나는 문제
모지바케 · 대체 문자 · 문자열 자르기 · 인코딩 불일치 · 보이지 않는 문자
코드 포인트를 견주고 바꾸는 문자열 연산
문자열 · 문자열 비교 · 정규 표현식 · 대소문자 변환 · 콜레이션
문자열 길이를 코드 유닛으로 세는 언어와 타입
Java · JavaScript · C · char
다른 이름: code point · codepoint · 코드포인트