rubrapack 매뉴얼←↑→

23 글자: 문자, 코드 포인트, 인코딩

파일은 바이트를 담고, 바이트는 수다(1장). 글 - 제품 이름, 파일 이름, 약관 - 은 문자마다 수를 주고 그 수를 바이트로 써서 저장한다. 둘 다 방법이 여러 가지이고, 방법을 잘못 고른 패키지는 깨진 글자를 보인다. 이 장은 rubrapack 과 Windows 가 쓰는 방법들을 설명한다.

23.1 문자와 코드 포인트#

유니코드는 세계의 문자 목록이다: 모든 문자 체계의 글자, 숫자, 문장 부호, 기호, 이모지 - 약 16만 개. 문자마다 코드 포인트라는 번호가 있고, U+ 와 16진수 네 자리 이상으로 쓴다:

문자코드 포인트목록의 이름
HU+0048LATIN CAPITAL LETTER H
éU+00E9LATIN SMALL LETTER E WITH ACUTE
€U+20ACEURO SIGN
한U+D55CHANGUL SYLLABLE HAN
😀U+1F600GRINNING FACE

코드 포인트는 U+10FFFF 까지 있다. 코드 포인트는 어떤 문자인지 말할 뿐, 어떤 바이트를 쓸지는 아직 말하지 않는다. 그것이 인코딩의 일이다.

23.2 ASCII#

아직도 어디에나 있는 가장 오래된 인코딩은 ASCII(1963년)다: 문자 128개, 코드 포인트 0~127, 한 문자에 한 바이트. 영어 글자, 숫자, 문장 부호와 제어 문자 몇 가지가 있다:

바이트문자
00-1f제어 문자: 09 탭, 0a 줄 바꿈(line feed), 0d 캐리지 리턴
20빈칸
30-390-9
41-5aA-Z
61-7aa-z
2e 2f 3a 5c. / : \

튜토리얼의 dist\docs\guide.txt 는 여섯 바이트다:

47 75 69 64 65 0a
 G  u  i  d  e  (new line)

유니코드의 처음 128 코드 포인트는 ASCII 의 것이고, 아래의 모든 인코딩이 그것들을 같은 한 바이트로 쓴다. 그래서 영어 글은 거의 어떤 인코딩으로든 똑같이 보이고 - 문제는 다른 글자에서만 드러난다.

Windows 는 줄 끝을 두 바이트 0d 0a(캐리지 리턴, 줄 바꿈)로, Linux 와 macOS 는 0a 하나로 쓴다. 대부분의 프로그램은 둘 다 읽는다.

23.3 코드 페이지: 한 바이트, 256 문자#

ASCII 는 바이트 값 128~255 를 쓰지 않는다. 수십 년 동안 언어권마다 그 자리를 다르게 채웠고, 채운 방식 하나하나가 Microsoft 가 번호를 붙인 코드 페이지다:

코드 페이지지역é€한
1252서유럽e980-
949한국어-a2 e6c7 d1
65001모두: 이것이 UTF-8 이다c3 a9e2 82 aced 95 9c

코드 페이지는 자기 지역의 문자만 담을 수 있다(949 는 한글에 두 바이트를 쓴다). 그리고 같은 바이트가 코드 페이지마다 다른 뜻이다: 바이트 e9 는 1252 에서는 é 지만 949 에서는 한국어 문자의 절반이다. 한쪽으로 쓰고 다른 쪽으로 읽은 글은 깨져 나온다. 흔히 "글자 깨짐"이라 하고, 영어로도 일본어 낱말 mojibake 로 부른다.

MSI 패키지는 모든 글에 코드 페이지 하나를 쓴다. rubrapack 은 늘 65001, UTF-8 로 써서 어느 언어든 패키지 하나에 들어간다. 튜토리얼 7장에서 inspect 가 그것을 보였다. 제4부는 함정 하나를 보인다: 요약 정보에는 코드 페이지가 따로 있다.

23.4 UTF-8#

UTF-8 은 모든 유니코드 코드 포인트를 1~4 바이트로 쓴다. ASCII 는 한 바이트 그대로이므로 ASCII 파일은 이미 UTF-8 이다. 더 큰 코드 포인트는 정해진 비트 무늬를 따라 바이트를 더 쓴다:

코드 포인트바이트비트 무늬(x = 코드 포인트의 비트)
U+0000 - U+007F10xxxxxxx
U+0080 - U+07FF2110xxxxx 10xxxxxx
U+0800 - U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000 - U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx

한, U+D55C 로 해 보면:

  D55C in binary:       1101 0101 0101 1100      (16 bits)
  split 4 + 6 + 6:      1101   010101   011100
  into the pattern:  1110 1101  10 010101  10 011100
  in hex:                ED         95         9C

그래서 한 은 UTF-8 로 ed 95 9c 다. 이 무늬 덕분에 UTF-8 은 확인하기 쉽다: 10 으로 시작하는 바이트는 결코 문자의 첫 바이트가 아니므로 읽는 쪽은 언제나 문자가 어디서 시작하는지 찾을 수 있고, 다른 인코딩의 바이트가 우연히 올바른 UTF-8 이 되는 일은 거의 없다. rubrapack 은 원본에서 무늬를 깨는 첫 바이트에서 멈춘다 (RP1001: not valid UTF-8). 짐작하지 않는다.

23.5 UTF-16#

UTF-16 은 U+FFFF 까지의 코드 포인트를 2바이트 단위 하나로, 나머지는 단위 둘(대리 쌍, surrogate pair)로 쓴다. Windows 는 안에서 이것을 쓴다: 파일 이름, 레지스트리, 복합 파일 디렉터리의 모든 글이 UTF-16 little-endian (2장)이다:

  H     e     l     l     o
  48 00 65 00 6c 00 6c 00 6f 00        "Hello" in UTF-16LE: ASCII letters get a 00 after them

  한
  5c d5                                U+D55C: the two bytes of D55C, little end first

  😀
  3d d8 00 de                          U+1F600: too big for one unit, so the pair D83D DE00

덤프에서 글자 사이사이에 점이 낀 것처럼 보이는 글 - H.e.l.l.o. - 은 UTF-16 이다.

23.6 바이트 순서 표시#

파일은 어떤 인코딩인지 밝히려고 코드 포인트 U+FEFF, 바이트 순서 표시(BOM)로 시작할 수 있다: UTF-8 은 ef bb bf, UTF-16LE 는 ff fe. Windows 메모장은 예전에 UTF-8 파일에 이것을 붙였다. rubrapack build 와 lint 는 BOM 이 있든 없든 UTF-8 원본을, 그리고 BOM 이 있는 UTF-16 원본을 읽는다. edit 은 UTF-8 원본만 고친다.

23.7 유니코드 정규화#

어떤 문자는 유니코드로 쓰는 방법이 둘 이상이다. é 는 코드 포인트 하나 U+00E9 이거나 - 둘이다: 글자 e U+0065 와, 그 앞 글자 위에 얹히는 결합 양음 부호 U+0301. 둘은 화면에서 완전히 똑같아 보인다:

형태코드 포인트UTF-8 바이트
합친 꼴(NFC)U+00E9c3 a9
푼 꼴(NFD)U+0065 U+030165 cc 81

가장 큰 예가 한국어다. 현대 한글 음절은 모두 합친 코드 포인트가 있고, 자모로 풀어 쓸 수도 있다:

형태코드 포인트UTF-8 바이트
NFCU+D55C 한ed 95 9c
NFDU+1112 ᄒ U+1161 ᅡ U+11AB ᆫe1 84 92 e1 85 a1 e1 86 ab

(합친 코드 포인트는 자모에서 계산된다: 0xAC00 + (18 x 21 + 0) x 28 + 4 = 0xD55C. 18, 0, 4 는 유니코드 표에서 ㅎ, ㅏ, ㄴ 의 번호다.)

정규화는 글을 합의된 한 형태로 바꾸는 것이다. NFC("합친 꼴")는 Windows, 웹, 거의 모든 자판이 만드는 형태다. NFD("푼 꼴")는 macOS 파일 시스템이 파일 이름을 저장하는 형태다 - 그래서 Mac 에서 복사한 파일의 이름은 Windows 에서 친 같은 이름과 바이트가 다를 수 있다. Windows 에게 둘은 다른 이름이다: 그러면 패키지가 한.txt 를 두 번 설치하거나, 바로가기가 대상을 놓칠 수 있다.

그래서 rubrapack 에는 파일, 폴더, 바로가기 이름을 NFC 로 바꾸는 build --nfc(튜토리얼 15장)가 있고, lint 는 NFC 가 아닌 글을 경고한다(RP2105, 튜토리얼 18장). rubrapack 의 정규화는 유니코드 17.0 표를 따른다.

23.8 글자가 이상하게 보일 때#

보이는 것흔한 원인
é 대신 éUTF-8 바이트 c3 a9 를 코드 페이지 1252 로 읽었다
한 대신 �븳 이나 ?UTF-8 바이트를 코드 페이지 949 로 읽었거나 그 반대
덤프에서 H.e.l.l.o.UTF-16 을 바이트로 읽었다
똑같아 보이는데 다른 두 이름하나는 NFC, 하나는 NFD

23.9 쓰이는 곳#