
한글 깨짐의 원인을 찾다 보면 EUC-KR과 UTF-8이라는 두 단어를 반드시 만나게 됩니다. 그리고 예전 홈페이지나 게시판에서 '똠'이나 '뷁' 같은 글자를 입력하면 물음표로 바뀌던 것도 결국 같은 이유입니다. 글꼴 문제가 아니라 글자표에 그 글자가 없었기 때문입니다.
| "DB에서 꺼낸 한글이 ??? 로 나와요" "똠방각하는 왜 똠이 안 써졌던 거죠?" "EUC-KR이랑 CP949는 같은 건가요?" "MySQL utf8로 했는데 이모지가 깨져요" |
인코딩은 한 번 이해하면 평생 써먹는 지식인데, 처음 접하면 용어가 많아서 어렵게 느껴집니다. 이 글은 "컴퓨터는 글자를 번호로 저장한다"는 한 가지 사실에서 출발해 전부 풀어보려고 합니다.
왜 2,350자밖에 없었는지, CP949는 뭘 고쳤는지, UTF-8은 어떻게 다 담았는지, 그리고 지금 PHP와 MySQL에서 한글이 깨지면 어디를 봐야 하는지까지 다룹니다. 레거시 시스템을 물려받은 분이나 인코딩 에러를 처음 만난 분에게 도움이 될 겁니다.
목차
- 한글 깨짐이 생기는 진짜 이유
- 완성형과 조합형, 1980년대 한글 코드 싸움
- EUC-KR이 담은 2,350자와 빠진 글자들
- CP949가 EUC-KR에서 고친 것
- UTF-8은 어떻게 한글 11,172자를 다 넣었나
- 지금도 한글이 깨지는 지점
- PHP와 MySQL에서 인코딩 맞추는 방법
- MySQL utf8과 utf8mb4의 차이
1. 한글 깨짐이 생기는 진짜 이유
컴퓨터는 글자를 저장하지 않습니다. 번호를 저장합니다. 'A'는 65번, '가'는 어떤 번호, 이런 식으로 글자마다 번호를 정해 둔 표가 있고, 그 표를 문자 집합이라고 부릅니다. 그리고 그 번호를 실제 바이트로 어떻게 적을지 정한 규칙이 인코딩입니다.
한글이 깨지는 건 아주 단순한 상황입니다. 쓰는 쪽은 A라는 표로 번호를 적었는데, 읽는 쪽은 B라는 표로 번호를 해석한 겁니다. 같은 번호가 다른 글자를 가리키니 엉뚱한 글자가 나오거나, 아예 해당 번호가 없어서 물음표가 나옵니다.
그래서 한글 깨짐을 고치는 작업은 늘 같습니다. 글자가 지나가는 모든 구간에서 같은 표를 쓰게 만드는 것. 파일, 웹 서버, PHP, DB 연결, DB 테이블, 브라우저까지 하나라도 다르면 그 지점에서 깨집니다.
| 📌 용어 두 개만 기억하면 됩니다 문자 집합: 어떤 글자에 어떤 번호를 줄지 정한 표 (예: KS X 1001, 유니코드) 인코딩: 그 번호를 바이트로 적는 규칙 (예: EUC-KR, UTF-8) |
2. 완성형과 조합형, 1980년대 한글 코드 싸움
한글을 컴퓨터에 넣는 방법은 크게 두 갈래였습니다. 하나는 '가', '각', '간'처럼 완성된 글자 하나하나에 번호를 주는 완성형. 다른 하나는 초성·중성·종성 자모에 번호를 주고 조합해서 글자를 만드는 조합형입니다.
| 항목 | 완성형 | 조합형 |
|---|---|---|
| 방식 | 글자 하나에 번호 하나 | 자모에 번호를 주고 조합 |
| 표현 범위 | 표에 넣은 글자만 가능 | 조합 가능한 한글 전부 |
| 장점 | 구현이 단순, 국제 표준과 호환 | 한글 원리에 충실, 빠진 글자 없음 |
| 대표 사용처 | 정부 표준, 유닉스, 웹 | 아래아한글 초기 버전 등 |
1987년 정부가 완성형을 표준(KS C 5601, 지금의 KS X 1001)으로 정하면서 싸움은 사실상 완성형의 승리로 끝났습니다. 당시에는 글자 하나에 2바이트를 쓰는 구조 안에서 한자, 기호까지 다 넣어야 했기 때문에, 한글에 줄 수 있는 자리가 넉넉하지 않았습니다.
그래서 자주 쓰는 글자만 골라 넣는 결정을 했습니다. 이 결정이 이후 30년 넘게 이어지는 '안 써지는 글자' 문제의 출발점입니다. 1992년 개정에서 조합형이 보조 방식으로 추가되긴 했지만, 실제 쓰임은 완성형이 압도적이었습니다.
3. EUC-KR이 담은 2,350자와 빠진 글자들
EUC-KR은 KS X 1001 문자 집합을 유닉스 환경에서 쓰기 위한 인코딩입니다. 영문은 1바이트, 한글과 한자는 2바이트로 적습니다. 그런데 KS X 1001에 들어간 한글은 2,350자뿐입니다.
현대 한글로 만들 수 있는 글자는 초성 19개, 중성 21개, 종성 28개(없음 포함)를 곱해서 11,172자입니다. 그중 2,350자만 표에 넣었으니 나머지 8,822자는 아예 번호가 없습니다. 번호가 없으면 저장할 방법이 없고, 저장이 안 되니 화면에 나올 수도 없습니다.
'똠', '뷁', '펲', '쓩' 같은 글자가 여기에 해당합니다. 1987년 당시 신문이나 책에서 쓰이는 빈도를 조사해서 골랐기 때문에, 의성어나 이름, 사투리에 나오는 글자는 상당수 빠졌습니다. 똠방각하라는 드라마 제목을 입력하면 '?방각하'가 되던 게 바로 이 때문입니다.
| 💡 글꼴 문제가 아닙니다 글꼴은 "이 번호의 글자를 어떻게 그릴까"를 담당합니다. 번호 자체가 없으면 글꼴이 아무리 좋아도 그릴 대상이 없습니다. '똠'이 안 써진 건 글꼴에 '똠'이 없어서가 아니라, 표에 '똠'의 번호가 없었기 때문입니다. |
4. CP949가 EUC-KR에서 고친 것
이 문제를 가장 먼저 실용적으로 해결한 건 마이크로소프트였습니다. 한국어 윈도우에서 쓰는 CP949(확장 완성형, 통합 완성형)는 EUC-KR의 2,350자를 그대로 두고, 빠진 8,822자를 비어 있던 바이트 영역에 추가로 채워 넣었습니다.
| 항목 | EUC-KR | CP949 |
|---|---|---|
| 한글 글자 수 | 2,350자 | 11,172자 전부 |
| 호환성 | CP949의 부분집합 | EUC-KR 상위 호환 |
| 주 사용 환경 | 유닉스, 리눅스, 옛 웹 | 한국어 윈도우 |
| 다른 이름 | KS X 1001, ks_c_5601-1987 | MS949, Windows-949 |
2,350자 범위 안에서는 두 인코딩의 바이트가 완전히 같습니다. 그래서 대부분의 글은 EUC-KR이든 CP949든 똑같이 보입니다. 문제는 '똠' 같은 추가 글자가 섞였을 때입니다. CP949로 저장한 파일을 EUC-KR만 아는 프로그램이 읽으면 그 글자만 깨집니다.
개발자들이 윈도우에서 CP949로 작업하면서 HTML에는 EUC-KR이라고 적는 경우가 너무 많아서, HTML5 표준은 아예 EUC-KR이라고 선언된 문서를 CP949로 해석하도록 정해 버렸습니다. 브라우저 입장에서는 둘이 같은 말입니다.
5. UTF-8은 어떻게 한글 11,172자를 다 넣었나
유니코드는 전 세계 모든 글자에 번호를 하나씩 주자는 프로젝트입니다. 한글은 U+AC00('가')부터 U+D7A3('힣')까지 11,172개 자리를 통째로 받았습니다. 2,350자를 고를 필요 없이, 조합 가능한 현대 한글이 가나다순으로 전부 들어가 있습니다.
UTF-8은 그 유니코드 번호를 바이트로 적는 규칙 중 하나입니다. 영문은 1바이트, 한글은 3바이트, 이모지는 4바이트처럼 글자에 따라 길이가 달라집니다. 영문이 1바이트라서 기존 ASCII 파일과 그대로 호환되는 게 UTF-8이 웹 표준이 된 가장 큰 이유입니다.
| 글자 | EUC-KR / CP949 | UTF-8 |
|---|---|---|
| A | 1바이트 | 1바이트 |
| 가 | 2바이트 | 3바이트 |
| 똠 | EUC-KR 없음 / CP949 2바이트 | 3바이트 |
| 😀 | 없음 | 4바이트 |
한글이 2바이트에서 3바이트로 늘어나 용량이 1.5배가 되는 건 사실입니다. 2000년대 초반에는 이걸 이유로 EUC-KR을 고집하는 경우도 있었습니다. 지금은 텍스트 용량이 문제가 되는 경우가 거의 없고, 이모지·외국어·특수문자를 한 번에 처리할 수 있는 이점이 훨씬 큽니다.
6. 지금도 한글이 깨지는 지점
UTF-8이 표준이 된 지 오래됐지만 한글 깨짐은 여전히 생깁니다. 이유는 1번 섹션에서 말한 그대로입니다. 글자가 지나가는 구간 중 어느 하나가 다른 표를 쓰고 있기 때문입니다.
| 깨지는 지점 | 흔한 원인 |
|---|---|
| 소스 파일 | 에디터가 파일을 CP949로 저장 |
| HTTP 응답 헤더 | Content-Type에 charset이 없거나 다름 |
| HTML meta | meta charset이 euc-kr로 남아 있음 |
| DB 연결 | 접속 후 문자 집합을 지정하지 않음 |
| DB 테이블·컬럼 | 테이블은 utf8인데 컬럼만 euckr |
| 외부 파일 입력 | 엑셀에서 내보낸 CSV가 CP949 |
진단 순서는 위에서 아래로 가면 됩니다. 브라우저 개발자 도구에서 응답 헤더의 charset을 먼저 보고, 그다음 소스 파일 인코딩, DB 연결 순으로 확인하면 대부분 10분 안에 어긋난 지점이 나옵니다.
특히 "DB에 넣을 때는 멀쩡했는데 꺼내니 깨진다"면 DB 연결 문자 집합이 범인일 확률이 높습니다. 넣을 때와 꺼낼 때 연결 설정이 달랐던 겁니다.
7. PHP와 MySQL에서 인코딩 맞추는 방법
PHP와 MySQL 조합에서 한글을 안전하게 다루려면 세 군데를 UTF-8로 못 박아야 합니다. 응답 헤더, HTML 선언, DB 연결입니다. 아래 코드는 PHP 8과 MySQL 8.0 기준(2026년 시점)의 동작 원리 예시입니다.
먼저 PHP가 브라우저에 보내는 응답에 문자 집합을 명시합니다. HTML의 meta 태그도 같이 적어 두면 파일을 직접 열었을 때도 안전합니다.
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
</head>
다음은 DB 연결입니다. 접속 직후 set_charset으로 연결 문자 집합을 지정해야 합니다. 이걸 빼먹으면 서버 기본값을 따라가는데, 그 기본값이 환경마다 달라서 사고가 납니다. 쿼리로 SET NAMES를 보내는 방법도 있지만, PHP 쪽 이스케이프 함수가 인코딩을 모르게 되므로 set_charset을 권합니다.
<?php
$db = new mysqli('localhost', 'user', 'pass', 'mydb');
$db->set_charset('utf8mb4');
$stmt = $db->prepare('INSERT INTO posts (title) VALUES (?)');
$stmt->bind_param('s', $title);
$title = '똠방각하와 뷁';
$stmt->execute();
레거시 데이터나 엑셀 CSV처럼 CP949로 들어오는 입력은 받자마자 변환합니다. 이때 변환 원본을 EUC-KR이 아니라 CP949로 지정해야 '똠' 같은 추가 글자가 안 깨집니다. 윈도우에서 만든 파일은 거의 항상 CP949입니다.
<?php
$raw = file_get_contents('legacy.csv');
$utf8 = iconv('CP949', 'UTF-8//IGNORE', $raw);
//IGNORE는 변환할 수 없는 바이트를 버리는 옵션입니다. 깨진 바이트 하나 때문에 전체 변환이 실패하는 걸 막아 주지만, 글자가 조용히 사라질 수 있으니 중요한 데이터라면 변환 전후 길이를 비교해 보는 게 좋습니다.
8. MySQL utf8과 utf8mb4의 차이
MySQL에는 함정이 하나 있습니다. utf8이라는 이름의 문자 집합이 진짜 UTF-8이 아닙니다. 글자당 최대 3바이트까지만 저장하는 utf8mb3의 별칭입니다. 한글은 3바이트라 문제없이 들어가지만, 4바이트가 필요한 이모지는 에러가 나거나 잘립니다.
| 항목 | utf8 (utf8mb3) | utf8mb4 |
|---|---|---|
| 글자당 최대 | 3바이트 | 4바이트 |
| 한글 | 가능 | 가능 |
| 이모지 | 불가 (에러 또는 잘림) | 가능 |
| MySQL 8.0 기본값 | 아님 (구버전 기본) | 기본값 |
"한글은 UTF-8로 잘 되는데 이모지만 깨진다"는 증상이 나오면 거의 이 문제입니다. 사용자 이름이나 댓글에 이모지가 들어가는 순간 저장이 실패합니다. 2,350자에 '똠'이 없어서 깨지던 것과 구조가 똑같습니다. 표가 작아서 못 담는 겁니다.
기존 테이블은 아래 쿼리로 바꿀 수 있습니다. 다만 인덱스가 걸린 VARCHAR 컬럼은 바이트 길이가 늘어나면서 인덱스 길이 제한에 걸릴 수 있으니, 운영 DB에서는 백업 후 테스트 환경에서 먼저 돌려보는 게 안전합니다.
ALTER TABLE posts
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
새로 만드는 프로젝트라면 고민할 것 없이 처음부터 utf8mb4로 시작하면 됩니다. PHP의 set_charset도 같은 값으로 맞춰야 연결과 테이블이 어긋나지 않습니다.
자주 묻는 질문 (FAQ)
EUC-KR로 운영 중인 사이트를 꼭 UTF-8로 바꿔야 하나요?
당장 깨지는 게 없다면 급하진 않지만, 바꾸는 쪽을 권합니다. 외부 API, 라이브러리, 이모지, 다국어 대응이 전부 UTF-8을 전제로 하기 때문에 EUC-KR을 유지할수록 변환 코드가 여기저기 쌓입니다.
EUC-KR과 CP949 중 뭘로 변환해야 하나요?
원본이 윈도우에서 만들어졌다면 CP949로 지정하세요. EUC-KR로 지정하면 2,350자 밖의 글자가 깨집니다. CP949는 EUC-KR의 상위 호환이라 EUC-KR 파일을 CP949로 읽어도 문제없습니다.
지금은 '똠'이나 '뷁'이 다 써지는데 왜 이 역사를 알아야 하나요?
옛 시스템에서 데이터를 가져올 때, 엑셀 CSV를 처리할 때, 레거시 DB를 마이그레이션할 때 지금도 그대로 마주치는 문제이기 때문입니다. 원인을 알면 증상만 보고도 어디를 고쳐야 할지 바로 보입니다.
한글 깨짐은 결국 표가 안 맞는 문제입니다
'똠'이 안 써지던 건 1987년에 자주 쓰는 2,350자만 표에 넣기로 한 결정 때문이었고, CP949가 8,822자를 보태서 막았고, UTF-8이 유니코드로 11,172자를 전부 담으면서 끝났습니다. MySQL의 utf8이 이모지를 못 담는 것도 같은 구조의 문제입니다.
그래서 한글 깨짐을 만나면 "어떤 글꼴을 깔아야 하지"가 아니라 "어느 구간에서 표가 어긋났지"를 찾으면 됩니다. 응답 헤더, 소스 파일, DB 연결, 테이블 문자 집합 순으로 확인하면 거의 다 잡힙니다.
📚 함께 보면 좋은 글
HTML 역사, 누가 왜 만들었을까 (태그 18개의 시작)
HTML 역사를 조금만 거슬러 올라가 보면, 우리가 매일 아무렇지 않게 넘기는 웹페이지가 원래는 스위스의 한 물리학 연구소에서 문서 몇 개를 편하게 주고받으려고 급조한 도구였다는 사실과 마
devwar.tistory.com
ASP(Active Server Pages)란 무엇인가 — 초기 웹을 이끈 서버 스크립트의 등장과 쇠퇴
ASP(Active Server Pages)는 마이크로소프트가 1996년에 내놓은 서버 사이드 스크립트 기술입니다. 윈도우 서버의 IIS 위에서 동적인 웹 페이지를 만들 수 있게 해준 기술로, 90년대 후반부터 2000년대 초
devwar.tistory.com
MySQL과 MariaDB는 원래 한 핏줄이었다 - 오라클 인수와 포크의 가족사
"MySQL이랑 MariaDB랑 뭐가 다른 거예요? 거의 똑같던데.""리눅스에 mysql 깔았더니 mariadb가 깔리던데 이거 버그인가요?""MariaDB가 MySQL 베낀 거예요?""왜 둘 다 이름 끝이 비슷하죠?" 실무에서 둘 다 써본
devwar.tistory.com
'서버의생각' 카테고리의 다른 글
| Redis 세션으로 옮겼더니 앱 서버 추가가 단순해졌습니다 (0) | 2026.10.03 |
|---|---|
| 서버 모니터링 항목 4가지, 이것만 보는 이유가 있습니다 (0) | 2026.08.24 |
| OSI 7계층 쉬운 이해 택배 배송 과정으로 풀어봤더니 (0) | 2026.08.22 |
| 리눅스 배포판 계보에 숨은 57년의 결별사, 유닉스부터 우분투까지 (0) | 2026.08.03 |
| 서버 보안 업데이트가 두렵다면 AWS를 써야 하는 이유 (0) | 2026.07.30 |