mygems

데이터 손실 없이 JSON을 CSV로 변환하는 방법

JSON은 트리, CSV는 표라서 변환기마다 선택이 다릅니다. 그 선택과 중첩 JSON을 펼치는 방법, 잘 처리하는 도구를 정리했습니다.

작성: mygems ·

JSON은 트리이고, CSV는 직사각형 표입니다. 하나를 다른 하나로 바꾸려면 트리를 어떻게 평평하게 펼칠지 정해야 하고, 모든 변환기는 그 결정을 대신 내립니다. 대개 무엇을 정했는지는 알려 주지 않습니다. 같은 파일을 온라인 변환기 세 곳에 넣으면 서로 다른 스프레드시트 세 개가 나오는 이유입니다.

세 가지 결정을 알면, 원하는 방식으로 결정해 주는 도구나 설정을 고를 수 있습니다.

빠른 답

  • 객체로 이루어진 평평한 배열([{"id":1,"name":"Ada"}, …]): 어떤 변환기든 됩니다. jq, Python, DuckDB, Excel, 앱 모두 쓸 수 있습니다.
  • 중첩된 객체("address": {"city": "Leeds"}): address.city처럼 점으로 구분한 열로 펼칩니다. Python의 json_normalize는 한 줄로 처리합니다.
  • 각 레코드 안의 배열("tags": ["a","b"]): 항목마다 열 하나, 항목마다 행 하나, 하나의 셀에 이어 붙이기 중에서 고릅니다. 정답은 없고, 결과를 어디에 쓸지에 맞는 방식이 답입니다.
  • 레코드가 최상위에 없을 때: 먼저 배열(보통 data, items, results)을 찾고, 그 배열을 변환합니다.

결정 1: 어떤 배열이 행이 되는가

CSV에는 비슷한 항목의 목록이 필요하고, 항목 하나가 행 하나입니다. 실제 JSON은 거의 그렇게 시작하지 않습니다. API 응답은 이렇게 생겼습니다.

{
  "status": "ok",
  "page": 1,
  "data": {
    "items": [
      { "id": 1, "name": "Ada", "address": { "city": "Leeds" }, "tags": ["admin"] },
      { "id": 2, "name": "Lin", "address": { "city": "Hue" }, "tags": [] }
    ]
  }
}

행이 되는 것은 data.items입니다. 루트를 대상으로 한 변환기는 객체 하나만 보고, status, page와 이스케이프된 JSON으로 가득 찬 열이 있는 행 하나를 돌려줍니다. "변환기가 고장 났다"는 불만 중 가장 흔한 경우이고, 변환기가 아니라 경로의 문제입니다.

결정 2: 중첩된 객체는 어떻게 하는가

address는 각 레코드 안에 있는 객체입니다. 보통은, 그리고 대부분의 용도에서 맞는 방법은 점으로 구분한 열 이름으로 펼치는 것입니다.

idnameaddress.city
1AdaLeeds
2LinHue

다른 방법은 객체 전체를 JSON 문자열로 하나의 셀에 넣는 것입니다. 모든 정보가 남지만, 정렬도 필터링도 할 수 없는 열이 됩니다.

결정 3: 레코드 안의 배열은 어떻게 하는가

tags는 목록이고, 목록은 셀 하나에 들어가지 않습니다. 선택지는 세 가지입니다.

방식결과적합한 용도
인덱스 열tags[0], tags[1], …최대 개수를 아는 짧은 목록
행으로 펼치기태그마다 한 행, 나머지 열은 반복태그별 집계나 필터링
한 셀에 이어 붙이기admin;editor읽기용, 분석에는 부적합

행으로 펼치면 행 수가 바뀝니다. 태그가 세 개인 사람은 이제 세 행입니다. "태그별로 몇 명인가"에는 맞지만 "전체 몇 명인가"에는 틀린 결과입니다. 고르기 전에 이 CSV로 어떤 질문에 답할지 먼저 정하세요.

그 밖에 잃는 것

완벽하게 펼쳐도 CSV가 표현하지 못하는 정보는 사라집니다.

  • 형식. "42"와 42는 모두 42가 됩니다. true는 텍스트 true가 됩니다.
  • null과 빈 값. null, "", 키가 없는 경우가 모두 빈 셀이 됩니다.
  • 듬성듬성한 레코드의 열. 10,000개 레코드 중 하나에만 coupon 필드가 있다면, 첫 번째 레코드의 키로 헤더를 만드는 변환기는 그 열을 버립니다. 좋은 변환기는 모든 레코드 키의 합집합을 사용합니다.

jq로 하기

최상위가 평평한 배열일 때:

jq -r '(.[0] | keys_unsorted) as $k | $k, (.[] | [.[$k[]]]) | @csv' in.json > out.csv

이 명령은 첫 번째 레코드로 헤더를 만들기 때문에, 첫 레코드에 없는 키는 버려집니다. 위에서 말한 듬성듬성한 레코드 문제입니다. 배열이 더 아래에 있으면 처음의 .를 .data.items 같은 경로로 바꿉니다. 중첩된 데이터도 jq로 처리할 수 있지만 식을 읽기 어려워집니다. 그럴 때는 Python을 쓰세요.

Python으로 하기

pandas.json_normalize는 세 가지 결정을 한 번의 호출로 처리하고, 키의 합집합도 사용합니다.

import json, pandas as pd

data = json.load(open("in.json"))
df = pd.json_normalize(data["data"]["items"], sep=".")
df.to_csv("out.csv", index=False)

중첩된 객체는 address.city가 됩니다. 목록은 한 셀 안에 목록으로 남습니다. 행으로 펼치려면 저장하기 전에 df = df.explode("tags")를 추가하세요.

DuckDB로 하기

큰 파일이라면 DuckDB가 JSON을 바로 읽고, 전체를 메모리에 올리지 않고 CSV로 씁니다.

duckdb -c "COPY (SELECT * FROM read_json_auto('in.json')) TO 'out.csv'"

평평한 배열이라면 가장 빠른 방법입니다. 중첩된 객체는 DuckDB 고유의 struct 표기로 한 열에 출력됩니다. SELECT에서 펼치거나, 중첩된 데이터는 Python을 쓰세요.

Excel로 하기

데이터 → 데이터 가져오기 → 파일에서 → JSON에서를 선택하면 Power Query가 열립니다. 테이블로를 클릭한 뒤 열 머리글의 확장 아이콘(화살표 두 개)으로 레코드와 목록을 한 단계씩 펼칩니다. 클릭만으로 할 수 있고, 각 결정을 내리는 즉시 결과를 볼 수 있습니다. 그다음 CSV로 저장합니다. 저장할 때의 형식 변환에 주의하세요. 설정하지 않으면 Excel은 CSV 데이터를 바꿉니다.

온라인 변환기

잘 동작하고, 공개 데이터라면 문제없습니다. 주의할 점은 두 가지입니다. 대부분은 세 가지 결정을 숨기므로, 출력의 행 수와 열 수가 예상과 맞는지 확인하세요. 또 API가 돌려주는 JSON에는 토큰, 이메일, 고객 정보가 들어 있는 경우가 많으니, 업로드하지 않고 브라우저 안에서 동작하는 변환기를 쓰는 편이 좋습니다.

iPhone과 iPad에서

Smart Json은 레코드 모드라는 방식으로 JSON을 CSV로 변환합니다. 객체 배열이 행이 되고, 중첩된 값은 점으로 구분한 열(address.city, tags[0])로 펼쳐지며, 헤더는 모든 레코드 키의 합집합입니다. 첫 번째 객체 배열은 자동으로 찾아 주고, 다른 배열을 고르거나 $.data.items 같은 JSONPath를 직접 입력할 수도 있습니다. 구분자는 바꿀 수 있고, 객체 배열이 없는 문서는 쓸모없는 두 열짜리 결과를 내는 대신 그 사실을 알려 줍니다.

YAML, CSV, TOML, XML 탭, 형식이 담을 수 없는 내용에 대한 안내, 변환된 문서의 미리보기가 있는 변환 시트
내보내기 전에 각 형식이 담을 수 없는 내용을 알려 줍니다

미리보기는 무료이므로 CSV가 어떤 모습이 될지 정확히 볼 수 있습니다. 결과를 복사하거나 저장하는 것은 Pro 기능입니다.

JSON Editor: Smart JsoniPhone 및 iPad

JSON을 CSV로 미리 보여 주고, 중첩된 값을 점으로 구분한 열로 펼칩니다. 레코드 배열을 JSONPath로 고를 수 있으며, CSV가 담을 수 없는 내용을 분명하게 알려 줍니다.

기능

CSV가 되고 나면 휴대폰에서 여는 일이 또 하나의 작은 문제입니다. 그 가이드에서 다룹니다.

결과 확인하기

어떤 도구를 쓰든 확인할 것은 세 가지입니다.

  1. 행 수. 배열의 레코드 수와 일치합니까(목록을 일부러 펼쳤다면 그보다 많습니다)?
  2. 열 수. 일부 레코드에만 있는 필드가 헤더에 들어갔습니까?
  3. 중첩된 값 하나. 중첩된 객체가 있는 레코드를 골라, 그 값이 올바른 점 구분 열에 들어갔는지 확인합니다.

자주 묻는 질문

중첩된 JSON을 CSV로 변환하려면 어떻게 하나요?
중첩된 객체를 address.city처럼 점으로 구분한 열 이름으로 펼칩니다. Python에서는 pandas.json_normalize가 한 번의 호출로 처리하고, Excel의 Power Query는 확장 버튼으로 처리하며, 자동으로 해 주는 JSON 앱도 있습니다. 레코드 안의 배열은 별도의 결정이 필요합니다. 인덱스 열, 항목마다 한 행, 이어 붙인 한 셀 중에서 고릅니다.
JSON을 CSV로 바꾸는 변환기가 한 행만 출력하는 이유는 무엇인가요?
레코드가 문서의 최상위에 있지 않기 때문입니다. API 응답은 보통 레코드를 data.items처럼 한 겹 감싸고 있습니다. 루트 객체가 아니라 그 배열을 경로로 지정해서 변환하세요.
Excel에서 JSON 파일을 열 수 있나요?
네. 데이터 → 데이터 가져오기 → 파일에서 → JSON에서를 선택하면 Power Query가 열립니다. 결과를 테이블로 변환하고 확장 버튼으로 중첩된 레코드를 펼친 다음 CSV로 저장하세요.
JSON을 CSV로 변환하면 무엇을 잃나요?
형식(숫자, 불리언, 문자열이 모두 텍스트가 됩니다), null과 빈 값과 누락의 구분, 그리고 중첩 구조 자체입니다. 일부 레코드에만 있는 필드는 첫 번째 레코드로 헤더를 만드는 변환기에서 버려질 수도 있습니다.
명령줄에서 JSON을 CSV로 변환하려면 어떻게 하나요?
평평한 배열이라면 jq의 @csv나 DuckDB의 COPY … TO 'out.csv' 모두 한 줄로 됩니다. 중첩된 JSON은 pandas.json_normalize를 쓴 짧은 Python 스크립트가 더 확실하고 읽기도 쉽습니다.

mygems — 중첩 JSON을 펼쳐 CSV로 내보내는 Smart Json과, 그 결과를 읽는 Smart CSV Viewer를 만듭니다.