💻 프로그래밍

UTF-8

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 ThompsonRob 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 바이트 구조

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 vs UTF-16
"UTF-8은 가변 길이라 ASCII는 1바이트, 한글은 3바이트입니다. UTF-16은 대부분 2바이트고 이모지 같은 확장 문자만 4바이트(Surrogate Pair)예요. 영어 위주면 UTF-8이 공간 효율적이고, 한글/한자 위주면 UTF-16이 유리합니다. 하지만 웹 호환성과 ASCII 호환 때문에 UTF-8이 사실상 표준이에요. Java와 JavaScript는 내부적으로 UTF-16을 쓰는데, 이 때문에 이모지 length가 2로 나오는 함정이 있어요."
🔧 기술 토론: DB 설정
"MySQL에서 utf8 말고 utf8mb4를 써야 해요. 레거시 utf8은 3바이트까지만 지원해서 이모지 저장이 안 됩니다. utf8mb4가 진짜 UTF-8이에요. 그리고 컬럼에 VARCHAR(100) 쓰면 문자 100개가 아니라 바이트 기준일 수 있으니 한글 들어가는 컬럼은 CHAR_LENGTH로 확인해보세요."

⚠️ 흔한 실수 & 주의사항

📄
BOM(Byte Order Mark) 처리

UTF-8 BOM(0xEF 0xBB 0xBF)은 Windows 메모장에서 자동 추가됩니다. JSON 파싱 오류, CSV 첫 컬럼 인식 실패의 원인이 됩니다. Python은 'utf-8-sig'로, 직접 \ufeff 체크로 제거하세요. Unix에서는 BOM 없는 UTF-8이 표준입니다.

📏
문자 길이 vs 바이트 길이

len("가나다")는 3이지만 바이트는 9입니다. DB VARCHAR, 파일 크기 계산, 네트워크 전송량은 바이트 기준입니다. API rate limit이 바이트 기준인 경우도 있으니, 한글 데이터는 3배로 계산하세요.

🗄️
데이터베이스 Collation 설정

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/스프레드 연산자 사용

🔗 관련 용어

📚 더 배우기