JSON이 안 읽히고 CSV 첫 열이 사라질 때, 파일 맨 앞 보이지 않는 3바이트(BOM) 확인하기
분명 형식이 맞는 JSON인데 파이썬이 첫 글자부터 오류를 내거나, CSV를 읽었는데 첫 번째 열만 이름으로 찾을 수 없는 경우가 있습니다. 메모장으로 열어 보면 아무 이상이 없어 보여서 원인을 찾기가 더 어렵습니다.
범인은 파일 맨 앞에 숨어 있는 3바이트, BOM(Byte Order Mark)인 경우가 많습니다. 화면에는 보이지 않지만 파일을 읽는 프로그램에게는 첫 글자로 보입니다. 직접 BOM을 붙인 파일을 만들어 여러 도구로 읽어 보니, 오류를 내는 도구도 있었지만 더 곤란한 쪽은 오류 없이 조용히 틀린 결과를 내는 도구였습니다.
측정 환경은 Ubuntu 24.04, 파이썬 3.13.16, pandas 3.0.5, Node.js 22.22입니다.
BOM은 'UTF-8로 저장했다'는 표시입니다
같은 내용을 BOM 없이, 그리고 BOM을 붙여 UTF-8로 저장했습니다. 두 파일의 차이는 맨 앞의 EF BB BF 세 바이트뿐이었고, 그만큼 파일 크기도 30바이트에서 33바이트로 늘었습니다.
BOM은 원래 이 파일이 어떤 인코딩인지 알려 주려는 표시입니다. 엑셀에서 'CSV UTF-8'로 저장하거나 메모장에서 'BOM 포함 UTF-8'을 고르면 붙습니다. 엑셀이 한글 CSV를 깨지지 않게 여는 데에는 오히려 도움이 되지만, 프로그래밍 도구에는 예상하지 못한 첫 글자가 됩니다.

오류를 내는 경우는 차라리 낫습니다
파이썬에서 utf-8로 연 파일을 json.load에 넘기자 'Unexpected UTF-8 BOM'이라는 오류가 났습니다. Node.js의 JSON.parse도 첫 글자에서 'Unexpected token' 오류를 냈습니다. 오류 메시지에 BOM이 직접 나오거나 위치가 첫 글자로 찍히기 때문에 원인을 짐작할 수 있습니다.
흥미로운 점은 같은 파일을 파이썬에서 바이트 그대로 읽어 json.loads에 넘기면 정상으로 읽혔다는 것입니다. 바이트로 받으면 json 모듈이 스스로 BOM을 알아보고 처리합니다. 같은 파일이 읽는 방법에 따라 되기도 하고 안 되기도 하는 셈입니다.
위험한 것은 조용히 틀리는 경우입니다
파이썬 csv 모듈로 BOM이 붙은 CSV를 읽으니 오류는 없었지만, 첫 열 이름이 'id'가 아니라 '\ufeffid'로 읽혔습니다. 그래서 DictReader로 row['id']를 꺼내면 오류 대신 None이 나왔습니다. 프로그램은 끝까지 돌아가는데 첫 열 값만 전부 비어 있는 결과가 만들어집니다.
명령 창에서 grep으로 'id'로 시작하는 줄을 찾았을 때도 0건이 나왔습니다. 첫 줄이 실제로는 BOM으로 시작하기 때문입니다. 반면 pandas의 read_csv는 BOM을 알아서 떼고 읽어서 열 이름이 정상이었습니다. 같은 파일인데 pandas에서는 되고 csv 모듈에서는 안 되니, 도구를 바꾸는 순간 문제가 드러나는 일이 생깁니다.
BOM이 있는지 확인하고 떼는 법
첫 세 바이트만 보면 바로 알 수 있습니다. 읽을 때는 utf-8-sig로 열면 BOM이 있든 없든 똑같이 처리되고, 다시 저장할 때 utf-8로 쓰면 BOM이 빠집니다.
path = "data.csv"
with open(path, "rb") as f:
print("BOM 있음" if f.read(3) == b"\xef\xbb\xbf" else "BOM 없음")
# BOM이 있든 없든 똑같이 읽고, BOM 없이 다시 저장
with open(path, encoding="utf-8-sig") as f:
text = f.read()
with open("data_nobom.csv", "w", encoding="utf-8") as f:
f.write(text)
BOM을 일부러 남겨야 하는 경우
한글이 든 CSV를 엑셀에서 더블클릭으로 열 사람에게 보낼 때는 BOM이 있는 편이 낫습니다. BOM이 없으면 엑셀이 파일을 한국어 윈도우 기본 인코딩으로 읽어 한글이 깨지기 때문입니다. 정리하면, 사람이 엑셀로 열 파일에는 BOM을 붙이고, 프로그램이 읽을 파일에서는 빼는 것이 안전합니다. 엑셀 쪽 사정은 아래 글에 실측과 함께 정리해 두었습니다.