JSON Toolkit

JSON과 CSV 및 표 형식 데이터 변환

핵심 요약: CSV는 이차원이고 JSON은 트리 모양입니다. 변환의 어려운 점은 코드가 아니라, 배열을 어떻게 평탄화하고 타입을 어떻게 보존할지 미리 결정하는 것입니다.

초급약 10분 소요2026-10-30 발행2026-10-30 업데이트
JSON ↔ CSV와 표 형식 데이터 변환 표지

핵심부터 말하면, 사람이 읽을 내보내기에는 점 표기 키와 JSON 문자열화한 배열로 평탄화하고, 데이터베이스 적재에는 부모 테이블과 관련 테이블로 분할하며, 피벗에만 배열을 추가 행으로 확장하세요. 복원할 때는 전역 추론 대신 명시적 열 정의로 변환하세요.

트리를 표로 평탄화하면 항상 계층이 사라지고, 표를 트리로 복원하면 항상 타입을 추측합니다. 양방향 모두 규칙을 미리 밝혀야 합니다.

1. CSV가 실제로 규정하는 것

RFC 4180은 몇 페이지에 불과하며 많은 사람이 가정하는 것보다 적게 규정합니다:

  1. 각 레코드는 한 줄을 차지하고 필드는 쉼표로 구분됩니다.
  2. 줄 종결자는 CRLF이지만, 실제로는 \n도 허용됩니다.
  3. 쉼표, 큰따옴표, 줄 바꿈이 포함된 필드는 큰따옴표로 감싸야 합니다.
  4. 필드 안의 큰따옴표는 큰따옴표 두 개 ""로 씁니다.
  5. 첫 줄은 헤더일 수 있습니다.
  6. 문자 인코딩을 규정하지 않으며, 타입도 규정하지 않습니다.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

3번 규칙에 주의하세요. 줄 바꿈이 포함된 필드는 합법이므로, \n으로 나눈 뒤 ,로 나누는 순진한 파서는 반드시 틀립니다.

2. JSON → CSV: 구조를 먼저 결정

트리 구조를 이차원 표로 평탄화하는 전략은 세 가지뿐입니다:

전략 1: 필드 평탄화(가장 흔함)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • 중첩 객체 키를 .으로 이어 붙입니다.
  • 배열은 열로 표현할 수 없으므로 JSON.stringify로 셀에 그대로 넣습니다(여전히 합법적인 CSV 필드입니다).
  • 단점: 스프레드시트 도구가 배열을 필터링할 수 없어 두 번째 파싱이 필요합니다.

전략 2: 배열을 여러 행으로 확장

id,user.name,tag
1,张三,a
1,张三,b

하류 소비자가 요소 단위로 집계하는 일대다 관계에 어울립니다. 단점은 부모 필드가 중복되고 행 수가 폭증한다는 것입니다.

전략 3: 여러 테이블로 분할

id로 연결된 메인 테이블과 관련 테이블 — 가장 깔끔하지만 "단일 내보내기"를 넘어 데이터 모델링에 가깝습니다.

어느 것을? 사람을 위한 내보내기에는 전략 1, 데이터베이스 적재에는 전략 3, 피벗에는 전략 2를 쓰세요.

3. CSV → JSON: 타입이 추측되는 방식

CSV에는 타입이 없으므로 복원의 모든 단계가 추측입니다:

셀 내용 가능한 의미 숫자로 바로 변환할 때의 위험
00123 사번, 우편번호 앞의 0 손실 → 123
1.10 버전 번호, 금액 1.1이 됨
1e5 식별자 또는 과학적 표기 100000이 됨
2026-10-30 날짜 또는 문자열 시간대와 형식의 모호함
13800138000 전화번호 11자리에서는 여전히 안전하지만 일부 ID는 정밀도를 잃음
"" 빈 문자열 또는 null 의미의 모호함

권장 기본값: 모든 것을 문자열로 취급하고, 스키마나 명시적 열 정의가 어떤 열을 숫자나 불리언으로 만들지 결정하게 하세요. "똑똑한 추론"은 내보낼 때는 즐겁고 가져올 때는 사고의 원천입니다.

type ColumnType = 'string' | 'number' | 'boolean' | 'json';

/**
 * CSV 행을 열 정의에 따라 객체로 변환합니다.
 * @param rows 원시 행 배열, 첫 행은 헤더
 * @param columnTypes 열 이름에서 타입으로의 매핑
 * @returns 변환된 객체 배열
 */
function rowsToRecords(
  rows: string[][],
  columnTypes: Record<string, ColumnType>,
): Array<Record<string, unknown>> {
  const [headerRow, ...dataRows] = rows;

  return dataRows.map((dataRow) => {
    const record: Record<string, unknown> = {};

    headerRow.forEach((columnName, columnIndex) => {
      const rawValue = dataRow[columnIndex] ?? '';
      const columnType = columnTypes[columnName] ?? 'string';

      if (columnType === 'number') {
        const numericValue = Number(rawValue);
        record[columnName] = Number.isFinite(numericValue) ? numericValue : null;
        return;
      }
      if (columnType === 'boolean') {
        record[columnName] = rawValue === 'true' || rawValue === '1';
        return;
      }
      if (columnType === 'json') {
        try {
          record[columnName] = JSON.parse(rawValue);
        } catch {
          record[columnName] = rawValue;
        }
        return;
      }
      record[columnName] = rawValue;
    });

    return record;
  });
}

4. Excel 관련 세 가지 함정

4.1 인코딩

Excel이 BOM 없는 UTF-8 CSV를 열면 중국어 텍스트가 깨져 보입니다. 내보낼 때 BOM(\uFEFF)을 더하는 것이 가장 저렴한 호환 수정입니다:

const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;

4.2 긴 숫자가 과학적 표기로 바뀜

18자리 주민등록번호나 19자리 주문 번호는 Excel에서 1.23457E+17로 표시되고, 값 자체가 이미 잘렸습니다. 필드 앞에 탭을 붙이거나 수식(="1234...")으로 감싸면 텍스트 서식이 유지되지만 셀이 수식이 됩니다. 더 안전한 접근은 사용자에게 "가져오기 기능을 쓰고 열을 텍스트로 지정하라"고 안내하거나, 내보내기와 함께 .xlsx를 제공하는 것입니다.

4.3 로케일에 영향받는 구분자

일부 유럽 로케일은 세미콜론 ;을 기본 구분자로 씁니다. 여러 지역에 배포할 때는 CSV를 쉼표로 고정하고 그렇게 밝히거나, TSV(탭 구분) 버전을 제공하세요. TSV가 로케일에 더 관대합니다.

5. 큰 파일 처리

10만 행을 넘는 CSV를 한 번에 메모리에 올리지 마세요:

import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';

const parser = createReadStream('orders.csv').pipe(
  parse({ columns: true, skipEmptyLines: true, bom: true }),
);

for await (const record of parser) {
  // Process one record at a time; memory usage is independent of file size
}

내보내기 방향도 같습니다. 하나의 거대한 문자열을 잇는 대신 처리하면서 스트림에 생성하고 쓰세요.

6. 왕복 일관성 자체 점검

CSV 왕복(JSON → CSV → JSON)은 거의 정확히 동일하지 않지만, 어떤 손실이 허용되는지 정의할 수 있습니다:

점검 기대
필드 수 동일
문자열 값 동일(앞의 0 포함)
숫자 타입 추론이 아니라 열 정의에 의해 결정
중첩 객체 점 경로로 복구 가능
배열 JSON 문자열로 직렬화한 뒤 보임
빈 값 null 또는 빈 문자열로 정규화 — 정확히 하나를 고르세요

이 표를 테스트에 넣는 것이 말로 된 합의보다 훨씬 효과적입니다.

7. 체크리스트

  • [ ] "한 행이 하나의 레코드"라는 의미가 명시되고 내보내기 파일 이름에 반영됩니다
  • [ ] 정밀도를 잃을 수 있는 모든 열이 문자열로 내보내집니다
  • [ ] 중국어 CSV에 BOM이 포함되거나 인코딩이 명시됩니다
  • [ ] 쉼표, 따옴표, 줄 바꿈이 포함된 필드가 올바르게 이스케이프됩니다(라이브러리를 쓰고 문자열을 직접 잇지 마세요)
  • [ ] 큰 파일이 스트리밍 읽기와 쓰기를 거칩니다
  • [ ] 가져오기가 열 정의에 따라 타입을 변환하고 전역 스마트 추론을 쓰지 않습니다

FAQ

평탄화, 행 확장, 테이블 분할 중 어떻게 고르나?

사람을 위한 내보내기에는 평탄화를 쓰세요. 중첩 키를 .으로 잇고 배열을 JSON.stringify로 셀에 넣으며, 그 배열을 스프레드시트에서 필터링할 수 없다는 비용을 받아들입니다. 대상이 데이터베이스라면 부모 테이블과 관련 테이블로 분할하세요. 하류 소비자가 요소 단위로 집계한다면 배열을 추가 행으로 확장하고 중복 부모 필드를 받아들입니다.

CSV 가져오기가 왜 타입을 자동 추론하면 안 되나?

RFC 4180은 구분자와 이스케이프만 규정하고 타입은 규정하지 않으므로, 모든 복원 단계가 추측입니다. 00123은 앞의 0을 잃고, 1.10은 1.1이 되며, 1e5는 100000이 됩니다. 기본적으로 모든 것을 문자열로 취급하고, 스키마나 명시적 열 정의가 어떤 열을 숫자나 불리언으로 만들지 결정하게 하세요.

CSV 왕복이 원본 JSON을 정확히 재현할 거라 기대해야 하나?

거의 아닙니다. 대신 허용 가능한 손실을 정의하세요. 필드 수 동일, 앞의 0을 포함한 문자열 값 동일, 숫자 타입은 열 정의에 의해 결정, 중첩 객체는 점 경로로 복구 가능, 배열은 직렬화 후 보임, 빈 값은 null 또는 빈 문자열로 정규화 — 정확히 하나를 고르세요.