사전 WHATWG Encoding Standard
표준

WHATWG Encoding Standard

gabury1고친 사람 github-actions[bot]

WHATWG Encoding Standard 는 브라우저가 바이트를 글자로 읽는 방법을 하나로 맞춥니다. 웹 페이지에 적힌 인코딩 이름을 브라우저가 어떤 규칙으로 읽을지 여기서 정합니다. 브라우저마다 글자가 다르게 깨지던 문제를 없애려고 만든 표준입니다. 새로 만드는 웹 문서에는 인코딩 하나만 쓰게 합니다.

쉽고 빠른 이해

브라우저가 바이트를 글자로 바꾸는 규칙을 적은 표준입니다. 헤더에 iso-8859-1 이라고 적혀 오면 브라우저가 어느 규칙으로 읽을지를 이 표준이 정합니다.

이 표준이 없으면 같은 페이지가 브라우저마다 다른 글자로 보입니다. 옛 웹 페이지는 인코딩 이름을 틀리게 적은 경우가 많습니다. 브라우저마다 그 실수를 다르게 받아 주면 한쪽에서만 글자가 깨집니다.

  1. 파일 맨 앞에 인코딩을 알리는 표시 바이트가 있으면 그쪽을 먼저 믿습니다
  2. 표시가 없으면 헤더나 페이지에 적힌 인코딩 이름을 정해진 목록에서 찾습니다
  3. 정해진 규칙으로 바이트를 글자로 바꿉니다. 못 읽는 바이트는 물음표 모양 글자로 바꿉니다

대가는 서버 쪽 라이브러리와 어긋날 수 있다는 점입니다. 서버가 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 그 글자를 &#54620; 꼴의 번호로 적고 계속 씁니다

U+FFFD 는 대체 문자입니다. 화면에는 검은 마름모 안의 물음표(�)로 보입니다. 원래 바이트를 알 수 없다는 표시입니다.

html 모드의 &#54620; 은 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 로 읽습니다.

JavaScript
const b = new Uint8Array([0xED, 0x95, 0x9C]);
new TextDecoder().decode(b);   // "한"

이름표를 넘기면 앞의 표대로 인코딩이 골라집니다. encoding 속성으로 무엇이 골라졌는지 볼 수 있습니다.

JavaScript
const d = new TextDecoder("latin1");
d.encoding;                    // "windows-1252"

오류 모드는 fatal 옵션으로 고릅니다. 기본은 replacement 오류 모드입니다. fatal: true 를 주면 잘못된 바이트에서 멈춥니다.

JavaScript
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 로만 씁니다. 다른 인코딩으로 쓰는 방법이 없습니다.

JavaScript
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 페이지에 이모지를 넣어 보내면 서버는 &#128512; 같은 문자열을 받습니다.

셋째는 서버에서 자바스크립트로 바이트를 다룰 때입니다. 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 · 코드 포인트 · 디코더 · 인코더 · 오류 모드 · 바이트 순서 표시

인코딩이 어긋나면 생기는 문제

모지바케 · 대체 문자 · 인코딩 불일치 · HTML 문자 참조

다른 이름: Encoding Standard · 인코딩 표준 · WHATWG 인코딩 표준