"안녕" 두 글자를 화면에 띄우면 파일이나 메모리에는 6 바이트가 저장된다. EC 95 88 EB 85 95. 왜 한글은 3 바이트인데 영어 A 는 1 바이트일까? Unicode codepoint U+C548 이 어떻게 EC 95 88 이 되는 걸까? 이 가이드는 UTF-8 의 변환 규칙, Unicode 와의 관계, BOM·surrogate pair 의 정체, 그리고 한글·이모지 다룰 때 자주 부딪히는 byte ≠ character 버그를 정리한다.
먼저 — Unicode vs UTF-8 은 다르다
- Unicode — 문자에 번호를 매기는 표준.
A= U+0041, 한글안= U+C548, 이모지🎉= U+1F389. 약 15 만 codepoint 정의 (Unicode 15). - UTF-8 / UTF-16 / UTF-32 — codepoint 를 바이트로 저장하는 인코딩 방식. 같은 글자라도 인코딩에 따라 바이트 수 다름.
"Unicode 로 저장한다" 는 표현은 부정확. 실제로는 "UTF-8 로 저장된 Unicode 텍스트".
UTF-8 의 핵심 — 가변 길이 인코딩
codepoint 의 크기에 따라 1~4 바이트:
| codepoint 범위 | 바이트 수 | 비트 패턴 |
|---|---|---|
| U+0000 ~ U+007F | 1 | 0xxxxxxx |
| U+0080 ~ U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
규칙:
- 첫 바이트의 1 의 개수 = 전체 바이트 수 (1 → 1 바이트, 110 → 2 바이트, 1110 → 3 바이트, 11110 → 4 바이트)
- 이어지는 바이트는 항상 10 으로 시작 — continuation byte
- x 자리에 codepoint 의 비트 박음
"안" 의 인코딩을 단계별로
"안" = U+C548
1. codepoint 를 2 진수로
0xC548 = 1100 0101 0100 1000
2. UTF-8 3 바이트 자리 (16 비트 < 21 비트)
1110 xxxx 10 xxxxxx 10 xxxxxx
↑ 16 비트 들어감
3. codepoint 비트 우측 정렬해 x 자리 채움
1100 0101 0100 1000 → 1100 / 010101 / 001000
(4 비트)(6 비트)(6 비트)
4. 합치기
1110 1100 1001 0101 1000 1000
= 0xEC 0x95 0x88
→ "안" = EC 95 88 (3 바이트)직접 시도 — URL 인코딩 / 디코딩 에 "안" 넣으면 결과가 %EC%95%88 (각 바이트의 hex 를 % 로 표시).
왜 영어는 1 바이트, 한글은 3 바이트인가
UTF-8 의 가장 중요한 설계 결정 — ASCII (1968) 와 byte-by- byte 호환. U+0000 ~ U+007F 의 1 바이트 표현이 ASCII 와 동일.
결과:
- 기존 ASCII 텍스트는 그대로 UTF-8 (변환 X)
- 영어로만 된 코드·로그·JSON 은 UTF-8 도입 비용 0
- 한글·이모지·아랍어 같은 비 ASCII 만 추가 바이트 — "쓴 만큼만 비용"
한국 입장에서는 한글 3 바이트가 "낭비" 처럼 보이지만, 글로벌 호환성 + ASCII 기반 인프라 보존의 트레이드오프. 한글 전용 EUC-KR 은 한글 2 바이트지만 영어 + 한글 혼용 문서에서 더 복잡.
UTF-16 — 또 다른 인코딩
JavaScript / Java / Windows API 가 내부적으로 UTF-16 사용. 기본 2 바이트, 4 바이트 보조 단위:
- U+0000 ~ U+FFFF (BMP, Basic Multilingual Plane) → 2 바이트
- U+10000 ~ U+10FFFF → 4 바이트 (surrogate pair)
한글은 U+AC00 ~ U+D7A3 범위 (BMP 안) → UTF-16 으로는 2 바이트. UTF-8 (3 바이트) 보다 작음. 한국어 dominant 시스템은 UTF-16 이 더 효율.
Surrogate pair — 이모지의 함정
"🎉" = U+1F389 (BMP 밖)
UTF-16 surrogate pair:
high = 0xD83C
low = 0xDF89
JavaScript:
"🎉".length === 2 ← 길이 1 이 아님!
"🎉".charAt(0) ← "�" 만 (깨짐)JavaScript 의 str.length 는 UTF-16 code unit 수. BMP 밖 글자 (대부분의 이모지·일부 한자 확장) 는 length 2 로 잡힘. 사람이 보는 "글자 수" 와 다름.
해결 — 새 API [...str] 또는 Array.from(str) 가 surrogate pair 인식:
[..."🎉"].length === 1 ✓
[..."안녕🎉"].length === 3 ✓BOM (Byte Order Mark) — 보이지 않는 첫 바이트
EF BB BF 3 바이트가 UTF-8 파일 맨 앞에 박힐 때가 있음. 원래 UTF-16 에서 little-endian / big-endian 구분용 markers. UTF-8 은 byte order 무관이라 BOM 불필요 — 그러나 Windows Notepad / Excel CSV 가 자주 박음.
문제:
- 웹 서버가 BOM 박힌 HTML 응답 → 브라우저 첫 줄 깨짐
- CSV 첫 컬럼이
"id"→ BOM 박혀서 실제로는"id"가 됨 → DB import 실패 - shell script 첫 줄
#!/bin/bash앞 BOM → "command not found"
해결 — 텍스트 에디터의 "BOM 없이 UTF-8" 옵션 또는 sed '1s/^\xEF\xBB\xBF//'.
URL 안 한글 — Percent-encoding
URL 은 ASCII 만 허용 (RFC 3986). 한글 URL 은 UTF-8 인코딩 후 각 바이트를 %XX:
https://example.com/검색?q=안녕
→ https://example.com/%EA%B2%80%EC%83%89?q=%EC%95%88%EB%85%95
"검" = EA B2 80 → %EA%B2%80
"색" = EC 83 89 → %EC%83%89
"안" = EC 95 88 → %EC%95%88
"녕" = EB 85 95 → %EB%85%95URL 인코딩 / 디코딩 가 자동 변환. host 부분 보존 토글로 path/query 만 인코딩 가능.
Punycode — 도메인 안 한글
한글 도메인 (한글.kr) 은 percent-encoding 못 씀 (DNS 가 % 의미 모름). 대신 Punycode 로 ASCII 만 사용해 한글 표현. 변환 알고리즘은 다르지만 결과는 항상 xn-- prefix:
한글.kr → xn--bj0bj06e.kr
한국.kr → xn--3e0b707e.krPunycode (국제화 도메인) 가 양방향 변환. IDN homograph attack (라틴 "a" vs 키릴 "а" 닮음) 의 detection 에도 유용.
HTML entity — 또 다른 인코딩
HTML/XML 안 특수문자 (<, >, &) 를 텍스트로 박을 때. <, >, &. 한글 같은 일반 문자는 그대로 박혀도 됨 (HTML 의 charset=utf-8 가 처리).
HTML 엔티티 인코딩 / 디코딩 가 양방향 변환.
흔한 버그
1. byte ≠ character
-- MySQL VARCHAR(10) 가 한글 10 글자 저장?
INSERT INTO users (name) VALUES ('가나다라마바사아자차카');
-- "Data too long for column 'name'" — 11 글자가 33 바이트DB 컬럼 길이를 byte 단위로 해석하는 시스템이 흔함. MySQLutf8mb4 의 VARCHAR(10) 은 10 글자 (40 바이트) 지만, 예전 utf8 (= utf8mb3) 는 BMP 만 → 이모지 깨짐.
2. 길이 검증 인코딩 무지
if (input.length > 100) throw new Error("too long");
// "안녕하세요" === 5 글자 vs JS length 5 vs UTF-8 byte 15
// 어느 정의로 100 인가?3. URL 의 한글 깨짐
encodeURIComponent 안 쓰고 raw 한글 박은 URL 을 다른 서버가 EUC-KR 로 해석. decodeURIComponent 시 잘못된 codepoint.
4. CSV 한글 BOM
Excel 이 만든 CSV → BOM 박힘. 파싱 라이브러리가 BOM 인식 못 하면 첫 컬럼 이름이 name.
5. JSON.stringify 의 한글 escape
일부 라이브러리는 ASCII 안전 위해 한글을 안 형태로 escape. 파일 크기 ↑. 대부분 JSON 표준은 UTF-8 그대로 박는 것 권장.
참고 자료
- RFC 3629 (UTF-8) — datatracker
- Unicode Standard — unicode.org
- Joel Spolsky — "The Absolute Minimum Every Developer Must Know About Unicode" — joelonsoftware.com
- UTF-8 Everywhere Manifesto — utf8everywhere.org
요약
- Unicode = 문자에 번호 부여. UTF-8 = 그 번호를 바이트로 저장하는 방식.
- UTF-8 가변 길이: ASCII 1 바이트 / 한글 3 바이트 / 이모지 4 바이트. 첫 바이트의 1 개수 = 전체 바이트 수.
- ASCII 호환이 UTF-8 의 가장 중요한 설계 결정 — 30 년 인프라 보존.
- UTF-16 은 한글에 더 효율 (2 바이트), JavaScript / Java 의 내부. 그러나 이모지는 surrogate pair (4 바이트).
- BOM (EF BB BF) 은 UTF-8 에 불필요. Windows / Excel 가 박는 BOM 이 흔한 버그.
- byte ≠ character. DB 길이 / 네트워크 검증 / 길이 제한 모두 정의 명확히.
- 실험 — URL 인코딩 / 디코딩 / Punycode (국제화 도메인) / HTML 엔티티 인코딩 / 디코딩 / Base64 인코딩 / 디코딩.