
아�니 이 글자 왜 들어간 거예요?
아�니 이 글자 왜 들어간 거예요? 관련
FEConf2024에서 발표한 <아�니 이 글자 왜 들어간 거예요? (부제: 알아두면 가끔 쓸모 있는 한글 유니코드 완전 정복)[1]>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 유니코드에 대해 알아보고, 유니코드에서의 한글에 대해 알아보겠습니다. 2회에서는 물음표 특수문자가 왜 생겨나는지, 이를 어떻게 해결하는지에 대해 알아봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다.
안녕하세요. 저는 ‘아�니 이 글자 왜 들어간 거예요?’라는 제목의 발표를 진행할 제한재입니다. 네이버 오피스에서 워드와 슬라이드 제품을 만들었고, 현재는 치과의사 커뮤니티를 운영하는 데니어에서 CTO로 의료인들을 위한 플랫폼을 만들고 있습니다.
이번에는 여러분들이 언젠가 한 번쯤은 만날 이 문제를 쉽게 해결하고, 기술 면접에서 발표하게 되었을 때 아주 뛰어난 답변을 할 수 있게 알찬 내용으로 구성했습니다. 잘 활용하시길 바랍니다.
이번 글에서 다루는 것들은 아래와 같습니다.
- 유니코드 안에 있는 Hangul Syllables 구성
- euc-kr/cp949의 한글 문자 구성과 한계
- 생활 속의 utf-8, ucs-2
- 조합형 완성형 글자 이해하기
- 한글 트러블 슈팅과 깨진 utf 글자의 해석 사례
유니코드
유니코드 지옥
이번 발표는 아래 사진에서 시작되었습니다. 같이 한번 볼까요? 함께 일하던 매니저님이 이렇게 슬랙 메시지를 보냈습니다.

“텍스트 에디터를 통해 글을 작성하는 도중에 긴 글을 작성하면 이상한 물음표 글자가 자꾸 생겨요.”
“이 글자를 지우기 위해 글을 수정하고 다시 업로드를 하면 또 다른 곳에 나타나요. 꼭 두더지 같아요!”
이 메시지를 보낸 매니저님은 이런 현상을 보고 ‘유니코드 지옥’이라고 표현했습니다. 위 이미지의 오른쪽 아래가 실제로 매니저님이 겪은 깨진 텍스트의 모습입니다. 이제 이 물음표가 왜 등장하는지 알아보고, 이 문제를 해결하기 위한 여정을 함께 해보겠습니다.
유니코드란?
먼저 유니코드에 대해 알아보겠습니다. 유니코드는 ISO 10646이라는 표준입니다. 다시 말해 유니코드란 전 세계에 있는 모든 문자를 표현하는 일관된 표준이자 동시에 이러한 문자들을 관리하는 기구를 말합니다. 이 유니코드 안에는 몇 가지 큰 덩어리가 나뉘어 있습니다.

첫 번째 영역은 BMP 영역이라고 합니다. 이 영역에는 기본 언어 평면이라고 부르는 텍스트 셋이 있습니다. 이 텍스트 셋에서는 U+0000~U+FFFF라는 표현을 사용합니다. 이는 유니코드를 쓸 때 많이 쓰는 표현 방법입니다. U+뒤에 나오는 16진수 숫자들이 보통 유니코드에서 말하는 숫자를 의미합니다. 숫자로만 표현하면 의미가 헷갈리기 때문에 U+를 관습적으로 붙인다고 이해하면 될 것 같습니다.
두 번째 영역인 SMP 영역은 보충 언어 평면이라고 합니다. 이 영역에는 여러분들이 사용하는 이모지나 기호들이 포함됩니다. 마지막으로 세 번째 영역은 표의 문자 평면이라고 하고 주로 한자와 같은 문자들이 여기에 포함됩니다.

위 그림은 첫 번째 영역인 기본 언어 평면입니다. 그림처럼 기본 언어 평면에는 들어있는 글자들이 정말 많습니다. 한 칸에는 256자 정도의 텍스트가 들어갈 수 있습니다. 00부터 02까지는 로마자가 들어가 있고, 로마자를 넘어 파란색의 유럽 문자 등을 지나, 34번부터는 CJK 문자도 볼 수 있습니다. CJK는 Chinese, Japanese, Korean의 약자로 세 나라에서 쓰는 한자가 포함된 칸입니다. 우리가 쓰는 문자는 동아시아 문자 영역에 들어가 있고, 빨간색으로 표시되어 있습니다. 빨간색으로 표시된 동아시아 문자는 11번, 30번, 31번에 들어가 있고, A0부터 D7에도 들어가 있습니다.
이 BMP 영역은 U+0000부터 U+FFFF까지 총 4개의 16진법 숫자가 들어갔기 때문에 2byte 표현 범위이며, 총 65,536자의 문자가 들어가 있습니다. 유니코드에서는 전 세계 대부분의 문자가 이 65,000여 개 범위에 포함된다고 설명하고 있습니다.
유니코드에서의 한글
그럼 우리가 표현하는 한글은 정확히 어디부터 포함되어 있을까요? 앞서 말한 것처럼 동아시아 문자열에 포함되어 있는데, 그중 AC로 시작하는 부분부터 포함되어 있습니다. 아래 그림은 유니코드 안에 있는 한글 목록의 일부입니다.

‘가’부터 시작해서 ‘힣’ 까지 표현하는 ‘U+AC00’부터 ‘U+D7A3’까지 영역에 우리가 사용하는 한글이 포함되어 있습니다. 여러분들이 한글인지 판별하기 위해 쓰는 정규 표현식을 사용할 때 ‘가’부터 ‘힣'까지로 쓰는 그 영역이라고 생각하면 됩니다. 이렇게 표현한 것이 바로 유니코드에서 정의하는 한글입니다.
그럼 이 영역에 한글이 몇 글자나 들어가 있을까요? 먼저 초성 19자, 중성 21자, 종성 27자에 비어 있는 종성 1자까지 포함하고, 19 X 21 X 28을 하면 11,172자가 됩니다. 이 11,172자를 다 표현하느냐에 따라 완성된 한글인지 부분적으로 빠져있는 한글인지 판별할 수 있습니다.
그리고 여기에 포함되지 않은 한글도 있습니다. 아래 그림과 같이 ‘아래 아’를 포함한 옛한글, 초성이 없는 글자, 중성이 없는 글자 등은 유니코드에 포함되어 있지 않습니다.

한글 정렬 기준
한글 정렬 기준에 대해서도 알아보겠습니다. ‘가나다라마바사’부터 ‘아자차카타파하’까지, 이 정렬 순서가 정해진지는 그렇게 오래되지 않았습니다. 쌍자음이 어디에 들어갈지, 겹 모음이 어디에 들어갈지 등에 대한 논의를 바탕으로 한글을 전산화하던 1988년 문교부 고시를 통해 확정이 되었습니다. 당시 확정한 고시를 바탕으로 유니코드에도 그 순서가 반영되었습니다.
몇 가지 연구를 더 찾아보니 이 순서에 대한 제시가 몇 차례 있었고, 80년대 이전에는 쌍자음이 뒤에 있는 등의 다른 순서를 가진 사전들이 종종 있었습니다. 이처럼 다양한 논의로 정해진 순서가 KS X 1026-1이라는 표준에 기록되어 있습니다.

그럼 여기서 잠깐 북한의 한글은 어떤지 살펴볼까요? 북한에서 사용하는 정렬은 우리와 조금 다릅니다. 아래 그림처럼 쌍자음이 뒤에 오고, 모음 ‘ㅇ’도 맨 마지막에 위치해 있습니다. 따라서 북한에 있는 개발자들이 한글을 표현하고 개발에 사용할 때는 유니코드 순서와 일치하지 않는 구현을 직접 해야 합니다.
1999년에 북한이 아래 그림처럼 교정할 것을 제안했지만, 유니코드 위원회에서는 이를 받아들이지 않았습니다. 이미 88년도에 한국에서 한글에 대한 정렬을 등록했기 때문입니다.

그럼 유니코드에서 한글은 얼마나 큰 비중을 차지할까요? 앞서 설명한 BMP 영역의 66,536자 중에 11,172자, 무려 6분의 1을 차지하고 있습니다. 그래서 유니코드에서도 이렇게 많은 글자를 모두 받아줄 것인지 대한 논의도 있었다고 합니다. 그 결과, 논의를 거쳐 일부 빠진 글자도 존재했습니다.
그 때문에 유니코드 1.0 스펙에는 지금보다 적은 한글이 기록되었습니다. 1996년에 나온 유니코드 2.0 스펙이 돼서야 11,172자의 모든 한글이 기록되었습니다.
조합형 유니코드 Hangul Jamo
조금 다른 한글 표현이 또 있습니다. 바로 조합형 유니코드인 Hangul Jamo입니다. 이 표현은 초성, 중성, 종성을 조합해서 표시할 수 있습니다.

첫가끝 유니코드
‘첫가끝’ 이라고도 부르는 이 표현 방법으로 제 이름의 ‘한'을 만들려면 ‘ㅎ’, ‘ㅏ’, ‘ㄴ’을 조합해 표현할 수 있습니다. 이런 조합으로 11,172자의 한글은 물론이고, 옛한글 글자도 하나의 글자로 표현할 수 있습니다. (물론 옛한글을 모두 구현하려면 이런 것을 완벽하게 지원하는 폰트를 찾아야 합니다.)
아래 그림은 조합형 텍스트의 예외 사례를 보여줍니다. ‘t’라는 변수에 ‘한재는 발표중!’이라는 값을 저장했습니다. 이 변수에 substring을 활용해 2개의 글자만 가져올 때, 우리는 ‘한재’라는 글자가 나오기를 기대할 것입니다. 그러나 실제론 ‘하’라는 글자가 표현됐습니다.

자바스크립트에서는 유니코드의 조합형 텍스트 구성 방법을 통해 초성, 중성, 종성 순으로 나란히 나열해서 표현합니다. 이 나열에서 2번째 글자까지 가져왔기 때문에 ‘하’라는 글자가 표현된 것입니다.
우리 주변의 첫가끝 유니코드
이런 조합형 텍스트인 첫가끝 유니코드는 우리 주변에서 종종 볼 수 있는데, 맥에서 생성한 파일을 예로 들 수 있습니다.

맥에서 작성한 한글 파일을 윈도우에서 열면 이렇게 보입니다. 그림의 파일명을 모아서 읽어보면 ‘개발자_매뉴얼’입니다. 맥 OS에서는 첫가끝 유니코드를 지원하고 저장할 때도 이 텍스트 방식으로 저장하지만, 윈도우 OS에서는 이를 지원하지 않습니다. 그래서 하나의 글자로 인식하지 못하고 하나하나 쪼개져 표현되는 것입니다.
그러면 브라우저에서는 첫가끝 유니코드를 잘 지원할까요? 다행히도 대부분의 현대 브라우저에서는 이를 잘 지원하고 있습니다. 하지만 이로 인해 생기는 문제가 있습니다. 브라우저에서는 잘 표현되던 한글이 윈도우 OS에 다운로드하여 확인하면 깨져있는 경우입니다.
첫가끝 유니코드 입력하기: 세벌식 키보드
첫가끝 유니코드의 특성을 보면, 그림과 같이 초성에 오는 글자와 종성에 오는 글자가 같더라도 실제로는 다른 글자로 취급합니다.

물론 우리가 흔히 사용하는 키보드에서는 초성과 종성을 분리해서 입력할 수는 없습니다. 대신 초성과 종성을 분리해서 입력할 수 있는 키보드가 있습니다. 바로 세벌식 키보드입니다. 아래 그림처럼 세벌식 키보드에서는 초성, 중성, 종성을 나눠서 입력할 수가 있습니다.

간혹 주변에서 세벌식 키보드가 좋다고 말하는 분을 볼 수 있습니다. 이런 분들은 빠른 타이핑을 위해 세벌식 키보드를 사용한다고 합니다.
우리가 흔히 쓰는 두벌식 키보드를 사용하다 보면 종종 아래와 같은 오타가 발생하곤 합니다. ‘옷이 없어요’라는 글자를 적으려 했는데, 급하게 입력하다 보니 ‘옷이 ㅇ벗어요’로 입력되는 것입니다. 두벌식 키보드는 초성과 종성에 대한 구분이 없기 때문에, 사용자가 글자를 입력하는 시간에 따라 초성인지 종성인지를 구분합니다. 그래서 이런 오타가 발생하는 것입니다.

우리 눈에는 같은 글자로 보이더라도, 세벌식 키보드에서는 초성과 종성을 다른 글자로 취급합니다. 그렇기 때문에 생기는 독특한 장점이자 기법도 있습니다. 바로 세벌식 모아치기입니다. ‘않’이라는 글자를 입력할 때, 세벌식 키보드는 초성과 종성의 차이를 알고 있기 때문에 ‘ㅏ’ → ‘ㄴ’ → ‘ㅎ’ → ‘ㅇ’ 순서로 입력해도 글자를 완성할 수 있습니다. 즉, 종성을 먼저 입력하든, 끝소리를 먼저 입력하든 문제없이 글자를 표현할 수 있습니다.
결합 방법: NFC / NFD
물론 지금까지 설명한 첫가끝은 이해하기도 쉽지 않으며, 어떻게 결합하고 사용해야 할지는 더 어려운 게 사실입니다. 이런 어려움을 해소하기 위해 유니코드에서 소개하는 결합 방법이 있습니다. 바로 NFC(Normalization Form Canonical Composition, 정준 결합)와 NFD(Normalization Form Canonical Decomposition, 정준 분해)입니다.
정준의 사전적 뜻은 ‘바른 틀’입니다. 유니코드에서는 정준 결합과 정준 분해를 소개하며, “이 규칙을 통해 올바르게 결합하고 분해하여 사용하라”고 말합니다.

위 예제의 ‘(ㄱ)’은 정준 분해를 통해 ‘(’, ‘ㄱ’, ‘)’ 세 개의 문자로 분해할 수 있습니다. 이는 다시 반대로 정준 결합을 통해 ‘(ㄱ)’ 한 글자로 결합할 수 있습니다. 마찬가지로 ‘한’이라는 글자도 정준 결합과 정준 분해를 통해 분해와 결합이 가능합니다.
위 규칙은 자바스크립트의 String, normalize 프로토타입에 정의되어 있으니 링크를 확인하여 보시면 좋을 것 같습니다.
한글 호환 자모 Hangul Compatibility Jamo
마지막으로 알아볼 것은 한글 호환 자모입니다. 이번 글에서 등장하는 세 번째 한글입니다.
한글 호환 자모는 우리가 흔히 쓰는 두벌식 키보드에서 사용하는 한글입니다. 첫가끝 표기와의 차이점은 초성과 종성의 차이가 없다는 점입니다. 여러분들이 두벌식 키보드를 사용하여 ‘ㄱ’을 입력하면 ‘한글 호환 자모의 313번에 있다’라고 할 수 있습니다.

지금까지 유니코드, 또 유니코드에서의 한글에 대해 알아봤습니다.
여태 알아본 내용을 바탕으로 다음 글에서는 서두에서 말한 물음표가 왜 등장하는지, 이 문제를 해결하기 위한 방법을 알아보겠습니다.
지난 글에서 유니코드에 대한 설명과 유니코드에서의 한글에 대해 알아봤습니다. 이번 글에서는 이상한 물음표가 왜 등장하는지 알아보고, 그 문제를 해결하기 위한 방법을 소개하겠습니다.
문제 분석 - �가 들어가는 이유*
그럼 본격적으로 알 수 없는 물음표가 들어가는 이유에 대해 알아보겠습니다.
윈도우가 문제인가?: ‘euc-kr’

먼저 ‘윈도우의 인코딩 방식 때문에 발생하는 문제인가’라는 가정을 두고 시작해 보겠습니다. 윈도우는 UTF-8이 아니라 euc-kr이라는 인코딩을 주로 사용해서 문자를 저장합니다. 표준명으로는 KS X 1001이고, 옛날 이름으로는 KSC 5601-1987입니다. 이 표준안에 한글과 한자를 배치하고 있습니다. 87년 스펙에 배치된 한글은 2,350자만 포함합니다. 가장 많이 쓰는 한글을 추려서 배치한 것인데, 전체가 11,172자인 것을 생각하면 20% 수준에 불과한 것입니다. 이렇게 빠진 글자가 많기 때문에 한글을 표기할 때 다양한 문제가 발생합니다.
인코딩 vs 문자표
여러 문제점에 대해 알아보기 전에, 인코딩과 문자표의 차이에 대해 간단하게 살펴보겠습니다. 문자표란 글자 하나가 어떤 숫자에 대응하는지 표기하는 것입니다. 즉, 유니코드는 어떤 문자가 어떤 숫자에 대응한다는 것을 설명하기 위한 문서입니다. 한편 인코딩이란 앞서 말한 것들을 메모리에 저장하거나 네트워크 트래픽에 전송할 때 표현하는 방법입니다.
간혹 인코딩과 문자표를 구분하지 않고 설명하기도 하는데, 많은 문자표는 그 자체가 인코딩으로 동작하는 경우도 많습니다. euc-kr 또한 한글을 숫자로 대체하는 표현이 있으며 그 숫자 그대로 저장하기 때문에, 그 자체로 문자표이면서 또 한편으론 인코딩이라고도 합니다.
인코딩의 대표적인 예는 UTF-8, 문자표의 대표적인 예는 유니코드라고 할 수 있습니다.

한글 코드 표준화로 인한 문제 발생: KSC-5601 / euc-kr
앞서 설명한 2,350자의 한글 코드가 먼저 표준화로 쓰이며 당시 전산망에서 대부분의 한글이 이 표준 기반으로 표현되기 시작했습니다. 곧 이로 인한 다양한 문제가 발생했습니다.

‘서설믜’라는 이름을 가진 분에 대한 이야기가 대표적입니다. ‘믜’라는 글자가 euc-kr 범위에 포함되지 않았기 때문에 이분은 인터넷 환경은 물론 은행에서 계좌를 개설할 때나, 수능 원서에 이름을 작성할 때도 문제가 생겼다고 합니다.
또 다른 재밌는 사례는 ‘한글 815’라는 에디터 툴입니다. euc-kr에는 ‘쓩’이라는 글자는 있지만, ‘쓔’라는 글자가 없습니다. 그 때문에 당시 다른 에디터에는 ‘ㅆ’과 ‘ㅠ’를 타이핑한 직후 문자를 표현할 방법이 없어 타이핑이 넘어가지 않는 버그가 있었습니다. 그래서 한글 815 에디터 광고에서 ‘쓩’이라는 글자를 표현할 수 있다고 자랑하기도 했습니다.
cp949
이러한 euc-kr의 문제를 해결하기 위해 나온 cp949라는 문자 셋이 있습니다. euc-kr의 2,350자를 제외한 나머지 글자까지 포함시킨 문자 셋입니다. 다만 이 cp949는 공식 이름은 아닙니다. HTML에서는 euc-kr로 표기하도록 되어 있죠. 다만 대부분의 머신에서 cp949로 처리를 합니다.

cp949는 2,350자의 위치를 유지하며 나머지 글자를 끼워 넣은 방식입니다. 따라서 가나다 정렬 순서가 맞지 않습니다. 아래 그림을 보면 노란색으로 표시되어 있는 앞부분은 기호나 숫자, 혹은 알파벳입니다. 그 뒤에 있는 KS X1001이 최초의 2,350자입니다. 나머지 글자들을 저 보라색 영역에 끼워 넣은 형태입니다. 그러다 보니cp949에 있는 문자열들을 가나다 순서대로 정렬하려면 별도로 순서를 맞추는 과정이 필요합니다.

그래서 euc-kr 문서의 인코딩이 깨지거나 잘못 처리한 경우에는 물음표, 또는 읽기 어려운 이상한 한글로 깨져서 나오게 됩니다.
HTML에서의 euc-kr
euc-kr로 이루어진 페이지는 보통 cp949로 표현합니다. 다만 실제 메타태그에는 아래와 같이 euc-kr로 저장을 하게 됩니다. 또 다른 의문점이 하나 생길 수 있습니다. euc-kr 페이지 DOM의 textContent를 읽으면 실제로 euc-kr로 처리가 될까요?

위 질문에 답하기 전, 그에 앞서 UTF-8을 알아보겠습니다.
UTF-8
UTF-8은 1992년에 고안되었습니다. 유니코드는 91년, 한글 표준이 87년도였으니, 비교적 최신 기술이라고 할 수 있습니다. UTF-8은 가변 길이로 텍스트를 표현하고, ASCII 코드 영역인 1바이트 영역부터 보통 3바이트, BMP 영역까지 표현합니다. 그래서 한글을 UTF-8로 표현하면 보통 3바이트 이하로 표현됩니다. 이모지는 4바이트로 표현됩니다.
Note
참고로 utf-8은 물리적으로 5바이트, 6바이트까지 표현할 수 있지만, 2003년 개정에서 5바이트 이상은 사용하지 않겠다고 선언했습니다.
::
자바스크립트에서의 문자열
그렇다면 자바스크립트에서 문자열은 어떻게 표현할까요? 자바스크립트는 UCS-2와 UTF-16을 주로 사용하여 문자열을 처리합니다. UCS-2란, 2바이트로 하나의 유니코드를 표현한다는 의미입니다. 다만 UCS-2는 BMP 영역까지만 표현할 수 있습니다. 그래서 좀 더 긴 영역을 표현하기 위한 방법으로 UTF-16을 사용합니다. UTF-16은 UCS-2표기를 그대로 준용하되, 이모지와 같은 더 긴 영역을 4바이트로 확장해서 표현하는 방법입니다.

UTF-8과 자바스크립트에서의 문자열 처리 방법에 대한 이해를 바탕으로 앞서 궁금증을 해결하겠습니다.
euc-kr 문서를 읽어 자바스크립트 변수에 저장하면 어떻게 될까요? 자바스크립트에서는 UCS-2로 변환되어 저장됩니다. 즉, 모든 브라우저는 여러 방법으로 인코딩된 문서를 읽지만, 이를 자바스크립트로 처리할 때는 UTF-16으로 바꿔서 저장하고 처리합니다.

그렇다면 많은 웹 문서가 UTF-8로 인코딩하는데, 왜 자바스크립트는 UTF-16으로 구현되었을까요? UTF-8은 알파벳이나 기호, ASCII 코드들은 1바이트에 표기를 하기에 압축해서 표현할 수 있으며, 한글이나 다른 기호들은 2바이트, 3바이트로 이어서 표기할 수 있습니다. 따라서 UTF-8은 전송에 유리한 표기법입니다.
다만, 그런 장점 대비 가변 길이 문자열의 길이를 구하는데 어려움이 있습니다. 자바스크립트는 어떤 길이의 문자열도 빠르게 처리하기 위해 모든 길이를 UTF-16 혹은 UCS-2로 표기합니다.
그렇다면 실제 자바스크립트에서는 어떻게 길이를 표현할까요? 아래 그림의 첫 번째 예시를 보면, 가로 막대 3개로 이루어진 특수문자는 UTF-16 문자 2개로 표현됩니다. 오리 이모지 같은 경우는 1F986 영역 즉, BMP 영역을 벗어났기 때문에 길이가 2입니다. 마지막으로 가족 이모지는 4명의 사람 이모지와 연결 글자를 포함해서 길이가 11이 됩니다.

앞선 UCS-2를 설명할 때, 자바스크립트는 모든 문자열을 2바이트로 처리한다고 했습니다. 그렇기 때문에 문자열에서 특정 문자를 확인하는 charAt은 무조건 2바이트 기반으로 값을 가져옵니다.

오리 이모지에 charAt(0)을 적용하면, ‘D83E’라고 하는 문자를 반환합니다. 반면 유니코드를 구하기 위해 codePointAt을 사용하면 ‘1F986’라는 값을 반환합니다.
그렇다면 �는 뭘까?
지금까지 euc-kr과 UTF, 자바스크립트에서의 문자열 표현까지 알아봤습니다. 사실 이 문제는 윈도우에서 euc-kr를 사용해 생긴 문제만은 아닙니다. 맥에서도 동일한 문제가 발생하기 때문입니다.
U+FFFD
다시 물음표 특수문자 그 자체에 대해 알아볼 필요가 있습니다. 유니코드 스펙에서 이 물음표 문자를 찾아보니 ‘U+FFFD’라는 BMP 영역의 끝자락에 기록된 문자란 것을 알게 되었습니다. 유니코드 스펙에서 이 문자는 유니코드로 표현이 불가능한 문자라고 설명하고 있습니다. 더 자세히, 유니코드 스펙 3.0을 살펴보니 아래와 같은 설명이 있습니다. ‘U+FFFD’라는 문자, UTF로 해석할 수 없는 값에 대해 추천하는 3가지 처리 방법입니다.

에러를 반환하거나, 해당 코드를 지우거나, U+FFFD 마커를 넣어 UTF 글자가 깨졌다는 것을 표기하라는 것입니다.
드디어 물음표 글자가 생기는 원인을 알았습니다. “UTF 상에서 글자가 깨졌기 때문”입니다. 이 힌트를 기반으로 제가 작성한 코드를 확인해 봤습니다.
문제 해결하기
에디터에서 발생하는 문제인지 살펴보니 http 응답상에는 문제가 없었습니다. DB에도 U+FFFD라는 표현이 저장되어 있으니 문제가 없다는 것도 확인했습니다. 마지막으로 백엔드 코드에서 문제가 생겼는지 살펴보았습니다. 곧 UTF-8 문자를 1000 바이트 단위 chunk로 나눠서 처리하는 코드를 발견했습니다.
3바이트인 UTF-8 한글 문자를 이 코드로 처리하다 보니 중간에 깨지는 문자가 생겼습니다. 다시 chunk에 toString을 실행하는 과정에서 U+FFFD가 생긴 것입니다.
문제를 알려준 매니저님의 말을 들어보니 항상 한글에서만 글자가 깨진다고 했습니다. 그 결과, 이 문제가 맞다는 확신을 가지게 되었습니다.
위와 같은 문제점을 확인하고 chunk를 쪼개는 과정에서 문자가 깨지지 않도록 백엔드 로직을 수정했고, 문제를 해결했습니다.
마치며
지금까지 물음표 문자가 생기는 이유를 알기 위해 유니코드에 대해 알아보고, 한글 문자 구성도 확인했고, euc-kr과 함께 UTF까지 알아봤습니다. 이 과정을 거쳐 유니코드의 U+FFFD라는 스펙까지 찾아냈고, 문제를 해결할 수 있었습니다.
이번 글로 한글이 어떤 식으로 저장이 되고, 브라우저에서는 어떤 식으로 표현되는지 기억해 주면 좋겠습니다. 또 제가 문제를 해결한 과정을 힌트 삼아 백엔드 인코딩 과정에서 문제가 생기지는 않았는지 살펴보면, 비슷한 문제를 해결하는 데 도움이 될 것이라 생각합니다.
‘아�니 이 글자 왜 들어간 거예요? (부제: 알아두면 가끔 쓸모 있는 한글 유니코드 완전 정복)’ (제한재 데니어 CTO) ↩︎