인코딩 스니핑 알고리즘
고친 사람 github-actions[bot]
인코딩 스니핑 알고리즘은 브라우저가 받은 웹 페이지를 어떤 글자 규칙으로 읽을지 정해 줍니다. 페이지의 바이트와 함께 온 단서를 정해진 순서로 하나씩 살핍니다. 맨 먼저 걸린 단서가 답이 됩니다. 이 순서를 알면 한글이 깨지는 까닭을 짚을 수 있습니다.
쉽고 빠른 이해
이 알고리즘은 바이트 덩어리를 받아 「이 규칙으로 읽어라」 하는 답 하나를 내놓습니다. 같은 바이트도 어느 규칙으로 읽느냐에 따라 「한글」이 되기도 하고 「ÇѱÛ」가 되기도 합니다.
규칙 이름 없이 오는 페이지가 많습니다. 이름이 와도 여러 군데서 서로 다른 이름을 댈 수 있습니다. 누구 말을 먼저 믿을지 정해 두지 않으면 브라우저마다 다른 글자를 보여 줍니다.
어떻게 도나:
- 파일 맨 앞에 규칙을 밝히는 표시가 있는지 봅니다
- 서버가 응답 헤더에 적어 보낸 규칙 이름을 봅니다
- 문서 앞부분에서 meta 태그에 적힌 이름을 찾습니다
- 다 없으면 지난 방문 기록이나 사용자의 언어 설정으로 짐작합니다
대가는 짐작이 틀릴 수 있다는 것입니다. meta 태그로 정한 답이나 짐작한 답은 나중에 뒤집힐 수 있습니다. 뒤집히면 브라우저가 페이지를 처음부터 다시 읽습니다.
이 순서는 HTML 로 된 웹 페이지를 읽을 때만 씁니다. 다른 형식의 데이터는 저마다 따로 정한 방법으로 규칙을 고릅니다.
상세
인코딩 스니핑 알고리즘은 HTML(HyperText Markup Language) 문서의 문자 인코딩을 정하는 절차입니다. 문자 인코딩은 글자를 바이트로 바꾸는 규칙입니다. 서버는 바이트만 보냅니다. 그 바이트를 다시 글자로 되돌리려면 어떤 규칙으로 적었는지 브라우저가 알아야 합니다.
이 절은 같은 바이트가 규칙에 따라 다른 글자가 되는 모습에서 시작합니다. 그다음 브라우저가 단서를 살피는 아홉 단계를 차례로 따라갑니다. 이어서 답에 붙는 확신도와 답이 뒤집힐 때 벌어지는 일을 다룹니다. 마지막은 서버를 만드는 쪽에서 이 순서를 어떻게 다루는지입니다.
같은 바이트, 다른 글자
EUC-KR(Extended Unix Code for Korean)은 옛 한국어 웹 페이지가 널리 쓰던 규칙입니다. 「한글」 두 글자를 EUC-KR 로 적으면 4바이트가 나옵니다.
Windows-1252는 서유럽 글자를 담는 규칙입니다. 앞의 4바이트를 Windows-1252 로 읽으면 전혀 다른 글자 넷이 됩니다. 아래 코드는 같은 4바이트를 두 규칙으로 읽어 봅니다.
b = "한글".encode("euc-kr")
b # b'\xc7\xd1\xb1\xdb'
b.decode("euc-kr") # '한글'
b.decode("cp1252") # 'ÇѱÛ'
cp1252 는 Windows-1252 를 부르는 다른 이름입니다. 바이트는 한 개도 안 바뀌었습니다. 읽는 규칙만 바꿨는데 글자가 달라졌습니다.
이렇게 엉뚱한 규칙으로 읽어 깨진 글자를 모지바케라고 부릅니다. 규칙에 없는 바이트를 만나면 대체 문자 � 가 찍히기도 합니다. 둘 다 바이트가 아니라 고른 규칙이 틀렸다는 신호입니다.
단서가 여럿이고 서로 다를 수 있다
브라우저가 규칙을 알아낼 단서는 여러 곳에 있습니다. 서버가 응답 헤더에 적어 보내는 Content-Type charset 이 하나입니다. 문서 안에 적는 meta charset 태그가 또 하나입니다. 파일 맨 앞에 붙는 바이트 순서 표시도 단서가 됩니다.
이 단서들이 늘 맞아떨어지지는 않습니다. 서버 설정은 euc-kr 이라고 합니다. 그런데 파일은 UTF-8(8-bit Unicode Transformation Format)로 저장돼 있는 일이 생깁니다. 아무 단서도 없는 옛 페이지도 있습니다. 어느 단서를 먼저 믿을지 정해 두지 않으면 같은 페이지가 브라우저마다 다르게 보입니다.
charset 에 적는 utf-8 · euc-kr 같은 인코딩 이름을 레이블이라고 부릅니다. 브라우저는 이 이름표를 보고 어떤 규칙으로 읽을지 압니다.
스니핑(sniffing)은 냄새를 맡아 알아낸다는 뜻입니다. 레이블이 없거나 못 믿을 때 내용을 살펴 짐작한다는 데서 붙은 이름입니다.
이 알고리즘은 레이블을 읽는 단계와 내용으로 짐작하는 단계를 한 줄로 세웁니다. 이 순서는 브라우저들이 함께 따르는 HTML 표준인 HTML Living Standard 안에 들어 있습니다. 그래서 브라우저들이 같은 페이지에 대체로 같은 답을 냅니다.
입력과 출력
알고리즘이 받는 입력은 어디서 오느냐에 따라 셋으로 나뉩니다.
| 입력 | 예 |
|---|---|
| 받은 바이트의 앞부분 | 바이트 순서 표시 · meta 태그 |
| 서버가 보낸 응답 헤더 | Content-Type 의 charset |
| 브라우저가 가진 정보 | 사용자가 고른 인코딩 · 부모 문서(이 페이지를 안에 품은 바깥 페이지) · 지난 방문 · 언어 설정 |
내놓는 값은 인코딩 하나와 확신도 하나입니다. 확신도는 이 답을 얼마나 믿을지를 나타내는 값입니다. 확정(certain)과 잠정(tentative) 둘 중 하나입니다.
확정인 답은 문서를 끝까지 읽는 동안 바뀌지 않습니다. 잠정인 답은 문서를 읽다가 더 나은 단서가 나오면 바뀔 수 있습니다. 바뀌는 과정은 아래 「잠정인 답이 뒤집히는 경우」에서 다룹니다.
아홉 단계
브라우저는 아래 단계를 위에서부터 차례로 시험합니다. 어느 단계에서 답이 나오면 거기서 멈춥니다. 단계마다 나온 답의 확신도도 정해져 있습니다.
| 순서 | 살피는 것 | 확신도 |
|---|---|---|
| 1 | 바이트 순서 표시 | 확정 |
| 2 | 사용자가 직접 고른 인코딩 | 확정 |
| 3 | 앞 1024바이트가 올 때까지 기다리기 | 답을 안 낸다 |
| 4 | 응답 헤더의 charset | 확정 |
| 5 | 미리 훑기로 찾은 meta 선언 | 잠정 |
| 6 | 부모 문서의 인코딩 | 잠정 |
| 7 | 지난 방문처럼 브라우저가 아는 정보 | 잠정 |
| 8 | 바이트 분포로 짐작 | 잠정 |
| 9 | 언어 설정에 따른 기본값 | 잠정 |
확신도는 4단계와 5단계 사이에서 갈립니다. 4단계까지는 확정이고 5단계부터는 잠정입니다. 응답 헤더에 charset 이 있으면 5단계 아래는 아예 돌지 않습니다.
확정을 내는 세 단계
1단계의 바이트 순서 표시는 파일 맨 앞에 붙는 고정된 몇 바이트입니다. UTF-8 이면 EF BB BF 세 바이트입니다. 이 바이트가 있으면 헤더가 무엇이라고 하든 그 인코딩으로 정합니다.
UTF-16(16-bit Unicode Transformation Format)은 글자를 2바이트 단위로 적습니다. 두 바이트 중 어느 쪽을 앞에 두느냐는 파일마다 다를 수 있습니다. 그래서 표시도 FF FE 와 FE FF 두 가지이고, 어느 쪽인지가 그 순서를 알려 줍니다.
바이트 순서 표시가 헤더보다 앞서는 까닭은 누가 붙였느냐에 있습니다. 이 표시는 파일을 저장한 프로그램이 파일 안에 직접 넣습니다. 서버 설정보다 파일 내용과 어긋날 일이 적습니다.
2단계는 사용자가 브라우저 메뉴에서 인코딩을 직접 고른 경우입니다. 이 단계는 브라우저가 둘 수도 있고 안 둘 수도 있습니다.
4단계는 응답 헤더 Content-Type: text/html; charset=euc-kr 의 charset 값을 봅니다. 이 값은 레이블이라서 브라우저가 인코딩 하나로 바꿔야 합니다. 같은 인코딩을 utf-8 · utf8 처럼 여러 이름으로 적을 수 있기 때문입니다.
그래서 이름 여럿을 인코딩 하나에 짝지어 둔 표를 씁니다. 이 표는 브라우저들이 함께 따르는 인코딩 표준인 WHATWG Encoding Standard(WHATWG 는 Web Hypertext Application Technology Working Group)에 들어 있습니다. 표에 없거나 브라우저가 지원하지 않는 인코딩이면 이 단계는 답을 못 낸 것으로 보고 다음으로 넘어갑니다.
기다리기와 미리 훑기
3단계는 바이트가 더 오기를 기다리는 단계입니다. 네트워크로 받는 문서는 조금씩 도착합니다. 5단계는 문서 앞부분에서 meta 선언을 찾습니다. 그러려면 앞부분 바이트가 먼저 모여 있어야 합니다.
그래서 브라우저는 되도록 앞 1024바이트까지 기다립니다. 그 뒤로는 바이트를 기다리느라 멈춰 서지 않습니다.
5단계의 미리 훑기(prescan)는 HTML 을 본격적으로 해석하기 전에 앞부분만 가볍게 읽는 절차입니다. 찾는 것은 인코딩을 밝힌 meta 선언 하나입니다. 주석은 건너뜁니다. 미리 훑기도 되도록 앞 1024바이트까지만 봅니다.
미리 훑기가 찾는 꼴은 둘입니다. 아래 두 줄은 같은 뜻입니다.
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
두 번째 줄의 http-equiv 는 응답 헤더에 적을 내용을 문서 안에 적는 방법입니다. 둘 중 어느 꼴이든 1024바이트 안에 있어야 이 단계에서 잡힙니다.
meta 에 utf-16 이라고 적혀 있으면 브라우저는 그 말을 그대로 믿지 않고 UTF-8 로 읽습니다. 까닭은 한 걸음씩 따라가면 보입니다.
- 미리 훑기는 한 바이트를 영문자 한 글자로 보고 읽습니다
- 그렇게 읽어서
utf-16이라는 선언을 찾아냈습니다 - UTF-16 이라면 영문자 하나에도 2바이트를 씁니다. 한 바이트씩 읽어서는 그 선언이 안 보였을 것입니다
- 그러니 이 파일은 UTF-16 일 수 없습니다
UTF-8 을 고르는 까닭은 적은 사람의 뜻에 가장 가까워서입니다. 유니코드로 저장했다는 뜻은 살리면서 영문자를 한 바이트로 적는 규칙이 UTF-8 입니다.
meta 로 정한 답이 잠정인 까닭은 가볍게 훑은 결과라서입니다. 본격적인 해석 중에 다른 선언이 나올 수 있습니다.
잠정을 내는 나머지 단계
6단계는 문서가 iframe 안에 실린 경우입니다. 이 페이지를 품은 부모 문서가 같은 출처에서 왔으면 부모 문서의 인코딩을 따릅니다.
출처는 주소의 프로토콜 · 도메인 · 포트를 묶은 것입니다. 셋이 모두 같아야 같은 출처입니다(동일 출처 정책).
7단계는 브라우저가 이 페이지의 인코딩을 이미 아는 경우입니다. 지난번 방문 때 정한 인코딩을 기억해 두었다가 쓰는 식입니다.
8단계는 바이트 분포를 보고 짐작합니다. 예를 들어 EUC-KR 에서 자주 나오는 바이트 쌍이 많이 보이면 EUC-KR 로 짐작합니다. 이런 어림 판단을 휴리스틱이라고 부릅니다.
브라우저는 네트워크로 받은 문서에는 이 단계를 되도록 쓰지 않습니다. 짐작하는 방법이 브라우저마다 달라서 같은 페이지가 다르게 보일 수 있기 때문입니다. 이 단계가 주로 도는 것은 디스크에서 바로 연 파일 같은 문서입니다.
9단계는 앞 단계가 모두 답을 못 냈을 때입니다. 브라우저는 사용자의 언어 설정, 곧 로케일에 따라 기본값을 고릅니다.
| 언어 설정 | 기본값 |
|---|---|
| 한국어 | EUC-KR |
| 일본어 | Shift_JIS |
| 그 밖의 대부분 | Windows-1252 |
선언을 안 하던 시절의 페이지는 그 언어권에서 널리 쓰던 인코딩으로 저장돼 있었습니다. 기본값은 그 페이지들을 살리려고 고른 값입니다. 그래서 단서 없이 UTF-8 로 저장한 한글 페이지는 한국어 브라우저에서 EUC-KR 로 읽혀 깨집니다.
잠정인 답이 뒤집히는 경우
HTML 파서는 HTML 을 끝까지 해석해 문서 구조를 만드는 부분입니다. 잠정인 답으로 문서를 읽어 나가다가 HTML 파서가 meta 선언을 만나는 일이 있습니다. 미리 훑기가 놓쳤거나 1024바이트 뒤에 있던 선언입니다. 이때 브라우저는 인코딩을 바꿀지 따집니다.
flowchart TD
A["잠정인 답으로 읽는 중"] --> B["meta 선언을 만난다"]
B --> C{"지금 인코딩과 같은가"}
C -->|같다| D["확신도만 확정으로 바꾼다"]
C -->|다르다| E{"읽은 바이트가 두 규칙에서 같은 글자인가"}
E -->|같다| F["새 규칙으로 이어서 읽는다"]
E -->|다르다| G["새 규칙으로 처음부터 다시 읽는다"]
갈래는 셋입니다. 선언이 지금 인코딩과 같으면 바꿀 것이 없어서 확신도만 확정으로 올립니다. 다르면 이미 읽은 글자가 문제가 됩니다. 그 글자가 새 규칙에서 다르게 읽히면 페이지를 처음부터 다시 읽습니다.
이때는 스니핑을 다시 돌리지 않습니다. 새 인코딩을 확정으로 놓고 읽습니다.
다시 읽을 때는 받아 둔 바이트를 쓰는 쪽이 낫습니다. 네트워크에 같은 요청을 다시 보내지 않아도 되기 때문입니다. 그래도 화면은 그만큼 늦게 뜹니다.
「새 규칙으로 이어서 읽는다」 갈래는 영문자 덕에 자주 걸립니다. 영문자와 숫자는 UTF-8 · EUC-KR · Windows-1252 에서 모두 같은 바이트입니다. 선언 전까지 영문만 읽었다면 규칙을 바꿔도 이미 읽은 글자가 그대로입니다.
드는 비용
스니핑에 드는 시간은 문서 크기와 거의 상관없습니다. 바이트 순서 표시는 앞 2~3바이트만 봅니다. 미리 훑기도 앞 1024바이트까지만 봅니다.
그래서 문서가 1킬로바이트든 10메가바이트든 스니핑 자체는 비슷하게 끝납니다. 8단계 짐작을 쓰면 더 많은 바이트를 볼 수 있습니다. 비용이 큰 것은 스니핑보다 답이 뒤집혀 처음부터 다시 읽는 경우입니다.
응답 헤더와 meta 가 다를 때
서버를 만드는 쪽에서 이 순서가 드러나는 것은 단서끼리 부딪힐 때입니다. 아래 응답은 헤더와 meta 가 서로 다른 인코딩을 댑니다.
HTTP/1.1 200 OK
Content-Type: text/html; charset=euc-kr
<!doctype html>
<meta charset="utf-8">
파일이 UTF-8 로 저장돼 있다면 이 페이지는 깨집니다. 4단계의 헤더가 5단계의 meta 보다 앞서기 때문입니다. 이 경우 meta 를 고쳐도 소용이 없습니다. 서버 설정의 charset 을 고쳐야 합니다.
반대로 파일과 맞는 charset 을 헤더에 적어 보내면 4단계에서 확정이 나옵니다. 그 뒤의 미리 훑기 · 짐작 · 다시 읽기는 돌지 않습니다. 헤더를 못 건드리는 경우에는 meta charset 을 문서 맨 앞쪽에 둡니다. 그러면 5단계에서 답이 나옵니다.
쓰는 곳과 안 쓰는 곳
이 알고리즘은 HTML 문서를 읽을 때 씁니다. 브라우저 밖에서 HTML 을 읽는 크롤러도 같은 순서를 따라야 브라우저와 같은 글자를 봅니다. 응답 헤더를 버리고 본문만 넘기면 4단계가 빠져서 브라우저와 다른 답이 나올 수 있습니다.
다른 형식은 인코딩을 다른 방법으로 정합니다.
| 읽는 것 | 인코딩을 정하는 법 |
|---|---|
| HTML 문서 | 이 알고리즘 |
| XML(Extensible Markup Language) 문서 | 바이트 순서 표시와 XML 선언 |
| JSON(JavaScript Object Notation) | 네트워크로 주고받을 때는 UTF-8 로 정해져 있다 |
이름이 비슷한 MIME 스니핑(MIME 은 Multipurpose Internet Mail Extensions)은 하는 일이 다릅니다. MIME 타입은 내용의 종류를 text/html 같은 이름으로 나타낸 것입니다. MIME 스니핑은 받은 내용이 HTML 인지 이미지인지 같은 종류를 짐작합니다. 인코딩 스니핑은 종류가 HTML 로 정해진 뒤에 글자 규칙을 정합니다.
관련 항목
인코딩 스니핑 알고리즘이 살피는 단서
바이트 순서 표시 · Content-Type charset · meta charset · http-equiv · 레이블 · XML 선언
인코딩 스니핑 알고리즘이 골라 내는 인코딩
UTF-8 · UTF-16 · EUC-KR · CP949 · Shift_JIS · Windows-1252
인코딩 스니핑 알고리즘을 정의하는 표준·문서
HTML Living Standard · WHATWG Encoding Standard · WHATWG · IANA 문자 집합 레지스트리
인코딩을 잘못 골랐을 때 나는 오류
인코딩 스니핑 알고리즘이 속하는 상위 분류
인코딩 스니핑 알고리즘과 헷갈리는 이웃
MIME 스니핑 · 콘텐츠 스니핑 · 인코딩 감지 · X-Content-Type-Options
인코딩 스니핑 알고리즘을 돌리는 프로그램
브라우저 · HTML 파서 · 크롤러 · iframe
인코딩 스니핑 알고리즘의 답을 바꾸는 바깥 정보
로케일 · 동일 출처 정책 · 브라우저 캐시 · Content-Type
다른 이름: 인코딩 스니핑 · encoding sniffing algorithm · encoding sniffing · 문자 인코딩 판별