Skip to Content

13장. 가변 길이의 우아함 — UTF-8은 어떻게 동작하나

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

UTF-8은 첫 바이트 접두 비트로 1~4바이트 길이를 선언하고 연속 바이트를 10으로 구분해, ASCII 호환·빈도 최적화·자기 동기화를 함께 이룬다.

면접 실전 질문: ① UTF-8 첫 바이트는 길이를 어떻게 알리는가? ② 연속 바이트가 10으로 시작하는 이유는? ③ ASCII 호환이 UTF-8 확산에 왜 중요했는가?


배경 — 세 가지 소원

12장 끝에서 우리는 이상적인 인코딩의 조건을 스스로 적어봤어요. 정리하면 소원이 셋입니다.

  1. ASCII 호환. 영문 A는 옛날처럼 딱 1바이트로. 그래야 반세기 쌓인 ASCII 파일·시스템이 그대로 살아요.
  2. 모든 유니코드 문자 표현. 한글도 한자도 이모지도, U+10FFFF까지 전부 담아야죠.
  3. 경계를 스스로 알 것. 가변 길이인데도, 어디서 한 글자가 시작하고 끝나는지 바이트만 보고 알 수 있어야 합니다(2장 모스의 그 숙제).

세 소원을 한꺼번에 이룬 인코딩이 1992년에 나옵니다. 켄 톰프슨롭 파이크 — 톰프슨은 유닉스를, 파이크는 훗날 Go 언어를 만든 벨연구소의 전설들이죠. 두 사람이 뉴저지의 한 식당에서 식탁 매트에 설계를 스케치했다고 전해지는 UTF-8이에요.

스토리 — 첫 바이트가 길이를 말한다

UTF-8의 핵심은 첫 바이트의 시작 비트가 전체 길이를 알려준다는 거예요. 규칙은 놀랄 만큼 단순합니다.

  • 0으로 시작하면 → 1바이트 글자 (0xxxxxxx). 이게 바로 ASCII예요.
  • 110으로 시작하면 → 2바이트 글자 (110xxxxx 10xxxxxx)
  • 1110으로 시작하면 → 3바이트 글자 (1110xxxx 10xxxxxx 10xxxxxx)
  • 11110으로 시작하면 → 4바이트 글자 (11110xxx 10xxxxxx ...)

그리고 뒤따르는 연속 바이트는 전부 10으로 시작합니다. 2바이트 이상이면 첫 바이트 맨 앞에 붙은 1의 개수가 곧 바이트 수예요(1바이트 ASCII만 예외로 0으로 시작하고요). 실제로 찍어볼까요.

for c in ["A", "é", "한", "😀"]: b = c.encode("utf-8") bits = " ".join(format(x, "08b") for x in b) print(f"{c} U+{ord(c):04X} {len(b)}바이트 {bits}") # A U+0041 1바이트 01000001 # é U+00E9 2바이트 11000011 10101001 # 한 U+D55C 3바이트 11101101 10010101 10011100 # 😀 U+1F600 4바이트 11110000 10011111 10011000 10000000

보세요. A0으로 시작하는 1바이트 — ASCII와 완전히 똑같아요. 은 첫 바이트가 1110으로 시작하니 3바이트, 뒤 두 바이트는 10으로 시작하는 연속 바이트. 첫 바이트만 봐도 “이 글자는 3바이트짜리”라는 걸 알 수 있죠. 세 번째 소원, 경계 문제가 이렇게 풀립니다.

핵심 — 우아함은 어디서 오나

UTF-8이 왜 인터넷을 정복했는지, 그 우아함을 셋으로 정리할 수 있어요.

① 빈도 최적화 — 자주 쓰는 걸 짧게. 이 말, 2장 모스에서 들었죠? 영어·숫자 같은 ASCII는 1바이트, 유럽 문자는 2바이트, 한중일은 3바이트, 이모지는 4바이트. 자주 쓰이는 문자일수록 짧게 배정한 겁니다. 모스가 E를 점 하나로 줄인 그 아이디어가, 150년을 건너 UTF-8에서 완성돼요. 영어권 텍스트나 HTML 태그·JSON 키처럼 ASCII가 대부분인 데이터는 용량이 거의 안 늘어납니다.

② 자기 동기화(self-synchronization). 연속 바이트는 무조건 10으로 시작하니까, 바이트 스트림 아무 데나 뚝 잘라 떨어져도 금방 경계를 되찾아요. 10으로 시작하는 바이트를 만나면 “여긴 글자 중간이구나” 하고 다음 시작 바이트까지 건너뛰면 되거든요.

b = "한글".encode("utf-8") # b'\xed\x95\x9c\xea\xb8\x80' — 6바이트 mid = b[2:] # 첫 글자 중간(3번째 바이트)부터 잘라 읽기 시작했다고 치자 # mid[0]=0x9C는 10______ (연속 바이트)이니 "여긴 글자 중간"임을 안다 → 다음 리더까지 skip print(mid[0] >> 6 == 0b10) # True — 이 바이트는 연속 바이트다

Shift-JIS 기억나세요? 아무 바이트나 ASCII 역슬래시(0x5C)로 오해돼 소스가 깨졌죠. UTF-8은 연속 바이트가 절대 ASCII 영역(0~127)과 안 겹칩니다. 10xxxxxx는 항상 128 이상이거든요. 9장의 그 재앙이 구조적으로 불가능해진 거예요.

③ ASCII 호환 — 비트 하나까지 동일. 0으로 시작하는 1바이트 영역이 ASCII와 비트 하나까지 동일합니다. 즉 순수 ASCII 텍스트는 그 자체로 이미 올바른 UTF-8이에요. 이 하위 호환이 UTF-8을 표준으로 밀어 올린 결정적 승부수인데, 그 얘기는 다음 장에서 제대로 합니다.

정리하면 UTF-8은 2장(가변 길이·빈도 최적화), 6장(선행 바이트로 길이 표시), 그리고 이 책이 쌓아온 모든 교훈을 한 그릇에 담아낸 인코딩이에요. 나라마다 다른 표도, 경계 모호도, ASCII 충돌도 — 지금까지의 문제를 전부 피해 갑니다.

정리

  • UTF-8(1992, 켄 톰프슨·롭 파이크)은 유니코드 코드포인트를 1~4바이트 가변 길이로 담는 인코딩.
  • 첫 바이트의 시작 비트가 길이를 선언한다: 0=1바이트(ASCII), 110=2, 1110=3, 11110=4바이트. 연속 바이트는 전부 10으로 시작 → 경계가 명확(2·6장의 숙제 해결).
  • 우아함 셋: ① 빈도 최적화(ASCII 1·유럽 2·CJK 3·이모지 4바이트, 2장 콜백) ② 자기 동기화(연속 바이트 10으로 아무 데서 잘려도 경계 복원) ③ ASCII와 비트 단위 동일 (순수 ASCII = 그대로 유효한 UTF-8, 9장식 0x5C 충돌 구조적으로 불가).
  • 지금까지 책이 쌓은 교훈(가변 길이, 선행 바이트, 하위 호환)을 한 그릇에 담았다.

생각해볼 질문: UTF-8의 세 가지 우아함 중, 세상을 실제로 뒤바꾼 건 아마 ③ ASCII 호환 일 거예요. 생각해보면 이건 기술이라기보다 전략에 가깝습니다. 새 인코딩을 퍼뜨리려면 이미 깔린 수십억 개의 ASCII 파일·프로그램과 싸워야 하는데, UTF-8은 싸우는 대신 “너희는 이미 UTF-8이야” 라고 끌어안았거든요. 이 하위 호환이라는 승부수가 없었다면, 과연 UTF-8이 UTF-16을 제치고 웹의 98%를 차지할 수 있었을까요? 다음 장에서 그 전략을 뜯어봅니다.

14장 · ASCII와의 약속 — 하위 호환성이라는 승부수

Last updated on