유니코드 #1 DarkKaiser, 2017년 9월 14일2023년 9월 5일 출처 : http://www.bsidesoft.com/?p=3435 개요 본래 우리가 작성한 문서에 있는 문자들은 그대로 저장될 수는 없습니다. 반드시 숫자로 바뀐 후 저장되죠. 따라서 문자를 숫자로 바꿔주는 표가 꼭 필요합니다. 이러한 문자를 숫자로 바꿔주는 표 중에 가장 유명한 건 아스키표입니다. 아스키표를 사용하면 영어, 숫자, 기초적인 기호들에 대해 고유한 숫자 값을 부여해 변환할 수 있습니다. 하지만 한국어나 일본어, 중국어 등 아스키표에 포함되지 않은 문자들은 각 나라마다 별도의 변환표를 만들어서 표준으로 지정했습니다. 보통 이러한 개별 표들은 아스키와 호환되면서 자기 나라 말이 잘 표현될 수 있도록 정의됩니다. 따라서 ‘영어 + 한국어’, ‘영어 + 일본어’는 잘 표현되지만, ‘영어 + 한국어 + 아랍어’는 깨지기 마련입니다. 한국에서 정의한 표에 아랍어를 위한 코드가 없기 때문이죠. 이를 해결하기 위해서는 전세계 모든 문자를 변환할 수 있는 거대한 표가 필요할 것입니다. 이러한 거대한 표 중 하나가 유니코드입니다. 웹사이트나 앱이 여러 나라에서 사용되게 만들려면 다양한 전략이 수반되어야 하는데, 단지 문자를 표현하는 것 외에도 도량형이나 번역, 상황 별 표현 등을 처리해야 합니다. 이러한 다국어 서비스의 기초가 되는 유니코드 문자열 처리에 대해서 알아봅니다. 여러 편에 걸쳐 차근차근 유니코드에 대해 학습한 뒤 자바스크립트에서 어떻게 유니코드가 쓰이는 지를 살펴볼 예정입니다. 유니코드는 체계적인 학문이라기보다 프로토콜에 가깝습니다. 즉 논리나 원리보다 그냥 외워야 하는 부분이 상당합니다. 유니코드와 관련된 용어를 완전히 습득할 때까지 반복해서 공부할 필요가 있습니다. 응? …그냥 외워야 해. 유니코드란? 유니코드는 근본적으로 전 세계의 고유한 문자 하나 하나를 숫자로 코드화한 것입니다. 애플과 제록스는 유니코드표준에 대한 조직결성과 표준안에 대한 협의를 진행했고, 1991년에는 유니코드 1.0이 발표되었습니다. 1.0이 발표된 뒤 1년 만에 2.0이 발표되면서 현재에 이르고 있습니다. 제록스의 조 베커와 애플의 리 콜린스, 마크 데이비스가 1987년에 만났습니다! 유니코드 1.0 최초 발표된 유니코드 1.0은 65,536개의 문자를 포함하는 규격입니다. 한국어, 일본어, 중국어를 포함하여 많은 언어에 관련된 문자가 포함되어 있습니다. 하지만 유니코드는 이 시점부터 표준을 만들어가는 성격을 띄게 되었으므로 유니코드 1.0에 포함되지 않은 문자에 대해 오히려 많은 클레임을 받게 되었습니다. 이는 표준안을 만들어가려는 입장에서 당연히 해결해야하는 문제였고 신속하게 대처하여 1년만에 개정하게 됩니다. 유니코드 2.0 유니코드 2.0은 1.0에 비해 17배나 많은 문자를 표현할 수 있는 방대한 규격입니다. 백만이상의 문자를 포함할 수 있으며 거의 모든 지구상의 문자를 표현할 뿐만 아니라 미래를 위해서도 충분한 예비공간을 확보하는 규격입니다. 현재 세계적으로 사용되고 있는 유니코드는 유니코드 2.0의 체계 하에 꾸준히 업데이트 되고 있습니다. 유니코드의 기본 유니코드의 규격을 이루는 수많은 개념과 용어가 있습니다. 모든 규격문서가 그러하듯 유니코드에서도 기본 개념을 이루는 용어의 의미를 이해하고 암기해야 전체적인 흐름을 따라갈 수 있습니다. 하지만 한 번에 용어를 정복하려고 해도 그 용어가 다른 용어의 이해를 전재하고 있는 경우가 많으므로 기초부터 차근차근 학습해보죠. 머..쩔 수 없지. 코드포인트 용어 중 가장 기본이자 쉬운 개념은 코드포인트입니다. 코드포인트란 문자에 부여한 고유한 숫자값입니다. 유니코드라는게 원래 문자 하나 당 고유한 숫자를 부여한 일종의 표입니다. 이 때 문자마다 고유한 숫자를 코드포인트라고 정의합니다. 쉬운 용어지만 외워야 합니다. 이후 더 이상 코드 값이라던가 숫자값 같은 표현을 쓰지 않고 ‘코드포인트’로 통일하여 표기할 것이기 때문입니다. ‘안녕’ 이라는 두 글자의 코드포인트는 0xC548와 0xB155지. 평면(Plane) 유니코드는 백만이 넘는 범위를 갖습니다. 이를 통으로 관리하는 것은 무리겠죠 ^^; 그래서 적당히 범위로 나눠서 관리하게 됩니다. 이러한 범위를 유니코드에서는 평면이라 부르고 있습니다. 따라서 평면(또는 Plane)이라는 단어가 유니코드에서 나온다면 ‘코드포인트를 그룹별로 묶은 범위’라고 생각하시면 됩니다. 유니코드의 역사 상 유니코드 1.0이 발표되고 이어서 2.0이 발표되었습니다. 유니코드 1.0은 0 ~ 65,536의 범위를 갖는데 이를 16진법으로 표현하면 0x0000 ~ 0xFFFF가 됩니다. 이 유니코드 1.0 시점의 범위를 0평면으로 우선 정의합니다. 0평면은 현대 언어 대부분이 들어있죠. 따라서 기본 다국어 평면이라 하여 약어로 BMP라는 이름이 붙어 있습니다. 즉 ‘0평면 = BMP = 기본다국어평면’ 이 전부 같은 의미로 0x0000 ~ 0xFFFF 사이에 값을 갖는 코드포인트의 범위를 의미합니다. 이제 0평면을 이해했으니 나머지 평면을 살펴볼 차례입니다. 0평면의 크기가 0xFFFF였기 때문에 다른 평면도 이 크기를 단위로 해서 범위를 분리했습니다. 1평면은 0x10000 ~ 0x1FFFF 의 범위를 갖고 3평면은 0x30000 ~ 0x3FFFF 의 범위를 갖는 식입니다. 따라서 평면의 범위는 몇번째 평면인지를 먼저 앞자리에 표시하고 뒤에 0000 ~ FFFF 까지를 붙이면 됩니다. 각 평면은 용도가 정해져 있고 0평면이 BMP로 불리는 것처럼 나름대로의 이름이 붙어있습니다. 전체 평면에 대해 정리한 표를 보죠. 0평면(BMP) 0x00000 ~ 0x0FFFF – 기본 다국어 평면. 현대 언어 대부분의 문자. 1평면(SMP) 0x10000 ~ 0x1FFFF – 보조 다국어 평면. 고어 및 여러 기호문자와 이모티콘. 2평면(SIP) 0x20000 ~ 0x2FFFF – 보조 상형문자 평면. 1평면에 포함되지 않은 한중일 통합 한자. 3 ~ 13평면 0x30000 ~ 0xDFFFF – 할당되지 않음(예비) 14평면(SSP) 0xE0000 ~ 0xEFFFF – 보조 특수 목적 평면. 태그 및 제어용 문자. 15 ~ 16평면(PUA) 0xF0000 ~ 0x10FFFF – 사용자 정의 영역. 3 ~ 13평면은 심지어 아직 사용하지도 않고 있네요 ^^; 한글은 대부분 BMP에 들어있습니다. 이모티콘의 경우 1평면인 SMP에 들어있으므로 폰트가 지원만 한다면 유니코드 표준으로 다양한 이모티콘을 사용할 수 있습니다. (참고로 유니코드2.0에서 정의된 1~16평면을 ‘보충평면’ 또는 ‘아스트랄플레인’이라고 통합하여 부르기도 합니다) 이모티콘이 유니코드 표준이었다니.. 잘 안 알려져 있지만 재밌는 거 많이 있어. 🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾🐿👀👁👂👃👄👅👆👇👈👉👊👋👌👍👎👏👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯👰👱👲👳👴👵👶👷👸👹👺👻👼👽👾👿💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯💰💱💲💳💴💵💶💷💸💹💺💻💼💽💾💿📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯📰📱📲📳📴📵📶📷📸📹📺📻📼📽📾📿🔀🔁🔂🔃🔄🔅🔆🔇🔈🔉🔊🔋🔌🔍🔎🔏🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯🔰🔱🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽🔾🔿🕀🕁🕂🕃🕄🕅🕆🕇🕈🕉🕊🕋🕌🕍🕎🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧🕨🕩🕪🕫🕬🕭🕮🕯🕰🕱🕲🕳🕴🕵🕶🕷🕸🕹🕺🕻🕼🕽🕾🕿🖀🖁🖂🖃🖄🖅🖆🖇🖈🖉🖊🖋🖌🖍🖎🖏🖐🖑🖒🖓🖔🖕🖖🖗🖘🖙🖚🖛🖜🖝🖞🖟🖠🖡🖢🖣🖤🖥🖦🖧🖨🖩🖪🖫🖬🖭🖮🖯🖰🖱🖲🖳🖴🖵🖶🖷🖸🖹🖺🖻🖼🖽🖾🖿🗀🗁🗂🗃🗄🗅🗆🗇🗈🗉🗊🗋🗌🗍🗎🗏🗐🗑🗒🗓🗔🗕🗖🗗🗘🗙🗚🗛🗜🗝🗞🗟🗠🗡🗢🗣🗤🗥🗦🗧🗨🗩🗪🗫🗬🗭🗮🗯🗰🗱🗲🗳🗴🗵🗶🗷🗸🗹🗺🗻🗼🗽🗾🗿 😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀🙁🙂🙃🙄🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏🙐🙑🙒🙓🙔🙕🙖🙗🙘🙙🙚🙛🙜🙝🙞🙟🙠🙡🙢🙣🙤🙥🙦🙧🙨🙩🙪🙫🙬🙭🙮🙯🙰🙱🙲🙳🙴🙵🙶🙷🙸🙹🙺🙻🙼🙽🙾🙿🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚋🚌🚍🚎🚏🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿🛀🛁🛂🛃🛄🛅🛆🛇🛈🛉🛊🛋🛌🛍🛎🛏🛐🛑🛒🛠🛡🛢🛣🛤🛥🛦🛧🛨🛩🛪🛫🛬🛰🛱🛲🛳🛴🛵🛶 이런 느낌이야. 민이 컴에서는 어디까지 보이려나. 다 잘 보여. 대단하네 이게 유니코드 안에 들어있다니.. 비트와 바이트 본격적인 내용에 앞서 기초적인 내용을 점검하고 가겠습니다. 코드포인트 중 가장 작은 값은 0 입니다. 이에 비해 가장 큰 값은 0x10FFFF 로 3바이트나 필요합니다. 어려운 내용이 나오기 전에 컴퓨터에서 사용하는 단위에 대해 복습해보죠. 우선 가장 작은 단위는 비트입니다. 비트는 0과 1만 표현할 수 있습니다만 한 자리가 늘어날 때마다 2의 승수만큼 표시할 수 있는 숫자가 늘어납니다. 예를 들어 1비트는 0과 1만 표현할 수 있지만 2비트 0~3까지 네 가지 경우를 표현할 수 있습니다. 3비트는 0~7까지 8가지를 표현할 수 있으며 4비트는 0~15까지 16가지를 표현할 수 있습니다. 바이트는 8비트를 모은 것으로 1바이트는 8비트와 같습니다. 8비트는 0~255까지 표현할 수 있고 1바이트도 마찬가지입니다. 1바이트의 범위를 10진수로 0~255까지라 한다면 16진법으로는 0x00 ~ 0xFF까지라 할 수 있습니다. 따라서 16진법 두자리 숫자는 1바이트에 해당됩니다. 코드포인트의 값이 0xC3FF 인 경우 16진수 숫자 4개가 들어있으므로 2바이트로 표현할 수 있습니다. 따라서 처음 언급한 0x10FFFF의 경우 16진수 숫자 6개가 들어있으므로(1, 0, F, F, F, F) 3바이트가 필요하다고 말한 것이죠. 1바이트는 16진법으로 00에서 FF까지야. 코드포인트의 길이 문제 기초적인 단위를 살펴봤으니 코드포인트의 값에 집중해보죠. 코드포인트는 0 ~ 0x10FFFF 의 범위이므로 작은 값들은 1바이트 안에 쏙 들어갈테고 큰 값은 2바이트, 3바이트라는 식으로 저장할 공간이 더 많이 필요해질 것입니다. 하지만 이렇게 저장하면 어디까지가 하나의 코드포인트를 나타내는지 구별할 방법이 없습니다. 이를 이해하기 위한 예를 들어보죠. “a안b녕” 라는 문자열이 있다고 해보죠. 우선 이 4글자에 대한 코드포인트는 각각 a – 0x61, 안 – 0xC548, b – 0x62, 녕 – 0xB155 입니다. 이를 차례로 바이트 단위로 적어보면 61 C5 48 62 B1 55 입니다. 하지만 차례로 적은 바이트를 보고 문자로 바꾸는 것은 불가능합니다. 왜냐면 원래대로 묶을 수도 있겠지만 그냥 앞에서부터 2바이트씩 묶어서 0x61C5(懅) 0x4862(ዾ) 0xB155(녕) 으로 볼 수도 있기 때문입니다. 만약 앞에서부터 2바이트씩 묶어버렸다면 ‘懅ዾ녕’ 이란 이상한 문자열로 복원될 것입니다. 쓸 때는 61 | C5 48 | 62 | B1 55 라는 식으로 구분지어 썼지만 해석할때 61 C5 | 48 62 | B1 55 라는 식으로 해석해버리면 엉망이 되는 것입니다. 어디까지를 문자 하나에 대한 코드포인트로 볼 것인지를 정할 기준이 필요합니다. 가장 쉬운 방법은 고정길이를 하나의 단위로 보는 것입니다. (..진짜로 복잡한데?) 코드유닛 언급한대로 바이트 덩어리를 어떻게 잘라서 하나의 문자의 코드포인트로 볼 것인가에 대한 방법은 바로 고정길이를 하나의 문자로 본다는 것입니다. 예를 들어 1바이트 단위로 문자의 코드포인트를 인식한다면 앞의 61 C5 48 62 B1 55는 바이트당 하나의 코드포인트에 대응하여 총 6글자로 해석될 것입니다. 결과적으로 ‘aÅ0b±7’ 라는 문자열로 환원됩니다. 이에 비해 2바이트 단위로 코드포인트를 인식하면 0x61C5, 0x4862, 0xB155 로 코드포인트에 대응하게 됩니다. ‘懅ዾ녕’로 환원됩니다. 이렇듯 같은 데이터라도 어떤 단위로 바이트를 나눠서 코드포인트를 해석하는가에 따라 결과는 완전히 달라집니다. 이때 바이트를 나누는 단위를 코드유닛이라 합니다. 앞의 예제에서 ‘a안b녕’의 경우 코드유닛을 2바이트로 해서 바르게 표현한다면 다음과 같은 바이트가 될 것입니다. a : 00 61 안 : C5 48 b : 00 62 녕 : B1 55 결과 : 00 61 C5 48 00 62 B1 55 위의 예에서 a와 b는 1바이트만 필요한데도 2바이트 코드유닛에 맞추기 위해 00을 삽입했습니다. 이를 패딩이라 부릅니다. (..패딩은 잠바잖아!) 코드유닛 크기가 클수록 패딩 크기도 커져서 메모리를 많이 써. (……패딩은 잠바라고!) 인코딩 여지껏 얘기했던 코드유닛으로 코드포인트를 표현하는 것을 인코딩이라고 합니다. 1바이트를 코드유닛으로 보는 인코딩 방식을 UTF8(1바이트 = 8비트)이라 합니다. 우선 이쪽부터 살펴보죠. 유니코드의 코드포인트는 최대 4바이트까지 커질 수 있는 숫자입니다. 헌데 UTF8에서는 코드유닛을 1바이트로 보고 있습니다. 게다가 이제와 고백하자면 UTF8의 코드유닛이 사용하는 8비트중 젤 앞에 1비트는 식별자로 사용되기 때문에 실제 가용한 공간은 온전한 8비트가 아니라 7비트 뿐입니다. 앞의 예제에서 ‘a’와 ‘b’는 코드포인트 값이 작아 7bit 안에 들어올 수 있었습니다만, 0x80 부터의 1바이트 값은 UTF8의 코드유닛에 들어올 수 없습니다. 인코딩을 적용하지 않았더니 어디까지가 하나의 문자를 가리키는 코드포인트인지가 애매했다면, 이번엔 고정크기의 코드유닛으로 인코딩을 적용했더니 코드유닛보다 코드포인트 쪽이 커지는 문제가 발생합니다. 코드포인트의 값은 2바이트인데 코드유닛은 1바이트라면 코드유닛안에 코드포인트를 담을 수 없죠(위의 예에서는 ‘안’의 경우 코드포인트가 0xC548로 2바이트가 필요합니다) 이런 경우는 코드포인트를 적절히 분리해서 여러 개의 코드 유닛에 담아야합니다. 이렇게 코드유닛의 크기보다 큰 코드포인트를 분해해서 적재하는 과정을 인코딩이라 합니다. (반대로 코드유닛으로부터 코드포인트를 복원하는 과정을 디코딩이라고 합니다) 코드유닛 크기보다 코드포인트가 크면 담을 수 없으니까 나눌 수 밖에 없어. 결론 위에서 설명한 것만으로는 감을 잡기 어려울거라 다음 포스팅에서는 차근차근 UTF8로 인코딩과 디코딩을 직접 진행해보면서 이해도를 높여보죠. (실은 이 내용으로 온라인으로 스터디를 진행한 적이 있는데 그 때 유튜브 영상은 아래와 같습니다) 개발++