it-box

CSV를 JSON으로 바꿀 때 생기는 문제 정리

엑셀 CSV의 CP949와 BOM 문제, 앞자리 0 손실, 따옴표와 줄바꿈 처리, 날짜 자동 변환, 중첩 JSON을 CSV로 펼칠 때의 실수를 도구 동작과 함께 정리합니다.

변환 전에 파일부터 확인합니다

업무에서 받는 CSV는 대부분 엑셀에서 저장한 파일입니다. 파일 한 장을 JSON으로 바꾸는 일은 금방 끝나지만, 바꾼 결과가 틀려 있어도 겉으로는 티가 나지 않습니다. 변환 도구에 넣기 전에 인코딩, 구분자, 앞자리 0, 날짜 값을 먼저 확인하면 대부분의 사고를 막을 수 있습니다.

CSV를 JSON으로 변환하는 도구는 쉼표 또는 탭을 구분자로 골라 JSON 배열을 만듭니다. 첫 줄을 헤더로 쓰고, 모든 값을 문자열로 둡니다. 입력은 UTF-8 기준 2MB까지 처리하도록 제한되어 있습니다. 이 도구의 동작을 기준으로 아래 문제를 설명합니다.

인코딩: CP949와 UTF-8

한국어 윈도우에서 엑셀의 기본 CSV 저장 형식은 CP949(MS949) 계열 인코딩으로 알려져 있습니다. 옵션을 바꿔 UTF-8로 저장하면 파일 맨 앞에 BOM이 붙는 경우가 많습니다. 파일 확장자는 같은 .csv라서 겉으로는 구별되지 않습니다.

같은 글자 한글이 두 인코딩에서 어떤 바이트가 되는지 확인해 보면 차이가 분명합니다.

// UTF-8: 한글 = ED 95 9C EA B8 80
// CP949(EUC-KR 호환): 한글 = C7 D1 B1 DB

new TextDecoder('euc-kr').decode(Buffer.from([0xc7, 0xd1, 0xb1, 0xdb]))  // '한글'
new TextDecoder('utf-8').decode(Buffer.from([0xc7, 0xd1, 0xb1, 0xdb]))   // '�ѱ�'

CP949 파일을 UTF-8로 읽으면 글자가 깨지고, 깨진 글자 자리에는 대체 문자(U+FFFD)가 들어갑니다. 이 상태로 JSON을 만들면 키와 값이 모두 깨진 채 저장됩니다. 파일을 열었을 때 한글이 정상이면 인코딩을 맞춘 것이고, 깨져 보이면 변환 전에 다시 저장해야 합니다.

BOM은 EF BB BF 세 바이트입니다. 보이지 않는 문자이지만 문자열 맨 앞에 붙습니다. CSV를 JSON으로 변환하는 도구는 입력 맨 앞의 BOM을 제거한 뒤 파싱합니다. 직접 파서를 쓴다면 같은 처리를 넣어야 합니다. 빠뜨리면 첫 번째 헤더가 name처럼 되어 name으로 찾는 코드가 실패합니다.

앞자리 0은 사라지기 쉽습니다

우편번호 04524, 전화번호 01012345678, 사원번호 0012는 숫자처럼 보이지만 식별자입니다. 엑셀이 셀을 숫자로 인식하면 앞자리 0이 사라지고 4524가 됩니다. 이 값이 이미 CSV에 기록된 뒤라면 복구할 수 없습니다.

변환 도구는 값을 모두 문자열로 두기 때문에 이 문제가 JSON 단계에서는 생기지 않습니다.

[
  {
    "zip": "04524",
    "name": "Kim"
  }
]

엑셀에서 열 때는 해당 열의 서식을 텍스트로 지정하거나, 데이터 가져오기 단계에서 열 형식을 텍스트로 정하세요. 이미 저장된 파일은 원본을 다시 받는 것이 가장 안전합니다.

JSON으로 바꾼 뒤에도 주의할 점이 있습니다. 값이 문자열이므로 "age": "30"은 숫자 30이 아닙니다. 나이 합계를 낼 때는 명시적으로 숫자로 바꿔야 합니다.

따옴표, 쉼표, 줄바꿈

CSV 규칙에서 쉼표나 줄바꿈이 값 안에 들어가면 큰따옴표로 감쌉니다. 값 안의 큰따옴표는 두 개("")로 씁니다. 변환 도구는 이 규칙을 따릅니다.

name,note
Ada,"hello, world"
Lin,"quoted ""text"""

위 입력은 다음과 같이 바뀝니다.

[
  {
    "name": "Ada",
    "note": "hello, world"
  },
  {
    "name": "Lin",
    "note": "quoted \"text\""
  }
]

따옴표 안의 줄바꿈도 값의 일부로 유지됩니다. "line1\nline2"는 JSON에서 "line1\\nline2"로 표시됩니다. 이 값을 다시 텍스트 파일로 쓸 때는 줄바꿈이 두 줄로 나뉘는지 확인하세요.

도구는 다음 경우에 오류를 냅니다. 오류 메시지를 보고 원본을 고치는 편이 낫습니다.

  • 헤더 줄의 빈 이름이나 중복 이름
  • 어떤 행의 칸 수가 헤더와 다른 경우 (예: 1,2,3을 a,b 헤더에 넣은 경우)
  • 닫히지 않은 따옴표
  • 따옴표로 감싸지 않은 칸 안에 따옴표가 들어 있거나, 닫는 따옴표 뒤에 다른 글자가 붙은 경우

헤더가 중복되면 객체 키가 하나로 합쳐지므로 조용히 값을 잃을 수 있습니다. 도구가 이를 막아 주는 것은 다행입니다.

탭으로 구분된 파일은 구분자를 탭으로 바꿔서 같은 도구에 넣습니다. 탭 안의 값은 따옴표로 감싸야 합니다.

날짜 자동 변환

엑셀은 3-4, 1/2처럼 날짜처럼 보이는 문자열을 날짜 값으로 바꿉니다. 저장된 CSV에는 날짜의 일련번호나 바뀐 표기가 남기도 합니다. 원래 "3-4"라는 상품 코드였다면 복구하기 어렵습니다.

CSV를 열 때 날짜 자동 변환을 피하려면 가져오기 기능에서 열 유형을 텍스트로 지정하는 것이 가장 확실합니다. 이미 변환된 값을 되돌리는 것은 원본 CSV를 다시 받아 확인하는 것이 낫습니다.

JSON으로 바꾼 뒤에도 날짜 문자열은 문자열입니다. 날짜 필드를 비교하거나 정렬하려면 형식을 명시해 Date로 바꿔야 합니다. 형식이 2026-11-10인지 2026/11/10인지 섞여 있는 파일에서는 파서가 다르게 해석할 수 있습니다.

중첩 JSON을 CSV로 펼칠 때

JSON 구조가 중첩되어 있으면 CSV로 바로 옮길 수 없습니다. CSV는 한 행에 평평한 칸만 담을 수 있습니다. JSON을 CSV로 변환하는 도구는 최상위가 객체의 배열이어야 하며, 중첩 구조를 펼치지 않습니다. 다음 입력을 넣으면 결과를 보고 놀랄 수 있습니다.

[{ "user": { "id": 1, "name": "Kim" }, "tags": ["x", "y"] }]
user,tags
[object Object],"x,y"

객체는 문자열로 바뀔 때 [object Object]가 되고, 배열은 x,y로 합쳐져 쉼표가 들어가므로 따옴표로 감싸집니다. 값이 빠지지 않았다는 점에서 조용하지만, 쓸모 있는 데이터는 아닙니다.

JSON 표 보기 도구는 같은 입력을 표로 보여 줍니다. 이 도구는 중첩된 값을 JSON 문자열로 넣습니다. 예를 들어 {"active":true}와 ["developer"]처럼 표시됩니다. 없는 키는 —로 표시됩니다. 열은 100개, 전체 칸은 10만 개까지 처리하고, 한 페이지에 50행씩 보여 줍니다. 표로 확인하는 용도에는 충분하지만, 구조를 CSV로 옮기는 용도에는 평탄화 과정이 따로 필요합니다.

평탄화는 직접 작성하는 편이 명확합니다. 객체는 상위.하위 형태의 키로 펼치고, 배열은 JSON 문자열로 보관합니다.

const flatten = (obj, prefix = '') =>
  Object.entries(obj).reduce((acc, [k, v]) => {
    const key = prefix ? `${prefix}.${k}` : k;
    if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(acc, flatten(v, key));
    else acc[key] = Array.isArray(v) ? JSON.stringify(v) : v;
    return acc;
  }, {});

flatten({ user: { id: 1, name: 'Kim' }, tags: ['x', 'y'], ok: true });
// { 'user.id': 1, 'user.name': 'Kim', tags: '["x","y"]', ok: true }

이 결과를 JSON-to-CSV 도구에 넣으면 다음처럼 나옵니다.

user.id,user.name,tags,ok
1,Kim,"[\"x\",\"y\"]",true

배열을 문자열로 두면 CSV를 다시 읽을 때 JSON으로 한 번 더 파싱해야 합니다. 배열 원소가 적고 목록 전체를 한 칸에 두어도 괜찮을 때 쓰는 방법입니다. 원소마다 행을 나눠야 한다면 배열을 펼쳐서 행 자체를 늘려야 합니다.

JSON을 CSV로 바꿀 때의 값 규칙

JSON을 CSV로 변환하는 도구는 JSON5로 입력을 읽습니다. 값 하나를 칸에 쓸 때 규칙은 다음과 같습니다. 이 규칙을 알아야 결과를 믿을 수 있습니다.

  • null은 null이라는 글자로 나옵니다.
  • 키가 아예 없는 행은 빈 칸으로 나옵니다.
  • 헤더는 모든 행의 키를 합친 목록이며, 처음 나온 순서를 따릅니다.
  • 쉼표가 들어 있는 값은 큰따옴표로 감쌉니다.
  • 줄바꿈은 실제 줄바꿈이 아니라 \n이라는 두 글자로 바뀝니다.
  • 큰따옴표는 \"로 바뀌며, CSV 방식인 ""로 바뀌지 않습니다.
convertArrayToCsv({ array: [{ a: null, b: 1 }, { b: 2 }] })
// 'a,b\nnull,1\n,2'

convertArrayToCsv({ array: [{ name: 'He said "hi"', n: 1 }] })
// 'name,n\nHe said \"hi\",1'

두 번째 예제의 결과 행은 따옴표로 감싸지 않은 채 \"가 들어 있습니다. 표준 CSV 파서는 이 행을 이상하게 읽습니다. 따옴표가 포함된 값을 엑셀이나 다른 시스템에 넘겨야 한다면, 이 도구의 출력을 그대로 쓰지 말고 CSV 표준을 따르는 라이브러리로 저장하세요.

또 최상위 값이 배열이 아니면 도구는 오류 대신 빈 결과를 보여 줍니다. JSON 형식은 맞지만 객체 하나만 넣었을 때 출력이 비어 보이면 최상위 구조를 먼저 확인하세요.

흔한 실수

  • 엑셀에서 파일을 열고 저장한 뒤 인코딩이 바뀐 것을 모릅니다.
  • 앞자리 0이 있는 값을 숫자 열로 열어 원본 값을 잃습니다.
  • 변환 결과를 숫자 연산에 그대로 씁니다. 값은 문자열입니다.
  • 중첩 객체를 CSV로 바로 내보내고 [object Object]를 데이터로 받아들입니다.
  • null과 빈 칸을 같은 값으로 취급합니다. 이 도구에서는 다르게 나옵니다.
  • 따옴표가 있는 값을 JSON-to-CSV로 만들고 표준 CSV 파서로 읽습니다.
  • 날짜 열을 파서가 임의로 해석하게 둡니다.

점검 목록

  • 파일 인코딩을 확인합니다. 한글이 깨지면 CP949 또는 UTF-8을 다시 맞춥니다.
  • BOM이 있는지 확인하고, 파서가 제거하는지 확인합니다.
  • 식별자 열(우편번호, 전화번호, 사원번호)의 앞자리 0이 보존됐는지 봅니다.
  • 날짜처럼 보이는 값이 원래 값과 같은지 원본과 대조합니다.
  • 헤더 수와 행의 칸 수가 맞는지, 중복 헤더가 없는지 확인합니다.
  • 중첩 구조는 평탄화 방식을 먼저 정하고 변환합니다.
  • JSON-to-CSV 출력을 외부 시스템에 넘긴다면 따옴표와 줄바꿈 규칙을 표준 CSV와 비교합니다.
  • 결과 행 수가 원본 행 수와 같은지 확인합니다.

관련 도구