API 디버깅
%E4%B8%AD%E6%96%87을 보면 UTF-8로 인코딩된 한자 둘이라는 것을 바로 알아볼 수 있습니다. JWT의 .으로 나뉜 세 토막 Base64를 보면 JSON으로 되돌리는 방법이 떠오릅니다. 요청 파라미터의 +와 %20이 왜 백엔드에서 다른 것으로 취급되는지도 이해하게 됩니다.
Guide
문자 인코딩 강좌는 14단계 인터랙티브 튜토리얼로, 개발 중에 자주 나오지만 제대로 설명해 주는 사람은 드문 질문에 답합니다. 한자는 몇 바이트나 차지할까? "😀".length는 왜 2일까? Base64 끝의 =는 무슨 일을 할까? 글자가 깨진 것은 파일이 망가진 걸까, 살릴 수 있을까? 단계마다 자유롭게 고칠 수 있는 실험용 텍스트 하나와 여러 뷰가 주어집니다. 문자마다의 비트, 바이트, Unicode 코드 포인트, UTF-8 바이트 단위 분해, Base64의 6비트 그룹, URL 퍼센트 인코딩, 그리고 「다른 인코딩으로 읽으면 어떻게 보이는가」입니다. 목표는 어떤 수를 읽어 내기, 어떤 16진수를 적기, 조건에 맞는 문자를 입력하기 등이고 판정기가 즉시 체크합니다.
2026-09-09 업데이트5 sources10 min read
문자 인코딩 강좌는 14단계 인터랙티브 튜토리얼로, 개발 중에 자주 나오지만 제대로 설명해 주는 사람은 드문 질문에 답합니다. 한자는 몇 바이트나 차지할까? "😀".length는 왜 2일까? Base64 끝의 =는 무슨 일을 할까? 글자가 깨진 것은 파일이 망가진 걸까, 살릴 수 있을까? 단계마다 자유롭게 고칠 수 있는 실험용 텍스트 하나와 여러 뷰가 주어집니다. 문자마다의 비트, 바이트, Unicode 코드 포인트, UTF-8 바이트 단위 분해, Base64의 6비트 그룹, URL 퍼센트 인코딩, 그리고 「다른 인코딩으로 읽으면 어떻게 보이는가」입니다. 목표는 어떤 수를 읽어 내기, 어떤 16진수를 적기, 조건에 맞는 문자를 입력하기 등이고 판정기가 즉시 체크합니다.
강좌 순서는 인코딩의 역사 순서와 같습니다. 비트와 바이트 → ASCII의 128가지 약속 → Unicode가 문자마다 번호를 부여 → UTF-8이 그 번호를 1에서 4바이트로 바꾸는 방법 → UTF-16과 JavaScript 문자열 길이 → Base64가 바이트를 순수 텍스트로 옮기는 방법 → URL 인코딩 → 마지막 두 단계는 글자 깨짐을 다룹니다. 어떻게 생기는지, 浣犲ソ에서 你好를 어떻게 역추적하는지 말입니다.
약 40분이면 끝납니다. 프런트엔드·백엔드 개발자, 데이터를 다루는 사람, 그리고 글자 깨짐과 Base64, 이모지 길이 때문에 곤란을 겪은 적이 있는 누구에게나 맞습니다. 2진수를 셀 줄만 알면 됩니다.
%XX로 바뀌는지 표시하고, 「다른 인코딩으로 읽기」는 같은 바이트열을 GBK, Latin-1, UTF-16으로 읽으면 어떻게 보이는지 보여 줍니다.?lesson=n으로 특정 단계에 바로 들어갈 수 있습니다.이 페이지 위쪽이 곧 강좌 본체입니다. 텍스트를 고치고, 뷰를 읽고, 답을 채워 보세요.
6단계 「UTF-8 2바이트」는 café를 예시로 é의 두 바이트를 쓰라고 합니다.
14단계 「깨진 글자 역추적」은 GBK로 읽힌 깨진 글자 浣犲ソ를 주고 실험용 텍스트에서 원문을 찾으라고 합니다.
문자 코드 포인트 2진 코드 포인트 UTF-8 템플릿 바이트
c U+0063 1100011 0xxxxxxx 63
a U+0061 1100001 0xxxxxxx 61
f U+0066 1100110 0xxxxxxx 66
é U+00E9 000 1110 1001 110xxxxx 10xxxxxx C3 A9
→ 11비트 페이로드 00011 101001을 템플릿에 채움 → 11000011 10101001
답 C3 A9 ✓실험용 텍스트 中文 UTF-8 바이트 E4 B8 AD E6 96 87 GBK로 읽으면 → 涓枃
실험용 텍스트 你好 UTF-8 바이트 E4 BD A0 E5 A5 BD GBK로 읽으면 → 浣犲ソ ← 문제와 일치
답 你好 ✓컴퓨터에는 0과 1밖에 없습니다. 비트(bit) 하나는 스위치 하나이고, 8개가 모여 바이트(byte) 하나가 되며 가지 값을 나타냅니다. 자릿값을 더하면 2진수가 10진수가 됩니다. 01000001 = 64 + 1 = 65입니다. 모든 글자는 결국 바이트열이고, 「문자 인코딩」은 어떤 수가 어떤 문자를 뜻하는지 정한 표입니다. 표가 다르면 인코딩이 다릅니다.
1963년의 ASCII는 7비트로 128개 문자를 정의했습니다. 0–31은 줄바꿈, 탭, 벨 같은 제어 문자이고, 48–57은 숫자 0–9, 65–90은 대문자, 97–122는 소문자입니다. 표는 마구 만든 것이 아닙니다. 대문자와 소문자는 32만큼 차이 나므로(2진수 6번째 비트) c | 0x20으로 대문자를 소문자로 바꿀 수 있고, 숫자 문자에서 48을 빼면 값이 됩니다. 16진수는 바이트를 읽을 때 쓰는 관용 표기이고 두 자리가 정확히 1바이트입니다. 0x48 0x69 = Hi입니다. ASCII는 7비트만 썼고 8번째 비트는 비어 있었습니다. 나중에 각 나라가 이 비트를 자기 방식으로 채웠고(Latin-1, GBK, Shift_JIS…), 서로 호환되지 않아 글자 깨짐의 역사적 뿌리가 되었습니다.
Unicode는 전 세계 문자에 번호를 통일해 붙입니다. 이 번호를 코드 포인트(code point)라고 하고 U+에 16진수를 붙여 씁니다. A는 U+0041, 中은 U+4E2D, 😀은 U+1F600입니다. 공간은 U+0000부터 U+10FFFF까지이고 17개 「평면」으로 나뉩니다. 자주 쓰는 문자는 거의 다 0번 기본 평면(U+0000–U+FFFF)에 있고, 이모지와 드문 한자는 보충 평면에 있습니다. 중요한 점은 코드 포인트는 번호일 뿐이고, 그것이 바이트가 되는 방식은 UTF-8, UTF-16, UTF-32 같은 「인코딩 형식」이 정한다는 것입니다. 「Unicode」는 하나의 인코딩이 아니고 utf-8이 인코딩입니다.
UTF-8(RFC 3629)은 코드 포인트 하나를 1–4바이트로 표현하고, 규칙을 전부 첫 바이트의 접두사에 적어 둡니다.
| 코드 포인트 범위 | 바이트 수 | 템플릿 | 페이로드 비트 |
|---|---|---|---|
| U+0000–U+007F | 1 | 0xxxxxxx |
7 |
| U+0080–U+07FF | 2 | 110xxxxx 10xxxxxx |
11 |
| U+0800–U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
16 |
| U+10000–U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
21 |
코드 포인트의 2진수를 왼쪽에 0을 채워 페이로드 비트 수에 맞춘 뒤 x 자리에 차례로 넣으면 바이트가 됩니다. é = U+00E9 = 11101001을 11비트로 채우면 00011 101001이고, 2바이트 템플릿에 넣으면 11000011 10101001 = C3 A9입니다.
이 설계에는 장점이 셋 있습니다. ASCII 텍스트는 UTF-8로 인코딩해도 바이트 하나가 바뀌지 않습니다(완전한 하위 호환). 뒤따르는 바이트는 모두 10으로 시작하므로 바이트열 중간 아무 위치에서도 다음 문자의 시작을 찾을 수 있습니다(자기 동기화, 바이트 하나를 잃어도 전체가 어긋나지 않음). 길이가 다른 인코딩은 서로 겹치지 않아 00 같은 0 바이트는 언제나 U+0000만 뜻합니다(C 문자열에 안전). 대가는 한자 한 글자가 3바이트라 GBK의 2바이트보다 50% 크다는 점입니다. 그래도 영어, 구두점, 코드는 영향을 받지 않아 UTF-8이 웹페이지 98% 이상의 인코딩이 되었습니다.
7, 8단계에서는 3바이트 문자와 4바이트 문자를 직접 입력합니다. 후자는 지금도 자주 걸리는 함정을 보여 줍니다. MySQL의 utf8 문자 집합은 실제로 3바이트까지만 지원해서 이모지를 저장하면 오류가 나거나 잘리고, 반드시 utf8mb4를 써야 합니다.
.lengthJavaScript, Java, C#, Windows API의 문자열은 내부적으로 UTF-16을 씁니다. 코드 단위(code unit)는 16비트입니다. 기본 평면 문자는 코드 단위 하나를 차지하고, 보충 평면의 코드 포인트는 코드 단위 둘로 쪼개지며 이를 서로게이트 쌍(surrogate pair)이라고 합니다. 범위는 D800–DBFF와 DC00–DFFF입니다. 그래서 "😀".length === 2이고, "😀".split("")은 의미 없는 반쪽 두 개로 자르며, str[0]은 상위 서로게이트를 가져옵니다. 실제 문자 수로 세려면 [...str].length(코드 포인트 단위 반복)를 쓰고, 사용자가 인식하는 「한 글자」(결합 이모지, 발음 구별 부호 포함)로 세려면 Intl.Segmenter를 씁니다.
Base64(RFC 4648)는 암호화가 아니라 임의의 바이트를 64개 인쇄 가능 ASCII 문자(A–Z a–z 0–9 + /)로 옮기는 운반 방식입니다. 바이너리를 텍스트만 받는 곳에 밀어 넣기 위한 것입니다. 메일 첨부, JSON 필드, data: URL, JWT가 그렇습니다. 방식은 3바이트 = 24비트를 6비트 네 토막으로 나누고() 토막마다 표에서 문자 하나를 찾는 것입니다. Man = 4D 61 6E = 01001101 01100001 01101110 → 010011 010110 000101 101110 → T W F u입니다. 그래서 크기가 원래의 4/3로 늘어납니다.
바이트 수가 3의 배수가 아니면 마지막 묶음이 차지 않습니다. 1바이트가 모자라면 = 하나, 2바이트가 모자라면 = 둘을 붙여 출력 길이가 언제나 4의 배수가 되게 합니다. A 한 바이트 8비트에 0을 채워 12비트로 만들고 010000 01(0000)으로 나누면 QQ가 되며 ==를 더합니다. URL-safe 변형은 + /를 - _로 바꾸고(URL에서 이스케이프되지 않게) 보통 =를 생략하는데, 디코딩할 때 길이를 보고 되돌립니다.
URL에서 그대로 나올 수 있는 ASCII 문자는 일부뿐입니다(RFC 3986 §2). 공백, &, =, ?, /처럼 문법적 의미가 있는 예약 문자와 모든 비 ASCII 문자는 %와 바이트의 16진수로 적어야 합니다. 그래서 URL 인코딩은 **먼저 UTF-8, 다음에 바이트마다 %**입니다. 中 → E4 B8 AD → %E4%B8%AD처럼 한자가 9개 문자로 늘어납니다. JavaScript에서는 encodeURIComponent가 가장 철저하게 인코딩하고(/ ? & =까지 인코딩) 파라미터 값에 넣기 좋으며, encodeURI는 구조 문자를 남겨 두어 주소 전체에 맞습니다. 폼 전송의 application/x-www-form-urlencoded는 공백을 +로 쓰는데, 이것은 HTML 폼의 역사적 약속이고 RFC 3986의 일부가 아닙니다.
글자 깨짐은 데이터 손상이 아니라 쓸 때 인코딩 A, 읽을 때 인코딩 B를 쓴 결과입니다. UTF-8의 中은 E4 B8 AD입니다.
ä¸ 세 개가 됩니다. 발음 기호가 붙은 문자 더미는 「UTF-8을 서유럽 인코딩으로 읽었다」는 표시입니다.E4 B8이 한자 涓이 되고 남은 AD는 반쪽만 남습니다. 「이상한 한자 + 반쪽 문자」는 「UTF-8을 GBK로 읽었다」는 표시이고 浣犲ソ, 涓枃이 여기에 속합니다.�(U+FFFD)가 잔뜩 나옵니다.원래 바이트만 살아 있으면 올바른 인코딩으로 다시 읽어 글자를 온전히 되살릴 수 있습니다. 그래서 깨진 글자를 고치는 첫 원칙은 원본 파일을 보존하고 변환을 반복하지 않는 것입니다. 변환할 때마다 잘못된 바이트가 ?나 �로 바뀔 수 있고, 그때부터가 진짜 정보 손실입니다. 14단계에서는 역방향 작업을 합니다. 깨진 글자 浣犲ソ를 GBK 기준으로 바이트 E4 BD A0 E5 A5 BD로 되돌리고, 다시 UTF-8로 읽어 你好를 얻습니다. 요즘 방식은 HTTP 헤더, HTML <meta charset>, 데이터베이스 연결, 파일 저장 대화상자에서 UTF-8을 통일해 선언해 불일치가 생길 기회를 없애는 것입니다.
UTF-16에는 빅 엔디언과 리틀 엔디언 두 가지 바이트 순서가 있고, 파일 앞에 바이트 순서 표시 BOM(FE FF 또는 FF FE)을 두는 일이 많습니다. UTF-8은 바이트 순서 문제가 없지만, Windows 메모장이 UTF-8 BOM EF BB BF를 기본으로 넣던 시절이 있어 셸 스크립트 첫 줄이 오류를 내거나 CSV 첫 열 이름 앞에 보이지 않는 문자가 붙곤 했습니다. 강좌의 UTF-8 분해 뷰에서 이 세 바이트가 맨 앞에 보이면 출처를 짐작할 수 있습니다.
TextDecoder에 의존하며, GB18030, Big5, Shift_JIS의 내부 구조는 다루지 않습니다.length가 곧 문자 수다」: JavaScript에서는 UTF-16 코드 단위 수라 이모지와 일부 드문 한자가 2로 셉니다. 데이터베이스 VARCHAR(n)의 n도 데이터베이스에 따라 바이트 수일 수도 문자 수일 수도 있습니다.encodeURIComponent를 써야 합니다. 그렇지 않으면 &, #, 공백이 구조를 깨뜨립니다.%E4%B8%AD%E6%96%87을 보면 UTF-8로 인코딩된 한자 둘이라는 것을 바로 알아볼 수 있습니다. JWT의 .으로 나뉜 세 토막 Base64를 보면 JSON으로 되돌리는 방법이 떠오릅니다. 요청 파라미터의 +와 %20이 왜 백엔드에서 다른 것으로 취급되는지도 이해하게 됩니다.
13–14단계의 방법으로 「UTF-8을 GBK로 읽은 것」인지 그 반대인지 판단한 뒤, 편집기의 「인코딩을 지정해 다시 열기」를 씁니다(「인코딩 변환」이 아닙니다). 원래 바이트를 지키는 것이 첫 원칙입니다.
이모지를 저장하려면 utf8mb4를 쓰고, UTF-8 저장량을 어림할 때 한자는 3바이트로 계산합니다. VARCHAR 길이 단위가 바이트인지 문자인지도 확인해야 합니다.
사용자 닉네임을 자를 때 str.slice(0, n) 대신 [...str].slice(0, n)을 써서 이모지가 반쪽으로 잘리는 일을 막습니다. 글자 수를 셀 때는 코드 단위, 코드 포인트, 자소 클러스터 중 무엇을 쓸지 정합니다.
피부색 수정자나 제로 폭 연결자(ZWJ)로 결합한 이모지(👍🏽, 👨👩👧 등)는 코드 포인트가 여러 개라 코드 포인트 뷰에 하나씩 나옵니다. 8단계는 정확히 4바이트 문자 하나를 요구하니 수정자 없는 기본 이모지(😀 등)를 쓰세요.
둘 다 필요 없습니다. c3a9, C3 A9, c3 a9 모두 정답입니다.
강좌는 브라우저 내장 TextDecoder('gbk')(실제로는 GB18030의 상위 집합)를 쓰고 잘못된 시퀀스에는 �를 출력합니다. 어떤 도구는 ?를 출력하거나 그냥 건너뛰는데, 글자는 달라도 뜻은 같습니다. 「여기는 해석할 수 없다」는 뜻입니다.
원래 바이트가 온전하고 읽는 방식만 틀렸다면 고칠 수 있습니다. 편집기에서 올바른 인코딩으로 다시 열면 됩니다. 파일이 이미 「변환」을 거쳐 ?가 잔뜩 생겼다면 그 부분의 정보는 이미 사라진 것이라 강좌로도 되살릴 수 없습니다.
현재 브라우저의 localStorage에 보관되고 업로드되지 않습니다. 「이 단계 공유」 링크에는 단계 번호만 실리고 진행 상황은 실리지 않습니다.
강좌는 전부 브라우저 로컬에서 돌아갑니다. 실험용 텍스트의 인코딩, 디코딩, Base64, URL 인코딩이 모두 페이지 안의 TextEncoder / TextDecoder와 순수 함수로 처리되어 어떤 서버로도 전송되지 않습니다. 학습 진행 상황은 이 기기의 localStorage에 저장됩니다.
TextDecoder가 지원하는 인코딩과 디코딩 알고리즘, 강좌의 글자 깨짐 시연에 쓰는 GBK / windows-1252 디코더 규범: https://encoding.spec.whatwg.org/(열람일: 2026-09-09)2026-09-09 업데이트
14단계로 비트, 바이트, ASCII, Unicode 코드 포인트, UTF-8 가변 길이 인코딩, Base64 그룹, URL 인코딩, 글자 깨짐 원인 이해
이 도구는 아직 완전히 번역되지 않아 일부 내용이 원문 또는 대체 언어로 표시됩니다.
목표샘플 텍스트는 단일 바이트가 01000001로 표시되는 문자 A입니다. 이 이진수를 십진수로 변환하여 입력해 보세요.
| 문자 | 코드 포인트 | UTF-8 바이트 | UTF-16 코드 단위 | 바이트 수 |
|---|---|---|---|---|
| A | U+0041 | 41 | 0041 | 1 |
컴퓨터는 0과 1만 저장합니다. 비트는 하나의 스위치입니다. 8비트는 0~255, 256개의 값을 저장할 수 있는 바이트를 구성합니다. 이진 합은 자리 값: 01000001 = 64 + 1 = 65입니다. 모든 텍스트, 이미지 및 사운드는 궁극적으로 바이트 시퀀스이며 "인코딩"은 어떤 숫자가 어떤 문자를 의미하는지 정의합니다.