문자 인코딩 이해하기: ASCII에서 UTF-8까지
텍스트가 가끔 물음표나 이상한 기호로 바뀌는 이유. 문자 인코딩 실전 가이드.
파일을 열었는데 u 대신 ü가 보여. 아니면 데이터베이스가 사람 이름이 있어야 할 자리에 ????를 반환해. 이메일 제목에 =?UTF-8?B?가 군데군데 섞여 있기도 해.
문자 인코딩 문제의 세계에 온 걸 환영해.
짧은 역사
컴퓨터는 숫자를 저장하지, 글자를 저장하지 않아. 그래서 누군가 어떤 숫자가 어떤 글자를 의미하는지 정해야 했어. 1960년대에 ASCII가 0-127 번호를 영어 문자, 숫자, 기본 기호에 할당했어. 글자 "A"는 65. 공백은 32. 단순하지.
근데 ASCII는 128개 문자만 커버해. 영어에는 괜찮아. 독일어 움라우트, 일본어 한자, 아랍어 문자, 또는 인간이 실제로 사용하는 수천 개의 다른 문자에는 안 통해.
유니코드 이전의 혼돈
각 지역이 자체 인코딩을 만들었어. 서유럽은 ISO-8859-1을 썼고, 일본은 Shift-JIS, 러시아는 KOI8-R, 중국은 GB2312를 사용했어. 각각의 체계 안에서는 잘 작동했지. 하지만 섞는 순간 모든 게 깨져버렸어.
한 인코딩으로 저장한 파일을 다른 인코딩으로 열면 글자깨짐(mojibake)이 발생해 -- 잘못된 문자들이 뒤죽박죽 섞인 그 혼란스러운 결과. UTF-8 파일을 ISO-8859-1로 읽으면 cafe가 café로 바뀌어.
유니코드가 매핑 문제를 해결했어
유니코드는 모든 문자에 고유한 번호("코드 포인트")를 부여했어. 라틴 A는 U+0041. 눈사람은 U+2603. 모든 이모지, 모든 문자 체계, 모든 수학 기호가 고유한 코드 포인트를 가져. 15만 개 이상이고 계속 늘어나는 중.
하지만 유니코드는 매핑일 뿐이야. 그 숫자들을 바이트로 어떻게 저장할지는 정하지 않아. 그게 인코딩이 하는 일이야.
UTF-8: 승리한 인코딩
UTF-8은 인터넷 대부분이 유니코드 텍스트를 저장하는 방식이야. 핵심 트릭은 문자당 가변 바이트 수를 사용하는 거야.
- ASCII 문자 (영어, 숫자): 각 1바이트
- 유럽 악센트 문자: 각 2바이트
- 아시아 문자 (한중일): 각 3바이트
- 이모지와 희귀 기호: 각 4바이트
즉, UTF-8의 영어 텍스트는 ASCII와 동일해. 기존 시스템이 깨지지 않아. 그러면서도 세상의 모든 문자를 표현할 수 있어.
현재 웹사이트의 98% 이상이 UTF-8을 사용해. 인코딩 전쟁은 끝났고, UTF-8이 이겼어.
UTF-8 vs UTF-16 vs UTF-32
UTF-8: 가변 너비 (1-4바이트). 영어 위주 텍스트에 효율적. 웹 표준.
UTF-16: 가변 너비 (2 또는 4바이트). JavaScript, Java, Windows에서 내부적으로 사용. 모든 문자가 최소 2바이트라 ASCII 텍스트에는 비효율적.
UTF-32: 고정 너비 (문자당 4바이트). 단순하지만 낭비적. 저장이나 전송에는 거의 안 써.
JavaScript의 string.length는 문자가 아니라 UTF-16 코드 유닛을 세. 그래서 "😀".length가 1이 아니라 2를 반환해.
인코딩이 깨질 때
잘못된 인코딩으로 파일 읽기. 바이트 자체는 괜찮지만 잘못 해석하는 거야. 해결: 열 때 올바른 인코딩을 지정해.
데이터베이스 문자셋 불일치. 앱은 UTF-8을 보내는데 데이터베이스 컬럼은 latin1로 설정돼 있어. ASCII 밖의 문자가 망가져. 해결: 데이터베이스를 utf8mb4로 설정해 (MySQL에서 utf8만 쓰면 3바이트 문자만 처리돼).
HTTP charset 헤더 누락. 서버가 Content-Type: text/html; charset=utf-8을 안 보내면 브라우저가 추측해야 해. 가끔 잘못 추측해.
실전 팁
항상 UTF-8을 써. 안 쓸 매우 구체적인 이유가 없다면 UTF-8이 모든 것에 맞는 선택이야.
인코딩을 명시적으로 선언해. HTML에서: <meta charset="utf-8">. HTTP에서: Content-Type: text/html; charset=utf-8. 시스템이 추측하게 하지 마.
문자열 길이에 주의해. JavaScript에서 문자 수 세기는 이모지와 결합 문자에서 까다로워져. 정확한 카운트를 위해 Array.from(str).length나 Intl.Segmenter API를 사용해.
BOM에 주의해. 바이트 순서 표시(U+FEFF)가 UTF-8 파일 시작에 가끔 나타나. 보이지 않지만 파서를 깨뜨릴 수 있어. 일부 편집기가 조용히 추가해.
문자 인코딩은 재밌는 주제는 아니지만 이해하면 디버깅 시간을 몇 시간이나 아낄 수 있어. 모든 곳에서 UTF-8을 쓰고, 명시적으로 선언하고, 깨진 텍스트를 보면 정확히 어디를 봐야 하는지 알게 될 거야.