본문 바로가기
Converter

CSV JSON 변환기

CSV를 JSON으로, JSON을 CSV로 양방향 변환

탭은 엑셀에서 그대로 붙여넣을 때 씁니다

split(“,”)은 지름길이 아니라 버그다

CSV 파싱을 한 줄로 끝내는 코드는 어디에나 있습니다.

const rows = text.split("\n").map(line => line.split(","));

이 코드는 테스트 데이터에서 완벽하게 동작합니다. 그게 문제입니다. 실제 데이터가 들어오는 순간 조용히 무너지는데, 예외를 던지지 않기 때문에 아무도 모릅니다.

고객 명단을 하나 봅시다.

이름,주소,전화번호
김철수,"서울시 강남구 테헤란로 152, 강남파이낸스센터",010-1234-5678

주소에 쉼표가 있습니다. 도로명 주소에 상세주소를 붙이면 거의 항상 쉼표가 들어갑니다. 위 코드로 자르면 필드가 3개가 아니라 4개가 되고, 전화번호는 강남파이낸스센터” 자리로 밀립니다. 결과는 오류가 아니라 전화번호 칸에 주소 조각이 들어 있는 그럴듯한 JSON입니다.

메모나 비고 열이 있으면 더 심합니다. 사용자가 “김,철수 님 요청”이라고 쓰는 순간 그 행 전체가 어긋납니다. 그리고 이런 데이터는 보통 수천 행 중 몇 십 행이라, 눈으로 스크롤해서는 절대 발견되지 않습니다. 몇 달 뒤 “왜 이 고객 전화번호가 주소죠?” 라는 질문으로 돌아옵니다.

올바른 파서는 문자를 하나씩 읽으면서 지금 따옴표 안인지 밖인지를 추적합니다. 상태는 그거 하나뿐인데, 이 상태가 쉼표·줄바꿈·따옴표 세 글자의 의미를 전부 뒤집습니다. 따옴표 안의 쉼표는 구분자가 아니라 글자이고, 따옴표 밖의 쉼표는 구분자입니다. 이 구분이 없으면 CSV를 읽는 게 아니라 그냥 문자열을 자르고 있는 것입니다.

줄 단위로 읽는 순간 이미 늦었다

따옴표 처리를 추가하더라도, split("\n")을 먼저 했다면 고칠 수 없는 버그가 하나 남습니다. 한 레코드가 여러 줄에 걸칠 수 있기 때문입니다.

이름,메모
김철수,"1차 확인 완료
2차는 다음 주"
이영희,확인 전

이건 4줄이지만 3행입니다. 따옴표 안의 줄바꿈은 레코드 구분자가 아니라 메모 값의 일부입니다. 줄 단위로 읽는 코드는 이걸 4행으로 세고, 두 번째 행은 따옴표가 열린 채로 끝나고, 세 번째 행은 2차는 다음 주"라는 정체불명의 한 칸짜리 행이 됩니다.

고칠 방법이 없습니다. 줄로 자른 뒤에는 그 줄바꿈이 따옴표 안이었는지 밖이었는지 알 수 없고, 그걸 알려면 결국 처음부터 문자를 스캔해야 합니다. 그래서 이 도구는 readLines 없이 입력 전체를 한 번에 스캔합니다. 그리고 이 케이스는 엑셀에서 셀 안에 Alt+Enter로 줄바꿈을 넣은 순간 반드시 생깁니다. 드문 예외가 아닙니다.

엑셀 한글 CSV 문제 — BOM 한 글자의 무게

한국에서 CSV를 다루면 반드시 만나는 문제입니다. 그리고 원인을 알면 5초에 끝납니다.

엑셀(윈도우판)은 “CSV(쉼표로 분리)”로 저장할 때 시스템 기본 인코딩인 CP949로 씁니다. 한편 웹 서버, 파이썬, 요즘 개발 도구는 전부 UTF-8을 가정합니다. 그래서 두 방향 모두 깨집니다.

  • 엑셀에서 저장 → 서버에 업로드: 서버가 UTF-8로 읽으니 한글이 �� 같은 것이 됩니다.
  • 서버에서 UTF-8로 내려받음 → 엑셀에서 더블클릭: 엑셀이 CP949로 읽으니 ê¹€ì² ìˆ˜가 됩니다. 파일은 멀쩡한데 화면만 깨진 상태입니다.

두 번째가 특히 억울한 경우입니다. 데이터는 정상인데 엑셀이 잘못 읽고 있을 뿐입니다. 그리고 여기서 사용자가 “깨졌네” 하고 그 상태로 저장하면 진짜로 깨집니다. 잘못 해석한 글자를 그대로 다시 쓰기 때문입니다. 되돌릴 수 없습니다.

해결: BOM을 붙인다

BOM(Byte Order Mark)은 파일 맨 앞에 붙는 EF BB BF 3바이트입니다. 눈에 보이지 않고 내용도 아닙니다. 그런데 엑셀은 이 3바이트를 보면 “아, UTF-8 이구나” 하고 인코딩을 바꿉니다. 그게 전부입니다. 이 3바이트 하나가 깨진 한글과 멀쩡한 한글을 가릅니다.

상황별로 이렇게 하면 됩니다.

  • 엑셀에서 UTF-8로 저장하고 싶다: 다른 이름으로 저장 → 파일 형식에서 “CSV UTF-8(쉼표로 분리)”를 고릅니다. 그냥 “CSV(쉼표로 분리)”와 다른 항목입니다. 이걸 고르면 엑셀이 BOM을 붙여 줍니다.
  • UTF-8 CSV를 엑셀에서 열어야 한다: 더블클릭하지 말고, 엑셀을 먼저 열어서 데이터 → 텍스트/CSV에서 가져오기 → 원본 파일을 “65001: 유니코드(UTF-8)”로 지정합니다.
  • 코드에서 CSV를 만들어 내려준다: 파일 맨 앞에 BOM을 붙입니다.
// 브라우저에서 다운로드시킬 때
const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" });

# 파이썬
open("out.csv", "w", encoding="utf-8-sig", newline="")

이 도구에서는 이 문제가 생기지 않습니다. 붙여넣기한 텍스트는 이미 브라우저가 디코딩을 끝낸 상태라 인코딩이라는 개념이 남아 있지 않기 때문입니다. 인코딩은 파일을 주고받을 때만 존재하는 문제입니다. 그래서 한글이 깨진 CSV를 받았다면 여기에 붙여넣어도 고쳐지지 않습니다 — 깨진 글자를 그대로 변환할 뿐입니다. 원본 파일을 올바른 인코딩으로 다시 여는 것이 유일한 해법입니다.

자동 형변환이 데이터를 파괴하는 방식

이 변환기에는 “숫자·불리언 자동 변환” 토글이 있고, 기본값이 꺼짐입니다. 편의 기능을 왜 꺼 두는지 설명할 필요가 있습니다.

CSV에는 타입이 없습니다. 모든 것이 텍스트입니다. 123이 숫자인지 문자열인지 파일 안에 적혀 있지 않습니다. 그래서 “숫자처럼 보이면 숫자로 바꾸자”는 추측이고, 추측은 틀립니다.

우편번호  01234        → 1234           (앞자리 0 소멸)
사번      007          → 7
전화번호  010-1234     → "010-1234"     (다행히 살아남음)
주문번호  1234567890123456789 → 1234567890123456800  (뒤 두 자리 증발)
버전      1.10         → 1.1            (1.10 ≠ 1.1)
날짜      2024-01-02   → 문자열 유지    (엑셀이라면 45293)

012341234가 되는 건 그냥 값이 바뀐 것입니다. 그리고 이건 복구 불가능합니다. 나중에 5자리로 다시 채우려 해도, 원래 4자리였던 우편번호와 구분할 방법이 없습니다.

판단 기준은 간단합니다 — 이 값으로 산수를 하는가? 우편번호를 더하지 않습니다. 사번을 평균 내지 않습니다. 전화번호를 곱하지 않습니다. 그러면 그건 숫자가 아니라 숫자 모양의 식별자이고, 문자열이어야 합니다. 수량·가격·점수처럼 실제로 계산하는 값만 숫자입니다.

그래서 이 도구는 토글을 켜더라도 앞자리가 0인 값과 정밀도를 잃는 값은 문자열로 남깁니다. “숫자로 바꿔 줘”는 “값을 바꿔도 좋아”와 다른 요청이기 때문입니다.

19자리 ID가 조용히 뭉개지는 이유

JSON의 숫자는 IEEE-754 배정밀도(double)입니다. 정수를 정확히 담을 수 있는 한계가 9007199254740991, 약 9천조입니다. 16자리까지는 안전하고 그 위는 아닙니다.

JSON.parse('{"id": 9007199254740993}').id
// → 9007199254740992   ← 마지막 자리가 다르다. 오류는 없다.

트위터가 API에서 idid_str을 나란히 주는 이유가 이것입니다. 자바스크립트 클라이언트가 id를 읽으면 다른 트윗을 가리키게 되기 때문입니다. 요즘 시스템의 주문번호·스노플레이크 ID는 대부분 18~19자리라 이 선을 넘습니다. 결론은 같습니다: ID는 문자열로 두세요.

엑셀은 CSV를 여는 게 아니라 해석한다

위의 자동 형변환 이야기는 이 도구의 토글만이 아니라 엑셀에도 그대로 적용됩니다. 그리고 엑셀은 토글을 끌 수 없습니다.

CSV를 더블클릭으로 여는 순간 엑셀은 모든 칸을 자기 마음대로 해석합니다. 앞자리 0을 떼어내고, 2024-01-02를 날짜 일련번호 45293으로 바꾸고, 1-2를 “1월 2일”로 만들고, SEPT2같은 유전자 이름을 날짜로 바꿉니다(유전학계가 실제로 유전자 이름을 개명한 사건이 있습니다). 그리고 그 상태로 저장을 누르면 원본이 그 값으로 덮어써집니다.

그래서 실무 규칙은 이렇습니다. 중요한 CSV를 엑셀로 더블클릭해서 열지 마세요. 봐야 한다면 데이터 → 텍스트/CSV에서 가져오기로 열고, 문제가 되는 열의 서식을 “텍스트”로 지정하세요. 아니면 이 도구처럼 텍스트를 텍스트로 다루는 것을 쓰세요.

RFC 4180은 표준이 아니다

CSV에 표준이 있냐고 물으면 보통 RFC 4180이 답으로 나옵니다. 그런데 그 문서의 초록에는 이렇게 적혀 있습니다 — “이 문서는 인터넷 표준을 규정하지 않는다.” RFC 4180은 2005년에 “사람들이 CSV라고 부르는 것이 대충 이렇게 생겼더라”를 정리한 관찰 기록입니다. 강제력이 없고, CSV는 그 문서보다 30년쯤 먼저부터 쓰이고 있었습니다.

그래서 “CSV 파일”이라는 말로 확정되는 게 사실상 없습니다.

  • 구분자: 쉼표라는 이름과 달리 탭·세미콜론·파이프가 다 쓰입니다. 독일어권 엑셀은 소수점에 쉼표를 쓰기 때문에 세미콜론으로 저장합니다.
  • 줄바꿈: RFC는 CRLF지만 유닉스 도구는 LF만 씁니다. 섞여 있는 파일도 있습니다.
  • 따옴표 이스케이프: RFC는 ""인데, 백슬래시(\")를 쓰는 도구가 있습니다. MySQL의 기본 출력이 그렇습니다.
  • 헤더: 있을 수도 없을 수도 있고, 파일 안에 그 정보가 없습니다.
  • 인코딩: 파일 어디에도 적혀 있지 않습니다. BOM이 없으면 추측입니다.

JSON에는 이런 모호함이 없습니다. 구분자가 정해져 있고, 인코딩이 UTF-8로 명시돼 있고, 문자열과 숫자가 문법으로 구분됩니다. CSV를 JSON으로 바꾸는 게 의미 있는 이유의 절반은 모호함을 없애는 것입니다.

그러니 CSV 파일을 받았을 때 첫 질문은 “어떻게 파싱하지?”가 아니라 “이거 뭘로 만든 거죠?”여야 합니다. 엑셀에서 나온 건지, 파이썬 스크립트가 뽑은 건지, DB에서 export한 건지에 따라 구분자도 인코딩도 따옴표 규칙도 다릅니다. 위 변환기에서 구분자를 바꿔 가며 미리보기 표를 보면 몇 초 만에 알 수 있습니다 — 열이 제대로 나뉘면 맞는 것이고, 한 칸에 다 뭉쳐 있으면 틀린 것입니다.

실패하면 실패를 반환한다

이 변환기는 열 개수가 헤더와 다르면 변환을 거부하고 몇 번째 행인지 알려줍니다. 부족한 칸을 null로 채우거나 남는 칸을 버리면 결과가 나오긴 합니다. 하지만 그 결과는 틀렸고, 사용자는 틀린 걸 모릅니다.

오류 메시지를 보는 건 잠깐 불편합니다. 반면 조용히 밀린 데이터는 몇 주 뒤에 발견되고, 그때는 이미 그 데이터로 만든 것들이 쌓여 있습니다. 시끄러운 오류가 조용한 오답보다 항상 낫습니다. JSON → CSV에서 중첩 객체를 만났을 때 [object Object]를 뱉지 않고 멈추는 것도 같은 이유입니다 — 그건 변환이 아니라 데이터를 지우는 것이니까요.

자주 묻는 질문

엑셀에서 저장한 CSV를 열면 한글이 깨집니다. 왜 그런가요?
엑셀은 기본적으로 CSV를 CP949(euc-kr 확장)로 저장하는데, 이걸 UTF-8로 읽으면 글자가 깨집니다. 반대로 UTF-8로 저장한 CSV를 엑셀에서 더블클릭으로 열면 엑셀이 CP949로 읽어서 또 깨집니다. 해결책은 저장할 때 '다른 이름으로 저장 → CSV UTF-8(쉼표로 분리)'를 고르거나, 파일 맨 앞에 UTF-8 BOM을 붙이는 것입니다. 이 변환기는 이미 디코딩된 텍스트를 붙여넣어 쓰는 것이라 여기서는 깨지지 않습니다 — 인코딩 문제는 파일을 주고받을 때 생깁니다.
우편번호 01234가 1234로 바뀌었습니다. 되돌릴 수 있나요?
이미 저장된 파일이라면 되돌릴 수 없습니다. 앞자리 0은 값에서 사라진 것이고, 어디가 5자리였는지 알 방법이 남아 있지 않기 때문입니다. 이 변환기의 '숫자·불리언 자동 변환'은 그래서 기본으로 꺼져 있고, 켜더라도 앞자리 0이 있는 값은 문자열로 남깁니다. 엑셀에서 열 때 잘리는 것이라면 열 서식을 '텍스트'로 지정한 뒤 가져오기하거나, 아예 엑셀로 열지 말고 이 도구처럼 텍스트로 다루세요.
쉼표가 들어 있는 값은 어떻게 처리하나요?
필드 전체를 큰따옴표로 감싸면 됩니다. `김철수,"서울시 강남구, 101동",30`은 3개 필드이고, 따옴표 안의 쉼표는 구분자가 아니라 그냥 글자입니다. 이 변환기는 문자 단위로 스캔하면서 따옴표 안팎을 구분하므로 이 경우를 정확히 처리합니다. 값 안에 따옴표 자체를 넣어야 한다면 큰따옴표를 두 번("") 쓰세요.
긴 ID가 JSON으로 바꾸니 마지막 자리가 달라졌습니다.
JSON의 숫자는 IEEE-754 배정밀도 실수라서 정수를 정확히 표현할 수 있는 한계가 9007199254740991(약 9천조, 16자리)입니다. 그보다 큰 19자리 주문번호나 스노플레이크 ID를 숫자로 넣으면 마지막 자릿수가 조용히 반올림됩니다. ID는 계산하지 않는 값이므로 항상 문자열로 두세요 — 이 변환기는 자동 변환을 켜도 정밀도를 잃는 숫자는 문자열로 남깁니다.
열 개수가 안 맞는다는 오류가 납니다. 그냥 변환할 수는 없나요?
일부러 막아 둔 것입니다. 열 개수가 헤더와 다르다는 건 어느 값이 어느 열인지 알 수 없다는 뜻이고, 짧은 쪽을 빈칸으로 채우면 오류 없이 틀린 데이터가 나옵니다. 대부분의 원인은 값 안에 구분자가 들어 있는데 따옴표로 감싸지 않은 것이니, 오류가 가리키는 행을 열어 그 필드를 따옴표로 감싸세요. 열 개수가 원래 들쭉날쭉한 데이터라면 헤더 사용을 끄고 배열의 배열로 변환하면 됩니다.
CSV에 표준이 있나요?
RFC 4180이 있긴 하지만 이건 표준이라기보다 '흔한 관행을 정리한 문서'에 가깝습니다. 실제로는 구분자(쉼표·탭·세미콜론), 줄바꿈(CRLF·LF), 인코딩(UTF-8·CP949), 따옴표 규칙이 프로그램마다 다릅니다. 독일·프랑스처럼 소수점에 쉼표를 쓰는 지역에서는 엑셀이 세미콜론으로 저장하기도 합니다. 그래서 'CSV 파일'은 하나의 포맷이 아니고, 받은 파일을 열기 전에 무엇으로 만든 건지 확인하는 게 먼저입니다.