문자 인코딩 완벽 가이드 | ASCII·UTF-8·UTF-16·EUC-KR 총정리

문자 인코딩 완벽 가이드 | ASCII·UTF-8·UTF-16·EUC-KR 총정리

이 글의 핵심

ASCII, ANSI, Unicode, UTF-8, UTF-16, UTF-32, EUC-KR, CP949 등 모든 문자 인코딩 방식의 원리와 차이점. 한글 깨짐 문제 해결부터 BOM, Endian까지 실전 예제로 완벽 이해.

들어가며: 왜 문자 인코딩을 알아야 하나?

개발하다 보면 한글이 깨지거나, 파일을 읽을 수 없거나, API 응답이 이상하게 나오는 경험을 합니다. 이 모든 문제의 근본 원인은 문자 인코딩입니다.

이 글에서 다룰 내용:

  • ASCII, ANSI, Unicode의 역사
  • UTF-8, UTF-16, UTF-32 인코딩 방식
  • 한글 인코딩 (EUC-KR, CP949)
  • BOM, Endian, 인코딩 감지
  • 실전 문제 해결

목차

  1. 문자 인코딩의 역사
  2. ASCII: 7비트 문자 집합
  3. ANSI와 코드 페이지
  4. Unicode: 전 세계 문자 통합
  5. UTF-8: 가변 길이 인코딩
  6. UTF-16과 UTF-32
  7. 한글 인코딩 (EUC-KR, CP949)
  8. BOM과 Endian
  9. 실전 문제 해결
  10. 프로그래밍 언어별 처리

1. 문자 인코딩의 역사

타임라인

timeline
    title 문자 인코딩 발전사
    1963 : ASCII 제정<br/>7비트, 128자
    1987 : ISO-8859-1 (Latin-1)<br/>8비트, 256자
    1991 : Unicode 1.0<br/>16비트 통합 문자셋
    1992 : UTF-8 발명<br/>가변 길이 인코딩
    1996 : UTF-16<br/>서로게이트 페어
    2003 : UTF-8 웹 표준화
    2008 : UTF-8이 웹에서<br/>가장 많이 사용
    2026 : UTF-8 점유율 98%

왜 여러 인코딩이 존재하나?

flowchart TB
    Problem[문제: 컴퓨터는<br/>숫자만 이해]
    
    ASCII[ASCII<br/>영문 128자]
    Extended[확장 ASCII<br/>각국 언어 256자]
    Unicode[Unicode<br/>전 세계 문자 통합]
    
    Problem --> ASCII
    ASCII --> Extended
    Extended --> Unicode
    
    ASCII --> Issue1[문제: 한글, 한자<br/>표현 불가]
    Extended --> Issue2[문제: 각국마다<br/>다른 코드 페이지]
    Unicode --> Solution[해결: 모든 문자에<br/>고유 번호 부여]

2. ASCII: 7비트 문자 집합

ASCII란?

ASCII(American Standard Code for Information Interchange)는 7비트(0-127)로 영문 알파벳, 숫자, 특수문자를 표현합니다.

ASCII 테이블

Dec  Hex  Char  |  Dec  Hex  Char  |  Dec  Hex  Char
-------------------------------------------------
 32  20   Space |  64  40   @      |  96  60   `
 33  21   !     |  65  41   A      |  97  61   a
 34  22   "     |  66  42   B      |  98  62   b
 35  23   #     |  67  43   C      |  99  63   c
...
 48  30   0     |  80  50   P      | 112  70   p
 49  31   1     |  81  51   Q      | 113  71   q
...
 57  39   9     |  90  5A   Z      | 122  7A   z

ASCII 제어 문자

# 주요 제어 문자
NUL = 0x00  # Null
LF  = 0x0A  # Line Feed (\n)
CR  = 0x0D  # Carriage Return (\r)
ESC = 0x1B  # Escape
DEL = 0x7F  # Delete

# 줄바꿈 방식
# Unix/Linux: LF (\n)
# Windows: CR+LF (\r\n)
# Mac (Classic): CR (\r)

ASCII 예제

# 문자 → 코드
ord('A')  # 65
ord('a')  # 97
ord('0')  # 48

# 코드 → 문자
chr(65)   # 'A'
chr(97)   # 'a'

# ASCII 범위 확인
def is_ascii(text):
    return all(ord(c) < 128 for c in text)

is_ascii("Hello")  # True
is_ascii("안녕")    # False

3. ANSI와 코드 페이지

ANSI란?

ANSI는 8비트(0-255)로 확장하여 각국 언어를 지원합니다. 하지만 코드 페이지(Code Page)마다 128-255 범위의 의미가 다릅니다.

주요 코드 페이지

코드 페이지 이름 지역 특징
CP437 OEM-US 미국 DOS 기본
CP850 Latin-1 서유럽 DOS 다국어
CP949 확장 완성형 한국 Windows 한글
CP932 Shift-JIS 일본 Windows 일본어
CP936 GBK 중국 Windows 중국어
ISO-8859-1 Latin-1 서유럽 Unix/Web
ISO-8859-15 Latin-9 서유럽 유로(€) 추가

코드 페이지 문제

# 같은 바이트 값이 다른 의미
byte_value = 0xC7

# CP949 (한글): '한'
text_korean = byte_value.to_bytes(1, 'big').decode('cp949')  # 에러 (2바이트 필요)

# ISO-8859-1 (Latin-1): 'Ç'
text_latin = byte_value.to_bytes(1, 'big').decode('latin-1')  # 'Ç'

# 같은 파일을 다른 인코딩으로 읽으면 깨짐!

4. Unicode: 전 세계 문자 통합

Unicode란?

Unicode는 전 세계 모든 문자에 고유한 코드 포인트(Code Point)를 부여한 문자 집합입니다.

Unicode 구조

U+0000 ~ U+10FFFF (1,114,112개 코드 포인트)

U+0000 ~ U+007F   : ASCII (128자)
U+0080 ~ U+00FF   : Latin-1 Supplement
U+0100 ~ U+017F   : Latin Extended-A
U+0370 ~ U+03FF   : Greek
U+0400 ~ U+04FF   : Cyrillic
U+0600 ~ U+06FF   : Arabic
U+0E00 ~ U+0E7F   : Thai
U+3040 ~ U+309F   : Hiragana (일본어)
U+30A0 ~ U+30FF   : Katakana (일본어)
U+4E00 ~ U+9FFF   : CJK Unified Ideographs (한중일 한자)
U+AC00 ~ U+D7AF   : Hangul Syllables (한글 11,172자)
U+1F600 ~ U+1F64F : Emoticons (이모지)

한글 Unicode 범위

# 한글 음절 (가-힣)
print(f"가: U+{ord('가'):04X}")  # U+AC00
print(f"힣: U+{ord('힣'):04X}")  # U+D7A3

# 한글 자모 (ㄱ-ㅎ, ㅏ-ㅣ)
print(f"ㄱ: U+{ord('ㄱ'):04X}")  # U+3131
print(f"ㅎ: U+{ord('ㅎ'):04X}")  # U+314E
print(f"ㅏ: U+{ord('ㅏ'):04X}")  # U+314F
print(f"ㅣ: U+{ord('ㅣ'):04X}")  # U+3163

# 이모지
print(f"😀: U+{ord('😀'):04X}")  # U+1F600

Unicode vs 인코딩

Unicode: 문자 집합 (Character Set)
         각 문자에 번호(코드 포인트) 부여
         예: '한' = U+D55C

UTF-8/UTF-16/UTF-32: 인코딩 방식 (Encoding)
                      코드 포인트를 바이트로 변환하는 방법
                      예: U+D55C → UTF-8: ED 95 9C (3바이트)
                                → UTF-16: D5 5C (2바이트)

5. UTF-8: 가변 길이 인코딩

UTF-8이란?

UTF-8은 1-4바이트 가변 길이로 Unicode를 인코딩합니다. 웹 표준이며, ASCII와 완벽히 호환됩니다.

UTF-8 인코딩 규칙

코드 포인트 범위        | 바이트 수 | 인코딩 패턴
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

UTF-8 인코딩 예시

영문 'A' (U+0041)

코드 포인트: U+0041 (65)
이진수: 0100 0001

UTF-8 인코딩:
0100 0001 = 0x41 (1바이트)

메모리: 41

한글 '한' (U+D55C)

코드 포인트: U+D55C (54,620)
이진수: 1101 0101 0101 1100

UTF-8 인코딩 (3바이트):
1110xxxx 10xxxxxx 10xxxxxx
1110 1101  10 010101  10 011100
   E   D      9   5      9   C

메모리: ED 95 9C

이모지 '😀' (U+1F600)

코드 포인트: U+1F600 (128,512)
이진수: 0001 1111 0110 0000 0000

UTF-8 인코딩 (4바이트):
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
11110 000  10 011111  10 011000  10 000000
   F   0      9   F      9   8      8   0

메모리: F0 9F 98 80

UTF-8 장점

flowchart TB
    UTF8[UTF-8]
    
    Adv1[✅ ASCII 호환<br/>영문은 1바이트]
    Adv2[✅ 자기 동기화<br/>중간부터 읽기 가능]
    Adv3[✅ 바이트 순서 무관<br/>Endian 문제 없음]
    Adv4[✅ 웹 표준<br/>98% 점유율]
    
    UTF8 --> Adv1
    UTF8 --> Adv2
    UTF8 --> Adv3
    UTF8 --> Adv4

Python으로 UTF-8 인코딩

# 문자열 → 바이트
text = "Hello 한글 😀"

# UTF-8 인코딩
utf8_bytes = text.encode('utf-8')
print(utf8_bytes)
# b'Hello \xed\x95\x9c\xea\xb8\x80 \xf0\x9f\x98\x80'

# 바이트 분석
for i, byte in enumerate(utf8_bytes):
    print(f"{i:2d}: 0x{byte:02X} ({byte:3d}) {chr(byte) if byte < 128 else '?'}")

# 출력:
#  0: 0x48 ( 72) H
#  1: 0x65 (101) e
#  2: 0x6C (108) l
#  3: 0x6C (108) l
#  4: 0x6F (111) o
#  5: 0x20 ( 32)  
#  6: 0xED (237) ?  ← '한' 시작
#  7: 0x95 (149) ?
#  8: 0x9C (156) ?
#  9: 0xEA (234) ?  ← '글' 시작
# 10: 0xB8 (184) ?
# 11: 0x80 (128) ?
# 12: 0x20 ( 32)  
# 13: 0xF0 (240) ?  ← '😀' 시작
# 14: 0x9F (159) ?
# 15: 0x98 (152) ?
# 16: 0x80 (128) ?

# 바이트 → 문자열
decoded = utf8_bytes.decode('utf-8')
print(decoded)  # "Hello 한글 😀"

6. UTF-16과 UTF-32

UTF-16

UTF-16은 2바이트 또는 4바이트로 인코딩합니다. Windows, Java, JavaScript 내부에서 사용됩니다.

UTF-16 인코딩 규칙

코드 포인트 범위        | 바이트 수 | 방식
U+0000   ~ U+FFFF     | 2바이트   | 그대로 인코딩
U+10000  ~ U+10FFFF   | 4바이트   | 서로게이트 페어

서로게이트 페어 (Surrogate Pair)

# 이모지 '😀' (U+1F600)를 UTF-16으로 인코딩

# 1. U+1F600 - 0x10000 = 0xF600
# 2. 상위 10비트: 0x3D (61)
# 3. 하위 10비트: 0x200 (512)
# 4. High Surrogate: 0xD800 + 0x3D = 0xD83D
# 5. Low Surrogate: 0xDC00 + 0x200 = 0xDE00

text = "😀"
utf16_bytes = text.encode('utf-16-le')
print(utf16_bytes.hex())  # '3dd8 00de' (Little-Endian)

# UTF-16 BE (Big-Endian)
utf16_be = text.encode('utf-16-be')
print(utf16_be.hex())  # 'd83d de00'

UTF-16 예시

text = "Hello 한글"

# UTF-16 LE (Little-Endian)
utf16_le = text.encode('utf-16-le')
print(utf16_le.hex())
# 48 00 65 00 6c 00 6c 00 6f 00 20 00 5c d5 00 ae 00 b8

# UTF-16 BE (Big-Endian)
utf16_be = text.encode('utf-16-be')
print(utf16_be.hex())
# 00 48 00 65 00 6c 00 6c 00 6f 00 20 d5 5c ae 00 b8 00

UTF-32

UTF-32는 모든 문자를 4바이트 고정 길이로 인코딩합니다.

text = "A한😀"

# UTF-32 LE
utf32 = text.encode('utf-32-le')
print(utf32.hex())
# 41 00 00 00  5c d5 00 00  00 f6 01 00

# 각 문자가 정확히 4바이트
# 'A':  0x00000041
# '한': 0x0000D55C
# '😀': 0x0001F600

인코딩 비교

text = "Hello 한글 😀"

encodings = ['utf-8', 'utf-16-le', 'utf-16-be', 'utf-32-le']

for enc in encodings:
    encoded = text.encode(enc)
    print(f"{enc:12s}: {len(encoded):2d} bytes | {encoded.hex()[:40]}...")

# 출력:
# utf-8       : 17 bytes | 48656c6c6f20ed959ceab880f09f9880
# utf-16-le   : 20 bytes | 480065006c006c006f002000d55c00aeb800...
# utf-16-be   : 20 bytes | 004800650069006c006f0020d55cae00b800...
# utf-32-le   : 36 bytes | 4100000065000000...

7. 한글 인코딩 (EUC-KR, CP949)

한글 인코딩 역사

timeline
    title 한글 인코딩 발전
    1987 : KS X 1001<br/>완성형 2,350자
    1992 : EUC-KR<br/>완성형 표준
    1996 : CP949 (MS)<br/>확장 완성형 11,172자
    2000s : UTF-8<br/>Unicode 기반

EUC-KR

EUC-KR은 2바이트로 한글 2,350자를 표현합니다.

# EUC-KR 인코딩
text = "한글"

euckr_bytes = text.encode('euc-kr')
print(euckr_bytes.hex())  # c7d1 b1db

# '한': 0xC7D1
# '글': 0xB1DB

# 문제: '똠', '쀍' 같은 글자는 표현 불가
try:
    "똠".encode('euc-kr')
except UnicodeEncodeError as e:
    print(f"❌ EUC-KR로 인코딩 불가: {e}")

CP949 (확장 완성형)

CP949는 EUC-KR을 확장하여 11,172자 모두 지원합니다.

# CP949 인코딩
text = "똠방각하"

cp949_bytes = text.encode('cp949')
print(cp949_bytes.hex())

# EUC-KR에 없는 글자도 표현 가능
text2 = "쀍똠뙠"
print(text2.encode('cp949').hex())

UTF-8 vs EUC-KR 비교

text = "Hello 한글"

# UTF-8: 영문 1바이트, 한글 3바이트
utf8 = text.encode('utf-8')
print(f"UTF-8:   {len(utf8)} bytes | {utf8.hex()}")
# UTF-8:   14 bytes | 48656c6c6f20ed959ceab880

# EUC-KR: 영문 1바이트, 한글 2바이트
euckr = text.encode('euc-kr')
print(f"EUC-KR:  {len(euckr)} bytes | {euckr.hex()}")
# EUC-KR:  10 bytes | 48656c6c6f20c7d1b1db

8. BOM과 Endian

BOM (Byte Order Mark)

BOM은 파일 시작 부분에 붙는 특수 바이트로, 인코딩 방식과 바이트 순서를 표시합니다.

인코딩      | BOM (hex)      | 크기
UTF-8      | EF BB BF       | 3바이트
UTF-16 LE  | FF FE          | 2바이트
UTF-16 BE  | FE FF          | 2바이트
UTF-32 LE  | FF FE 00 00    | 4바이트
UTF-32 BE  | 00 00 FE FF    | 4바이트

BOM 예시

# UTF-8 with BOM
text = "Hello"
with open('file_with_bom.txt', 'wb') as f:
    f.write(b'\xef\xbb\xbf')  # BOM
    f.write(text.encode('utf-8'))

# 파일 내용 (hex):
# EF BB BF 48 65 6C 6C 6F
# ^^^^^^^^ BOM
#          ^^^^^^^^^^^^^^ "Hello"

# UTF-8 without BOM (권장)
with open('file_no_bom.txt', 'wb') as f:
    f.write(text.encode('utf-8'))

# 파일 내용 (hex):
# 48 65 6C 6C 6F

BOM 감지 및 제거

def detect_and_remove_bom(data):
    """BOM 감지 및 제거"""
    bom_signatures = [
        (b'\xef\xbb\xbf', 'utf-8-sig'),
        (b'\xff\xfe\x00\x00', 'utf-32-le'),
        (b'\x00\x00\xfe\xff', 'utf-32-be'),
        (b'\xff\xfe', 'utf-16-le'),
        (b'\xfe\xff', 'utf-16-be'),
    ]
    
    for bom, encoding in bom_signatures:
        if data.startswith(bom):
            return data[len(bom):], encoding
    
    return data, None

# 사용
with open('file.txt', 'rb') as f:
    data = f.read()

data, encoding = detect_and_remove_bom(data)
if encoding:
    print(f"✅ BOM detected: {encoding}")
    text = data.decode(encoding.replace('-sig', ''))
else:
    print("ℹ️  No BOM, assuming UTF-8")
    text = data.decode('utf-8')

Endian (바이트 순서)

# Big-Endian: 큰 바이트가 먼저
# Little-Endian: 작은 바이트가 먼저

# 예: 0x1234를 메모리에 저장
# Big-Endian:    12 34
# Little-Endian: 34 12

# UTF-16에서 중요
text = "한"  # U+D55C

# UTF-16 BE (Big-Endian)
be = text.encode('utf-16-be')
print(be.hex())  # d5 5c

# UTF-16 LE (Little-Endian)
le = text.encode('utf-16-le')
print(le.hex())  # 5c d5

# UTF-8은 바이트 단위라 Endian 무관
utf8 = text.encode('utf-8')
print(utf8.hex())  # ed 95 9c (항상 같음)

9. 실전 문제 해결

문제 1: 한글 깨짐 (���)

원인

# ❌ UTF-8로 저장했는데 EUC-KR로 읽음
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write("한글")

# 잘못된 읽기
with open('file.txt', 'r', encoding='euc-kr') as f:
    text = f.read()
    print(text)  # '���' (깨짐)

해결

# ✅ 올바른 인코딩으로 읽기
with open('file.txt', 'r', encoding='utf-8') as f:
    text = f.read()
    print(text)  # '한글' (정상)

# ✅ 인코딩 자동 감지
import chardet

with open('file.txt', 'rb') as f:
    raw_data = f.read()
    result = chardet.detect(raw_data)
    encoding = result['encoding']
    confidence = result['confidence']
    
    print(f"Detected: {encoding} ({confidence*100:.1f}% confidence)")
    
    text = raw_data.decode(encoding)
    print(text)

문제 2: UnicodeDecodeError

# ❌ 잘못된 인코딩으로 디코딩
utf8_bytes = "한글".encode('utf-8')

try:
    text = utf8_bytes.decode('ascii')
except UnicodeDecodeError as e:
    print(f"❌ {e}")
    # 'ascii' codec can't decode byte 0xed in position 0

# ✅ 에러 처리 옵션
# 1. 무시
text = utf8_bytes.decode('ascii', errors='ignore')
print(text)  # "" (한글 제거됨)

# 2. 대체
text = utf8_bytes.decode('ascii', errors='replace')
print(text)  # "������" (? 문자로 대체)

# 3. XML/HTML 엔티티
text = utf8_bytes.decode('ascii', errors='xmlcharrefreplace')
print(text)  # "&#54620;&#44544;" (숫자 참조)

문제 3: 웹에서 한글 깨짐

import requests

# ❌ 잘못된 방법
response = requests.get('https://example.com/korean-page')
print(response.text)  # 깨질 수 있음

# ✅ Content-Type 헤더 확인
response = requests.get('https://example.com/korean-page')
content_type = response.headers.get('Content-Type', '')
print(f"Content-Type: {content_type}")
# Content-Type: text/html; charset=euc-kr

# ✅ 올바른 인코딩으로 디코딩
if 'euc-kr' in content_type.lower():
    text = response.content.decode('euc-kr')
else:
    text = response.text  # requests가 자동 감지

# ✅ 또는 chardet으로 자동 감지
import chardet
detected = chardet.detect(response.content)
text = response.content.decode(detected['encoding'])

문제 4: CSV 파일 인코딩

import csv

# ❌ Windows Excel에서 저장한 CSV (CP949)
with open('data.csv', 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)  # UnicodeDecodeError!

# ✅ 올바른 인코딩
with open('data.csv', 'r', encoding='cp949') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

# ✅ 인코딩 자동 감지
import chardet

with open('data.csv', 'rb') as f:
    raw_data = f.read()
    detected = chardet.detect(raw_data)
    encoding = detected['encoding']

with open('data.csv', 'r', encoding=encoding) as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

10. 프로그래밍 언어별 처리

Python

# 기본 인코딩: UTF-8
text = "Hello 한글 😀"

# 인코딩
utf8 = text.encode('utf-8')
utf16 = text.encode('utf-16')
euckr = text.encode('euc-kr')  # 이모지는 에러

# 디코딩
text = utf8.decode('utf-8')

# 파일 I/O
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write(text)

with open('file.txt', 'r', encoding='utf-8') as f:
    text = f.read()

# 바이트 문자열 리터럴
utf8_bytes = b'\xed\x95\x9c\xea\xb8\x80'
text = utf8_bytes.decode('utf-8')  # "한글"

JavaScript/Node.js

// JavaScript 내부: UTF-16
const text = "Hello 한글 😀";

// 문자열 길이 (주의: 서로게이트 페어)
console.log(text.length);  // 11 (😀가 2로 계산됨)

// 올바른 길이
console.log([...text].length);  // 10

// UTF-8 인코딩 (Node.js)
const buffer = Buffer.from(text, 'utf-8');
console.log(buffer);  // <Buffer 48 65 6c 6c 6f 20 ...>

// 디코딩
const decoded = buffer.toString('utf-8');
console.log(decoded);  // "Hello 한글 😀"

// 지원 인코딩
// utf-8, utf-16le, latin1, base64, hex, ascii

Java

// Java 내부: UTF-16
String text = "Hello 한글 😀";

// UTF-8 인코딩
byte[] utf8Bytes = text.getBytes(StandardCharsets.UTF_8);
System.out.println(Arrays.toString(utf8Bytes));

// 디코딩
String decoded = new String(utf8Bytes, StandardCharsets.UTF_8);
System.out.println(decoded);

// 파일 I/O
// UTF-8로 쓰기
Files.writeString(
    Path.of("file.txt"), 
    text, 
    StandardCharsets.UTF_8
);

// UTF-8로 읽기
String content = Files.readString(
    Path.of("file.txt"), 
    StandardCharsets.UTF_8
);

C++

#include <iostream>
#include <fstream>
#include <string>
#include <codecvt>
#include <locale>

int main() {
    // UTF-8 문자열 (C++11)
    std::string utf8_str = u8"Hello 한글 😀";
    
    // UTF-16 문자열
    std::u16string utf16_str = u"Hello 한글 😀";
    
    // UTF-32 문자열
    std::u32string utf32_str = U"Hello 한글 😀";
    
    // UTF-8 → UTF-16 변환
    std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter;
    std::u16string utf16 = converter.from_bytes(utf8_str);
    
    // 파일 쓰기 (UTF-8)
    std::ofstream file("file.txt", std::ios::binary);
    file << utf8_str;
    file.close();
    
    // 파일 읽기
    std::ifstream input("file.txt", std::ios::binary);
    std::string content((std::istreambuf_iterator<char>(input)),
                        std::istreambuf_iterator<char>());
    
    std::cout << content << std::endl;
    
    return 0;
}

Go

package main

import (
    "fmt"
    "unicode/utf8"
    "golang.org/x/text/encoding/korean"
    "golang.org/x/text/transform"
    "io"
    "strings"
)

func main() {
    // Go 내부: UTF-8
    text := "Hello 한글 😀"
    
    // 바이트 길이 vs 문자(rune) 길이
    fmt.Println("Bytes:", len(text))           // 17
    fmt.Println("Runes:", utf8.RuneCountInString(text))  // 10
    
    // UTF-8 → EUC-KR 변환
    encoder := korean.EUCKR.NewEncoder()
    euckrBytes, _, _ := transform.Bytes(encoder, []byte(text))
    fmt.Printf("EUC-KR: %x\n", euckrBytes)
    
    // EUC-KR → UTF-8 변환
    decoder := korean.EUCKR.NewDecoder()
    utf8Text, _, _ := transform.String(decoder, string(euckrBytes))
    fmt.Println(utf8Text)
}

고급 주제

정규화 (Normalization)

import unicodedata

# 한글 '가'를 표현하는 두 가지 방법
# 1. 완성형 (NFC): U+AC00
nfc = "가"
print(f"NFC: {len(nfc)} chars, {nfc.encode('utf-8').hex()}")
# NFC: 1 chars, eab080

# 2. 조합형 (NFD): U+1100 + U+1161 (ㄱ + ㅏ)
nfd = unicodedata.normalize('NFD', nfc)
print(f"NFD: {len(nfd)} chars, {nfd.encode('utf-8').hex()}")
# NFD: 2 chars, e384 80e185a1

# 비교
print(nfc == nfd)  # False (다른 바이트 시퀀스)

# 정규화 후 비교
print(unicodedata.normalize('NFC', nfc) == 
      unicodedata.normalize('NFC', nfd))  # True

인코딩 감지

import chardet

def detect_encoding(file_path):
    """파일 인코딩 자동 감지"""
    with open(file_path, 'rb') as f:
        raw_data = f.read()
    
    result = chardet.detect(raw_data)
    
    return {
        'encoding': result['encoding'],
        'confidence': result['confidence'],
        'language': result.get('language', '')
    }

# 사용
info = detect_encoding('unknown.txt')
print(f"Encoding: {info['encoding']}")
print(f"Confidence: {info['confidence']*100:.1f}%")

# 올바른 인코딩으로 읽기
with open('unknown.txt', 'r', encoding=info['encoding']) as f:
    content = f.read()

인코딩 변환

def convert_file_encoding(input_file, output_file, from_enc, to_enc):
    """파일 인코딩 변환"""
    # 원본 읽기
    with open(input_file, 'r', encoding=from_enc) as f:
        content = f.read()
    
    # 새 인코딩으로 저장
    with open(output_file, 'w', encoding=to_enc) as f:
        f.write(content)
    
    print(f"✅ Converted: {from_enc} → {to_enc}")

# EUC-KR → UTF-8 변환
convert_file_encoding('old.txt', 'new.txt', 'euc-kr', 'utf-8')

웹 개발에서의 인코딩

HTML

<!DOCTYPE html>
<html>
<head>
    <!-- ✅ UTF-8 선언 (필수) -->
    <meta charset="UTF-8">
    <title>한글 페이지</title>
</head>
<body>
    <h1>안녕하세요</h1>
</body>
</html>

HTTP 헤더

from flask import Flask, Response

app = Flask(__name__)

@app.route('/korean')
def korean_page():
    content = "<h1>안녕하세요</h1>"
    
    # ✅ Content-Type에 charset 명시
    return Response(
        content,
        mimetype='text/html; charset=utf-8'
    )

# ❌ charset 없으면 브라우저가 추측 (깨질 수 있음)

JSON

import json

data = {"name": "홍길동", "message": "안녕하세요"}

# JSON은 기본적으로 UTF-8
json_str = json.dumps(data, ensure_ascii=False)
print(json_str)
# {"name": "홍길동", "message": "안녕하세요"}

# ensure_ascii=True (기본값)
json_str_ascii = json.dumps(data, ensure_ascii=True)
print(json_str_ascii)
# {"name": "\ud64d\uae38\ub3d9", "message": "\uc548\ub155\ud558\uc138\uc694"}

URL 인코딩

from urllib.parse import quote, unquote

# URL에 한글 포함
text = "한글 검색"

# URL 인코딩 (UTF-8 기반)
encoded = quote(text)
print(encoded)
# %ED%95%9C%EA%B8%80%20%EA%B2%80%EC%83%89

# URL 디코딩
decoded = unquote(encoded)
print(decoded)  # "한글 검색"

# 완전한 URL
url = f"https://example.com/search?q={encoded}"
print(url)
# https://example.com/search?q=%ED%95%9C%EA%B8%80%20%EA%B2%80%EC%83%89

데이터베이스 인코딩

MySQL

-- 데이터베이스 생성 (UTF-8)
CREATE DATABASE mydb
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

-- utf8mb4: 4바이트 UTF-8 (이모지 지원)
-- utf8: 3바이트 UTF-8 (이모지 미지원, deprecated)

-- 테이블 생성
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100) CHARACTER SET utf8mb4
);

-- 연결 시 인코딩 설정
SET NAMES utf8mb4;

PostgreSQL

-- 데이터베이스 생성
CREATE DATABASE mydb
ENCODING 'UTF8'
LC_COLLATE 'ko_KR.UTF-8'
LC_CTYPE 'ko_KR.UTF-8';

-- 클라이언트 인코딩 확인
SHOW client_encoding;

-- 인코딩 변경
SET client_encoding TO 'UTF8';

Python + DB

import psycopg2

# PostgreSQL 연결
conn = psycopg2.connect(
    host='localhost',
    database='mydb',
    user='user',
    password='pass',
    client_encoding='utf8'
)

cursor = conn.cursor()

# 한글 데이터 삽입
cursor.execute(
    "INSERT INTO users (name) VALUES (%s)",
    ("홍길동",)
)

# 조회
cursor.execute("SELECT name FROM users")
name = cursor.fetchone()[0]
print(name)  # "홍길동"

실전 도구

명령줄 도구

# 1. file 명령어로 인코딩 확인
file -i file.txt
# file.txt: text/plain; charset=utf-8

# 2. iconv로 인코딩 변환
iconv -f EUC-KR -t UTF-8 old.txt > new.txt

# 3. 여러 파일 일괄 변환
find . -name "*.txt" -exec iconv -f EUC-KR -t UTF-8 {} -o {}.utf8 \;

# 4. hexdump로 바이트 확인
echo "한글" | hexdump -C
# 00000000  ed 95 9c ea b8 80 0a

# 5. BOM 제거
tail -c +4 file_with_bom.txt > file_no_bom.txt  # UTF-8 BOM (3바이트)

Python 스크립트

#!/usr/bin/env python3
"""
파일 인코딩 일괄 변환 도구
"""
import os
import sys
import chardet
from pathlib import Path

def convert_directory(directory, from_enc=None, to_enc='utf-8'):
    """디렉토리 내 모든 텍스트 파일 인코딩 변환"""
    for file_path in Path(directory).rglob('*.txt'):
        try:
            # 원본 읽기
            with open(file_path, 'rb') as f:
                raw_data = f.read()
            
            # 인코딩 감지
            if from_enc is None:
                detected = chardet.detect(raw_data)
                source_enc = detected['encoding']
                confidence = detected['confidence']
                
                if confidence < 0.7:
                    print(f"⚠️  {file_path}: Low confidence ({confidence:.2f})")
                    continue
            else:
                source_enc = from_enc
            
            # 이미 UTF-8이면 스킵
            if source_enc.lower().replace('-', '') == 'utf8':
                print(f"✓ {file_path}: Already UTF-8")
                continue
            
            # 변환
            text = raw_data.decode(source_enc)
            
            # 저장
            with open(file_path, 'w', encoding=to_enc) as f:
                f.write(text)
            
            print(f"✅ {file_path}: {source_enc} → {to_enc}")
            
        except Exception as e:
            print(f"❌ {file_path}: {e}")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python convert_encoding.py <directory>")
        sys.exit(1)
    
    convert_directory(sys.argv[1])

인코딩 비교표

저장 공간 비교

text = "Hello 한글 😀"

encodings = {
    'ASCII (영문만)': 'ascii',
    'UTF-8': 'utf-8',
    'UTF-16 LE': 'utf-16-le',
    'UTF-16 BE': 'utf-16-be',
    'UTF-32 LE': 'utf-32-le',
    'EUC-KR': 'euc-kr',
    'CP949': 'cp949',
}

print(f"원본 텍스트: {text}\n")
print(f"{'인코딩':15s} | {'바이트':6s} | Hex")
print("-" * 50)

for name, enc in encodings.items():
    try:
        encoded = text.encode(enc)
        hex_str = encoded.hex()[:30] + ('...' if len(encoded) > 15 else '')
        print(f"{name:15s} | {len(encoded):4d}B | {hex_str}")
    except UnicodeEncodeError:
        print(f"{name:15s} | {'N/A':6s} | (인코딩 불가)")

# 출력:
# 원본 텍스트: Hello 한글 😀
# 
# 인코딩           | 바이트 | Hex
# --------------------------------------------------
# ASCII (영문만)   |    N/A | (인코딩 불가)
# UTF-8           |   17B | 48656c6c6f20ed959ceab880f0...
# UTF-16 LE       |   20B | 480065006c006c006f00200000...
# UTF-16 BE       |   20B | 004800650069006c006f002000...
# UTF-32 LE       |   36B | 410000006500000069000000...
# EUC-KR          |    N/A | (인코딩 불가)
# CP949           |    N/A | (인코딩 불가)

특성 비교

인코딩 바이트/문자 ASCII 호환 한글 효율 이모지 주요 사용처
ASCII 1 영문 전용
EUC-KR 1-2 ✅✅ 한국 레거시
CP949 1-2 ✅✅ Windows 한글
UTF-8 1-4 웹, Linux, 현대 표준
UTF-16 2-4 ✅✅ Windows, Java 내부
UTF-32 4 내부 처리

실전 시나리오

시나리오 1: 레거시 시스템 연동

# 문제: 은행 API가 EUC-KR 응답
import requests

response = requests.get('http://legacy-bank-api.com/account')

# ❌ 자동 디코딩 (UTF-8 가정)
# print(response.text)  # 깨짐

# ✅ 올바른 처리
content = response.content  # 바이트
text = content.decode('euc-kr')
print(text)

# ✅ 또는 requests에 힌트 제공
response.encoding = 'euc-kr'
print(response.text)

시나리오 2: 다국어 지원 애플리케이션

import locale
import sys

def setup_encoding():
    """시스템 인코딩 설정"""
    # 표준 출력 인코딩 확인
    print(f"stdout encoding: {sys.stdout.encoding}")
    
    # 시스템 로케일
    print(f"System locale: {locale.getpreferredencoding()}")
    
    # UTF-8 강제 (Python 3.7+)
    if sys.stdout.encoding != 'utf-8':
        sys.stdout.reconfigure(encoding='utf-8')

# 다국어 텍스트 처리
texts = {
    'en': "Hello",
    'ko': "안녕하세요",
    'ja': "こんにちは",
    'zh': "你好",
    'ar': "مرحبا",
    'ru': "Здравствуйте",
    'emoji': "👋🌍"
}

for lang, text in texts.items():
    utf8 = text.encode('utf-8')
    print(f"{lang:5s}: {text:15s} | {len(utf8):2d} bytes | {utf8.hex()[:30]}")

시나리오 3: 파일 업로드 처리

from flask import Flask, request
import chardet

app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload_file():
    file = request.files['file']
    
    # 바이너리로 읽기
    content = file.read()
    
    # 인코딩 감지
    detected = chardet.detect(content)
    encoding = detected['encoding']
    confidence = detected['confidence']
    
    print(f"Detected: {encoding} ({confidence*100:.1f}%)")
    
    # UTF-8로 변환
    if encoding.lower() != 'utf-8':
        try:
            text = content.decode(encoding)
            utf8_content = text.encode('utf-8')
            
            return {
                'status': 'converted',
                'from': encoding,
                'to': 'utf-8',
                'content': text
            }
        except Exception as e:
            return {'status': 'error', 'message': str(e)}, 400
    
    return {
        'status': 'ok',
        'encoding': 'utf-8',
        'content': content.decode('utf-8')
    }

베스트 프랙티스

1. 항상 UTF-8 사용

# ✅ 파일 I/O
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write("한글")

# ✅ 소스 코드 인코딩 선언 (Python 2)
# -*- coding: utf-8 -*-

# ✅ HTML
# <meta charset="UTF-8">

# ✅ HTTP 헤더
# Content-Type: text/html; charset=utf-8

# ✅ 데이터베이스
# CREATE DATABASE mydb CHARACTER SET utf8mb4;

2. 바이너리 모드로 읽고 명시적 디코딩

# ✅ 안전한 방법
with open('file.txt', 'rb') as f:
    raw_data = f.read()

# 인코딩 확인 후 디코딩
text = raw_data.decode('utf-8')

# ❌ 위험한 방법 (시스템 기본 인코딩 사용)
with open('file.txt', 'r') as f:  # encoding 미지정
    text = f.read()

3. 에러 처리

# ✅ 에러 처리 전략
def safe_decode(data, encodings=['utf-8', 'cp949', 'euc-kr', 'latin-1']):
    """여러 인코딩 시도"""
    for enc in encodings:
        try:
            return data.decode(enc), enc
        except UnicodeDecodeError:
            continue
    
    # 모두 실패하면 에러 무시하고 디코딩
    return data.decode('utf-8', errors='replace'), 'utf-8'

# 사용
with open('unknown.txt', 'rb') as f:
    data = f.read()

text, encoding = safe_decode(data)
print(f"Decoded as {encoding}: {text}")

4. BOM 처리

# ✅ UTF-8 BOM 자동 처리
with open('file.txt', 'r', encoding='utf-8-sig') as f:
    text = f.read()  # BOM이 있으면 자동 제거

# ✅ BOM 없이 저장 (권장)
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write(text)

# ❌ BOM 포함 저장 (피하기)
with open('file.txt', 'w', encoding='utf-8-sig') as f:
    f.write(text)

문제 해결 체크리스트

한글이 깨질 때

# 1. 파일 인코딩 확인
import chardet

with open('file.txt', 'rb') as f:
    result = chardet.detect(f.read())
    print(result)

# 2. 올바른 인코딩으로 읽기
with open('file.txt', 'r', encoding='cp949') as f:
    text = f.read()

# 3. UTF-8로 재저장
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write(text)

웹에서 한글이 깨질 때

# 1. HTTP 헤더 확인
import requests

response = requests.get('https://example.com')
print(response.encoding)  # ISO-8859-1 (잘못된 추측)

# 2. 올바른 인코딩 설정
response.encoding = 'utf-8'
print(response.text)

# 3. Content-Type 헤더 확인
print(response.headers.get('Content-Type'))
# text/html; charset=euc-kr

# 4. 명시적 디코딩
text = response.content.decode('euc-kr')

데이터베이스에서 한글이 깨질 때

# 1. 연결 인코딩 확인
import pymysql

conn = pymysql.connect(
    host='localhost',
    user='user',
    password='pass',
    database='mydb',
    charset='utf8mb4'  # ✅ 명시적 지정
)

# 2. 테이블 인코딩 확인
cursor = conn.cursor()
cursor.execute("SHOW CREATE TABLE users")
print(cursor.fetchone())

# 3. 인코딩 변환
# ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4;

정리

인코딩 선택 가이드

flowchart TD
    Start[새 프로젝트 시작] --> Q1{언어는?}
    
    Q1 -->|영문만| ASCII[ASCII<br/>또는 UTF-8]
    Q1 -->|다국어| UTF8[✅ UTF-8<br/>권장]
    Q1 -->|레거시 연동| Q2{시스템은?}
    
    Q2 -->|Windows 한글| CP949[CP949]
    Q2 -->|Unix 한글| EUCKR[EUC-KR]
    Q2 -->|일본어| SJIS[Shift-JIS]
    
    UTF8 --> Best[✅ 최선의 선택<br/>- 웹 표준<br/>- 모든 문자 지원<br/>- ASCII 호환]

핵심 원칙

# 1. 항상 UTF-8 사용
encoding = 'utf-8'

# 2. 인코딩 명시
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write(text)

# 3. 바이너리 모드 + 명시적 디코딩
with open('file.txt', 'rb') as f:
    data = f.read()
text = data.decode('utf-8')

# 4. 에러 처리
try:
    text = data.decode('utf-8')
except UnicodeDecodeError:
    text = data.decode('utf-8', errors='replace')

# 5. 테스트
assert "한글 😀".encode('utf-8').decode('utf-8') == "한글 😀"

인코딩별 요약

인코딩 바이트 장점 단점 사용 시기
UTF-8 1-4 웹 표준, ASCII 호환 한글 3바이트 모든 새 프로젝트
UTF-16 2-4 한글 2바이트 ASCII 비호환 Windows/Java 내부
UTF-32 4 고정 길이 공간 낭비 내부 처리
EUC-KR 1-2 한글 2바이트 일부 한글 미지원 레거시 시스템
CP949 1-2 모든 한글 지원 Windows 전용 Windows 한글

디버깅 도구

Python 인코딩 디버거

def analyze_encoding(file_path):
    """파일 인코딩 상세 분석"""
    with open(file_path, 'rb') as f:
        raw_data = f.read()
    
    print(f"📄 파일: {file_path}")
    print(f"📊 크기: {len(raw_data)} bytes\n")
    
    # BOM 확인
    if raw_data.startswith(b'\xef\xbb\xbf'):
        print("🔖 BOM: UTF-8")
    elif raw_data.startswith(b'\xff\xfe'):
        print("🔖 BOM: UTF-16 LE")
    elif raw_data.startswith(b'\xfe\xff'):
        print("🔖 BOM: UTF-16 BE")
    else:
        print("🔖 BOM: None")
    
    # 인코딩 감지
    detected = chardet.detect(raw_data)
    print(f"\n🔍 감지된 인코딩: {detected['encoding']}")
    print(f"📈 신뢰도: {detected['confidence']*100:.1f}%")
    
    # 여러 인코딩으로 시도
    print("\n🧪 디코딩 테스트:")
    encodings = ['utf-8', 'cp949', 'euc-kr', 'utf-16', 'latin-1']
    
    for enc in encodings:
        try:
            text = raw_data.decode(enc)
            preview = text[:50].replace('\n', '\\n')
            print(f"  ✅ {enc:10s}: {preview}")
        except UnicodeDecodeError as e:
            print(f"  ❌ {enc:10s}: {e}")
    
    # Hex dump (처음 100바이트)
    print(f"\n🔢 Hex Dump (first 100 bytes):")
    for i in range(0, min(100, len(raw_data)), 16):
        hex_str = ' '.join(f'{b:02x}' for b in raw_data[i:i+16])
        ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in raw_data[i:i+16])
        print(f"  {i:04x}: {hex_str:48s} | {ascii_str}")

# 사용
analyze_encoding('mystery.txt')

참고 자료

한 줄 요약: 모든 새 프로젝트는 UTF-8을 사용하고, 레거시 시스템 연동 시에만 EUC-KR/CP949를 고려하며, 항상 인코딩을 명시적으로 지정하여 한글 깨짐 문제를 예방하세요.

---
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3