WHATWG Encoding Standard
고친 사람 github-actions[bot]
WHATWG Encoding Standard 는 브라우저가 바이트를 글자로 읽는 방법을 하나로 맞춥니다. 웹 페이지에 적힌 인코딩 이름을 브라우저가 어떤 규칙으로 읽을지 여기서 정합니다. 브라우저마다 글자가 다르게 깨지던 문제를 없애려고 만든 표준입니다. 새로 만드는 웹 문서에는 인코딩 하나만 쓰게 합니다.
쉽고 빠른 이해
브라우저가 바이트를 글자로 바꾸는 규칙을 적은 표준입니다. 헤더에 iso-8859-1 이라고
적혀 오면 브라우저가 어느 규칙으로 읽을지를 이 표준이 정합니다.
이 표준이 없으면 같은 페이지가 브라우저마다 다른 글자로 보입니다. 옛 웹 페이지는 인코딩 이름을 틀리게 적은 경우가 많습니다. 브라우저마다 그 실수를 다르게 받아 주면 한쪽에서만 글자가 깨집니다.
- 파일 맨 앞에 인코딩을 알리는 표시 바이트가 있으면 그쪽을 먼저 믿습니다
- 표시가 없으면 헤더나 페이지에 적힌 인코딩 이름을 정해진 목록에서 찾습니다
- 정해진 규칙으로 바이트를 글자로 바꿉니다. 못 읽는 바이트는 물음표 모양 글자로 바꿉니다
대가는 서버 쪽 라이브러리와 어긋날 수 있다는 점입니다. 서버가 iso-8859-1 로 적어 보낸
바이트를 브라우저는 더 넓은 인코딩으로 읽습니다. 브라우저를 거치지 않는 서버 사이 통신에는
이 규칙이 걸리지 않습니다.
상세
Encoding Standard 가 정하는 것을 생긴 까닭 · 인코딩 이름 · 디코더와 인코더 · 자바스크립트에서 부르는 함수 차례로 봅니다. 마지막에 백엔드 개발자가 이 표준을 만나는 경우를 봅니다.
이 표준이 생긴 까닭
컴퓨터는 글자를 바이트로 바꿔 저장하고 보냅니다. 글자를 바이트로 적는 규칙을 문자 인코딩이라고 부릅니다. 받는 쪽은 같은 규칙으로 바이트를 글자로 되돌려야 합니다.
웹에서는 이 규칙의 이름을 문서가 스스로 밝힙니다. HTTP(HyperText Transfer Protocol) 응답의
Content-Type 헤더에 charset=utf-8 처럼 적습니다. HTML(HyperText Markup Language) 파일
안에 <meta charset="utf-8"> 로 적기도 합니다. 여기서 utf-8 은 UTF-8(Unicode Transformation
Format 8-bit, 유니코드 변환 형식 8비트)을 가리키는 이름입니다.
인코딩 이름의 공식 목록은 따로 있었습니다. IANA(Internet Assigned Numbers Authority, 인터넷 할당 번호 관리 기관)가 관리하는 IANA 문자 집합 레지스트리가 그 목록입니다. 그런데 브라우저는 이 목록대로만 읽지 않았습니다. 이름을 틀리게 적은 옛 페이지가 많아서 브라우저마다 나름의 방식으로 받아 주었기 때문입니다.
그 결과 브라우저를 새로 만들려면 다른 브라우저의 동작을 뜯어보고 흉내 내야 했습니다. Encoding Standard 는 브라우저들이 하던 일을 문서로 적어 이 흉내를 없앱니다.
WHATWG(Web Hypertext Application Technology Working Group, 웹 하이퍼텍스트 애플리케이션 기술 작업반)가 이 표준을 냅니다. 브라우저 제작사들이 함께 웹 표준을 쓰는 모임입니다. 판 번호를 매겨 끝내지 않습니다. 한 문서를 계속 고쳐 나가는 Living Standard 방식입니다.
인코딩과 이름표
인코딩은 바이트와 코드 포인트 사이를 오가는 규칙 한 벌입니다. 코드 포인트는
유니코드가 글자마다 매긴 번호입니다. 「한」의 코드 포인트는 U+D55C 입니다.
앞에서 인코딩 이름이라 부른 것, 곧 헤더나 페이지에 적혀 오는 문자열을 이름표(label)라고 합니다. 인코딩 하나에 이름표가 여럿 붙습니다. 브라우저는 이름표를 보고 인코딩을 고릅니다.
이름표를 찾을 때는 앞뒤 공백을 걷고 대소문자를 가리지 않습니다. " UTF-8 " 과 utf-8 은
같은 이름표입니다. 목록에 없는 이름표는 브라우저가 받아 주지 않습니다.
몇몇 이름표는 공식 목록과 다른 인코딩을 가리킵니다. 옛 페이지를 깨지지 않게 읽으려고 넓은 쪽 인코딩으로 묶었기 때문입니다. 아래 표에는 한국어 인코딩인 EUC-KR(Extended Unix Code for Korean, 한국어 확장 유닉스 코드)의 이름표도 넣었습니다.
| 적혀 오는 이름표 | 브라우저가 고르는 인코딩 |
|---|---|
utf-8 · utf8 |
UTF-8 |
iso-8859-1 · latin1 · us-ascii · ascii |
windows-1252 |
euc-kr · korean · ks_c_5601-1987 |
EUC-KR |
표의 둘째 줄이 대표적입니다. windows-1252 는 ISO-8859-1 에 글자 몇 개를 더 얹은 서유럽 인코딩입니다. ISO-8859-1 의 ISO 는 국제 표준화 기구(International Organization for Standardization)입니다.
옛 페이지는 iso-8859-1 이라고 적어 놓고 windows-1252 바이트를 보내는 일이 많았습니다.
넓은 쪽으로 읽으면 두 경우 모두 깨지지 않습니다.
셋째 줄은 한국어 이름표입니다. CP949(Code Page 949, 코드 페이지 949)는 EUC-KR 에 한글 글자를 더 얹은 마이크로소프트의 확장입니다. 브라우저가 고르는 EUC-KR 은 CP949 가 더한 한글까지 읽습니다.
담긴 인코딩
이 표준은 인코딩을 둘로 가릅니다. 하나는 UTF-8 입니다. 새로 만드는 프로토콜과 포맷은 UTF-8 만 반드시(MUST) 써야 합니다. 이미 있는 포맷을 새 곳에 들여올 때도 같습니다.
나머지는 모두 레거시 인코딩입니다. 레거시 인코딩은 이미 퍼진 페이지를 읽으려고 남겨 둔 옛 인코딩을 말합니다. 새로 쓰라고 둔 것이 아닙니다.
| 묶음 | 들어 있는 인코딩의 예 |
|---|---|
| UTF-8 | UTF-8 |
| 한 바이트 레거시 | windows-1252 · windows-1251 · ISO-8859-2 |
| 여러 바이트 레거시 · 중국어 | gb18030 · Big5 |
| 여러 바이트 레거시 · 일본어 | Shift_JIS 외 둘 |
| 여러 바이트 레거시 · 한국어 | EUC-KR |
| 그 밖의 레거시 | replacement · UTF-16BE · UTF-16LE |
한 바이트 레거시는 바이트 하나가 글자 하나입니다. 그래서 담을 수 있는 글자가 256개를 넘지 못합니다. 여러 바이트 레거시는 바이트 두세 개로 한 글자를 적어 한자와 한글을 담습니다.
UTF-16도 레거시 묶음에 듭니다. 웹 문서를 주고받는 인코딩으로는 새로 쓰지 않는다는 뜻입니다.
UTF-16 은 두 바이트를 한 단위로 씁니다. UTF-16BE 와 UTF-16LE 는 그 두 바이트를 적는 순서만 다릅니다. BE(Big Endian)는 큰 쪽 바이트를 먼저, LE(Little Endian)는 작은 쪽 바이트를 먼저 적습니다.
디코더와 인코더
인코딩 하나는 두 부품으로 움직입니다. 디코더는 바이트를 코드 포인트로 바꿉니다. 인코더는 코드 포인트를 바이트로 바꿉니다.
바꾸다 보면 막히는 경우가 생깁니다. 디코더는 그 인코딩 규칙에 맞지 않는 바이트를 만납니다. 인코더는 그 인코딩에 없는 글자를 만납니다.
이때 무엇을 할지를 오류 모드가 정합니다. 오류 모드는 디코더와 인코더가 받는 값이 다릅니다. 인코더의 html 모드는 없는 글자를 번호로 적습니다.
| 부품 | 오류 모드 | 막혔을 때 |
|---|---|---|
| 디코더 | replacement | 그 대목을 U+FFFD 로 바꾸고 계속 읽습니다 |
| 디코더 | fatal | 바로 멈추고 오류를 냅니다 |
| 인코더 | fatal | 바로 멈추고 오류를 냅니다 |
| 인코더 | html | 그 글자를 한 꼴의 번호로 적고 계속 씁니다 |
U+FFFD 는 대체 문자입니다. 화면에는 검은 마름모 안의 물음표(�)로 보입니다. 원래 바이트를
알 수 없다는 표시입니다.
html 모드의 한 은 HTML 이 글자를 번호로 적는 방법입니다. 54620 은 「한」의 코드 포인트
U+D55C 를 십진수로 적은 값입니다. 브라우저는 이 번호를 다시 「한」으로 보여 줍니다.
잘못된 바이트 열 하나가 대체 문자 몇 개가 되는지도 정해져 있습니다. 「😀」는 UTF-8 로
F0 9F 98 80 네 바이트입니다. 끝 바이트가 잘린 F0 9F 98 은 대체 문자 하나로 읽힙니다.
이 개수가 구현마다 같아야 같은 입력이 같은 글자열로 읽힙니다.
이름표보다 앞서는 바이트 순서 표시
바이트 순서 표시(BOM, Byte Order Mark)는 파일 맨 앞에 붙는 몇 바이트입니다. 이 파일이
어느 유니코드 인코딩으로 적혔는지를 알려 줍니다. UTF-8 이면 EF BB BF 세 바이트입니다.
브라우저는 바이트 순서 표시를 다른 무엇보다 먼저 믿습니다. 헤더의 이름표가 euc-kr 이어도
본문이 EF BB BF 로 시작하면 UTF-8 로 읽습니다.
flowchart TD
A["바이트와 이름표를 받는다"] --> B{"맨 앞에 바이트 순서 표시가 있나"}
B -->|있다| C["표시가 가리키는 인코딩으로 고른다"]
B -->|없다| D["이름표로 찾은 인코딩으로 고른다"]
C --> E["표시를 떼고 나머지를 디코더에 넣는다"]
D --> F["처음부터 디코더에 넣는다"]
표시는 글자로 읽히지 않고 떼어집니다.
서버 설정의 이름표는 옛 인코딩 그대로 두고 파일만 UTF-8 로 저장한 페이지가 그런 경우입니다. 이런 페이지는 바이트 순서 표시를 믿어야 제대로 읽힙니다. 브라우저들이 이미 이렇게 읽고 있어서 이 순서를 그대로 적었습니다.
replacement 인코딩
replacement 인코딩은 앞의 디코더 오류 모드 replacement 와 이름만 같은 별개의 인코딩입니다.
바이트를 글자로 읽지 않습니다. 디코더만 있고 인코더는 없습니다. 입력이 비어 있지 않으면
전부를 U+FFFD 하나로 읽고 끝냅니다.
iso-2022-kr · iso-2022-cn · hz-gb-2312 같은 이름표가 replacement 인코딩을 가리킵니다.
원래의 ISO-2022-KR 같은 인코딩은 중간에 끼운 전환 바이트에 따라 같은 바이트를 다른 글자로
읽습니다.
ISO-2022-KR 을 모르는 서버는 전환 바이트를 무시하고 바이트를 읽습니다. 그러면 서버가 본 글자와 브라우저가 읽은 글자가 달라집니다. 서버 필터가 걸러 낸 줄 알았던 스크립트가 브라우저에서 되살아날 수 있습니다(XSS(Cross-Site Scripting, 사이트 간 스크립팅)).
이런 이름표를 바이트를 읽지 않는 인코딩에 묶어 두면 이 틈이 닫힙니다.
자바스크립트 API
이 표준은 자바스크립트에서 부르는 API(Application Programming Interface)도 정합니다. TextDecoder 는 바이트를 문자열로, TextEncoder 는 문자열을 바이트로 바꿉니다. 브라우저뿐 아니라 Node.js 같은 서버 쪽 런타임에도 같은 이름으로 들어 있습니다.
먼저 UTF-8 바이트 세 개를 문자열로 바꿔 봅니다. 이름표를 안 넘기면 UTF-8 로 읽습니다.
const b = new Uint8Array([0xED, 0x95, 0x9C]);
new TextDecoder().decode(b); // "한"
이름표를 넘기면 앞의 표대로 인코딩이 골라집니다. encoding 속성으로 무엇이 골라졌는지
볼 수 있습니다.
const d = new TextDecoder("latin1");
d.encoding; // "windows-1252"
오류 모드는 fatal 옵션으로 고릅니다. 기본은 replacement 오류 모드입니다. fatal: true 를
주면 잘못된 바이트에서 멈춥니다.
const bad = new Uint8Array([0xFF]);
new TextDecoder().decode(bad); // "�"
const s = new TextDecoder("utf-8",
{ fatal: true });
s.decode(bad); // TypeError
0xFF 는 UTF-8 에서 나올 수 없는 바이트입니다. 기본 모드는 대체 문자 하나를 돌려줍니다.
fatal 모드는 예외를 던집니다.
ignoreBOM 옵션도 있습니다. 이 옵션을 켜면 앞에 붙은 바이트 순서 표시를 떼지 않고 글자로
남깁니다.
TextEncoder 는 UTF-8 로만 씁니다. 다른 인코딩으로 쓰는 방법이 없습니다.
const e = new TextEncoder();
e.encoding; // "utf-8"
e.encode("한"); // [237, 149, 156]
237 · 149 · 156 은 앞에서 디코더에 넣은 0xED 0x95 0x9C 를 십진수로 적은 값입니다.
인코더와 디코더가 같은 바이트를 오갑니다.
새로 쓰는 것은 UTF-8 만이라는 원칙이 API 에도 들어가 있습니다. 레거시 인코딩은 읽을 수만 있고 쓸 수는 없습니다.
백엔드에서 만나는 경우
백엔드 개발자가 이 표준을 직접 읽을 일은 드뭅니다. 그래도 서버가 보낸 바이트를 브라우저가 이 규칙으로 읽기 때문에 결과가 서버로 돌아옵니다.
첫째는 헤더에 적는 이름표입니다. 서버 쪽 라이브러리는 대개 IANA 목록을 따릅니다. 브라우저는 앞의 이름표 표를 따릅니다. 그래서 같은 바이트가 두 쪽에서 다른 글자가 됩니다.
파이썬은 latin-1 로 받은 바이트 0x80 을 제어 문자 U+0080 으로 읽습니다. 브라우저는 같은
바이트를 windows-1252 의 「€」로 읽습니다. 0x80 부터 0x9F 까지가 이렇게 갈립니다.
EUC-KR 도 같습니다. EUC-KR 표대로만 읽는 서버는 CP949 에만 있는 한글을 못 읽습니다.
헤더에 utf-8 을 적으면 두 쪽이 같은 인코딩으로 읽어 이 어긋남이 생기지 않습니다.
둘째는 폼 전송입니다. 레거시 인코딩으로 적힌 페이지의 폼은 그 인코딩으로 값을 보냅니다.
그 인코딩에 없는 글자는 html 모드의 번호로 바뀌어 옵니다. EUC-KR 페이지에 이모지를 넣어
보내면 서버는 😀 같은 문자열을 받습니다.
셋째는 서버에서 자바스크립트로 바이트를 다룰 때입니다. Node.js 의 TextDecoder 도 앞의 이름표 표로 인코딩을 고릅니다.
반대로 이 표준을 따를 필요가 없는 곳도 있습니다. 브라우저를 거치지 않는 서버 사이 통신과 파일 처리는 각 언어의 인코딩 라이브러리가 정한 이름을 따릅니다. 이 경우에도 UTF-8 하나로 맞추면 두 목록이 어긋날 일이 없습니다.
관련 항목
이 표준을 내는 모임과 함께 묶이는 WHATWG 표준
WHATWG · Living Standard · HTML Living Standard · Fetch · WHATWG URL 표준 · MIME Sniffing · Streams
이 표준이 정의하는 인코딩
UTF-8 · UTF-16 · Windows-1252 · ISO-8859-1 · EUC-KR · CP949 · Shift_JIS · EUC-JP · ISO-2022-JP · GBK · GB18030 · Big5 · KOI8-R
이 표준이 정의하는 자바스크립트 API
TextDecoder · TextEncoder · TextDecoderStream · TextEncoderStream · Encoding API
인코딩 이름을 다르게 묶는 공식 목록
IANA 문자 집합 레지스트리 · IANA · RFC 2978
이 표준의 이름표를 적는 헤더와 선언
Content-Type · Content-Type charset · meta charset · 인코딩 스니핑 알고리즘 · XML 선언
이 표준이 바이트를 글자로 바꿀 때 쓰는 개념
문자 인코딩 · Unicode · 코드 포인트 · 디코더 · 인코더 · 오류 모드 · 바이트 순서 표시
인코딩이 어긋나면 생기는 문제
다른 이름: Encoding Standard · 인코딩 표준 · WHATWG 인코딩 표준