Content-Type charset
고친 사람 github-actions[bot]
Content-Type charset 은 글로 된 본문을 어떤 규칙으로 바이트에 적었는지 받는 쪽에 알려 줍니다. 웹 응답의 Content-Type 줄 끝에 붙는 이름 하나입니다. 받는 쪽은 이 이름을 보고 바이트를 다시 글자로 되돌립니다. 이 값이 빠지거나 틀리는 것은 한글 깨짐의 흔한 원인입니다.
쉽고 빠른 이해
본문 글자를 바이트로 적는 규칙의 이름표입니다. 본문과 함께 헤더에 붙여 보냅니다.
서버가 한글 페이지를 보낼 때 이 값에 utf-8 을 적습니다. 받는 쪽은 그 규칙으로 바이트를
읽어 한글을 그대로 보여 줍니다.
같은 글자도 규칙에 따라 다른 바이트가 됩니다. 이름표가 없으면 받는 쪽이 규칙을 짐작해야 합니다. 짐작이 빗나가면 글자가 알아볼 수 없는 기호로 깨집니다.
- 보내는 쪽이 본문을 적은 규칙의 이름을 헤더에 적습니다
- 받는 쪽은 그 이름에 맞는 규칙으로 바이트를 글자로 바꿉니다
- 이름이 없으면 다른 단서를 찾고, 그것도 없으면 짐작합니다
대가는 이름표가 본문과 맞는지 아무도 확인해 주지 않는다는 것입니다. 잘못 적으면 받는 쪽은 틀린 규칙을 믿고 읽습니다.
이름표는 글자로 된 본문(웹 페이지·텍스트)에만 붙입니다. 그림처럼 글자가 아닌 본문이나, 처음부터 한 규칙으로만 적게 정해진 형식에는 필요 없습니다.
상세
이 절은 charset 값이 무엇을 알리는지부터 봅니다. 그다음 이 값이 필요한 형식과 필요 없는 형식을 가르고, 다른 신호와 부딪칠 때 누가 이기는지를 봅니다.
Content-Type 은 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 메시지의 헤더 필드입니다. 본문이 어떤 형식인지 받는 쪽에 알립니다. 실제 줄은 이렇게 생겼습니다.
Content-Type: text/html; charset=utf-8
앞쪽 text/html 은 본문이 웹 페이지라는 뜻입니다. 이렇게 본문 형식을 적는 이름을
미디어 타입이라고 부릅니다. 받는 쪽은 이 이름을 보고 본문을 어떻게 다룰지 정합니다.
뒤쪽 ; charset=utf-8 처럼 세미콜론 뒤에 덧붙이는 값은 매개변수라고
부릅니다. 미디어 타입만으로 모자란 정보를 보탭니다. charset 은 그 매개변수 가운데 하나입니다.
여기서는 페이지의 글자를 UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식
8비트) 규칙으로 적었다는 뜻입니다.
글자가 바이트가 되는 규칙
컴퓨터는 글자를 바이트로 바꿔서 담습니다. 어떤 글자를 어떤 바이트로 바꿀지 정한 규칙을 문자 인코딩이라고 합니다. 개요부터 「규칙」이라고 부른 것이 이것입니다. 이 규칙은 하나가 아니라 여럿입니다.
같은 「가」 한 글자를 두 규칙으로 바꿔 보면 바이트가 이렇게 갈립니다. 파이썬에서 문자열을 바이트로 바꾸는 호출입니다.
"가".encode("utf-8") # b'\xea\xb0\x80'
"가".encode("euc-kr") # b'\xb0\xa1'
UTF-8 은 「가」를 세 바이트로 적습니다. EUC-KR(Extended Unix Code for Korean, 한국어 확장 유닉스 코드)은 두 바이트로 적습니다. 바이트 길이부터 다릅니다.
위 출력의 \xea\xb0\x80 은 바이트 세 개를 16진수로 적은 것입니다. 같은 세 바이트를 줄여서
EA B0 80 이라고도 적습니다.
이 세 바이트를 EUC-KR 규칙으로 읽으면 「가」가 나오지 않습니다. 앞 두 바이트 EA B0 은
「媛」이라는 한자로 읽힙니다. 남은 80 은 짝이 없어 � 같은 알아볼 수 없는 기호가 됩니다.
바이트만 받아서는 어느 규칙으로 읽어야 할지 알 수 없다는 뜻입니다.
이렇게 규칙이 어긋나 글자가 엉뚱한 기호로 보이는 고장을 모지바케라고 합니다. charset 은 받는 쪽에 올바른 규칙의 이름을 건네서 이 고장을 막습니다.
이름과 달리 인코딩을 가리키는 값
charset 은 character set 의 줄임으로, 우리말로는 문자 집합입니다. 문자 집합은 어떤 글자들을 다룰지 모아 둔 목록입니다. 글자마다 번호를 매깁니다. 그 번호를 어떤 바이트로 적을지는 정하지 않습니다.
이 값에 들어가는 것은 목록이 아니라 인코딩 이름입니다. 유니코드는 문자 집합 하나입니다.
그 글자들을 바이트로 적는 규칙은 UTF-8 과 UTF-16(Unicode Transformation Format 16-bit,
유니코드 변환 형식 16비트)으로 갈립니다. charset=utf-8 은 「유니코드를 쓴다」가 아니라
「UTF-8 규칙으로 적었다」를 알립니다.
EUC-KR 처럼 글자 목록과 바이트로 적는 규칙이 한 이름으로 묶인 것도 있습니다. EUC-KR 은 문자 집합 이름으로 읽든 인코딩 이름으로 읽든 같은 것을 가리킵니다. 매개변수 이름만 charset(문자 집합)으로 남았습니다. 받는 쪽이 알아야 하는 것은 언제나 바이트로 적는 규칙입니다.
값에 적는 이름
값에는 아무 이름이나 적지 않습니다. IANA(Internet Assigned Numbers Authority, 인터넷 할당
번호 관리 기관)가 인코딩 이름을 모아 둔 목록이 있습니다. 값에는 거기 오른 이름을 적습니다.
utf-8 · euc-kr 이 그런 이름입니다.
이름은 대소문자를 가리지 않습니다. charset=UTF-8 과 charset=utf-8 은 같은 뜻입니다.
이 값이 뜻을 갖는 형식
charset 은 본문이 글자일 때만 뜻이 있습니다. text/html · text/plain · text/csv 처럼
사람이 읽는 글을 담는 text 형식에 주로 붙습니다. 그림이나 압축 파일은 바이트 자체가 내용입니다.
바이트를 글자로 되돌릴 일이 없습니다.
JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)은 글이지만 사정이 다릅니다. 여러 시스템이 주고받는 JSON 은 반드시 UTF-8 로 적어야 합니다. 받는 쪽이 누구든 같은 규칙으로 읽게 하려는 것입니다. 규칙이 하나로 정해져 있으니 알릴 것이 없습니다.
그래서 application/json 에는 charset 매개변수가 정의돼 있지 않습니다. 서버가
application/json; charset=utf-8 을 붙여 보내는 일은 흔합니다. 규칙대로 만든 받는 쪽에는 아무
효과가 없습니다.
요청 본문에 붙는 charset
charset 은 응답에만 붙는 값이 아닙니다. 클라이언트가 본문을 실어 보내는 요청에도 같은 문제가 있습니다. 서버도 받은 바이트를 글자로 되돌려야 하기 때문입니다.
폼 입력값을 보내는 application/x-www-form-urlencoded 가 흔한 예입니다. 한글 입력값은 바이트로
바뀐 뒤 %EA%B0%80 처럼 퍼센트 기호가 붙은 글자로 실려 옵니다. 서버는 이 바이트를 어떤 규칙으로
글자로 되돌릴지 알아야 합니다.
요청에 charset 이 없으면 서버는 자기 기본 규칙으로 읽습니다. 보낸 쪽의 규칙과 서버의 기본 규칙이 다르면 입력값의 한글이 깨진 채로 저장됩니다. 응답에서 보던 모지바케가 요청 쪽에서도 똑같이 생깁니다.
다른 신호와 부딪칠 때의 순서
HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어) 문서는 인코딩을 알리는 통로가 charset 말고도 둘 더 있습니다. 하나는 본문 맨 앞에 붙는 바이트 순서 표시입니다. 파일 첫머리의 고정된 몇 바이트로 인코딩을 알립니다.
다른 하나는 문서 안에 적는 meta charset 태그입니다. <meta charset="utf-8"> 처럼 HTML
머리에 인코딩 이름을 적어 둡니다. 서버 설정을 못 건드리는 사람도 파일만 고쳐 인코딩을 알릴 수
있습니다.
세 신호가 서로 다른 이름을 말하면 브라우저는 정해진 순서로 하나를 고릅니다. 사용자가 메뉴에서 인코딩을 직접 고른 때를 빼면 순서는 이렇습니다.
flowchart TD
A["HTML 본문이 도착한다"] --> B{"맨 앞에 바이트 순서 표시가 있나"}
B -->|있다| C["그 표시대로 읽는다"]
B -->|없다| D{"Content-Type 에 charset 이 있나"}
D -->|있다| E["charset 대로 읽는다"]
D -->|없다| F{"문서 안에 meta charset 이 있나"}
F -->|있다| G["meta charset 대로 읽는다"]
F -->|없다| H["브라우저가 짐작한다"]
바이트 순서 표시가 제일 앞서고, 헤더의 charset 이 그다음입니다. 문서 안의 meta charset 은 그 둘이 없을 때만 쓰입니다.
이 순서 때문에 헤더와 문서가 어긋나면 헤더가 이깁니다. 서버가 charset=euc-kr 을 붙여 보내면,
파일 안에 <meta charset="utf-8"> 을 적어도 브라우저는 EUC-KR 로 읽습니다. 깨진 페이지를 고칠
때 문서만 보면 원인을 못 찾는 까닭입니다.
짐작에 맡겨질 때
세 신호가 다 없으면 받는 쪽은 바이트 모양을 보고 인코딩을 짐작합니다. 짐작하는 방법은 받는 프로그램과 그 컴퓨터의 언어 설정마다 다릅니다.
그래서 같은 응답이 어떤 곳에서는 멀쩡하고 어떤 곳에서는 깨집니다. charset 없이 보낸 EUC-KR 페이지를 예로 들면, 한국어 설정 컴퓨터에서는 멀쩡하게 보입니다. 다른 언어 설정 컴퓨터에서는 같은 페이지가 깨져 보입니다. charset 을 적어 보내면 받는 쪽이 짐작 단계까지 내려가지 않습니다.
관련 항목
이 값이 붙는 헤더와 그 구성 요소
Content-Type · 미디어 타입 · 미디어 타입 매개변수 · 헤더 · HTTP
이 값에 적히는 인코딩 이름
UTF-8 · EUC-KR · UTF-16 · ISO-8859-1 · Shift_JIS · CP949 · US-ASCII
이 값이 이름을 대는 글자 규칙의 바탕 개념
문자 인코딩 · 문자 집합 · 유니코드 · 코드 포인트 · 바이트
같은 인코딩을 다른 통로로 알리는 표시
meta charset · 바이트 순서 표시 · XML 선언 · 인코딩 선언
이 값과 이름이 비슷해 헷갈리는 헤더
Accept-Charset · Content-Encoding · Transfer-Encoding · Content-Language
이 값이 뜻을 갖는 본문 형식
HTML · JSON · XML · CSV · application-x-www-form-urlencoded · multipart-form-data
이 값이 빠지거나 틀릴 때 터지는 문제
모지바케 · 인코딩 스니핑 · MIME 스니핑 · 대체 문자 · 퍼센트 인코딩
이 값의 이름과 순서를 정하는 표준 문서
RFC 9110 · RFC 2046 · RFC 8259 · IANA 문자 집합 레지스트리 · WHATWG Encoding Standard · HTML Living Standard
다른 이름: charset 매개변수 · charset 파라미터 · Content-Type 의 charset