JSON Toolkit

JSON을 CSV로: 헤더는 어디서 오고, 값은 왜 변하는가

핵심 요약: 실제 컨버터로 아홉 가지 실험을 수행했습니다. 첫 행 키 방식은 필드를 조용히 유실하고, null·누락·빈 문자열은 모두 빈 셀이 되며, 20자리 정수는 JSON.parse 단계에서 이미 정밀도를 잃고, 점이 포함된 키는 서로 다른 경로를 한 열로 충돌시킵니다. 헤더 생성 방법, 값의 변형, 다운스트림 용도별 전략 선택을 설명합니다.

중급약 6분 소요2026-11-04 발행2026-11-04 업데이트
JSON에서 CSV로의 헤더 전략과 값 보존 검증

CSV 변환에서 데이터가 사라지는 것은 대부분 "변환" 동작 자체가 아니라, 아무도 묻지 않은 두 가지 질문에서 일어납니다. 헤더는 누가 정했는가, 그리고 각 셀의 값이 여전히 원래 값을 알아볼 수 있는가.

30초 만에 재현하는 필드 유실

상용 컨버터 상당수(jq의 관용적 작성법 포함)는 헤더를 첫 번째 레코드에서만 가져옵니다. Python으로 그 결과를 재현해 봅니다.

data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys())          # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# 두 번째 레코드의 email은 오류도 경고도 없이 사라진다

email 키는 헤더에 없으므로 빈 셀이 될 자격조차 없습니다. 열 자체가 만들어지지 않았고 값은 조용히 버려집니다. 해결책은 키 합집합입니다. 모든 레코드를 훑어 어디선가라도 나타난 키를 전부 열로 만듭니다. 같은 데이터를 합집합 방식으로 처리하면 헤더는 id,name,email이 되고, name이 없는 곳은 빈 셀로 채워져 아무것도 유실되지 않습니다.

이 사이트의 /convert/csv 순방향 변환이 바로 이 방식을 씁니다. [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}]을 넣으면 다음을 출력합니다.

id,name,email
1,A,
2,,b@x.com

키는 처음 나타난 순서대로 배열되고, 누락된 필드는 빈 셀이 됩니다. jsonoperations와 jsonbeam 두 경쟁사 튜토리얼이 공통으로 강조하는 "고전적 버그 수정법"입니다. 다만 "합집합을 써라"만으로는 부족하고, 합집합에도 고유한 대가가 있습니다. 다음 절에서 다룹니다.

헤더의 세 가지 생성 방식과 각각의 적합한 용도

헤더 전략 생성 방법 장점 대가
첫 행 키 첫 레코드만 훑음 열이 적고 안정적, jq 한 줄로 가능 이후 레코드의 새 필드가 조용히 유실
키 합집합 모든 레코드를 훑음 필드 유실 없음 레코드 모양이 들쭉날쭉하면 열이 희소해지고 빈 셀 증가
명시적 열 목록 사람이나 schema가 지정 완전히 제어 가능, 재현 가능 유지보수 필요, 새 필드는 수동 추가

선택 기준은 "어느 쪽이 고급인가"가 아니라 다운스트림이 이 CSV로 무엇을 하는가입니다.

  • 동료가 잠깐 열어보는 용도: 첫 행 키도 충분합니다. 레코드 모양은 보통 균일합니다.
  • 보고서나 데이터 파이프라인: 키 합집합이 최소선입니다. 빈 셀 자체가 정보입니다.
  • 정기 자동 내보내기: 명시적 열 목록. 모양 변화(상류의 필드 추가, 키 이름 변경)는 아무도 이상을 발견하지 못하는 표를 조용히 만들어내는 것이 아니라 명확히 오류를 내야 합니다.

세 번째가 가장 소홀히 다뤄집니다. 자동화에서 "변환이 성공했다"와 "변이 올바르다"는 별개의 주장이며, 명시적 열 목록만이 이 둘을 구분하는 유일한 장치입니다.

키에 점이 들어가면 플래트닝 경로가 충돌한다

점 표기법 플래트닝은 {"a": {"b": 2}}를 열 a.b로 바꿉니다. 그런데 키 자체에 점이 들어 있다면? pandas로 실측합니다.

import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# 열이 ['a.b'] 하나만 남는다 —— 전혀 다른 두 키가 같은 경로로 충돌

리터럴 키 a.b와 중첩 경로 a → b가 같은 이름의 열을 만들고, 하나가 다른 하나를 덮어쓰며 데이터가 조용히 망가집니다. 인위적인 예이지만 실제 데이터에서도 충분히 발생합니다. 모니터링 지표 이름(cpu.usage)과 로그 필드(error.type)는 점을 즐겨 씁니다.

두 가지 해법이 있습니다.

  1. 구분자를 바꾼다: sep="_" 등. 먼저 원래 키에 그 문자가 없음을 확인합니다.
  2. 경로 표기법을 바꾼다: JSON Pointer는 /를 구분자로 씁니다(/a/b). toolvore의 Excel 컨버터가 이 방식입니다. 키에 /가 있으면 ~1로 써야 하는 같은 종류의 모호함이 있지만, 이스케이프 규칙이 RFC 6901로 표준화되어 있다는 점이 다릅니다.

무엇을 선택하든 규칙은 내보내기 문서에 기록해야 합니다. CSV 헤더는 이 데이터가 가진 유일한 "schema"이며, 독자는 당신의 플래트닝 규칙을 복원할 다른 장소가 없습니다.

배열은 어떻게: 세 가지 처분, 각기 다른 것을 잃는다

배열은 JSON → CSV의 진짜 고비입니다. 한 셀에는 여러 값을 넣을 수 없습니다. 세 가지 전략과 실측된 각각의 손실:

전략 출력 형태 잃는 것 적합한 용도
JSON-in-cell 배열을 JSON 문자열로 셀에 저장 스프레드시트 도구로 요소를 필터·집계 불가 보존 우선, 육안 확인
인덱스 열 tags.0, tags.1 각각 한 열 가장 긴 배열에 맞춰 열 수 폭발, 빈칸 많음 길이가 고정된 작은 배열
행 폭발 요소마다 한 행, 부모 필드 복제 행 수 팽창, 빈 배열 처리는 별도 정의 필요 상세 분석, 요소별 집계

이 사이트의 /convert/csv와 /convert/table은 JSON-in-cell을 채택합니다. [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}]을 넣으면 다음을 출력합니다.

orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"

행 폭발이 필요하면 pandas의 record_path를 씁니다.

orders = [{"orderId": "A001", "total": 99,
           "items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
                          meta=["orderId", "total"], sep=".")
# 상세 두 행, orderId와 total이 각 행에 복제된다

행 폭발에는 미리 정해야 할 함정이 세 가지 있습니다. 여러 배열을 동시에 폭발시키면 데카르트 곱이 생기고, 빈 배열은 기본적으로 행을 남기지 않으며(고객 목록에서 사람이 사라짐), 부모 필드가 복제되므로 다운스트림의 orderId 집계에는 중복 제거가 필요합니다. convertcsvonline 가이드가 "먼저 입도(grain)를 정한다"를 첫 단계로 드는 이유가 정확히 이것입니다.

실측: 변환을 한 번 거치면 변해버리는 값들

이 사이트의 실제 컨버터(Node v22.13.1 + papaparse, /convert/csv와 동일 구현)로 수행한 보존 실험입니다.

1. null·누락·빈 문자열의 세 상태가 모두 빈 셀로. 입력 [{"a":null},{"b":""},{"c":1}]에서 세 셀 모두 빈 셀로 출력됩니다. CSV에는 세 상태가 없고 "문자가 있다/없다"만 있습니다. 왕복 복원 후 셋 모두 ""가 되어 원래 의미를 구별할 수 없습니다.

2. 중첩 객체와 배열은 JSON 문자열로 직렬화. 따옴표는 RFC 4180 규칙대로 두 배가 되고({""name"":""张三""}), 셀을 아무 JSON 파서에 넣으면 복원됩니다. JSON-in-cell이 보존을 지키는 이유입니다.

3. 20자리 정수는 파싱 단계에서 이미 정밀도를 잃는다. 책임은 CSV에 없습니다. 입력 "id": 9007199254740993에 대해 CSV에는 이미 9007199254740992가 들어 있습니다. JavaScript의 JSON.parse는 IEEE 754 배정밀도를 쓰므로 손실은 어떤 CSV 컨버터도 실행되기 전의 파싱 단계에서 일어납니다. Python의 json.load는 완전히 보존합니다. 식별자성 필드(스노우플레이크 ID, 주문번호)는 상류에서 문자열로 보내야 합니다. 숫자 정밀도에 대해서는 큰 숫자가 정밀도를 잃는 이유를 참고.

4. 문자열 앞의 0은 CSV 텍스트 층에서는 안전. "phone": "00123"은 CSV에서도 00123 그대로입니다. 처음부터 끝까지 문자열이라 벗겨낼 주체가 없기 때문입니다. 앞의 0을 벗기는 것은 이후 Excel이 셀을 숫자로 취급하는 단계입니다. JSON을 Excel로 변환하는 네 가지 방법 참고.

5. 쉼표·따옴표·줄바꿈이 포함된 필드. papaparse는 RFC 4180을 따릅니다. 필드를 따옴표로 감싸고 내부 따옴표를 두 배로 합니다. "He said \"hi\", ok"는 "He said ""hi"", ok"로 출력되고 왕복해도 무손실입니다. 라이브러리를 우회해 직접 문자열을 조립하면 거의 반드시 여기서 실패합니다.

왕복 복원: 추론 스위치의 차이

CSV → JSON은 모든 셀의 타입을 추측해야 합니다. 이 사이트의 컨버터는 기본으로 타입 추론을 끄고 전부 문자열로 복원합니다.

[{ "id": "9007199254740992", "phone": "00123" }]

typeInference를 켜면 id는 숫자가 되고(정밀도는 이미 돌아오지 않음), phone은 문자열로 남습니다. 추론 규칙이 010 같은 선행 0 형태(합법적 숫자 리터럴이 아님)를 배제하기 때문입니다.

이 비대칭을 기억할 가치가 있습니다. 추론을 끄면 잃는 것은 많아야 타입이고, 추론을 켜면 데이터를 잃고 있을 수 있습니다. 숫자가 필요한 열은 명시적 열 정의로 변환하고 전역 추론을 믿지 마십시오. 이전 글 JSON과 CSV·표 데이터의 상호 변환의 rowsToRecords가 열 정의별 변환이라는 이 원칙의 구현입니다.

다운스트림 용도별 전략 선택: 한 장의 의사결정표

CSV를 누구에게 전달하나 헤더 전략 배열 처리 타입 처리
동료가 한 번 열어봄 키 합집합 JSON-in-cell 추론 없음
Excel / 보고서 키 합집합 JSON-in-cell, 길이 고정이면 인덱스 열 식별자 열은 강제 텍스트
데이터베이스 적재 명시적 열 목록 마스터+상세 테이블 분리 또는 행 폭발 명시적 열 정의
BI / 집계 분석 명시적 열 목록 행 폭발, 입도 = 분석 최소 단위 명시적 열 정의
자동화 파이프라인 명시적 열 목록 JSON-in-cell, 행 폭발은 다운스트림 추론 끔, 열별 변환

공통 법칙: 사람에게 가까울수록 느슨해도 되고, 기계에 가까울수록 명시적이어야 한다.

복사해 쓸 수 있는 자가 점검 목록

  • [ ] 헤더가 전체 레코드 키 합집합인가, 명시적 열 목록인가? 후자가 아니라면 필드를 잃은 레코드가 없음을 증명할 수 있는가?
  • [ ] 키에 점이나 슬래시가 들어갈 가능성이 있는가? 플래트닝 구분자와의 충돌을 점검했는가?
  • [ ] 배열 처리 전략을 문서화했는가? 빈 배열의 행 처리를 결정했는가?
  • [ ] null·누락·빈 문자열 세 입력을 다운스트림은 각각 무엇으로 읽는가? 하나의 규칙을 골라 고정하라.
  • [ ] 식별자 열(ID, 주문번호, 전화번호)은 처음부터 끝까지 문자열로 운반되는가?
  • [ ] 행 폭발 후 행 수 = 각 배열 길이의 합을 표본 확인했는가?
  • [ ] 쉼표·따옴표·줄바꿈이 포함된 표본 필드가 왕복 후에도 변하지 않는가?
  • [ ] 역방향 복원에서 전역 타입 추론을 껐는가, 숫자가 필요한 열에 명시적 정의가 있는가?

이 여덟 항목 모두에 명확한 답이 있을 때 비로소 그 CSV는 "변환에 성공했다"가 아니라 "변환이 올바르다"고 말할 수 있습니다.

자주 묻는 질문

컨버터는 중첩 객체를 기본으로 여러 열로 펼치지 않나요?

펼치면 열 수가 제어 불가능해집니다. 깊은 중첩은 user.address.geo.lat 같은 긴 열 이름을 만들고, 키 합집합과 결합하면 모양이 다른 레코드 하나로 열이 한꺼번에 늘어납니다. 셀에 JSON 문자열을 넣는 방식은 보존과 안정된 열 수를 함께 지킵니다. 필요하면 명시적 규칙으로 직접 펼치십시오.

헤더에 중복 열 이름이 생기면?

플래트닝 후 서로 다른 키가 충돌하는 경우(예: a.b와 중첩 a→b)는 상류에서 해결해야 합니다. 구분자를 바꾸거나 JSON Pointer로 전환하십시오. 같은 객체 안의 중복 키는 보통 마지막 값이 남고, 헤더 수준의 중복은 구현에 따릅니다. papaparse의 unparse는 중복을 제거하지 않으며, Excel은 파일을 열 때 조용히 합치거나 버립니다. 그래서 점검 목록에 있습니다.

행 폭발 후 부모 필드가 반복되는 것은 버그인가요?

아니요, 설계입니다. 행 폭발의 의미는 "상세 행"이며, 상세 행은 부모 키로 주 레코드에 조인될 수 있어야 합니다. 다운스트림에서는 부모 키로 그룹화해 집계하면 됩니다. 크기가 걱정되면 상세 파일과 마스터 테이블을 따로 내보내십시오.

변환이 필드를 유실하지 않았는지 검증하려면?

키 감사를 하십시오. 원본 JSON의 전체 키 합집합과 CSV 헤더의 열 집합을 구해 차집합을 계산합니다. 차집합이 비어 있지 않으면 필드가 유실된 것입니다. 배치 파이프라인에서는 이 단언을 테스트에 넣어 컨버터 변경이 필드를 흘리기 시작하면 자동으로 잡히게 하십시오.