문자 인코딩 완벽 가이드 | 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, 인코딩 감지
- 실전 문제 해결
목차
- 문자 인코딩의 역사
- ASCII: 7비트 문자 집합
- ANSI와 코드 페이지
- Unicode: 전 세계 문자 통합
- UTF-8: 가변 길이 인코딩
- UTF-16과 UTF-32
- 한글 인코딩 (EUC-KR, CP949)
- BOM과 Endian
- 실전 문제 해결
- 프로그래밍 언어별 처리
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) # "한글" (숫자 참조)
문제 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')
참고 자료
- Unicode Standard
- UTF-8 Specification (RFC 3629)
- Character Encoding in Python
- The Absolute Minimum Every Software Developer Must Know About Unicode
한 줄 요약: 모든 새 프로젝트는 UTF-8을 사용하고, 레거시 시스템 연동 시에만 EUC-KR/CP949를 고려하며, 항상 인코딩을 명시적으로 지정하여 한글 깨짐 문제를 예방하세요.