CS · Encoding · Unicode

아스키에서 유니코드로 - 번호부를 하나로 합치기까지

처음엔 글자 128개면 충분했다. 나라마다 남은 자리를 제 맘대로 쓰면서 같은 번호가 서로 다른 글자를 가리키게 됐고, 문서는 국경을 넘을 때마다 깨졌다. 유니코드는 그 혼란을 어떻게 끝냈나.

·0과 1로 어떻게 담나 5편
목차
  1. 처음엔 128개면 충분했다
  2. 남은 자리를 나라마다 다르게 썼다
  3. 문서는 국경을 넘을 때마다 깨졌다
  4. 그래서 세상의 모든 글자에 번호를 매겼다
  5. 번호를 매기는 층과 담는 층은 다르다
  6. 유니코드가 정하지 않은 것
  7. 실무에서: 아직도 옛 번호부를 만난다
  8. 정리

여기서부터 다시 글자 이야기다. 세상의 모든 글자에 번호를 매긴다는 발상이 어디서 왔는지 본다.

처음엔 128개면 충분했다

초창기 컴퓨터는 미국에서 영어를 다뤘다. 영어에 필요한 글자를 세어 보면 대문자 26개, 소문자 26개, 숫자 10개, 그리고 마침표와 괄호 같은 기호 몇십 개다. 다 합쳐도 백 개 남짓이다.

그래서 7비트, 즉 128개짜리 번호부를 만들었다. 이것이 아스키다.

번호글자
48 ~ 570 ~ 9
65 ~ 90A ~ Z
97 ~ 122a ~ z
32공백

번호를 아무렇게나 매기지 않았다. A부터 Z가 연달아 붙어 있어서 번호를 빼면 알파벳 순서가 나오고, 대문자와 소문자는 정확히 32 차이라 한 비트만 뒤집으면 대소문자가 바뀐다. 지금도 프로그래밍 언어들이 이 성질에 기대고 있다.

남은 자리를 나라마다 다르게 썼다

그런데 앞 글에서 봤듯 컴퓨터는 8비트, 즉 256칸을 쓴다. 아스키는 그중 절반만 채웠다. 남은 128칸이 문제의 시작이었다.

각 나라가 그 빈자리를 제 맘대로 채웠다. 서유럽은 é·ü 같은 글자를 넣었고, 러시아는 키릴 문자를 넣었고, 그리스는 그리스 문자를 넣었다. 같은 번호가 나라마다 다른 글자를 가리키게 된 것이다.

200 파일에 들어 있는 번호
하나뿐이다
서유럽 번호부 È 악센트 붙은 라틴 문자
키릴 번호부 И 러시아 문자
그리스 번호부 Θ 그리스 문자
한글·한자는 사정이 더 나빴다. 256칸에 애초에 안 들어가서 바이트 두 개를 이어 붙이는 방식을 따로 만들었다

번호는 하나인데 이 표, 저 표, 그 표가 서로 다른 글자를 가리킨다. 문서가 국경을 넘을 때마다 깨진 이유다.

이건 나라마다 전화번호부를 따로 만든 것과 같다. 번호 200으로 전화를 걸면 프랑스에서는 이 사람이 받고 러시아에서는 저 사람이 받는다. 번호는 같은데 가리키는 대상이 다르다.

한글은 사정이 더 나빴다. 256칸에는 애초에 안 들어간다. 한글 음절만 만 자가 넘고 한자까지 세면 수만 자다. 그래서 동아시아 국가들은 바이트 두 개를 이어 붙여 한 글자로 쓰는 방식을 따로 만들었다. 한국의 EUC-KR과 CP949, 일본의 Shift-JIS, 중국의 GB2312가 그것이다.

문서는 국경을 넘을 때마다 깨졌다

이 상태가 만든 결과는 예측 가능하다. 파일 하나에 두 나라 글자를 같이 담을 수 없었다. 한국어 번호부를 쓰면 러시아 글자가 들어갈 자리가 없고, 그 반대도 마찬가지다.

주고받는 것도 문제였다. 파일에는 어느 번호부를 썼는지 적혀 있지 않으니, 받는 쪽은 자기 번호부로 읽고 글자는 깨졌다. 웹이 퍼지면서 이 문제는 매일 겪는 일이 됐다.

방법은 하나뿐이었다. 번호부를 하나로 합치는 것.

그래서 세상의 모든 글자에 번호를 매겼다

유니코드가 한 일이 이것이다. 나라별 번호부를 이어 붙이는 대신, 세상의 모든 문자에 겹치지 않는 번호를 하나씩 부여했다.

이 번호를 코드포인트라고 부르고 U+ 뒤에 16진수로 적는다.

글자코드포인트십진수
AU+004165
U+D55C54,620
😀U+1F600128,512

아스키 번호를 그대로 물려받았다는 점이 중요하다. A는 유니코드에서도 65다. 기존 자산을 버리지 않겠다는 결정이었고, 다음 글에서 볼 UTF-8이 이긴 이유의 절반이 여기서 나온다.

지금 유니코드에는 15만 자가 넘게 등록되어 있다. 현대에 쓰는 문자뿐 아니라 고대 문자, 수학 기호, 그리고 이모지까지 전부 여기 들어 있다.

번호를 매기는 층과 담는 층은 다르다

여기가 이 시리즈에서 가장 자주 헷갈리는 자리다. 유니코드는 번호부일 뿐 저장 방식이 아니다.

글자
U+D55C유니코드가 정한 번호 (54,620)
여기까지가 번호부의 일 - 아래는 아직 안 정해졌다
UTF-8로 담으면 ED959C 3바이트
UTF-16으로 담으면 D55C 2바이트

번호는 하나로 정해졌다. 그 번호를 이렇게 담을지 저렇게 담을지는 다음 층의 결정이다.

의 번호가 54620이라는 건 정해졌다. 그런데 그 수를 파일에 어떻게 적을지는 아직 안 정해졌다. 3바이트로 적을 수도 있고 2바이트로 적을 수도 있다. 그 방법을 정하는 것이 UTF-8이고 UTF-16이다.

그래서 이런 말들은 정확하지 않다.

  • “유니코드로 저장했다” → 번호부만 정하고 담는 법은 안 말한 것이다
  • “유니코드와 UTF-8 중 뭐가 낫나” → 층이 다르다. 비교 대상이 아니다

“글자 → 번호”가 한 층이고, “번호 → 바이트”가 다른 층이다. 이 둘을 갈라 두면 나머지 글이 전부 쉬워진다.

유니코드가 정하지 않은 것

유니코드는 번호만 정한다. 나머지는 다른 데가 맡는다. 이걸 알아 두면 나중에 이상한 현상을 만났을 때 어디를 봐야 할지가 갈린다.

  • 글자의 생김새는 폰트가 정한다. 같은 U+D55C도 폰트에 따라 다르게 그려진다. 번호는 아는데 그릴 그림이 없으면 네모 상자가 뜬다. 이건 인코딩 사고가 아니라 폰트 사고다.
  • 정렬 순서는 지역 규칙이 정한다. 코드포인트 순서와 사람이 기대하는 사전 순서는 다르다. 독일어 äa 옆에 둘지 z 뒤에 둘지 같은 문제라, DB의 정렬 규칙 설정이 따로 있는 것이다.
  • 같아 보이는 두 글자가 같은 글자인지도 늘 자명하지 않다. 이건 뒤에서 한 편을 통째로 쓴다.

실무에서: 아직도 옛 번호부를 만난다

유니코드가 표준이 된 지 오래인데도 옛 번호부는 계속 나타난다. 만나는 자리가 정해져 있다.

  • 엑셀로 여는 CSV - 한국어 윈도우의 엑셀은 CSV를 열 때 여전히 옛 번호부(CP949)로 읽으려 든다. UTF-8로 저장한 파일이 엑셀에서만 깨지는 이유다. 앞 글에서 본 BOM을 일부러 붙여 “이건 UTF-8이다”라고 알려 주는 우회법을 쓰기도 한다.
  • 오래된 DB - 컬럼 문자셋이 옛 번호부로 잡혀 있으면 이모지 같은 글자를 넣는 순간 저장이 실패하거나 잘린다.
  • 파일 이름 - 운영체제마다 파일명을 담는 방식이 달라서, 압축 파일을 다른 OS에서 풀면 이름만 깨지는 일이 잦다.
  • 외부 연동 - 오래된 기관 시스템은 아직 EUC-KR로 응답을 준다. 받는 쪽이 UTF-8로 읽으면 통째로 깨진다.

대응 원칙은 앞 글과 같다. 경계마다 번호부 이름을 못 박는다. 안쪽은 전부 유니코드로 다루고, 옛 번호부는 들어오는 자리와 나가는 자리에서만 변환한다. 시스템 안쪽까지 옛 번호부를 끌고 들어오면 어디서 깨졌는지 영영 못 찾는다.

정리

  • 아스키는 128칸짜리 번호부로 시작했다. 대소문자가 한 비트 차이인 것 같은 성질이 지금도 쓰인다.
  • 남은 절반을 나라마다 다르게 채우면서 같은 번호가 다른 글자를 가리키게 됐다.
  • 한글·한자는 256칸에 아예 안 들어가 바이트 두 개를 이어 쓰는 방식이 따로 생겼다.
  • 유니코드는 세상의 모든 글자에 겹치지 않는 번호를 매겨 이 혼란을 끝냈다. 아스키 번호는 그대로 물려받았다.
  • 유니코드는 번호부일 뿐 저장 방식이 아니다. 번호를 바이트에 담는 건 다음 층의 일이다.
  • 실무에서는 안쪽은 유니코드, 변환은 경계에서만.

다음 글은 그 다음 층이다. 15만 개나 되는 번호를 바이트에 어떻게 욱여넣었길래 UTF-8이 세상을 다 먹었는지 본다.