i18n, L10n 그리고 유니코드 (2)

등록일: 2014. 10. 30

i18n, L10n 그리고 유니코드 (2)

  • 지음
  • 0쪽
  • 0원
  • 1970년 1월 1일

유니코드란?

유니코드에 대해 많은 사람들은 유니코드가 무엇인지 그리고 소프트웨어 개발에서 어떠한 의미를 갖는지에 대해 선입관을 가지고 있다. 우리는 이러한 편견을 없애기 위한 방법으로 먼저 기본적인 원리부터 살펴볼 것이다. 그림 4.1보다 더 기본적인 표현을 하기는 어려울 것이다.

그림 4.1 | 문자 "a"

이 모양이 나타내는 것은 무엇일까? 이것은 영문 소문자 “a”이다. 글쎄. 실제로는 종이 위의 잉크로 그려진 (또는 이 내용이 전달되는 매개체에 따라 화면 상의 픽셀일 수도 있는) 하나의 형태로서 사람들이 동의하는 글자의 형태를 나타내고 있다. 이러한 형상을 상형 기호 또는 글리프(glyph)라고 부른다. 앞의 그림은 영문 소문자 “a”를 나타내는 여러 글리프 중 하나를 보여준다. 그림 4.2 또한 하나의 글리프이다.

그림 4.2 | 다른 글리프

조금 다른 모양과 곡선을 가지고 있지만 여전히 같은 문자를 나타낸다. 자, 여기까지는 간단했다. 각 문자는 다양한 형태의 글리프를 가지며 컴퓨터에서는 이러한 글리프들의 집합을 폰트라고 부른다. 일련의 문자들을 디지털화하여 저장할 때는 보통 문자만을 저장하고 글리프를 저장하지는 않는다. 어떤 폰트를 사용하여 문자를 글리프로 표현할 것인지를 같이 저장할 수 있지만 저장하는 주된 정보는 문자 자체다.

그렇다면 어떻게 영문 소문자 “a”에서 이진수 01100001로 비약적인 변경이 일어나는 것일까? 이러한 변경에는 (대개 하나로 묶이기는 하지만) 두 가지 변환 단계가 필요하다. 첫 번째 단계에서는 문자 집합이 어떻게 추상적인 문자들을 숫자들로 바꾸는지 정의하며, 두 번째 단계에서는 인코딩을 통해 어떻게 이러한 숫자들 또는 코드 포인트가 비트(bit)와 바이트(byte)로 변경되는지 정의한다. 자, 그럼 다시 확인해보자. 그림 4 3는 무엇일까?

그림 4.3 | 다시 문자 "a"에 관한 문제

이것은 영문 소문자 “a”를 상징하는 글리프이다. ASCII 문자 집합에서 “a”라는 영문 소문자는 0x61이라는 (십진수로 97) 코드 포인트 값을 가진다. 그리고 ASCII 인코딩은 0x61이라는 코드 포인트를 한 바이트의 0x61 값으로 표현할 수 있음을 알려준다.

유니코드는 ASCII와 매우 잘 호환될 수 있도록 설계되었으며 모든 ASCII 코드 포인트는 유니코드에서 같은 값을 가진다. 다시 말하자면 유니코드에서도 영문 소문자 “a”는 0x61이라는 코드 포인트 값을 가진다는 것이다. UTF-8 인코딩을 통해 0x61이라는 코드 포인트는 ASCII 인코딩과 마찬가지로 한 바이트의 0x61 값을 갖게 된다. UTF-16 인코딩에서는 0x61이라는 코드 포인트가0x00과 0x61이라는 두 바이트 값을 갖게 된다. 각종 유니코드 인코딩에 대해서는 잠시 후에 살펴볼 것이다.

표기(formatting) 관례

유니코드에서는 코드 포인트를 U+AAAA의 형식으로 표기하며 여기서 AAAA는 네 자리의 16진수 숫자를 의미한다. U+FFFF 이상의 코드 포인트는 표기를 위한 최소 자릿수를 표시해야 한다. 예를 들어, 카로슈티(Kharoshthi) 문자 “A”는 U+10A00으로 표현된다. 앞으로 이 책에서는 이러한 형식으로 코드 포인트를 표기할 것이며 인코딩된 바이트는 0xAA의 형식으로 표기할 것이다.

그렇다면 ASCII와 매우 비슷한 유니코드를 왜 사용할까? 그림 4 4와 같이 ASCII가 표현할 수 없는 문자를 통해 쉽게 그 해답을 찾을 수 있다.

그림 4.4 | ASCII에 없는 문자

이것은 벵골(Bengali Vocalic RR) 문자이며 유니코드에서 U+09E0이라는 코드 포인트 값을 가지고 있다. 이 코드 포인트는 UTF-8 인코딩을 통해 0XE0 0xA7 0xA0 바이트로 변환되며 UTF-16 인코딩을 따르면 0x09 0xE0 바이트로 변환된다.

유니코드 인코딩

유니코드 데이터를 저장하기 위한 인코딩에는 몇 가지가 있으며 고정폭(fixed width) 및 가변폭(variable width)의 두 가지 형식이 다 존재한다. 고정폭 인코딩은 모든 코드 포인트를 정해진 수의 바이트 내에서 표현하며 가변폭 인코딩은 문자별로 다른 개수의 바이트를 사용하여 표현한다.

UTF-32 및 UCS2는 고정폭 인코딩이고, UTF-7과 UTF-8은 가변폭 인코딩이며, UTF-16은 고정폭 인코딩처럼 보이지만 실은 가변폭 인코딩이다. UTF-32 (그리고 거의 동일한 UCS4) 인코딩은 각 코드 포인트를 4개의 바이트를 이용해 인코딩하므로 U+0000에서 U+FFFFFFFF까지 모든 코드 포인트를 인코딩할 수 있다. 현재로서는 그렇게 많은 코드 포인트가 정의되어 있지 않기 때문에 UTF-32는 과다한 감이 없지 않다. UCS2는 각 코드 포인트를 두 개의 바이트로 인코딩하므로 U+0000에서 U+FFFF까지의 모든 코드 포인트를 인코딩할 수 있다. UTF-16 또한 대부분의 문자를 두 개의 바이트로 인코딩하지만 U+D800부터 U+DFFF를 대행 바이트(surrogate pair)라는 이름으로 사용하여 U+0000에서 U+10FFFF까지의 코드 포인트를 인코딩할 수 있게 한다.

UTF-8은 한 개에서 네 개까지의 바이트를 사용하여 U+0000에서 U+10FFFF까지의 코드 포인트를 인코딩할 수 있다(ISO 10646 버전에서는 한 개에서 일곱 개까지의 바이트를 사용하여 U+0000에서 U+3FFFFFFFFFF까지의 코드 포인트를 인코딩한다). UTF-8에 대해서는 잠시 후에 좀 더 자세히 다루겠다. UTF-7은 7비트의 안전한 형태의 인코딩을 사용하므로 이메일에서 base64나 quoted-printable 인코딩의 도움 없이도 사용할 수 있다.[^5] 하지만 UTF-7은 많은 인기를 얻지 못했고 널리 사용되고 있지 않다. 그 이유는 UTF-7이 UTF-8과 같은 ASCII 투명성(transparency)[^6]이 없으며 QP(quoted-printable) 인코딩을 통해 UTF-8로 이메일을 전송해도 충분하기 때문이다.

자, 그러면 앞서 언급한 ISO 10646은 무엇인지 잠시 살펴보자. 유니코드는 너무나도 훌륭한 개념이었기 때문에 유니코드 협회(Unicode Consortium)와 국제 표준화 기구(ISO, International Organization for Standardization)의 두 집단이 동시에 작업을 시작하였다. 표준을 발표하기 전에 서로 통합을 하긴 했지만 그래도 여전히 별개의 이름을 고수하였다. 시간이 가면서 계속 동일한 내용을 유지했지만 이 두 표준은 별개의 문서로 발표되고 있으며 인코딩에 있어서는 약간의 차이를 보이고 있다. 이 책에서는 이 두 가지 표준을 하나의 같은 표준으로 간주할 것이다.

여기서 짚고 넘어가야 할 요점은 유니코드에는 (코드 포인트를 바이트로 변환하는) 다양한 인코딩이 있지만 (문자를 코드 포인트로 변환하는) 단 하나의 문자 집합만이 있다는 점이다. 이것은 유니코드의 핵심적인 개념이며 다시 말해 단 하나의 코드 포인트 집합을 모든 애플리케이션이 사용하고 일련의 다양한 인코딩을 제공하여 애플리케이션이 원하는 방식으로 데이터를 저장할 수 있다는 의미이다. 모든 유니코드 인코딩은 손실이 없으며 따라서 (UTF-16은 UTF-32가 표현할 수 있는 많은 사설 코드 포인트를 표현할 수 없다는 점을 무시한다면) 항상 코드 포인트와 바이트 간의 상호 변환을 정보의 손실 없이 수행할 수 있다. 유니코드에서 U+09E0은 저장에 사용하는 인코딩과 상관 없이 항상 벵갈 문자를 표시하는 것을 확신할 수 있다.

코드 포인트와 문자, 글리프와 문자소

앞서 문자는 사람들이 동의하는 의미를 지닌 기호이고 코드 포인트를 통해 표현된다고 하였다. 그리고 코드 포인트는 인코딩을 통해 하나 이상의 바이트로 표현될 것이다. 이렇게 간단하게 정리되면 좋겠지만 이게 끝이 아니다.

문자는 꼭 사람이 생각하는 방식의 문자로 표현되지는 않는다. 예를 들어, “ã”와 같이 물결 무늬를 가진 소문자 “a”는 U+00E3(“ã”)으로 표현하거나 소문자 ’a’인 U+0061과 물결 무늬인 U+0303의 두 개의 코드 포인트를 조립하여 표현할 수도 있다. 이처럼 한 개 이상의 문자로 (하나의 기반 문자와 0 개 이상의 조합 문자) 조립될 때 각 문자 단위를 문자소(grapheme)라고 한다.

이러한 상황은 합자(ligature)로 인해 더 복잡해진다. 합자는 두 개 또는 그 이상의 문자로 하나의 글리프를 만드는 것을 지칭한다. 각 문자는 하나의 코드 포인트나 두 개의 일반적인 코드 포인트로 표현될 수 있다. 예를 들어, (“f” 다음에 “i”가 오는) 합자 fi는 소문자 “f”의 U+0066과 점이 없는 소문자 “i”의 U+0131을 조합하여 표현하거나 또는 소문자 “fi”를 나타내는 U+FB01을 사용하여 표현할 수 있다.

그렇다면 이런 것들이 실무에서는 어떤 의미가 있을까? 그것은 바로 일련의 코드 포인트로 이루어진 데이터 스트림에서 임의로 (substring 함수 같이) 일부를 잘라내면 예상했던 문자소들을 얻을 수는 없다는 것이다. 또한 순서가 다른 합자를 이용하거나 (유니코드 정규화 규칙은 기능적으로 분해된 문자소의 비교를 허용하지만) 문자들을 조립하여 같은 모양의 문자소를 만드는 방법 등 하나의 문자소를 표현하는 데 한 가지 이상의 방법이 있음을 의미하기도 한다. 따라서 UTF-8을 통해 인코딩된 문자열의 길이, 즉 문자의 개수를 파악하기 위해 바이트의 개수를 셀 수는 없다. 또한 코드 포인트 값의 개수도 조합되는 문자들은 별도의 문자소가 되지 않기 때문에 도움이 되지 않는다. 이를 해결하기 위해서는 바이트 안에서 코드 포인트가 존재하는 위치와 해당 코드 포인트가 어떠한 문자 부류에 속하는지 알아야 한다. 유니코드에서 문자의 종류에 대한 정의는 다음의 표 4.1과 같다.

표 4.1 유니코드의 범용 카테고리

코드 설명
Lu 대문자
Ll 소문자
Lt 제목 스타일 문자 (대/소문자 조합)
Lm 한정자 문자 (쉼표, 크로스 악센트, 이중 프라임 등 이전 문자의 수정을 표시)
Lo 기타 문자 (고딕 문자 등의 기타 문자)
Mn 간격 없음 표시
Mc 결합 표시
Me 묶기 표시
Nd 10진수 숫자 (0-9)
Nl 글자 숫자
No 기타 숫자 (고대 이태리 숫자 등)
Zs 공백 구분선
Zl 줄 구분선
Zp 단락 구분선
Cc 기타 컨트롤 (탭 및 줄 바꿈 등 유니코드 제어 문자)
Cf 기타 서식 (양방향 제어 문자 등의 서식 지정 제어 문자)
Cs 대행 바이트 (surrogate pair)
Co 기타 전용 항목
Cn 지정되지 않은 기타 항목 (유니코드 문자와 매핑되지 않는 문자)
Pc 연결자 문장 부호
Pd 대시 문장 부호
Ps 열린 문장 부호 (열린 대괄호 및 중괄호)
Pe 닫힌 문장 부호 (닫힌 대괄호 및 중괄호)
Pi 처음 따옴표 (처음에 나오는 큰 따옴표)
Pf 마지막 따옴표 (작은 따옴표와 닫는 큰 따옴표)
Po 기타 문장 부호
Sm 수학 기호
Sc 통화 기호
Sk 한정자 기호
So 기타 기호

실제로 유니코드는 문자별로 범용 카테고리 외에도 더 많은 것을 정의하는데, 여기에는 이름, 일반 속성(알파벳, 표의 문자 등), 형태 정보(아랍어에서 양방향성, 좌우역전 등), 격(대/소문자 등), 숫자 값, 정규화 속성, 범위 및 그 외 많은 유용한 정보들이 있다. 대부분은 별로 신경 쓰지 않아도 될 내용들이며 백그라운드(background)에서 마법처럼 처리되기 때문에 이러한 정보들을 사용하고 있다는 점도 알아차리지 못할 것이다. 하지만 코드 포인트와 더불어 이러한 속성 정보들이 유니코드 규격의 핵심을 이루고 있다는 것을 주의할 필요는 있다.

이러한 속성과 특성, 정규화 규칙에 대해서는 유니코드 웹사이트에서^7 사람이 읽을 수 있는 형식과 기계가 읽을 수 있는 형식으로 두 가지 형태로 제공되고 있다.

바이트 순서 표기

바이트 순서 표기(BOM, Byte Order Mark)는 유니코드 문자열의 제일 앞에 쓰이는 바이트를 가리키며, 인코딩 방식을 나타내는 데 사용된다. 시스템이 사용하는 엔디언(endian) 방식이 빅 엔디언(big endian)이거나 리틀 엔디언(little endian)일 수도 있기 때문에 UTF-16과 같이 다중 바이트를 사용하는 인코딩은 코드 포인트 값을 어떤 엔디언 방식으로든지 저장한다. BOM은 이러한 목적으로 지정된 코드 포인트인 U+FEFF를 파일의 앞에 넣는 것으로 설정한다. 실제 바이트 값은 사용하는 인코딩에 따라 다르기 때문에 첫 네 개의 바이트를 읽어 사용하는 인코딩을 알아낼 수 있다(표 4.2 참조).

표 4.2 일반적인 유니코드 인코딩의 BOM

인코딩 BOM
UTF-16 빅 엔디언 FE FF
UTF-16 리틀 엔디언 FF FE
UTF-32 빅 엔디언 00 00 FE FF
UTF-32 리틀 엔디언 FF FE 00 00
UTF-8 리틀 엔디언 EF BB BF

SCSU, UTF-7, UTF-EBCDIC와 같은 다른 대부분의 유니코드 인코딩들도 고유의 BOM을 가지고 있으며 모두 U+FEFF 코드 포인트를 나타낸다. 일부 브라우저에서는 BOM이 장애를 일으키기 때문에 HTML이나 XML 문서에서는 BOM을 사용하는 것을 자제해야 한다. 또한 PHP는 BOM의 사용을 허용하지 않기 때문에 UTF-8로 인코딩되어 있더라도 PHP 템플릿이나 소스 코드 파일에서 BOM을 사용하는 것은 피해야 한다.

유니코드 규격에 대한 좀 더 자세한 내용에 대해서는 유니코드 협회 사이트^8를 방문하거나 유니코드 관련 서적^9을 구입하여 읽어보기 바란다.

[^5]: UTF-7은 ASCII 문자의 스트림으로 보낼 수 있도록 하여 base64 등의 인코딩의 도움 없이도 이메일 등에 바로 텍스트 형식으로 사용될 수 있다.

[^6]: 여기서 투명성(transparency)이란 사용자가 ASCII와 유니코드 인코딩에 대한 차이를 느끼지 못하는 것을 의미한다.