UTF-8
Unicode Transformation Format - 8-bit
유니코드 인코딩 방식. 가변 길이. 웹 표준 문자 인코딩. ASCII 호환. 전 세계 문자 지원.
Unicode Transformation Format - 8-bit
유니코드 인코딩 방식. 가변 길이. 웹 표준 문자 인코딩. ASCII 호환. 전 세계 문자 지원.
UTF-8(Unicode Transformation Format - 8-bit)은 유니코드 문자를 1~4바이트의 가변 길이로 인코딩하는 방식입니다. 전 세계 모든 문자(한글, 한자, 이모지 등 14만 개 이상)를 하나의 인코딩으로 표현할 수 있어 국제화(i18n)의 표준으로 자리잡았습니다. ASCII 문자는 1바이트, 한글은 3바이트, 이모지는 4바이트로 인코딩됩니다.
UTF-8은 1992년 9월, Bell Labs의 Ken Thompson과 Rob Pike가 설계했습니다. 당시 Plan 9 운영체제를 위해 개발되었으며, 기존 시스템과의 호환성을 최우선으로 고려했습니다. Thompson은 C언어와 Unix의 공동 창시자이며, Pike는 Go 언어의 창시자로 알려져 있습니다. 이 두 거장이 하룻밤 만에 설계한 UTF-8은 컴퓨터 역사상 가장 성공적인 문자 인코딩이 되었습니다.
ASCII 호환성은 UTF-8의 핵심 장점입니다. ASCII(0-127)는 UTF-8에서도 동일한 1바이트로 표현되므로, 기존 ASCII 텍스트 파일은 수정 없이 UTF-8로 인식됩니다. 또한 멀티바이트 문자의 후속 바이트는 항상 10xxxxxx 형태여서, 바이트 스트림에서 문자 경계를 쉽게 찾을 수 있고 중간부터 읽어도 동기화가 가능합니다. 이 self-synchronizing 특성 덕분에 데이터 손상에도 강합니다.
2026년 현재 웹의 98% 이상이 UTF-8을 사용합니다(W3Techs 기준). HTML5는 UTF-8을 기본 인코딩으로 권장하며, 모든 주요 프로그래밍 언어(Python 3, Go, Rust, JavaScript 등)가 UTF-8을 기본 또는 권장 인코딩으로 채택했습니다. Git, JSON, YAML 등 개발 도구와 데이터 포맷도 UTF-8이 사실상의 표준입니다.
UTF-8은 코드 포인트 범위에 따라 1~4바이트를 사용합니다.
| 바이트 수 | 코드 포인트 범위 | 바이트 패턴 | 예시 |
|---|---|---|---|
| 1바이트 | U+0000 ~ U+007F | 0xxxxxxx |
A (0x41) |
| 2바이트 | U+0080 ~ U+07FF | 110xxxxx 10xxxxxx |
e (0xC3 0xA9) |
| 3바이트 | U+0800 ~ U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
가 (0xEA 0xB0 0x80) |
| 4바이트 | U+10000 ~ U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
😀 (0xF0 0x9F 0x98 0x80) |
# UTF-8 인코딩/디코딩 기초
text = "안녕하세요 Hello 🚀"
# 문자열 → UTF-8 바이트
encoded = text.encode('utf-8')
print(f"바이트: {encoded}")
# b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94 Hello \xf0\x9f\x9a\x80'
# UTF-8 바이트 → 문자열
decoded = encoded.decode('utf-8')
print(f"문자열: {decoded}") # 안녕하세요 Hello 🚀
# ⚠️ 문자 길이 vs 바이트 길이 (핵심!)
print(f"문자 수: {len(text)}") # 14 (실제 글자 수)
print(f"바이트 수: {len(encoded)}") # 26 (저장 공간)
# 각 문자별 바이트 길이 확인
for char in "A가😀":
byte_len = len(char.encode('utf-8'))
print(f"'{char}' → {byte_len}바이트")
# 'A' → 1바이트 (ASCII)
# '가' → 3바이트 (한글)
# '😀' → 4바이트 (이모지)
# BOM 처리 (주의!)
with open("file.txt", "r", encoding="utf-8-sig") as f:
content = f.read() # BOM 자동 제거
# BOM 수동 제거
if content.startswith('\ufeff'):
content = content[1:]
# MySQL/MariaDB 한글 저장 시 주의
# utf8 (3바이트) 대신 utf8mb4 (4바이트) 사용!
# CREATE TABLE users (name VARCHAR(100)) CHARACTER SET utf8mb4;
// UTF-8 인코딩/디코딩 (Node.js & Browser)
const text = "안녕하세요 Hello 🚀";
// TextEncoder/TextDecoder (표준 Web API)
const encoder = new TextEncoder(); // 항상 UTF-8
const decoder = new TextDecoder('utf-8');
// 문자열 → UTF-8 바이트 (Uint8Array)
const encoded = encoder.encode(text);
console.log("바이트:", encoded);
// Uint8Array(26) [236, 149, 136, ...]
// UTF-8 바이트 → 문자열
const decoded = decoder.decode(encoded);
console.log("문자열:", decoded); // 안녕하세요 Hello 🚀
// ⚠️ 문자 길이 vs 바이트 길이
console.log(`문자 수: ${text.length}`); // 15 (JS는 UTF-16 기준!)
console.log(`실제 문자 수: ${[...text].length}`); // 14 (정확한 문자 수)
console.log(`바이트 수: ${encoded.length}`); // 26
// 이모지 주의! (Surrogate Pair)
const emoji = "😀";
console.log(emoji.length); // 2 (JS 내부는 UTF-16)
console.log([...emoji].length); // 1 (실제 문자 수)
// Buffer 활용 (Node.js)
const buf = Buffer.from(text, 'utf-8');
console.log(`바이트 길이: ${buf.length}`); // 26
console.log(`Buffer: ${buf.toString('hex')}`);
// 바이트 길이 계산 함수
function getByteLength(str) {
return new TextEncoder().encode(str).length;
}
console.log(getByteLength("가나다")); // 9 (3 x 3바이트)
// BOM 감지 및 제거
function removeBOM(str) {
return str.charCodeAt(0) === 0xFEFF ? str.slice(1) : str;
}
import java.nio.charset.StandardCharsets;
import java.io.*;
public class UTF8Example {
public static void main(String[] args) throws Exception {
String text = "안녕하세요 Hello 🚀";
// 문자열 → UTF-8 바이트
byte[] encoded = text.getBytes(StandardCharsets.UTF_8);
System.out.println("바이트 수: " + encoded.length); // 26
// UTF-8 바이트 → 문자열
String decoded = new String(encoded, StandardCharsets.UTF_8);
System.out.println("문자열: " + decoded);
// ⚠️ 문자 길이 vs 바이트 길이
System.out.println("length(): " + text.length()); // 15 (UTF-16 기준)
System.out.println("codePointCount: " +
text.codePointCount(0, text.length())); // 14 (실제 문자 수)
// 각 문자별 바이트 길이
for (String ch : new String[]{"A", "가", "😀"}) {
int byteLen = ch.getBytes(StandardCharsets.UTF_8).length;
System.out.println("'" + ch + "' → " + byteLen + "바이트");
}
// 파일 읽기/쓰기 (UTF-8 명시!)
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("test.txt"),
StandardCharsets.UTF_8))) {
writer.write(text);
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("test.txt"),
StandardCharsets.UTF_8))) {
String line = reader.readLine();
System.out.println("읽은 내용: " + line);
}
// BOM 제거
String withBOM = "\uFEFF안녕하세요";
if (withBOM.charAt(0) == '\uFEFF') {
withBOM = withBOM.substring(1);
}
}
}
"CSV 파일 한글이 깨지는 건 인코딩 불일치 문제예요. 엑셀에서 저장한 CSV는 기본이 EUC-KR인데, 우리 서버는 UTF-8로 읽고 있어요. 파일을 열 때 encoding='euc-kr'로 바꾸거나, 엑셀에서 UTF-8 CSV로 다시 저장해달라고 요청하세요. 아니면 chardet 라이브러리로 인코딩 자동 감지하는 것도 방법이에요."
"UTF-8은 가변 길이라 ASCII는 1바이트, 한글은 3바이트입니다. UTF-16은 대부분 2바이트고 이모지 같은 확장 문자만 4바이트(Surrogate Pair)예요. 영어 위주면 UTF-8이 공간 효율적이고, 한글/한자 위주면 UTF-16이 유리합니다. 하지만 웹 호환성과 ASCII 호환 때문에 UTF-8이 사실상 표준이에요. Java와 JavaScript는 내부적으로 UTF-16을 쓰는데, 이 때문에 이모지 length가 2로 나오는 함정이 있어요."
"MySQL에서 utf8 말고 utf8mb4를 써야 해요. 레거시 utf8은 3바이트까지만 지원해서 이모지 저장이 안 됩니다. utf8mb4가 진짜 UTF-8이에요. 그리고 컬럼에 VARCHAR(100) 쓰면 문자 100개가 아니라 바이트 기준일 수 있으니 한글 들어가는 컬럼은 CHAR_LENGTH로 확인해보세요."
UTF-8 BOM(0xEF 0xBB 0xBF)은 Windows 메모장에서 자동 추가됩니다. JSON 파싱 오류, CSV 첫 컬럼 인식 실패의 원인이 됩니다. Python은 'utf-8-sig'로, 직접 \ufeff 체크로 제거하세요. Unix에서는 BOM 없는 UTF-8이 표준입니다.
len("가나다")는 3이지만 바이트는 9입니다. DB VARCHAR, 파일 크기 계산, 네트워크 전송량은 바이트 기준입니다. API rate limit이 바이트 기준인 경우도 있으니, 한글 데이터는 3배로 계산하세요.
MySQL utf8 != 진짜 UTF-8. utf8mb4를 사용하세요. Collation은 utf8mb4_unicode_ci(일반적)나 utf8mb4_0900_ai_ci(MySQL 8.0+)를 권장합니다. PostgreSQL은 기본 UTF-8이지만 LC_COLLATE 확인 필요.
1) 프로젝트 전체 UTF-8 통일 (파일, DB, API 응답) 2) Content-Type: text/html; charset=utf-8 명시 3) HTML에 <meta charset="UTF-8"> 최상단 배치 4) 이모지 처리 시 codePointCount/스프레드 연산자 사용