UTF-8은 뛰어난 설계임
(iamvishnu.com)- UTF-8은 수백만 개의 문자를 표현하면서도 ASCII와의 하위 호환성을 유지하는 가변 길이 인코딩 방식
- ASCII와 동일한 7비트 영역(
U+0000
~U+007F
)은 1바이트 그대로 사용해, ASCII 파일은 곧 유효한 UTF-8 파일이 됨 - 그 외 문자는 2~4바이트 시퀀스로 표현되며, 선두 바이트의 비트 패턴이 길이를 정의하고 이후 바이트는
10
으로 시작해 연속 바이트임을 구분 - 이런 설계 덕분에 UTF-8은 범용 문자 집합을 다루면서도 기존 ASCII 시스템과 완벽하게 호환되어 가장 널리 사용되는 문자 인코딩이 됨
- UTF-16, UTF-32 같은 다른 유니코드 인코딩은 이런 ASCII 호환성을 제공하지 않음
UTF-8 설계의 탁월함
- UTF-8 인코딩을 처음 접했을 때, 서로 다른 언어와 문자의 수백만 가지 캐릭터를 하나의 체계로 아우르면서도 기존 ASCII와 호환되는 구조에 큰 인상을 받음
- 기본적으로 UTF-8은 최대 32비트를 활용하지만, ASCII는 7비트만 사용함
- UTF-8의 설계 원칙은 다음과 같음
- 모든 ASCII 인코딩 파일이 유효한 UTF-8 파일임
- 모든 ASCII 문자만 가진 UTF-8 파일이 유효한 ASCII 파일임
- 불과 128문자에 한정된 구식 시스템과 수백만 문자를 아우르는 체계를 접목하는 발상이 매우 혁신적
UTF-8의 기본 개념
- UTF-8은 유니코드 문자 집합의 모든 문자를 표현할 목적으로 설계된 가변 길이 문자 인코딩(variable-width encoding)
- 1~4바이트로 각 문자를 인코딩함
- 첫 128개 문자(
U+0000
~U+007F
)는 단일 바이트로 저장되어 ASCII와 하위 호환성 확보 - 그 외 문자는 두, 세, 네 바이트로 인코딩
- 첫 번째 바이트의 선행 비트가 인코딩에 필요한 전체 바이트 수를 결정함
1바이트 패턴 | 바이트 수 | 전체 바이트 시퀀스 패턴 |
---|---|---|
0xxxxxxx | 1 | 0xxxxxxx (일반적인 ASCII) |
110xxxxx | 2 | 110xxxxx 10xxxxxx |
1110xxxx | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
11110xxx | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- 다중 바이트 시퀀스의 2, 3, 4번째 바이트는 항상
10
으로 시작하며, 이는 연속 바이트임을 명확히 표시함 - 주바이트와 연속 바이트의 나머지 비트를 결합해 하나의 코드 포인트를 만듦
- 코드 포인트는 고유 유니코드 문자 식별자로, "U+" 접두사와 16진수로 표현됨
- 예: "A"의 코드 포인트는
U+0041
임
- UTF-8 인코딩 바이트로부터 문자를 해석하는 흐름은 다음과 같음
- 1. 바이트를 읽고, 처음이 0이면 단일 바이트 문자(ASCII)로 간주해서 나머지 7비트로 문자를 표시하고 다음 바이트로 이동
- 2. 0이 아니라면
- 110이면 2바이트 문자로 다음 바이트 한개 추가로 읽음
- 1110이면 3바이트 문자로 다음 2개 바이트 읽음
- 11110이면 4바이트 문자로 추가 3개 바이트 읽음
- 3. 결정된 바이트에서 선두 비트 제외한 나머지 비트를 결합해서 코드 포인트의 이진값으로 활용
- 4. 유니코드 문자 집합에서 코드 포인트 찾아 화면에 표시
- 5. 다음 바이트로 반복
예: 힌디어 문자 "अ"
- UTF-8 표현:
11100000 10100100 10000101
(3바이트) - 첫 바이트(
11100000
) → 3바이트 문자임을 나타냄 - 세 바이트의 유효 비트 조합 →
00001001 00000101
= 16진수0x0905
- 코드포인트
U+0905
는 데바나가리 문자 "अ"를 의미함
파일 예제
-
1.
Hey👋 Buddy
- 총 13바이트로 구성
- ASCII 문자(H, e, y, B, u, d, d, y, 공백) → 1바이트씩
- 👋 (U+1F44B) → 4바이트
11110000 10011111 10010001 10001011
- 이 파일은 유효한 UTF-8 파일이지만, 비ASCII 문자(이모지)가 포함되어 있으므로 ASCII와의 하위 호환성은 아님
- 총 13바이트로 구성
-
2.
Hey Buddy
- 총 9바이트, 모두 ASCII 범위
- 따라서 이 파일은 동시에 유효한 ASCII 파일이자 유효한 UTF-8 파일
다른 인코딩 비교
- ASCII와 호환성을 제공하는 인코딩은 몇 가지 있으나, UTF-8만큼 널리 쓰이지 않음
- GB18030 (중국 표준) 등도 ASCII 호환성을 제공하지만 널리 쓰이지 않음
- ISO/IEC 8859 계열은 단일 바이트 확장(최대 256자)이라 한계 존재
-
UTF-16/UTF-32는 ASCII 호환성이 없음
- 'A' (U+0041): UTF-16은
00 41
, UTF-32는00 00 00 41
- 'A' (U+0041): UTF-16은
보너스: UTF-8 Playground
- UTF-8 인코딩 과정을 시각적으로 탐험할 수 있는 인터랙티브 도구
- https://utf8-playground.netlify.app/
Hacker News 의견
-
UTF-8에서 연속 바이트가 항상 10으로 시작됨에 따라 임의 바이트로 점프하더라도 그 위치가 문자 시작인지, 연속 바이트인지 바로 확인 가능함, 그래서 다음 또는 이전 문자 시작점을 쉽게 찾을 수 있음. EBML의 가변 길이 정수 인코딩 방식(단일 바이트의 ASCII 호환성 유지 목적의 1/0 반전 방식)과 같이 인코딩할 경우, 임의 위치에서 문자의 시작점을 바로 알기 어려움. 자세한 내용은 RFC8794 section 4.4 참고
-
맞음, UTF-8의 큰 장점임. UTF-8 문자열을 처음부터 읽지 않아도 앞뒤로 자유롭게 이동 가능함. Python의 경우, 문자열 인덱스를 문자 단위로 가능하게 하려고 CPython에서 wide characters를 사용함. 한때 2바이트 또는 4바이트 문자를 선택할 수 있었고, 이후 실행 시 자동으로 전환되었음. 하지만 여전히 wide character임, UTF-8 아님. 예를 들어 이모티콘 하나로도 문자열 크기가 네 배로 늘어남. 나는 오히려 내부를 UTF-8로 쓰고, 인덱스 타입을 불투명한 객체로 만들고, 작은 정수 만큼 더하거나 빼면 문자열 내에서 앞뒤로 이동하도록 구현을 고민했음. 실제로 정수로 변환하거나 직접 서브스크립트 하면 문자열의 인덱스를 계산하는 방식임. 이런 접근에서는 정규표현식 등도 불투명 인덱스 객체를 활용해 UTF-8 표현에서 잘 동작함
-
LEB128/VLQ가 EBML 가변길이 정수 방식보다 낫다고 생각함. 바이트 내 MSB(최상위 비트)로 구분함 - 0이면 시퀀스 끝, 다음 바이트가 새로운 시퀀스, 1이면 MSB 0이 나올 때까지 뒤로 감음. SIMD 최적화된 효율적 구현도 있음. LEB128과 VLQ의 차이는 엔디안 차이 뿐임. ASCII는 0xxxxxxx, 확장 글자는 1xxxxxxx 0xxxxxxx, 1xxxxxxx 1xxxxxxx 0xxxxxxx 등으로 3바이트에 최대 0x1FFFFF까지 인코딩 가능, 유니코드에 필요한 것보다 많음. 자기동기화(self-synchronizing)는 안 되지만 더 압축적임. ASCII는 여전히 1바이트, 수학기호나 일본어 처럼 U+3FFF 이하 코드포인트는 2바이트로 표현 가능해서 코드 크기를 줄이는 데 유리함
-
텍스트가 깨지거나 악의적으로 변조되지 않았다는 전제 하에서만 가능하다고 봄. 잘못된 UTF-8 시퀀스를 파싱 혹은 이스케이프 처리할 때 수많은 보안 취약점이 발생해왔음. 관련 사례는 CVE-2025-1094 PostgreSQL 문제, 또 UTF-8 관련 CVE 목록에서 확인 가능함
-
반드시 맞는 이야기는 아님. 잘못된 UTF-8일 때는 연속 바이트(continuation byte)로 문자가 바뀌기도 함. 예를 들어 0b01100001 0b10000000 0b01100001 식으로 들어오면, a�a로 세 글자가 나옴. 출력 문자의 시작점 여부는 직전 1~3바이트를 봐야 함
-
최대 4바이트 멀티바이트 크기라면, 뒤로 최대 3바이트만 확인하면 현재 위치가 연속 바이트인지 판단 가능함. 시작 바이트가 안 나오면, 단일 바이트 문자임을 알 수 있음. 라이브러리가 UTF-8을 제대로 인식하지 못해도, 잘라낸 슬라이스에서 선두와 말단의 잘못된 바이트를 무시하고, 그나마 합리적인 문자열을 뽑아내는 복구 목적으로 이렇게 설계된 것이라 추측함
-
-
UTF-8 정말 훌륭하다고 생각함. 핵심은 ASCII가 7비트만 사용했다는 결정에 있음. 1963년에도 7비트 선택이 좀 특이했음. 이게 단순한 역사적 우연인지 궁금함. ASCII를 설계한 사람들이 하나 더 비트를 이용해서 추가 기호 넣을 생각도 있었는지, 아니면 코드페이지나 확장성을 염두에 뒀는지 궁금함
-
정확한 이유는 모르지만, 예전엔 8비트가 항상 주어진 건 아니었음. 7비트 + 1 parity 혹은 플래그 비트가 흔했음 (그래서 e-mail은 아직도 quoted-printable로 7비트만으로 8비트를 인코딩함). 8비트를 그대로 다 전달 가능한 것을 8-bit clean이라고 함. 이런 맥락에서, UTF-8도 결국 ASCII에 남는 8번째 비트를 잘 활용한 사례임. 참고로 8-bit clean에 대한 설명도 있음
-
전문가 아니지만, ASCII의 역사를 예전에 읽어 봤음. ASCII는 텔레타이프 코드(전신 코드에서 발전된 것)가 뿌리임. 모스 부호는 가변 길이라 기계 구현이 번거로웠음. 그래서 5비트 Baudot 코드가 나왔음. 고정 길이 코드로 기계를 단순화하려 한 것이고, 오퍼레이터 피로도를 줄이는 목적도 있었음. Baudot 코드 때문에 지금도 심볼레이트를 Baud라 부름. 이후에는 타자기를 이용한 펀치 테이프 입력 방식으로 유연성이 올라가면서, Carriage Return(복귀)과 Line Feed(줄바꿈) 같은 특수기호가 추가됨. 초기 컴퓨터 산업은 펀치카드를 인풋으로 채택했는데, IBM은 카드를 더 빠르게 처리하는 새로운 8비트 체계를 개발했고, 그게 ASCII 기반이 됨. 결국 기술 발전에 따라 2진 코드를 확장해 왔음. ASCII도 8비트 byte 관행보다 먼저 나온 과도기적 산물임
-
실제로 남는 비트는 parity(패리티) 용도로 재활용하기 위한 것이었음
-
ASCII의 8비트 확장(ISO 8859-x 류)이 수십년간 광범위하게 쓰였고, Windows 표준 코드페이지에서도 여전히 사용 중임. 만약 ASCII가 처음부터 8비트였다 해도 핵심 문자가 첫 128개에 몰려 있을 테니 UTF-8에도 적합했을 것이라 생각함. 역사적 우연이라고 하면 ASCII가 7비트인 게 아니라, 당시 컴퓨터 발전이 영어권에서 주로 이뤄졌고, 영어가 7비트만으로 충분히 표현 가능했던 점임
-
7비트 자체가 특별히 이상하지 않음. Baudot는 5비트였고, 그게 부족해서 6비트 코드가 생겼고, 이후 7비트 ASCII가 만들어짐. IBM은 System/360에서 8비트 바이트(EBCDIC 코드)를 표준화했지만, 다른 컴퓨터 벤더들은 바이트 길이가 일정하지 않았음. 7비트가 보기엔 이상해도, 문자와 시스템 워드가 반드시 neatly fit되지 않은 상황임
-
-
UTF-8은 기대 이상의 설계임에 동의함. 그런데 Unicode는 적용 범위(scppe)가 너무 넓어지는 문제 있음. Unicode에 무엇이 포함돼야 할지 물음표가 생김. 직관적으로 생각하면, "인류가 의사소통 목적으로 쓰는 구별되는 인쇄 가능한 글자 전부"일 것 같지만, 실제로는 그렇지 않음.
-
구분이 명확하지 않음. 코드포인트가 결합용(combining)으로 존재하기도 함
-
구체적이지 않음. 한 글자를 여러 방식으로 쓸 수 있음. 겉보기에 같은 글자도 서로 다른 코드포인트와 의미를 가짐
-
프린트불이 다 아님. 제어 문자(control char)가 존재함. ASCII 호환성 때문에 넣긴 했지만, 자체적인 제어 문자도 늘어남 애니메이션되는 유니코드 포인트는 아직 없는 듯함. 적어도 프린트 가능한 건 종이에 뿌릴 수 있음. 하지만 미래에도 이 불변성이 지켜질지 모르겠음. 참고로 utf 인코딩 중에 저자가 언급하지 않은 utf-7도 있음. utf-8과 비슷하지만, 80년대 네트워크 환경에서 마지막 비트 사용이 안전하지 않다는 가정 하에 만들어졌음. 우연히 utf-7 인코딩으로 메일을 받은 적 있음. 어떻게 보낸 건지 아직도 모름
-
UTF-7은 주로 이메일처럼 8-bit clean이 아닌 전송 환경을 위해 만들어짐. 지금은 시대에 뒤떨어졌고, 서플리멘탈 플레인 인코딩도 안 됨(UTF-16 surrogate pair로만 가능). UTF-9도 있는데, 만우절용 RFC에 소개된 패러디임(PDP-10 같은 36비트 환경용)
-
-
나는 언제나 궁금했던 점이 있음: 유니코드 코드포인트를 불필요하게 긴 바이트 시퀀스로도 인코딩 가능하다는 것임. UTF-8은 이를 금지하고, 가장 짧은 시퀀스만 허용함. 예를 들어 00000001도 되고, 11000000 10000001도 같음. 그렇다면 아예 다른 방식으로 불법 인코딩이 존재할 수 없게 만들 수 있지 않을까? 예를 들어 2바이트 시퀀스의 맨 앞을 가장 마지막 유효 값으로 하면, 11000000 10000001이 128+1이 되고, 0-127은 1바이트로 처리. 그러면 불법 코드도 없고, edge case에서 문자열이 조금 더 짧아질 텐데, 아마도 당시 하드웨어 비용 때문에 고려되지 않았던 건지 궁금함. (업데이트: 실제 비트 시퀀스는 10000001이어야 하고, 수정함)
- 여러 답변이 동기화 마커(synchronization indicator)에 대해 언급하지만, 본질적 질문은 U+0080이 c2 80이 아니라 왜 c0 80(7f 넘어서 첫번째)이 아닌가에 관한 것임. 그 이유는 아래와 같다고 생각함 a) Overlong encoding을 허용할 경우, 일부가 짧은 시퀀스만 체크하는 데 보안 허점이 됨 b) 표준 UTF-8 인코딩/디코딩은 마스킹(bitmask) 및 쉬프트(bitshift)만으로 처리 가능함. 제안하는 방식은 추가로 뺄셈 연산이 필요해짐 1992년 이메일 토론 중에 이에 대한 논의가 있었고, FSS-UTF에서는 additive constants를 포함함(아래 참고)
2바이트 시퀀스가 2^11 코드를 가질 수 있는데, 0-7f는 불법임. 이게 아마도 특별한 보상 없는 additive constants 대신 더 낫다고 본 듯함
자세한 내용은 utf-8-history.txt 맨 아래 참고-
바이트 패턴의 self-synchronicity(자기 동기성) 유지가 핵심임. 만일 11000000 10000001처럼 연속 바이트를 유지하지 않으면, 트렁크된 UTF-8 스트림에서 코드포인트 경계를 항상 찾는 특성을 잃게 됨. 이 방식에서 추가/감산 연산까지 들어가면, 디코더 성능이 떨어짐. 현재는 비트연산만으로 처리 가능함
-
quectophoton의 코멘트처럼, 연속 바이트가 항상 10으로 시작해야 파서가 어느 지점에서든 코드포인트 경계를 찾을 수 있음. 실제로 90년대 초에 UTF-8 설계 당시에, 신뢰성 없는 전송 환경이 많은 것을 고려한 결과임
-
제안 방식 사용 시, 인코딩/디코딩 계산이 더 복잡하고 느려짐. 현재는 비트 쉬프트 몇 번이면 끝나지만, 당시(90년대) 느린 컴퓨터 환경에서는 중요한 점이었음
-
UTF-8의 설계에 대해 더 보고 싶으면 Russ Cox의 one-pager, Rob Pike의 역사 정리 참고
-
UTF-8은 훌륭하고 모든 환경에서 쓰이면 정말 좋겠음(JavaScript 보고 있음). 하지만 유일한 단점은 유효하지 않은 바이트 시퀀스를 해석하는 방안이 표준에 명확하지 않다는 것임. "모든 바이트 시퀀스에 대해 반드시 해석 방법을 명시"하는 설계가 더 완벽했을 것이라 생각함. HTML5 스펙처럼 하면 성공적으로 운용될 수 있다고 봄
- 보안 측면에서는, 잘못된 UTF-8은 다루지 말고 바로 데이터 자체를 위험물 취급하듯 폐기하고 에러처리를 해야 함. 그렇지 않으면 검증 우회를 통한 공격에 무방비로 노출됨
-
나는 뒤로 호환(backwards compatibility)에는 애증이 있음. 혼란스러운 점은 싫지만, 뭔가를 깨면서까지 진보하겠다는 움직임은 좋게 봄. 하지만 동시에, UTF-8이나 EAN처럼 호환성을 유지하면서도 영리하게 설계된 사례는 기분 좋게 생각함. 솔직히 UTF-8은 호환성을 위해 거의 아무 것도 희생하지 않은 듯함
-
UTF-8은 호환성을 위해 거의 아무 것도 희생하지 않은 듯함
21비트 이상 인코딩이 막힘. 이는 UTF-16 호환성 때문임(UTF-16의 서러깃 메커니즘은 2^21-1까지 가능). 언젠가 이 한계를 후회하게 될 수도 있음. 21비트 넘는 코드포인트를 막는 실질적 이유는 이밖에 없는 듯함 -
진보 명분으로 권력자가 무언가를 과감하게 바꾸는 걸 좋아함
하지만 의존 중인 시스템이 단지 누가 파라미터 이름을 바꿨거나 표준 라이브러리 일부가 ‘지저분’해 보여서 깨지는 건 재미없는 일임 -
굳이 바꿀 게 있다면, 제어 문자의 일부를 더 흔한 문자로 바꿔서 공간을 조금이라도 줄였을 것 같음(Unicode 호환성까지 깬다고 하면). 다중 바이트 문자 인코딩 포맷으로선 독립적으로 봐도 거의 최적이라 생각함
-
-
UTF-8 playground 링크(utf8-playground.netlify.app) 정말 마음에 듦. 코드포인트 직접 입력도 UI에서 되면 좋겠음(지금은 URL로만 가능했음). (업데이트: 이미 PR 머지돼서 가능해졌음)
- 기여에 감사함, 지금 머지되어 바로 적용돼 있음
-
이 주제에 더 깊이 파고들고 싶고, Advent of Code 같은 방식 좋아하면 i18n-puzzles에 텍스트 인코딩 관련 퍼즐이 여럿 있음. UTF-8과 UTF-16 등의 작동 원리를 완전히 내재화하는 데 도움이 됨
-
좋은 글 고맙게 읽었음. 나도 UTF-8을 추천하지만 반드시 BOM과 함께 사용할 때만 좋다고 생각함. 그렇지 않으면 애플리케이션이 UTF-8임을 알 수 없고, 저장도 UTF-8로 해야 한다는 사실을 놓침. 예를 들어 윈도우에서 새 텍스트 문서를 만들면, 파일이 비어 있을 때 BOM만 있으면, 어느 앱이든 이후 편집/저장 시 UTF-8로 저장해야 함을 자동 인식함. BOM 없는 상태에서는 앱이 인코딩을 자동 감지하려고 해도 완벽하게 신뢰할 수 없고, 악센트 등 특수문자 추가 시 혼란이 커짐(에디터가 언어를 잘못 추정하거나, Notepad가 업데이트 후 기본 인코딩을 바꾸기도 함). 그래서 UTF-8을 쓰는 건 동의하지만, 반드시 BOM이 OS/앱의 기본 설정이 되어야 함