트리는 표에 그대로 들어가지 않는다
CSV(RFC 4180)는 레코드의 나열이며, 모든 레코드가 같은 순서의 텍스트 필드를 갖습니다. JSON에는 이런 보장이 없습니다. 객체 안에 객체가 들어가고, 배열은 원소가 0개일 수도 50개일 수도 있으며, 같은 배열 안의 두 레코드가 전혀 다른 키를 가질 수도 있습니다. 이를 행과 열로 바꾸려면 두 가지를 정해야 합니다. 중첩된 경로를 열 이름으로 어떻게 바꿀지, 그리고 레코드마다 길이가 다른 배열을 어떻게 처리할지입니다.
변환기는 이 질문에 세 가지 방식 중 하나로 답하며, 어느 쪽도 모든 입력에서 무손실은 아닙니다. 객체는 쉬운 편입니다. customer.name은 그 자체로 좋은 열 이름입니다. 어려운 쪽은 배열입니다. 주문 항목 목록은 사실 첫 번째 표 안에 들어 있는 두 번째 표이기 때문입니다. 이 글은 작은 입력 하나로 세 방식을 직접 비교합니다.
모든 예시에 쓰는 입력은 주문 두 건입니다. 각 주문에는 중첩된 customer 객체와 객체 배열 items가 있고, 배열 길이는 서로 다릅니다(항목 2개와 1개). 아래 출력은 모두 이 사이트의 변환기 코드를 이 텍스트에 실제로 실행해 얻은 결과이며, 손으로 쓴 것이 아닙니다.
[
{
"id": 1001,
"customer": { "name": "Kim Min-su", "city": "Seoul" },
"items": [
{ "sku": "A-1", "qty": 2 },
{ "sku": "B-7", "qty": 1 }
]
},
{
"id": 1002,
"customer": { "name": "Lee Ha-na", "city": "Busan" },
"items": [
{ "sku": "C-3", "qty": 5 }
]
}
]전략 1: 점 경로 열, 배열은 텍스트로 유지
가장 단순한 방식은 키를 점으로 잇고, 배열은 한 셀에 압축된 JSON 문자열로 남겨 두는 것입니다. JSON to CSV 도구의 기본값(배열 옵션이 JSON)이 이 방식입니다. 행 수는 레코드 수와 같고 새로 만들어 내는 값도 없습니다. 대신 items 열은 따로 파싱해야 하는 문자열이 됩니다.
- 배열이 태그나 원본 페이로드처럼 참고용 정보여서 거의 필터링하지 않을 때 가장 알맞습니다.
- 엑셀에서 수량을 합산해야 할 때는 최악입니다. 숫자가 텍스트 셀 안에 들어 있기 때문입니다.
- 헤더는 모든 레코드의 키를 처음 나온 순서대로 합친 목록이어서, 키가 없는 레코드는 빈 셀이 됩니다.
id,customer.name,customer.city,items
1001,Kim Min-su,Seoul,"[{""sku"":""A-1"",""qty"":2},{""sku"":""B-7"",""qty"":1}]"
1002,Lee Ha-na,Busan,"[{""sku"":""C-3"",""qty"":5}]"전략 2: items.0.sku 같은 인덱스 열과 그 대가
두 번째 방식은 배열 안으로 들어가 열 이름에 위치를 붙입니다. JSON to CSV 도구에서는 배열 옵션을 인덱스로 바꾸면 됩니다. 값마다 셀이 따로 생기므로 스프레드시트에서 바로 읽고 필터링할 수 있습니다. 샘플의 출력은 이 절 끝에 있습니다.
표의 너비는 파일 전체에서 가장 긴 배열이 정합니다. 항목이 40개인 주문이 하나만 있어도 모든 행에 항목 열이 80개 생기고, 대부분의 셀은 비게 됩니다. 도구는 200열을 넘기면 변환을 거부하며, 스프레드시트는 그보다 훨씬 전부터 읽기 어려워집니다. 스키마도 불안정해집니다. 가장 긴 배열이 바뀌면 열이 조용히 늘어나서, 고정된 헤더를 기대하는 가져오기 작업이 깨집니다.
인덱스 방식은 좌표 쌍, 전화번호 두 개, 고정된 세 가지 옵션처럼 배열이 짧고 상한이 있을 때 쓰세요. 같은 경로를 다른 표기로 만들어 주는 것은 JSON Flatten 도구입니다. 점 스타일에서는 items[0].sku, 포인터 스타일에서는 /items/0/sku(RFC 6901 JSON Pointer)로 적습니다. JSON to CSV 도구 자체는 대괄호 없이 items.0.sku로 씁니다.
| 표기 | 첫 번째 SKU의 예 | 만들어 주는 도구 |
|---|---|---|
| 점 키, 셀에 JSON 배열 | items (셀에 배열 전체가 들어감) | JSON to CSV, 배열 옵션 JSON |
| 점 키 + 인덱스 | items.0.sku | JSON to CSV, 배열 옵션 인덱스 |
| 대괄호가 있는 점 스타일 | items[0].sku | JSON Flatten, 점 스타일 |
| JSON Pointer (RFC 6901) | /items/0/sku | JSON Flatten, 포인터 스타일 |
id,customer.name,customer.city,items.0.sku,items.0.qty,items.1.sku,items.1.qty
1001,Kim Min-su,Seoul,A-1,2,B-7,1
1002,Lee Ha-na,Busan,C-3,5,,전략 3: 배열 원소마다 한 행
세 번째 방식은 배열을 진짜 표로 취급합니다. 원소 하나가 행 하나가 되고, 부모 필드는 모든 행에 반복됩니다. 행 하나에 사실 하나가 담기므로 피벗 테이블, SQL 가져오기, BI 도구가 기대하는 모양입니다. JSON to CSV 도구는 레코드 수를 바꾸지 않기 때문에 이 방식을 직접 지원하지 않습니다. 먼저 JSON을 재구성해야 합니다. 예를 들어 짧은 스크립트로 주문마다 항목당 객체 하나를 만든 뒤 그 결과를 변환하세요.
id,customer.name,customer.city,sku,qty
1001,Kim Min-su,Seoul,A-1,2
1001,Kim Min-su,Seoul,B-7,1
1002,Lee Ha-na,Busan,C-3,5세 방식 중 고르는 기준
기준은 JSON의 생김새가 아니라 파일을 받는 쪽이 무엇을 하느냐입니다. 사람이 엑셀에서 훑어본다면 짧은 인덱스 열이나 원소별 행이 읽기 좋습니다. 데이터베이스나 피벗 테이블이 받는다면 원소별 행이 낫습니다. 사람이 읽을 백업 용도이고 나중에 JSON으로 되돌릴 계획이라면, 점 키에 배열을 텍스트로 두는 방식이 손실이 가장 적습니다.
| 질문 | 점 키 + JSON 텍스트 | 인덱스 열 | 원소별 행 |
|---|---|---|---|
| 행 수가 레코드 수와 같은가 | 예 | 예 | 아니오, 배열 원소 수만큼 |
| 엑셀에서 값을 필터링할 수 있는가 | 배열은 불가 | 가능 | 가능 |
| 열 개수 | 고정 | 가장 긴 배열에 따라 증가 | 고정 |
| 부모 값이 반복되는가 | 아니오 | 아니오 | 예 |
| JSON으로 되돌리기 쉬운가 | 쉬움 | 별도 로직 필요 | 키별 그룹화 필요 |
RFC 4180 기준 이스케이프
RFC 4180의 규칙은 많지 않습니다. 필드는 쉼표로, 레코드는 CRLF로 구분합니다. 쉼표, 큰따옴표, 줄바꿈을 포함한 필드는 큰따옴표로 감쌉니다. 따옴표로 감싼 필드 안의 큰따옴표는 두 개로 씁니다. 마지막 레코드 뒤의 줄바꿈은 있어도 없어도 됩니다. 백슬래시 이스케이프는 이 형식에 없으므로, 그것을 기대하는 리더는 규격에 맞는 파일을 잘못 읽습니다.
도구는 이 규칙을 따르고, 앞뒤에 공백이 있는 필드도 따옴표로 감쌉니다. RFC가 허용하되 요구하지는 않는 동작입니다. 아래는 특수한 경우를 한 레코드에 모두 담아 JSON to CSV 도구로 LF 줄바꿈으로 쓴 결과입니다. 기본값은 CRLF이며 엑셀에 넘길 파일은 CRLF를 유지하는 편이 좋습니다.
- 값에 쉼표가 있으면 필드 전체를 따옴표로 감쌉니다.
- 값에 따옴표가 있으면 필드를 감싸고 안의 따옴표를 모두 두 개로 씁니다.
- 값에 줄바꿈이 있으면 필드를 감싸고 줄바꿈은 그 안에 그대로 둡니다. 레코드 하나가 물리적으로 두 줄에 걸치므로
wc -l이나grep같은 줄 단위 도구는 개수를 잘못 셉니다. - 구분자를 세미콜론, 탭, 파이프로 바꾸면 쉼표 대신 그 문자를 기준으로 따옴표를 붙입니다. 탭 구분 다운로드 파일명은 data.tsv입니다.
name,note,memo,plain,pad
"Kim, Min-su","He said ""hi""","line1
line2",ok," lead"엑셀: BOM, 앞자리 0, 지수 표기
엑셀은 더블클릭으로 연 CSV의 인코딩을 추측합니다. 바이트 순서 표시(BOM)가 없는 UTF-8 파일은 시스템의 기존 코드 페이지로 읽히는 경우가 많아서, 한국어 윈도우에서는 한글이 깨져 보일 수 있습니다. 해결책은 두 가지입니다. 데이터 탭의 텍스트/CSV 가져오기에서 UTF-8(코드 페이지 65001)을 고르거나, 파일 맨 앞에 UTF-8 BOM(EF BB BF, 3바이트)을 붙여 저장하세요. JSON to CSV 도구는 내려받는 파일에 기본으로 BOM을 붙이고(엑셀 호환 (UTF-8 BOM) 스위치), 복사한 텍스트와 화면 출력에는 붙이지 않습니다. BOM을 첫 헤더 이름의 일부로 읽는 스크립트나 DB용이라면 스위치를 끄세요. CSV to JSON 도구는 앞에 붙은 BOM을 파싱 전에 제거하고, 엑셀의 sep=; 힌트 줄도 이해합니다.
엑셀은 숫자처럼 보이는 텍스트도 바꿉니다. 01234 같은 우편번호나 ID는 앞자리 0이 사라지고, 16자리 카드번호나 운송장번호 같은 긴 숫자열은 1.23457E+15로 표시되며 유효숫자 15자리만 남습니다. 이 변환은 파일을 쓴 뒤 엑셀이 열면서 일어나므로 CSV 자체는 멀쩡합니다. 해당 열을 텍스트로 가져오거나, 붙여 넣기 전에 열 서식을 텍스트로 지정하세요. 반대 방향인 CSV to JSON에서는 도구가 보호해 줍니다. 앞자리 0이 있는 값의 열은 텍스트로 남아서 01234와 00789가 문자열로 돌아오고, 알림에 해당 열 이름이 표시됩니다.
수식 주입: = + - @ 로 시작하는 셀
스프레드시트는 텍스트가 =, +, -, @로 시작하는 셀을 수식으로 취급합니다. JSON이 사용자 입력에서 온 것이라면 =HYPERLINK(...) 같은 값이 CSV를 여는 순간 살아 있는 수식이 됩니다. 표준적인 완화책은 그런 텍스트 셀 앞에 작은따옴표 하나를 붙여 텍스트로 보이게 하는 것입니다. JSON to CSV 도구는 수식 무력화 옵션이 켜져 있는 기본 상태에서 문자열 값에만 이 처리를 합니다. -5처럼 음수인 실제 숫자는 건드리지 않습니다.
문자열 네 개에 옵션을 켠 결과는 아래와 같습니다. 옵션을 끄면 같은 필드가 =SUM(A1:A2)처럼 그대로 나옵니다.
- 도구는 맨 앞의 탭, 캐리지 리턴, 줄바꿈도 위험으로 보고 처리합니다. 단순한 필터를 우회하는 알려진 방법이기 때문입니다.
- 대가도 분명합니다.
+82-10-1234같은 전화번호도 플러스 기호로 시작하는 문자열이라 작은따옴표가 붙습니다. 이런 값은 앞의 플러스 없이 저장하거나, 신뢰할 수 있는 데이터에 한해 옵션을 끄세요. - 작은따옴표는 데이터에 포함됩니다. 이 파일을 CSV to JSON으로 다시 읽으면
'=SUM(A1:A2)가 그대로 나오므로, 왕복 변환을 한다면 직접 제거해야 합니다.
f,p,m,at
'=SUM(A1:A2),'+82-10-1234,'-5 apples,'@home2^53을 넘는 정수는 정밀도를 잃는다
JavaScript는 모든 숫자를 64비트 부동소수점으로 표현하므로 정수는 2^53 - 1, 즉 9007199254740991까지만 정확합니다. JSON 텍스트 12345678901234567890은 유효한 JSON이지만 숫자로 읽는 순간 12345678901234567000이 되고, 9007199254740993은 9007199254740992가 됩니다. JSON to CSV 도구는 입력을 JavaScript 숫자로 읽으므로 바로 그렇게 바뀐 값을 쓰지만, 이제는 결과 아래 알림으로 자릿수를 잃은 숫자가 몇 개인지와 첫 번째 값을 JSON 경로와 원문 그대로 알려 줍니다. 값이 바뀌는 시점은 CSV가 만들어지기 전인 파싱 단계라 알림이 자릿수를 되살려 주지는 못합니다.
해결은 데이터 쪽에서 합니다. 2^53을 넘을 수 있는 식별자(주문번호, 스노우플레이크 ID, 카드번호 등)는 JSON에서 문자열로 적으세요. 실험에서 문자열 "9007199254740993"은 알림 없이 CSV에서도 9007199254740993 그대로였습니다. 반대 방향인 CSV to JSON 도구는 신중합니다. 2^53을 넘는 정수가 담긴 열은 반올림하지 않고 텍스트로 남기며 알림으로 열 이름을 알려 줍니다. 다만 그 셀을 숫자로 열면 엑셀의 15자리 제한이 다시 적용된다는 점은 기억해 두세요.
input : [{"id":12345678901234567890,"n":9007199254740993,"s":"9007199254740993"}]
csv : id,n,s
12345678901234567000,9007199254740992,9007199254740993CSV to JSON으로 왕복하기
CSV to JSON은 행마다 객체를 만들고 열 단위로 타입을 정합니다. 값이 있는 모든 셀이 맞을 때만 숫자나 불리언 열이 되고, 아니면 텍스트로 남습니다. 빈 셀과 NA, null 같은 단어는 null이 됩니다. 점이 들어 있는 헤더로 중첩 구조를 되살려 주지는 않습니다. 위의 첫 번째 CSV를 넣으면 customer.name, customer.city가 평평한 키 그대로 나오고 items는 JSON이 담긴 문자열로 나옵니다. 인덱스 열 CSV를 넣으면 항목이 하나뿐이던 주문에서는 빈 셀이 null이 되어 items.1.sku: null이 나옵니다.
중첩을 되살리려면 평평해진 객체를 JSON Flatten의 되돌리기(unflatten) 모드에 넣으세요. 표기에 주의해야 합니다. 점 스타일은 items[0].sku를 배열로 만들지만 items.0.sku는 키가 "0"인 객체로 읽습니다. 실험에서 앞의 표기는 items: [ { sku, qty } ]를, 뒤의 표기는 items: { "0": { sku, qty } }를 돌려주었습니다. 그러니 JSON Flatten으로 평탄화할 때 대괄호 표기를 쓰거나, JSON to CSV가 만든 열 이름은 배열로 되돌리기 전에 별도 변환 단계가 필요하다고 보세요.
[
{
"id": 1001,
"customer.name": "Kim Min-su",
"customer.city": "Seoul",
"items": "[{\"sku\":\"A-1\",\"qty\":2},{\"sku\":\"B-7\",\"qty\":1}]"
}
]