지난 글에서는 대부분의 인코딩 문제를 해결할 수 있는 간단한 과정을 소개했습니다. 하지만 그중 하나, 훨씬 더 다루기 까다로운 인코딩 문제가 있습니다.
여러분도 분명 본 적이 있을 겁니다. 컬리 쿼트(구부러진 따옴표)가 ’로 바뀌거나, 엠대시(—)가 —로 변하는 현상이죠. 이걸 처음 보면 "내가 미쳤나?" 싶을 정도입니다. 분명 잘 동작해야 하는데 말이죠!
물론 깨진 문자와 올바른 문자를 일일이 짝지어 거대한 치환 테이블을 만드는 방법도 있습니다:
[{broken: '–', fixed: "—"}
{broken: "—", fixed: "–"}
{broken: "‘", fixed: "‘"}
{broken: "’", fixed: "’"}
{broken: "“", fixed: "“"}
{broken: "â€", fixed: "”"}, ...]
하지만 이보다 더 쉽고 확실한 방법이 있습니다.
왜 좋은 타이포그래피는 항상 깨질까?
지난 글에서 배웠듯이, 인코딩이란 의미 없는 바이트 묶음을 화면에 표시할 수 있는 문자로 바꿔주는 규칙입니다. 가능한 문자의 수가 256개를 넘기 때문에 모든 문자를 한 바이트로 표현할 수는 없습니다. 그래서 컬리 쿼트 ’ 같은 일부 문자는 두 개 이상의 바이트로 표현됩니다:
irb(main):001:0> "they’re".bytes
=> [116, 104, 101, 121, 226, 128, 153, 114, 101]
문자열은 겨우 7글자인데, 무려 9바이트로 표현되고 있죠!
컬리 쿼트만 따로 살펴보면:
irb(main):002:0> "’".bytes
=> [226, 128, 153]
3바이트를 사용한다는 걸 알 수 있습니다. 그리고 깨진 문자열인 they’re는 원래 한 글자여야 할 자리에 세 글자가 들어가 있습니다. 우연이라고 하기엔 너무 일치하죠?
사실 이 세 바이트는 UTF-8로 읽으면 컬리 쿼트가 됩니다. 그런데 실제로는 각 바이트가 서로 다른 문자로 표시되고 있는 것이죠. 그렇다면 어떤 인코딩이 [226, 128, 153]을 ’로 표현할까요? 주요 인코딩들의 표를 몇 개 살펴보면 답은 Windows-1252라는 걸 알 수 있습니다.
irb에서 직접 확인해볼 수도 있습니다:
irb(main):003:0> "they’re".force_encoding("Windows-1252").encode("UTF-8")
=> "they’re"
(마지막의 .encode("UTF-8")은 콘솔에 문자열을 제대로 출력하기 위해 필요합니다.)
네, 바로 이게 문제입니다. 그런데 상황은 더 나빠집니다.
데이터는 원래 UTF-8인데 Windows-1252로 잘못 읽힌 것뿐입니다. 하지만 여러분은 아마 그 데이터를 데이터베이스나 파일에 UTF-8로 저장하겠죠. Ruby는 친절하게(?) 그 데이터를 UTF-8로 변환해주기 때문에 결과적으로 이런 꼴이 됩니다:
irb(main):004:0> "they’re".force_encoding("Windows-1252").encode("UTF-8")
=> "they’re"
irb(main):005:0> "they’re".force_encoding("Windows-1252").encode("UTF-8").bytes
=> [116, 104, 101, 121, 195, 162, 226, 130, 172, 226, 132, 162, 114, 101]
문자열이 두 번 잘못 인코딩된 겁니다. 이제 깨진 문자들이 마치 원래부터 있었던 것처럼 보이죠. 이 과정을 모르는 상태에서는 거의 풀 수 없는 실타래가 됩니다.
어떻게 고치면 될까?
원래 상태로 되돌리려면 어떻게 해야 할까요? 문제를 거꾸로 거슬러 올라가 봅시다:
-
UTF-8 문자열이 있다 (they’re)
-
Windows-1252 문자열에서 변환된 것이다 (they’re)
-
그 바이트들은 원래 UTF-8로 읽혔어야 했다 (they’re)
고치는 방법은 이 단계를 역순으로 따라가는 것뿐입니다. 먼저 encode로 UTF-8 문자열을 Windows-1252 문자열로 되돌립니다. 그다음 force_encoding으로 잘못 지정된 Windows-1252 문자열을 UTF-8로 강제로 읽으면 됩니다:
irb(main):006:0> "they’re".encode("Windows-1252").force_encoding("UTF-8")
=> "they’re"
복구 완료!
그런데 사소한 문제가 하나…
아쉽게도 여러분이 이 문제를 발견하게 된 계기는 아마 파일이나 데이터베이스 레코드 여러 곳에 깨진 데이터가 들어있었기 때문일 겁니다. 그런데 모든 파일과 레코드가 반드시 깨져 있는 건 아니죠. 정상 데이터와 깨진 데이터가 섞여 있을 수 있습니다. 특히 사이트 방문자들이 입력한 데이터라면 더더욱 그렇습니다.
이런 경우에는 모든 문자열에 무작정 위 코드를 실행하면 안 됩니다:
irb(main):007:0> "they’re".encode("Windows-1252").force_encoding("UTF-8")
=> "they’re"
irb(main):008:0> "they’re".encode("Windows-1252").force_encoding("UTF-8")
=> "they\x92re"
정상 데이터에 실행하면 오히려 정상 데이터를 망가뜨리게 됩니다. 그럼 어떻게 해야 할까요?
휴리스틱(경험적 판단 기준)을 활용하면 됩니다. â 같은 깨진 문자가 포함된 문자열만 골라서 변경하는 거죠. 정상적인 문자열에는 절대 â 같은 문자가 나타나지 않는다면 이 방법이 잘 통합니다.
하지만 제가 비슷한 버그를 마지막으로 고쳤을 때는 좀 더 안전하게 가고 싶었습니다. 그래서 또 하나의 유용한 도구를 동원했는데요, 바로 제 눈이었습니다.
깨진 문자열을 발견할 때마다 원본과 교체 결과를 함께 출력한 겁니다:
Changing title with ID 6 from "They’re over there!" to "They’re over there!"
이렇게 하면 실제로 변경된 소수의 문자열만 골라 검토할 수 있고, 추가로 뭔가 깨지지 않았는지 확인할 수 있습니다.
머리가 지끈지끈하다면
지난 글에서 말씀드렸듯이, 같은 데이터에 대한 서로 다른 해석을 머릿속으로 정리하는 건 정말 어렵습니다! 헷갈린다면 irb 콘솔에서 직접 실험해보세요. 콘솔을 열고 —와 —, 또는 “와 “ 사이를 오가며 변환해보는 겁니다.
이렇게 복잡한 개념을 직접 연습해보는 것이, 실전에서 필요할 때 자신감 있게 대응하는 가장 빠른 방법입니다. Practicing Rails의 무료 샘플 챕터에서는 바로 그런 연습 기법과 프로세스를 배울 수 있습니다.