CSV를 JSON으로: 따옴표, 타입, 인코딩의 함정
CSV는 직접 파싱하기 전까지는 사소해 보입니다. 따옴표 규칙, 모든 값이 문자열이라는 문제, 필드 안의 줄바꿈, 그리고 깔끔해 보이던 스프레드시트를 깨진 JSON으로 바꿔 버리는 인코딩 문제를 다룹니다.
CSV는 누구나 간단하다고 여기는 형식입니다. 값을 쉼표로 구분한 행들을 쉼표 기준으로 나누면 끝이라고 생각합니다. 그러다 어떤 필드에 쉼표가 들어가거나, 따옴표가 들어가거나, 줄바꿈이 들어가거나, 의미 있는 앞자리 0이 들어가면 단순한 분할은 무너집니다. CSV를 JSON으로 변환하면 이런 예외 사례가 모두 드러납니다. JSON에는 타입과 구조가 있지만 CSV에는 텍스트만 있기 때문입니다. 실제로 문제가 되는 부분은 다음과 같습니다.
CSV는 표준이라고 하기 어렵습니다
CSV를 설명하는 RFC(4180)가 있지만, 실제 파일은 그보다 먼저
등장했고 이를 마음대로 무시합니다. 구분자는 쉼표일 수도, 세미콜론일
수도(쉼표가 소수점 구분자인 로케일에서 흔합니다), 탭일 수도 있습니다.
줄바꿈은 \n일 수도 \r\n일 수도 있습니다. 헤더 행이 있을 수도
없을 수도 있습니다. CSV 파싱의 첫 번째 규칙은 아무것도 가정할 수
없다는 것입니다. 한 내보내기 파일에서 작동하던 파서가 다음 파일에서
깨집니다. 그래서 "그냥 쉼표로 나누면 된다"는 방식은 어려운 부분에
도달하기도 전에 이미 틀린 방식입니다.
따옴표가 핵심 규칙입니다
CSV에서 명확하게 정의된 유일한 부분은 따옴표가 특수 문자를 어떻게 살려 내는가입니다. 큰따옴표로 감싼 필드는 쉼표, 줄바꿈, 따옴표를 담을 수 있습니다.
name,note
"Smith, John","He said ""hi"" twice"
이것은 두 개의 필드입니다. Smith, John과
He said "hi" twice입니다. 따옴표 안의 쉼표는 구분자가 아니라
데이터이며, 두 번 겹친 따옴표 ""는 이스케이프된 한 개의
따옴표입니다. 단순한 split(",")는 이것을 깨진 네 조각으로
만듭니다. 올바른 CSV-JSON 변환이라면 따옴표 규칙을 지켜야 하며,
이것이 바로 한 줄짜리 코드 대신 제대로 된 파서를 찾게 되는
이유입니다.
모든 값이 문자열입니다
이것이 두 형식 사이의 개념적 간극입니다. CSV에는 타입이 없습니다. 모든 셀이 텍스트입니다. JSON에는 숫자, 불리언, null이 있습니다. 그래서 변환은 결정을 강요합니다. 모든 값을 문자열로 유지할 것인가, 아니면 타입을 추론할 것인가입니다.
타입 추론은 편리하면서도 위험합니다.
007이7이 됩니다. 그러면 우편번호, 제품 SKU, 또는 전화번호의 앞자리 0을 망가뜨린 것입니다.1.0이1이 되거나, 더 나쁘게는 긴 소수가 정확히 표현하지 못하는 부동소수점이 됩니다.TRUE/FALSE가 도구에 따라 불리언이 되거나 문자열로 남습니다.- 빈 셀이
""가 되거나,null이 되거나, 아예 생략될 수 있습니다.
보편적으로 옳은 답은 없습니다. 데이터 교환 용도라면 모든 값을 문자열로 유지하고 소비하는 쪽이 의도적으로 형변환하도록 두는 것이 안전한 기본값입니다. 직접 통제하는 분석 용도라면 타입 추론도 괜찮습니다. 다만 변환기가 둘 중 무엇을 하는지는 알아야 합니다. 조용한 타입 추론은 "데이터는 멀쩡해 보였는데 가져오기가 잘못됐다"는 문제의 전형적인 원인이기 때문입니다.
필드 안의 줄바꿈은 줄 기반 도구를 깨뜨립니다
따옴표로 감싼 필드는 실제 줄바꿈을 담을 수 있기 때문에, CSV
레코드는 파일의 한 줄과 같지 않습니다. CSV를 한 줄씩 처리하는
도구는, 예를 들어 wc -l, 단순한 readline 반복문, 로그
파이프라인 등은 여러 줄에 걸친 필드를 가진 레코드를 잘못 세고
쪼갭니다. 행 수가 너무 많아 보인다면 따옴표 필드 안의 줄바꿈을
가장 먼저 의심해야 합니다.
인코딩과 BOM
CSV는 인코딩 선언을 담지 않으므로 바이트가 모호합니다. 반복해서 발생하는 두 가지 문제가 있습니다.
- BOM. 스프레드시트는 파일 시작 부분에 바이트 순서 표시를
붙인 UTF-8로 저장하는 경우가 많습니다. 그러면 첫 번째 헤더
이름이 조용히
name이 아니라name이 되어,"name"으로 조회하면 눈에 보이는 이유 없이 실패합니다. 읽을 때 BOM을 제거해야 합니다. - UTF-8이 아닌 내보내기. Latin-1이나 지역별 코드 페이지로 저장된 파일은 UTF-8로 읽으면 악센트가 붙은 문자가 깨진 문자로 바뀝니다. 이를 알려 주는 플래그는 없으므로, 원본 인코딩을 알거나 탐지해야 합니다.
짧은 점검 목록
CSV-JSON 변환이 잘못된 출력을 내놓을 때는 다음을 순서대로 확인하십시오.
- 구분자가 맞습니까? (쉼표 대 세미콜론 대 탭)
- 따옴표 규칙을 지켰습니까? 겹친 따옴표와 필드 안의 쉼표를 포함해서 말입니다.
- 헤더 행이 있고 BOM으로 오염되지 않았습니까?
- 타입은 추론한 것입니까 문자열로 유지한 것입니까? 그리고 그것이 원하던 것입니까?
- 인코딩이 실제로 UTF-8입니까?
따옴표를 올바르게 처리하고 타입에 대해 명확히 선택해서 파일을 변환하려면, 저희 CSV ↔ JSON 변환기가 브라우저 안에서 처리하므로 데이터가 로컬에 머뭅니다. JSON으로 변환한 뒤, 다운스트림으로 넘기기 전에 형태를 강제하고 싶다면 JSON Schema로 검증하기가 다음 단계입니다. 그리고 표 형식 데이터가 아니라 설정용 형식을 고르는 중이라면 JSON 대 YAML이 그 절충점을 다룹니다.