본문으로 건너뛰기
AZ Tools

글자가 깨져서 이상한 문자로 보이는 이유

디스크 위에 순수한 텍스트 같은 것은 없다. 파일에는 바이트가 들어 있고, 인코딩은 어떤 바이트 혹은 바이트 묶음이 어떤 문자를 뜻하는지에 대한 약속이다. 평범한 텍스트 파일은 그 약속을 기록해 두지 않으므로, 그것을 여는 모든 프로그램은 추측을 하고 있는 셈이다. 추측이 틀리면 글자가 깨지고, 깨진 모양은 무슨 일이 있었는지를 꽤 정확히 알려주는 단서가 된다.

깨진 글자에는 알아둘 만한 서명이 있다

가장 익숙한 것은 문자 하나가 둘로 늘어나는 경우다. UTF-8에서 é는 C3과 A9 두 바이트로 저장되는데, 그 파일을 서유럽 코드 페이지로 한 바이트씩 읽는 프로그램은 그것을 Ã와 ©로 찍는다. UTF-8 파일을 옛 단일바이트 인코딩으로 읽으면 영어가 아닌 모든 문자가 이런 식으로 두세 글자가 되고, 그것이 줄줄이 이어진 결과가 한국어나 일본어 텍스트가 라틴 문자 잔해의 벽처럼 보이는 이유다.

다른 세 가지 표시는 뜻이 다르다. 파일이 로 시작한다면 바이트 순서 표시가 붙은 UTF-8을 다른 것으로 읽고 있는 것이다. 물음표가 든 검은 마름모는 대체 문자로, 지정된 인코딩에서 유효할 수 없는 바이트를 만났을 때 디코더가 써 넣는 기호다. 글자가 있어야 할 자리의 맨 물음표는 대개 그 글자를 담을 자리가 없는 인코딩으로 변환됐다는 뜻이다.

그중 둘은 이미 텍스트가 사라졌다는 뜻이다

é는 되살릴 수 있다. 원래 바이트가 파일에 그대로 남아 있고 해석만 틀렸기 때문이다. 같은 바이트를 맞는 인코딩으로 읽으면 텍스트가 정확히 돌아온다.

대체 문자와 맨 물음표는 다르다. 그것이 지금 파일에 실제로 들어 있는 내용이다. 원래 바이트는 변환하는 순간 버려졌고, 그 마름모가 é였는지 ü였는지는 이후의 어떤 단계로도 알아낼 수 없다. 인코딩 문제를 고칠 때 첫 번째 규칙이 파일에 덮어쓰기를 멈추는 것인 이유가 이것이다. 잘못된 인코딩으로 한 번 저장할 때마다 되돌릴 수 있는 문제가 되돌릴 수 없는 문제로 바뀐다.

이중 인코딩, 그리고 그것이 반복되는 이유

어떤 프로그램이 UTF-8을 단일바이트 인코딩으로 읽은 다음 그 결과를 다시 UTF-8로 저장하면, 실수는 영구적인 것처럼 보이게 된다. 이제 파일에는 Ã와 ©라는 문자가 진짜로 들어 있고, 각각이 자기 몫의 두 바이트를 차지한다. 두 번 겪으면 글자 하나가 네 문자가 되고, 그다음엔 여덟이 된다.

그래도 되돌릴 수는 있다. 몇 번을 겪었는지만 알면 과정을 그대로 뒤집을 수 있기 때문이다. 회차마다 단일바이트 코드 페이지로 다시 인코딩한 뒤 UTF-8로 디코딩하면 된다. 이 현상이 유난히 자주 보이는 이유는 대개 파이프라인 안에서 일어나기 때문이다. 폼, 데이터베이스 컬럼, 내보내기 같은 단계에서 이미 한 번 변환을 거친 데이터에 같은 변환이 또 돌아간다.

아직도 마주치는 옛 인코딩들

서구권 텍스트는 대개 Windows-1252로 온다. 사람들이 Latin-1이라고 말할 때 실제로 가리키는 것이 보통 이것이다. 차이는 중요하다. Windows-1252는 Latin-1이 비워둔 구간을 둥근 따옴표와 긴 대시, 말줄임표로 채우는데, 스마트 따옴표가 그렇게 자주 “로 튀어나오는 이유가 여기 있다. 옛 동아시아 시스템에서 온 텍스트는 Shift_JIS, EUC-KR, Big5, GB18030으로 오고, 키릴 문자는 Windows-1251이나 KOI8-R로 온다.

사무실에서 가장 흔한 출처는 스프레드시트다. 윈도우에서 CSV를 내보내면 여전히 UTF-8이 아니라 시스템 코드 페이지로 쓰는 경우가 많아서, 만든 컴퓨터에서는 완벽해 보이는 파일이 다른 모든 곳에서는 깨진다. 게다가 같은 컴퓨터에서 다시 열면 멀쩡하니, 만든 사람 눈에는 문제가 보이지 않는다.

탐지는 추측이고, 짧은 텍스트에서는 더 나쁘다

UTF-8은 스스로를 검증한다. 여러 바이트로 이루어진 문자가 엄격한 규칙을 따르기 때문에, UTF-8로 깔끔하게 디코딩되는 파일은 거의 확실히 UTF-8이다. 탐지기가 자신 있게 내놓을 수 있는 답은 이것 하나다.

반면 단일바이트 인코딩은 어떤 바이트든 유효하기 때문에, 무엇 하나 배제할 수 없다. 탐지기는 실제 언어에서 어떤 글자와 글자 쌍이 흔한지를 보고 고르는데, 이 방법은 문단에서는 통하고 이름 하나나 열 제목에서는 실패한다. 스무 바이트짜리 문자열에 대해 확신 낮음이라며 Windows-1252를 제시하는 도구는 불친절한 게 아니라 정직한 것이다.

파일이 스스로를 밝히도록 되어 있는 자리

선언을 담을 수 있는 형식은 훨씬 다루기 쉽다. HTML에는 meta charset이 있고, XML에는 선언문이 있으며, HTTP로 전달되는 것은 Content-Type 헤더에 charset을 적을 수 있다. 그리고 이 헤더는 문서가 스스로에 대해 말하는 것보다 우선하므로, 문서가 올바르더라도 헤더가 틀리면 여전히 깨진다.

UTF-8 파일 앞머리의 바이트 순서 표시는 또 다른 형태의 선언이고, 양날의 칼이다. 그것을 이해하는 편집기에서는 추측을 없애주지만, 이해하지 못하는 도구는 망가뜨린다. 0번 위치에서 예기치 않은 문자를 만났다는 JSON 파서, 첫 줄이 더 이상 셔뱅이 아니게 된 셸 스크립트, 첫 열 이름에 보이지 않는 문자가 붙어버린 CSV가 그런 예다.

깨진 파일을 실제로 고치는 법

화면이 아니라 바이트에서 출발하라. 파일을 16진수로 열어서 악센트 문자가 한 바이트로 저장돼 있는지 두 바이트인지 보라. 그것만으로도 옛 인코딩 파일인지 UTF-8을 잘못 읽고 있는 것인지가 갈린다. 그다음 후보 인코딩으로 디코딩해 보고 진짜 단어가 돌아오는지 확인하라. 기준은 결과가 바뀌었는지가 아니라 텍스트가 맞는지다.

읽히기 시작하면 사본을 UTF-8로 저장하고, 확신이 설 때까지 원본은 건드리지 마라. 그리고 중간이 아니라 경계에서 고쳐라. 데이터가 들어올 때 디코딩하고, 내부에서는 한 가지 인코딩만 쓰고, 나갈 때 그것을 선언하는 식이다. 같은 문제를 반복해서 겪는 쪽은 대개 중간 어딘가에서 변환하는 파이프라인이고, 하나의 실수가 두 번 적용되는 경로가 바로 그것이다.

관련 도구