UTF-8은 Unicode 문자를 1~4바이트로 저장하는 가변 길이 인코딩입니다. 영문자는 1바이트 그대로라 기존 ASCII 파일이 수정 없이 UTF-8로도 유효하고, 한글은 3바이트, 이모지는 4바이트를 씁니다. 하나의 방식으로 전 세계 모든 문자를 처리할 수 있어 웹 표준이 됐습니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
Unicode code point는 무엇인가요?
문자 하나에 붙은 고유 번호입니다. Unicode는 전 세계 문자를 하나의 표에 정리한 표준인데, 그 표의 각 칸에 붙은 번호가 code point입니다. U+ 뒤의 16진수가 code point이며, 현재 U+0000 ~ U+10FFFF 범위로 약 110만 개의 슬롯이 있습니다.
code point는 번호일 뿐이고, 이를 메모리에 어떻게 담을지는 UTF-8, UTF-16 같은 인코딩이 결정합니다.
UTF-8이 ASCII와 호환된다는 말은?
ASCII 문자(U+0000-U+007F)는 UTF-8에서도 같은 1바이트 값으로 표현됩니다. 예를 들어 A는 ASCII에서도 UTF-8에서도 0x41 한 바이트입니다. 기존 ASCII 파일을 UTF-8로 읽어도 변환 없이 그대로 동작하는 이유입니다.
UTF-8은 가변 길이인데, 왜 좋은가요?
문자마다 필요한 만큼의 바이트만 씁니다. 영어처럼 ASCII 범위에 속하는 문자는 1바이트로 처리하고, 한글이나 이모지는 3~4바이트를 씁니다. UTF-32처럼 모든 문자를 고정 4바이트로 저장하면 영문 텍스트가 4배 커집니다. 가변 길이이기 때문에 공간 효율과 전 세계 문자 지원을 동시에 달성합니다.
UTF-8은 바이트 경계를 어떻게 구분하나요?
각 바이트의 첫 비트 패턴이 역할을 알려줍니다.
0xxxxxxx → 1바이트 문자 (ASCII)110xxxxx → 2바이트 문자의 첫 번째 바이트1110xxxx → 3바이트 문자의 첫 번째 바이트11110xxx → 4바이트 문자의 첫 번째 바이트10xxxxxx → 연속 바이트 (멀티바이트의 중간/끝)
10으로 시작하는 바이트는 항상 연속 바이트(continuation byte)입니다. 덕분에 바이트 스트림 어디서부터 읽어도 해당 바이트가 문자의 시작인지 중간인지 즉시 판단할 수 있습니다.
잘린 바이트열이 유효하지 않은 UTF-8 시퀀스가 되어 문자가 깨집니다. 예를 들어 가(EA B0 80, 3바이트)를 2바이트만 읽으면 10xxxxxx 형태의 연속 바이트가 혼자 남아 파싱 오류가 나거나 ?로 치환됩니다. 언어마다 문자열 슬라이싱이 바이트 기준인지 code point 기준인지를 확인해야 합니다.
UTF-8에서 한글은 몇 바이트인가요?
한글 음절은 U+AC00-U+D7A3 범위에 있어 UTF-8에서 3바이트로 표현됩니다. 영문자는 1바이트, 이모지는 보통 4바이트입니다. 100자 한글 문자열을 UTF-8로 저장하면 300바이트가 됩니다.
저장할 때 사용한 인코딩과 읽을 때 해석한 인코딩이 다르면 같은 바이트열을 다른 문자로 해석해 깨집니다. 예를 들어 UTF-8로 저장한 한글 파일을 EUC-KR로 읽으면 바이트 경계와 문자 매핑이 달라 글자가 깨집니다. HTTP 헤더의 Content-Type: text/html; charset=UTF-8이나 HTML <meta charset="UTF-8">이 인코딩을 명시하는 이유입니다.
이렇게 인코딩 불일치로 생기는 깨진 텍스트를 mojibake(문자 둔갑)라고 합니다. 占쏙옙이 대표적인 사례로, U+FFFD로 대체된 UTF-8 바이트열을 EUC-KR로 다시 잘못 읽으면서 생기는 이중 변환 결과입니다.
다이아몬드 물음표(�)는 왜 나타나나요?
Unicode 표준에 정의된 대체 문자(Replacement Character) U+FFFD입니다. 디코더가 유효하지 않은 바이트 시퀀스를 만났을 때 크래시 대신 이 문자로 교체합니다. 예를 들어 가(EA B0 80)에서 80만 혼자 남으면 continuation byte가 문자 시작 없이 등장한 것이라 유효하지 않아, 디코더가 해당 바이트를 버리고 U+FFFD를 찍습니다.
"占쏙옙" 占쏙 占쏙옙 占 占쏙옙占쏙?
바이트는 유효하지만 잘못된 인코딩으로 읽혀서 생깁니다. �와는 다른 현상입니다. UTF-8로 저장된 한글 바이트열을 EUC-KR로 읽으면, 디코더 입장에서는 바이트가 유효하지만 EUC-KR 문자 표에서 전혀 다른 문자로 매핑됩니다. "못 읽는" 게 아니라 "엉뚱하게 읽는" 것입니다. 이렇게 인코딩 불일치로 생기는 깨진 텍스트를 mojibake(문자 둔갑)라고 부릅니다.
"占쏙옙"은 두 현상이 겹친 사례입니다. U+FFFD로 대체된 UTF-8 바이트열을 EUC-KR로 잘못 읽으면서 생기는 이중 변환 결과입니다.
BOM(Byte Order Mark)이란 무엇인가요?
파일 앞에 붙이는 특수 바이트 시퀀스로, 인코딩 방식을 알려주는 역할을 합니다. UTF-8의 BOM은 EF BB BF입니다. UTF-8은 byte order가 고정되어 있어 BOM이 필수가 아니며, 일부 파서에서 BOM을 텍스트로 오해하거나 파싱 오류를 일으킬 수 있어 UTF-8에서는 쓰지 않는 편이 일반적입니다.
URL에서 한글이 %EC%9C%A0처럼 보이는 이유는?
URL은 ASCII 범위 밖 문자를 직접 쓸 수 없어서, UTF-8 바이트 각각을 %XX 형식으로 변환하는 퍼센트 인코딩을 사용합니다.
유(U+C720)의 UTF-8 바이트는 EC 9C A0이므로 URL에서 %EC%9C%A0이 됩니다. 한글 한 글자가 3바이트이므로 URL에서 %XX 세 쌍으로 나타납니다.
참고로, U+C720을 이진수로 바꾸면 1100 0111 0010 0000(16비트)이고, 3바이트 틀 1110xxxx 10xxxxxx 10xxxxxx에 앞 4자리, 중간 6자리, 마지막 6자리를 채우면 11101100 10011100 10100000 = EC 9C A0이 됩니다.
UTF-16, UTF-32와의 차이는?
인코딩
바이트 수
특징
UTF-8
1~4바이트
ASCII 호환, 영문 효율적, 엔디언 무관
UTF-16
2~4바이트
대부분 2바이트, 엔디언 문제로 BOM 필요
UTF-32
고정 4바이트
처리 단순, 공간 낭비 큼
UTF-16은 가(U+AC00)를 2바이트로 저장할 때 AC 00인지 00 AC인지 (big-endian / little-endian) 두 가지 해석이 생겨 BOM이 필요합니다. UTF-8은 1바이트씩 처리하므로 바이트 순서 문제가 없어 BOM 없이도 단일 방식으로 동작합니다. 웹과 파일 저장에는 UTF-8이 사실상 표준이고, Java나 JavaScript 엔진은 내부적으로 UTF-16을 씁니다.
한국에서 EUC-KR을 쓰던 시절은 왜 있었나요?
Unicode가 보급되기 전에는 각 언어마다 자체 인코딩을 사용했습니다. EUC-KR은 한국어를 표현하기 위해 만들어진 인코딩으로, 한글 음절을 2바이트로 처리하지만 Unicode 전체를 표현하지는 못합니다. Unicode와 UTF-8이 표준으로 자리 잡으면서 대체되었고, 현재는 일부 레거시 시스템에서만 남아 있습니다.
부가 설명
Unicode는 전 세계 문자 각각에 고유 번호를 붙인 표준입니다. 이 번호를 code point라고 하며, A는 U+0041, 가는 U+AC00, 😀는 U+1F600입니다. code point는 번호일 뿐, 메모리나 파일에 실제로 어떻게 저장할지는 정하지 않습니다. Unicode의 범위는 U+0000 ~ U+10FFFF로, 17개 평면(plane) × 65,536개 슬롯 = 약 110만 개입니다. U+10FFFF가 상한인 이유는 UTF-16의 surrogate pair가 표현할 수 있는 최대치가 그 범위이기 때문입니다.
UTF-8은 code point를 바이트열로 변환하는 규칙입니다. code point 범위에 따라 바이트 수가 달라집니다.
범위
바이트 수
예
U+0000 - U+007F
1바이트
영문, 숫자, 특수문자
U+0080 - U+07FF
2바이트
라틴 확장, 아랍어
U+0800 - U+FFFF
3바이트
한글, 한자
U+10000 - U+10FFFF
4바이트
이모지, 희귀 문자
ASCII(U+0000-U+007F)가 1바이트로 그대로 유지되기 때문에, 기존 ASCII 파일을 UTF-8로 읽어도 동일하게 동작합니다.
웹에서 UTF-8이 표준이 된 결정적 이유는 ASCII와 완전히 호환된다는 점입니다. HTTP 헤더와 HTML 태그가 ASCII 기반이라, 기존 서버와 파서를 수정 없이 그대로 쓸 수 있었습니다.
UTF-16은 A(0x41)를 00 41로 저장하는데, 앞에 붙는 00이 null byte라서 C로 짠 대부분의 프로그램이 문자열의 끝으로 오해합니다. 웹 초창기 인프라가 전부 C 기반이었으니 UTF-16은 처음부터 선택지가 아니었습니다. EUC-KR 같은 국가별 인코딩은 특정 언어만 담을 수 있어 전 세계 문자를 하나의 방식으로 처리하는 건 불가능했고요.
한 줄 정리
UTF-8은 Unicode 문자를 1~4바이트의 가변 길이 바이트열로 표현하는 문자 인코딩 방식입니다.