🍰 디저트 아틀리에

보이지 않는 글자 세 개를 지웠더니 배포가 죽었다

주석 한 줄을 고쳤을 뿐인데, 손대지도 않은 283번째 줄에서 문법 오류가 났습니다.

개발노트 · 도구 · 윈도우

무엇을 하려던 참이었나

이 게임의 배포 스크립트는 PowerShell로 돼 있습니다. 빌드하고, 버전을 확인하고, 검사를 돌리고, 저장소에 복사해서 올립니다. 여기에 한 줄을 고치고 주석을 몇 줄 넣으려던 참이었습니다.

고치고 문법 검사를 돌렸더니 이렇게 나왔습니다.

오류: The string is missing the terminator: '.
오류: Missing closing ')' in expression.

release.ps1:283  ... (Join-Path $repo ($_ -replace '/', '\')).ToLowerInvariant()

283번째 줄은 제가 건드리지도 않은 줄입니다. 게다가 원래부터 잘 돌아가던 코드입니다. '\'는 PowerShell에서 그냥 역슬래시 한 글자이고, 아무 문제가 없습니다.

범인은 파일 맨 앞에 있었습니다

고친 줄이 아니라 저장하는 방식이 문제였습니다. 파일을 다시 쓸 때 이렇게 저장했습니다.

[IO.File]::WriteAllText($p, $t, (New-Object Text.UTF8Encoding($false)))
                                                              ↑ BOM 없이

BOM(Byte Order Mark)은 파일 맨 앞에 붙는 세 바이트(239 187 191)입니다. 화면에는 아무것도 안 보입니다. 「이 파일은 UTF-8입니다」라고 알려 주는 표식일 뿐입니다.

그런데 Windows PowerShell 5.1은 BOM이 없는 파일을 UTF-8이 아니라 시스템 기본 인코딩(한국어 윈도우에서는 CP949)으로 읽습니다.

그러면 이 스크립트 안의 한글 주석과 한글 문자열이 전부 깨진 바이트로 해석됩니다. 그 과정에서 따옴표로 읽히는 바이트가 하나 생기면, 파서는 그때부터 «문자열 안»이라고 착각한 채 계속 읽어 내려갑니다. 그러다 한참 뒤인 283번째 줄에서 “따옴표가 안 닫혔다”고 터진 것입니다.

오류가 난 줄과 원인이 있는 줄이 다를 수 있습니다. 문자열이나 괄호가 안 닫히면, 파서는 훨씬 뒤에서 넘어집니다.

확인과 복구

파일의 첫 세 바이트를 찍어 보면 바로 나옵니다.

현재 첫 3바이트: 60 35 10        ← "<#" 로 바로 시작. BOM 없음
복원 후 첫 3바이트: 239 187 191   ← BOM
  문법 OK

복구는 같은 내용을 UTF8Encoding($true)로 다시 쓰는 것뿐이었습니다. 내용은 한 글자도 안 바뀌었고, 앞에 세 바이트만 붙었습니다.

파일마다 답이 다릅니다

재미있는 건, 이 프로젝트의 다른 파일들은 BOM이 없어야 한다는 점입니다.

파일BOM이유
release.ps1있어야 함PowerShell 5.1이 없으면 CP949로 읽음
src/*.js없어야 함이어 붙여 한 파일로 만드는데, 중간에 BOM이 끼면 깨짐
*.css, *.html없어야 함<meta charset>로 충분하고, 앞에 붙으면 방해

그러니까 「전부 BOM 없이」도, 「전부 BOM 붙여서」도 답이 아닙니다. 그 파일을 누가 읽느냐에 달렸습니다.

남은 생각

이 사고에서 실제로 위험했던 부분은 따로 있습니다. 이 프로젝트는 git 저장소가 아니라 로컬 폴더입니다. 스크립트를 깨뜨렸을 때 되돌릴 방법이 없었습니다. 다행히 원인이 인코딩이라 내용은 멀쩡했지만, 잘못 지웠으면 그대로 날아갔을 겁니다.

그래서 지금은 파일을 손대기 전에 임시 폴더로 복사본을 하나 떠 둡니다. 한 줄이면 되는 일인데, 그걸 안 해서 아찔했던 적이 벌써 두 번입니다.

그리고 보이지 않는 문자는 언제나 의심 목록 맨 위에 둘 만합니다. BOM, 줄바꿈 방식(CRLF/LF), 눈에 안 보이는 공백. 화면에서 똑같아 보이는 두 파일이 전혀 다르게 동작할 수 있습니다.

← 개발노트 목록