CSV to JSON JavaScript 가이드

·Front-end & Node.js Developer·검토자Sophie Laurent·게시일

무료 CSV to JSON 변환기을 브라우저에서 직접 사용하세요 — 설치 불필요.

CSV to JSON 변환기 온라인으로 사용하기 →

실제로 접하는 CSV 데이터는 대부분 파일 업로드, 데이터베이스 내보내기, 또는 1970년대 형식을 여전히 사용하는 API에서 평평한 문자열로 도착합니다. JavaScript에서 CSV를 JSON으로 변환하려면 언어가 기본 제공하는 두 가지가 필요합니다: 행을 파싱하는 문자열 분할과 JSON.stringify() 결과 직렬화입니다. 기본적인 용도에는 npm 패키지가 필요 없습니다 — 이 가이드는 재사용 가능한 csvToJson() 유틸리티부터 PapaParse와 Node.js 파일 I/O까지 전체 파이프라인을 다룹니다. 코드 없이 빠르게 변환하려면 온라인 CSV to JSON 변환기 를 사용하면 즉시 처리됩니다. 모든 예제는 Node.js 18+와 최신 브라우저를 대상으로 합니다.

  • CSV를 줄바꿈으로 분할하고, 첫 번째 행에서 헤더를 추출하고, 나머지 행을 객체로 변환한 다음 JSON.stringify(array, null, 2)로 보기 좋은 출력을 만듭니다.
  • JSON.stringify()는 문자열을 반환하고, JSON.parse()는 다시 살아있는 JavaScript 배열로 변환합니다 — 어느 것을 다루고 있는지 파악한 후 작업하세요.
  • Map 인스턴스는 자동으로 JSON으로 직렬화되지 않습니다 — 먼저 Object.fromEntries(map)을 호출하세요.
  • 따옴표 처리된 필드, 값 안의 쉼표, 셀 안의 줄바꿈이 있는 CSV에는 수동 분할 대신 PapaParse나 csv-parse를 사용하세요.
  • csvtojson (npm)은 단일 호출로 타입 강제 변환, 스트리밍, RFC 4180 엣지 케이스를 모두 처리합니다.

CSV to JSON 변환이란?

CSV to JSON 변환은 평평한 쉼표 구분 텍스트 형식을 각 행이 열 헤더를 키로 하는 JavaScript 객체가 되는 구조화된 객체 배열로 변환합니다. CSV 형식에는 데이터 타입이 없습니다 — 모든 것이 문자열입니다. JSON은 구조, 중첩, 명시적 타입(숫자, 불리언, null)을 추가합니다. 이 변환은 스프레드시트 내보내기, 레거시 시스템 덤프, 또는 사용자 업로드 파일로 시작하는 거의 모든 데이터 파이프라인의 첫 번째 단계입니다. 기반 데이터는 동일하게 유지되고, 형식만 위치 기반 열에서 이름 있는 속성으로 변경됩니다.

Before · json
After · json
name,email,role,active
김민준,minjun@nexuslabs.io,Engineering Lead,true
이서연,seoyeon@nexuslabs.io,Product Manager,true
[
  {
    "name": "김민준",
    "email": "minjun@nexuslabs.io",
    "role": "Engineering Lead",
    "active": "true"
  },
  {
    "name": "이서연",
    "email": "seoyeon@nexuslabs.io",
    "role": "Product Manager",
    "active": "true"
  }
]

csvToJson() — 재사용 가능한 변환 함수 만들기

JavaScript의 CSV-to-JSON 파이프라인 전체는 세 단계로 나뉩니다: CSV 문자열을 줄바꿈으로 분할하여 행을 가져오고, 첫 번째 행에서 split(',')으로 헤더를 추출한 다음, 나머지 각 행을 헤더를 키로, 해당 열 위치의 값을 값으로 하는 순수 JavaScript 객체로 변환합니다. 마지막 JSON.stringify() 호출이 객체 배열을 JSON 문자열로 변환합니다. 다음은 최소한의 동작 버전입니다:

JavaScript — 최소한의 csvToJson
function csvToJson(csv) {
  const lines = csv.trim().split('\n')
  const headers = lines[0].split(',').map(h => h.trim())

  const rows = lines.slice(1)
    .filter(line => line.trim() !== '')
    .map(line => {
      const values = line.split(',')
      return Object.fromEntries(
        headers.map((header, i) => [header, values[i]?.trim()])
      )
    })

  return JSON.stringify(rows, null, 2)
}

const csv = `server,port,region,status
api-gateway,8080,us-east-1,healthy
auth-service,8443,eu-west-1,degraded
payments-api,9090,ap-south-1,healthy`

console.log(csvToJson(csv))
// [
//   { "server": "api-gateway", "port": "8080", "region": "us-east-1", "status": "healthy" },
//   { "server": "auth-service", "port": "8443", "region": "eu-west-1", "status": "degraded" },
//   { "server": "payments-api", "port": "9090", "region": "ap-south-1", "status": "healthy" }
// ]

이 함수는 기본 사항을 처리합니다: 끝의 줄바꿈, 빈 줄, 값 주변의 공백. 모든 CSV 필드는 문자열로 전달됩니다. port 8080 (숫자)이 아닌 "8080" (문자열)임을 주의하세요. JSON 출력에 올바른 타입이 필요하면 직접 강제 변환해야 합니다. 다음은 타입 감지 기능이 포함된 확장 버전입니다:

JavaScript — 타입 강제 변환이 있는 csvToJson
function coerceValue(val) {
  if (val === undefined || val === '') return null
  if (val === 'true') return true
  if (val === 'false') return false
  if (val === 'null') return null
  const num = Number(val)
  if (!isNaN(num) && val.trim() !== '') return num
  return val
}

function csvToJson(csv, { coerce = false } = {}) {
  const lines = csv.trim().split('\n')
  const headers = lines[0].split(',').map(h => h.trim())

  const rows = lines.slice(1)
    .filter(line => line.trim() !== '')
    .map(line => {
      const values = line.split(',')
      return Object.fromEntries(
        headers.map((header, i) => [
          header,
          coerce ? coerceValue(values[i]?.trim()) : values[i]?.trim()
        ])
      )
    })

  return JSON.stringify(rows, null, 2)
}

const csv = `endpoint,port,max_connections,debug
/api/v2/orders,8443,500,true
/api/v2/health,8080,100,false`

console.log(csvToJson(csv, { coerce: true }))
// [
//   { "endpoint": "/api/v2/orders", "port": 8443, "max_connections": 500, "debug": true },
//   { "endpoint": "/api/v2/health", "port": 8080, "max_connections": 100, "debug": false }
// ]

coerce 플래그는 선택적으로 활성화하도록 설계되었습니다 — 자동 타입 감지는 역효과를 낼 수 있기 때문입니다. 예를 들어 우편번호("07302")는 숫자로 변환하면 앞의 0이 사라집니다. 스키마를 직접 제어하는 경우에만 강제 변환을 활성화하세요. 참고로: JSON.stringify()는 들여쓰기를 위한 세 번째 space 인수를 받습니다. 두 칸 공백에는 2, 네 칸에는 4, 탭에는 "\t" 를 전달하세요. 공백이 대역폭을 낭비하는 API 요청 본문으로 JSON 문자열을 보낼 때는 완전히 생략하여 간결한 한 줄 출력을 만드세요.

참고:원시 CSV 문자열은 JSON이 아닙니다. CSV 텍스트에 직접 JSON.parse()를 호출하면 쉼표와 줄바꿈이 유효한 JSON 구문이 아니므로 SyntaxError가 발생합니다. 먼저 csvToJson() 함수를 사용하여 CSV를 JavaScript 객체로 변환해야 하며, 이 함수 내부에서 JSON.stringify()를 호출하여 실제 JSON 문자열을 생성합니다.

CSV 데이터의 Map, Date, 커스텀 객체 처리

모든 CSV 변환이 평평한 객체 배열로 끝나는 것은 아닙니다. 때로는 헤더-값 쌍에서 Map을 만들거나, 날짜 문자열을 Date 객체로 파싱하거나, 직렬화 전에 계산된 속성을 추가해야 합니다. JavaScript에는 사람들이 자주 실수하는 부분이 있습니다: Map 인스턴스는 JSON.stringify()로 직렬화되지 않습니다. 빈 객체가 반환됩니다. 해결책은 문자열화하기 전에 Object.fromEntries() 를 사용하여 Map을 순수 객체로 다시 변환하는 것입니다.

Map이 {}로 직렬화되는 이유는 JSON.stringify()가 객체 자체의 열거 가능한 속성을 순회하기 때문입니다. Map은 내부 슬롯에 항목을 저장하지, 객체의 열거 가능한 속성으로 저장하지 않습니다. 따라서 직렬화기는 키가 없는 객체를 봅니다. Map 프로토타입에도 toJSON() 메서드가 없으며, 이것이 JSON.stringify()가 직렬화 방법을 결정하기 전에 모든 값에 먼저 호출하는 훅입니다. 값에 toJSON()가 있으면 객체 자체가 아닌 메서드의 반환 값이 직렬화됩니다. 이것이 Date 객체가 올바르게 직렬화되는 이유입니다: Date.prototype.toJSON이 ISO 8601 문자열을 반환하므로 JSON.stringify(new Date())는 빈 객체 대신 따옴표로 감싸진 타임스탬프를 생성합니다. 이 훅을 이해하면 아래의 EmployeeRecord 예제처럼 자신의 클래스에 동일한 동작을 정의하여 최종 JSON 출력에 어떤 CSV 파생 필드가 나타날지 정확히 제어할 수 있습니다.

Map을 JSON으로 변환하기

JavaScript — Object.fromEntries()를 통한 Map to JSON
// CSV 헤더→값 쌍에서 Map 만들기
const headers = ['server', 'port', 'region']
const values = ['payments-api', '9090', 'ap-south-1']
const rowMap = new Map(headers.map((h, i) => [h, values[i]]))

// Map은 직접 직렬화되지 않음
console.log(JSON.stringify(rowMap))
// "{}"  — 빈 객체, 데이터 손실!

// 먼저 순수 객체로 변환
const rowObj = Object.fromEntries(rowMap)
console.log(JSON.stringify(rowObj, null, 2))
// {
//   "server": "payments-api",
//   "port": "9090",
//   "region": "ap-south-1"
// }

날짜 문자열과 toJSON()

CSV 날짜 필드는 문자열로 도착합니다. 처리 중에 Date 객체로 파싱하면 Date에는 ISO 8601 문자열을 반환하는 내장 toJSON() 메서드가 있으므로 올바르게 직렬화됩니다. 또한 자신의 클래스에 커스텀 toJSON()을 정의하여 직렬화된 출력에 어떤 CSV 열이 나타날지 제어할 수 있습니다 — 예를 들어 _rowIndex같은 내부 추적 필드를 제외하는 것입니다.

JavaScript — 커스텀 직렬화를 위한 toJSON()
class EmployeeRecord {
  constructor(csvRow) {
    this._rowIndex = csvRow._rowIndex  // 내부용, JSON에 포함하지 않음
    this.employeeId = csvRow.employee_id
    this.name = csvRow.name
    this.hiredAt = new Date(csvRow.hired_date)
    this.salary = Number(csvRow.salary)
  }

  toJSON() {
    // JSON 출력에 노출할 필드만 지정
    return {
      employee_id: this.employeeId,
      name: this.name,
      hired_at: this.hiredAt,  // Date.toJSON() → ISO 문자열 자동 변환
      salary: this.salary,
    }
  }
}

const csvRow = {
  _rowIndex: 42,
  employee_id: 'EMP-2847',
  name: '김민준',
  hired_date: '2024-03-15',
  salary: '128000',
}

const record = new EmployeeRecord(csvRow)
console.log(JSON.stringify(record, null, 2))
// {
//   "employee_id": "EMP-2847",
//   "name": "김민준",
//   "hired_at": "2024-03-15T00:00:00.000Z",
//   "salary": 128000
// }
// 참고: _rowIndex는 제외되고, salary는 숫자, date는 ISO 형식

날짜 역직렬화를 위한 Reviver

CSV에서 파생된 JSON을 파일에 쓰거나 네트워크로 전송한 후, JSON.parse()는 순수 객체를 반환합니다 — Date 객체는 다시 문자열이 됩니다. 파싱 중에 ISO 8601 문자열을 다시 Date 객체로 변환하려면 reviver 함수를 사용하세요:

JavaScript — Date 객체를 재구성하는 reviver
const jsonString = '{"employee_id":"EMP-2847","name":"김민준","hired_at":"2024-03-15T00:00:00.000Z","salary":128000}'

const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/

const parsed = JSON.parse(jsonString, (key, value) => {
  if (typeof value === 'string' && isoDatePattern.test(value)) {
    return new Date(value)
  }
  return value
})

console.log(parsed.hired_at instanceof Date)  // true
console.log(parsed.hired_at.getFullYear())    // 2024
경고:JSON.stringify()는 함수, Symbol, 또는 undefined 속성을 포함하는 값에 대해 문자열 대신 undefined를 반환합니다. CSV에서 파생된 객체가 누락된 열로 인해 undefined 값을 가지게 되면, 해당 속성은 JSON 출력에서 조용히 사라집니다. 누락된 값은 항상 null로 기본값을 설정하세요.

JSON.stringify() 매개변수 참조

함수 시그니처는 JSON.stringify(value, replacer?, space?)입니다. replacer space 인수는 모두 선택적입니다 — 들여쓰기만 필요할 때는 replacer에 null을 전달하세요.

매개변수
타입
기본값
설명
value
any
(필수)
직렬화할 JavaScript 값 — 객체, 배열, 문자열, 숫자, 불리언, 또는 null
replacer
Function | Array | null
undefined
직렬화 중 값을 필터링하거나 변환합니다. 배열은 속성 이름을 허용 목록에 추가하고, 함수는 (key, value) 쌍을 받습니다
space
number | string
undefined
들여쓰기: 공백의 경우 0~10 사이의 숫자, 커스텀 들여쓰기는 문자열 (예: "\t"). 간결한 한 줄 출력을 원하면 생략

JSON.parse() 매개변수:

매개변수
타입
기본값
설명
text
string
(필수)
파싱할 JSON 문자열 — 유효한 JSON이어야 하며, 그렇지 않으면 SyntaxError가 발생합니다
reviver
Function | undefined
undefined
각 키-값 쌍에 대해 호출됩니다. 반환된 값이 원본을 대체하며, undefined를 반환하면 해당 속성이 삭제됩니다

JSON.parse() — JSON 출력 소비하기

csvToJson()에서 JSON 문자열을 얻은 후, 다음 단계는 보통 필터링, 매핑, 또는 API에 전달하기 위해 살아있는 JavaScript 배열로 다시 파싱하는 것입니다. JSON 문자열(typeof === "string")과 JavaScript 객체의 차이는 중요합니다. 문자열에는 .filter()를 호출하거나 [0].name에 접근할 수 없습니다 — 먼저 JSON.parse()가 필요합니다. 이 왕복(stringify 후 parse)은 유효성 검사 기법으로도 작동합니다: CSV 변환이 유효하지 않은 JSON을 생성하면 parse가 예외를 던집니다. 선택적인 reviver 인수를 사용하면 파싱 중 각 키-값 쌍을 변환할 수 있습니다 — ISO 문자열 에서 Date 객체를 복원하거나 별도의 패스 없이 키를 이름 바꾸기에 유용합니다.

JavaScript — JSON 출력을 파싱하고 행 조회하기
const csv = `endpoint,method,avg_latency_ms,error_rate
/api/v2/orders,POST,342,0.02
/api/v2/health,GET,12,0.00
/api/v2/payments,POST,890,0.15
/api/v2/users,GET,45,0.01`

// Step 1: CSV를 JSON 문자열로 변환
const jsonString = csvToJson(csv, { coerce: true })

// Step 2: JavaScript 배열로 다시 파싱
const endpoints = JSON.parse(jsonString)

// 배열인지 확인
console.log(Array.isArray(endpoints))  // true

// 지연 시간이 높은 엔드포인트 필터링
const slow = endpoints.filter(ep => ep.avg_latency_ms > 200)
console.log(slow.map(ep => ep.endpoint))
// ["/api/v2/orders", "/api/v2/payments"]

// 첫 번째 행 구조 분해
const [first, ...rest] = endpoints
console.log(first.endpoint)  // "/api/v2/orders"
console.log(rest.length)     // 3

다운스트림 처리 전에 변환 출력을 검증할 때 JSON.parse()의 안전한 래퍼가 유용합니다. CSV 변환이 어떤 이유로든(잘린 입력, 인코딩 오류) 잘못된 JSON을 생성하면, 충돌 없이 오류를 잡습니다:

JavaScript — 안전한 parse 래퍼
function safeParse(jsonString) {
  try {
    return { data: JSON.parse(jsonString), error: null }
  } catch (err) {
    return { data: null, error: err.message }
  }
}

// 유효한 출력
const result = safeParse(csvToJson(csv))
if (result.error) {
  console.error('CSV 변환이 유효하지 않은 JSON을 생성했습니다:', result.error)
} else {
  console.log(`${result.data.length}개 행이 파싱되었습니다`)
}

// 원시 CSV를 JSON.parse에 직접 전달 — 실패
const bad = safeParse('name,email\n김민준,minjun@nexuslabs.io')
console.log(bad.error)  // "Unexpected token 'a', "name,email"... is not valid JSON"

키 이름 변경 및 검증을 위한 Reviver

reviver 함수는 파싱 중 가장 내부 속성부터 바깥쪽으로 모든 키-값 쌍을 받습니다. undefined를 반환하면 해당 키가 결과에서 완전히 제거됩니다. 다른 값을 반환하면 그 값으로 대체됩니다. reviver는 헤더 이름 바꾸기(camelCase를 snake_case로), 내부 필드 제거, 필수 열 존재 여부 확인에 유용합니다. 루트 값(빈 문자열 키)은 마지막으로 호출되며, 결과가 배열이 아닌 경우 여기서 예외를 던집니다.

JavaScript — 키 이름 변경 및 구조 검증을 위한 reviver
const jsonString = csvToJson(`employeeId,firstName,hiredDate
EMP-2847,김민준,2024-03-15
EMP-3012,이서연,2023-11-01`, { coerce: false })

const camelToSnake = str => str.replace(/[A-Z]/g, c => '_' + c.toLowerCase())

const employees = JSON.parse(jsonString, function(key, value) {
  // 루트 값 — 구조 검증
  if (key === '') {
    if (!Array.isArray(value)) throw new Error('CSV에서 JSON 배열이 필요합니다')
    return value
  }
  // camelCase 헤더 키를 snake_case로 이름 변경
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
    return Object.fromEntries(
      Object.entries(value).map(([k, v]) => [camelToSnake(k), v])
    )
  }
  return value
})

console.log(employees[0])
// { employee_id: 'EMP-2847', first_name: '김민준', hired_date: '2024-03-15' }

파일과 API 응답에서 CSV 변환하기

실제 운영 환경에서 CSV 데이터가 오는 두 곳: 디스크의 파일과 HTTP 응답. 두 시나리오 모두 입력이 외부적이고 제어할 수 없으므로 오류 처리가 필요합니다.

CSV 파일 읽기, 변환, JSON 쓰기

Node.js 18+ — 파일 변환
import { readFileSync, writeFileSync } from 'node:fs'

function csvToJsonFromFile(inputPath, outputPath) {
  let csvText
  try {
    csvText = readFileSync(inputPath, 'utf8')
  } catch (err) {
    throw new Error(`${inputPath} 읽기 실패: ${err.message}`)
  }

  const lines = csvText.trim().split('\n')
  if (lines.length < 2) {
    throw new Error(`${inputPath}에 데이터 행이 없습니다 (행 수: ${lines.length})`)
  }

  const headers = lines[0].split(',').map(h => h.trim())
  const rows = lines.slice(1)
    .filter(line => line.trim() !== '')
    .map(line => {
      const values = line.split(',')
      return Object.fromEntries(headers.map((h, i) => [h, values[i]?.trim()]))
    })

  const jsonOutput = JSON.stringify(rows, null, 2)
  writeFileSync(outputPath, jsonOutput, 'utf8')
  console.log(`${rows.length}개 행 변환 완료 → ${outputPath}`)
  return rows
}

// 사용 예
const data = csvToJsonFromFile('inventory.csv', 'inventory.json')
console.log(data[0])
// { sku: "WDG-2847", warehouse: "us-east-1", quantity: "150", ... }

API 엔드포인트에서 CSV 가져오기

Node.js 18+ — API 응답 변환
async function fetchCsvAsJson(url) {
  const response = await fetch(url)
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  }

  const contentType = response.headers.get('content-type') || ''
  if (!contentType.includes('text/csv') && !contentType.includes('text/plain')) {
    console.warn(`예상치 못한 content-type: ${contentType}`)
  }

  const csvText = await response.text()
  const lines = csvText.trim().split('\n')
  const headers = lines[0].split(',').map(h => h.trim())
  const rows = lines.slice(1)
    .filter(line => line.trim() !== '')
    .map(line => {
      const values = line.split(',')
      return Object.fromEntries(headers.map((h, i) => [h, values[i]?.trim()]))
    })

  return rows
}

// 예: 데이터 제공업체에서 환율 CSV 가져오기
try {
  const rates = await fetchCsvAsJson('https://data.ecb.internal/rates/daily.csv')
  console.log(JSON.stringify(rates.slice(0, 3), null, 2))
  // 다운스트림 서비스에 JSON으로 전송
  await fetch('https://api.internal/v2/rates', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ data: rates }),
  })
} catch (err) {
  console.error('환율 동기화 실패:', err.message)
}
참고:JSON.stringify()의 replacer 인수를 사용하면 CSV에서 특정 열만 화이트리스트에 추가할 수 있습니다. 헤더 이름 배열을 전달하여 해당 필드만 포함시키세요: JSON.stringify(rows, ['name', 'email', 'department']). 배열에 없는 속성은 출력에서 조용히 제외됩니다.

커맨드라인 CSV to JSON 변환

Node.js는 인라인 스크립트를 실행할 수 있으며, 스크립트를 작성하지 않고도 CSV-to-JSON 변환을 처리하는 전용 CLI 도구들이 있습니다.

bash — Node.js 한 줄 명령어
# CSV를 Node.js 인라인 스크립트로 파이프
cat servers.csv | node -e "
  const lines = require('fs').readFileSync('/dev/stdin','utf8').trim().split('\n');
  const h = lines[0].split(',');
  const rows = lines.slice(1).map(l => Object.fromEntries(h.map((k,i) => [k.trim(), l.split(',')[i]?.trim()])));
  console.log(JSON.stringify(rows, null, 2));
"
bash — CSV to JSON에 Miller (mlr) 사용
# Miller는 구조화된 데이터를 위한 만능 도구입니다
# 설치: brew install miller (macOS) 또는 apt install miller (Debian/Ubuntu)
mlr --icsv --ojson cat inventory.csv

# 변환 중 행 필터링
mlr --icsv --ojson filter '$quantity > 100' inventory.csv

# 특정 열 선택
mlr --icsv --ojson cut -f sku,warehouse,quantity inventory.csv
bash — csvtojson CLI
# 전역 설치
npm install -g csvtojson

# 파일 변환
csvtojson servers.csv > servers.json

# stdin에서 파이프
cat exports/q1-metrics.csv | csvtojson > q1-metrics.json

대용량 파일에는 보통 csvtojson보다 Miller가 더 좋은 선택입니다. Miller는 C로 구현되어 있으며 전체 파일을 메모리에 로드하지 않고 CSV를 스트림으로 처리하므로, 수 기가바이트의 내보내기를 일정한 메모리 사용량으로 처리합니다. 또한 데이터가 JSON이 되기 전에 인플레이스 필드 수준 작업 — 열 이름 바꾸기, 값 타입 변환, 행 필터링 — 을 지원하므로 두 단계의 파싱-후-변환 파이프라인을 피할 수 있습니다. 반면 csvtojson은 Node.js에서 실행되므로 나머지 툴체인이 JavaScript인 경우 더 편리합니다: 출력을 Node 스트림으로 직접 파이프하거나, 라이브러리로 가져오거나, 코드에서 열별 타입 강제 변환을 위한 colParser API를 사용할 수 있습니다. 순수 처리량과 셸 파이프라인에는 Miller를, Node.js 애플리케이션과 긴밀한 통합이 필요할 때는 csvtojson을 선호하세요.

참고:jq는 CSV를 기본적으로 파싱하지 않습니다. 파이프라인에서 jq가 필요하다면 먼저 csvtojson 또는 mlr로 JSON으로 변환한 다음, JSON 출력을 jq에 파이프하여 필터링 및 변환을 수행하세요.

고성능 대안 — PapaParse

수동 split(',') 방식은 실제 CSV 파일에서 실패합니다. 쉼표가 포함된 따옴표 처리 필드, 내장된 줄바꿈, 이스케이프된 큰따옴표 — 이 모두가 단순 분할기를 망가뜨립니다. PapaParse는 CSV가 알 수 없는 출처에서 올 때 제가 선택하는 라이브러리입니다. 모든 RFC 4180 엣지 케이스를 처리하고, 구분자를 자동 감지하며, Node.js와 브라우저 모두에서 작동합니다.

bash — PapaParse 설치
npm install papaparse
JavaScript — 타입 강제 변환이 있는 PapaParse
import Papa from 'papaparse'

const csv = `product,description,price,in_stock
"Widget, Large","A premium widget with ""extra"" features",29.99,true
Bolt Assembly,Standard M8 bolt kit,4.50,true
"Gasket Set","Includes gasket, seal, and O-ring",12.75,false`

const { data, errors, meta } = Papa.parse(csv, {
  header: true,
  dynamicTyping: true,     // 숫자와 불리언 자동 변환
  skipEmptyLines: true,
  transformHeader: h => h.trim().toLowerCase().replace(/\s+/g, '_'),
})

if (errors.length > 0) {
  console.error('파싱 오류:', errors)
}

console.log(JSON.stringify(data, null, 2))
// [
//   {
//     "product": "Widget, Large",
//     "description": "A premium widget with \"extra\" features",
//     "price": 29.99,
//     "in_stock": true
//   },
//   ...
// ]
console.log(`${data.length}개 행 파싱 완료, 구분자: "${meta.delimiter}"`)

PapaParse의 dynamicTyping 옵션은 타입 강제 변환을 자동으로 처리합니다 — 숫자는 숫자가 되고, "true"/"false"는 불리언이 됩니다. transformHeader 콜백은 열 이름을 snake_case로 정규화하여 다양한 CSV 내보내기에서 일관성 없는 헤더를 처리할 필요가 없게 합니다. 파싱 코드를 작성하지 않고 빠르게 변환하려면 CSV to JSON 변환기가 브라우저에서 이 모든 것을 처리합니다.

터미널 출력에 구문 강조 추가하기

대용량 JSON 배열을 터미널에 출력하면 금방 눈이 침침해집니다. 출력에 구문 강조를 추가하면 디버깅 및 개발 중에 가독성이 높아집니다. cli-highlight 패키지는 Node.js 터미널에서 JSON 출력을 색상으로 표시합니다.

bash — cli-highlight 설치
npm install cli-highlight
JavaScript — 터미널에서 색상이 있는 JSON 출력
import { highlight } from 'cli-highlight'

// CSV를 JSON 배열로 변환한 후
const jsonOutput = JSON.stringify(rows, null, 2)

// 구문 강조와 함께 출력
console.log(highlight(jsonOutput, { language: 'json' }))
// 키, 문자열, 숫자, 불리언이 각각 다른 색상으로 표시됩니다

색상이 있는 출력은 대용량 변환 결과를 대화식으로 검사할 때 진가를 발휘합니다. JSON 키, 문자열 값, 숫자, 불리언이 각각 다른 ANSI 색상을 가지므로, 예를 들어 포트 번호가 강제 변환이 비활성화되어 문자열로 강조 표시될 때처럼 타입이 잘못된 필드를 쉽게 발견할 수 있습니다. 특히 행 간에 열 타입이 일관성 없는 스프레드시트 도구에서 내보낸 CSV 파일을 디버깅할 때 유용합니다. 색상 없이 50개 행의 JSON에서 잘못 입력된 단일 필드를 찾으려면 모든 값을 개별적으로 읽어야 합니다. 색상이 있으면 문자열로 색상이 지정된 숫자가 즉시 눈에 띕니다.

경고:구문 강조는 출력에 ANSI 이스케이프 코드를 추가합니다. 파일에 JSON을 쓰거나, 다른 프로그램으로 파이프하거나, API 응답 본문으로 보낼 때는 사용하지 마세요 — 이스케이프 코드가 JSON을 손상시킵니다. 터미널 표시에만 강조 표시를 사용하세요.

대용량 CSV 파일 처리하기

500MB CSV 파일을 readFileSync()로 문자열에 로드하면 메모리를 잡아먹고 잠재적으로 프로세스가 충돌합니다. 대용량 파일의 경우 CSV를 한 줄씩 스트리밍하고 JSON 객체가 도착하는 대로 내보내세요. csv-parse 패키지(npm의 csv 생태계 일부)는 Node.js 스트림과 함께 작동하는 스트리밍 파서를 제공합니다.

csv-parse로 CSV를 NDJSON으로 스트리밍하기

NDJSON(줄바꿈 구분 JSON)은 출력 파일의 각 줄이 독립적인 JSON 객체인 형식입니다. 전체 파일이 메모리에 있어야 읽기 시작할 수 있는 단일 대용량 JSON 배열과 달리, NDJSON 파일은 한 줄씩 처리할 수 있습니다. 이는 NDJSON을 로그 프로세서, 스트림 파이프라인, 또는 대량 가져오기 API가 있는 데이터베이스에서 소비할 대용량 데이터셋에 이상적으로 만듭니다. csv-parse패키지는 객체 모드에서 CSV 행당 하나의 JavaScript 객체를 내보내므로, 각 JSON.stringify(row)뒤에 \n을 추가하는 변환 스트림으로 직접 파이프할 수 있습니다.

Node.js 18+ — CSV를 NDJSON으로 스트리밍
import { createReadStream, createWriteStream } from 'node:fs'
import { parse } from 'csv-parse'
import { Transform } from 'node:stream'
import { pipeline } from 'node:stream/promises'

// 각 CSV 행 객체를 JSON 줄로 변환
const toNdjson = new Transform({
  objectMode: true,
  transform(record, encoding, callback) {
    callback(null, JSON.stringify(record) + '\n')
  },
})

await pipeline(
  createReadStream('telemetry-2026-03.csv'),
  parse({
    columns: true,       // 첫 번째 행을 헤더로 사용
    skip_empty_lines: true,
    trim: true,
    cast: true,          // 숫자와 불리언 자동 변환
  }),
  toNdjson,
  createWriteStream('telemetry-2026-03.ndjson')
)

console.log('스트리밍 변환 완료')
// 출력 파일의 각 줄은 하나의 JSON 객체입니다:
// {"timestamp":"2026-03-15T08:22:00Z","service":"gateway","latency_ms":42,"status":200}
// {"timestamp":"2026-03-15T08:22:01Z","service":"auth","latency_ms":156,"status":401}
// ...

브라우저와 Node.js를 위한 PapaParse 스트리밍

PapaParse의 스트리밍 모드는 모든 행을 메모리에 수집하지 않고 행당 한 번씩 실행되는 step 콜백을 사용합니다. Node.js ReadStream(Node.js에서) 또는 File 객체(브라우저에서)를 전달하면 PapaParse가 내부적으로 청킹을 처리합니다. 연결할 스트림 파이프라인이 필요 없습니다 — 콜백만 있으면 됩니다. csv-parse 없이 RFC 4180 준수가 필요할 때 사용하세요.

Node.js — PapaParse로 대용량 파일 스트리밍
import Papa from 'papaparse'
import { createReadStream } from 'node:fs'

let rowCount = 0
const errors = []

const fileStream = createReadStream('warehouse-inventory.csv')

Papa.parse(fileStream, {
  header: true,
  dynamicTyping: true,
  step(result) {
    // 한 번에 한 행씩 처리 — 일정한 메모리 사용
    rowCount++
    if (result.data.quantity === 0) {
      errors.push(`행 ${rowCount}: ${result.data.sku} 재고 없음`)
    }
  },
  complete() {
    console.log(`${rowCount}개 행 처리 완료`)
    if (errors.length > 0) {
      console.log(`발견된 문제: ${errors.length}개`)
      errors.forEach(e => console.log(`  ${e}`))
    }
  },
  error(err) {
    console.error('파싱 실패:', err.message)
  },
})
참고:CSV 파일이 50MB를 초과하거나 무한한 입력(WebSocket 피드, server-sent events, 파이프된 stdin)을 처리할 때 스트리밍으로 전환하세요. NDJSON 형식(줄당 하나의 JSON 객체)은 대용량 데이터셋에서 전체 파일을 메모리에 로드하지 않고 줄별로 처리할 수 있어 단일 대용량 JSON 배열보다 더 나은 출력 형식인 경우가 많습니다.

자주 하는 실수

CSV 문자열에 직접 JSON.parse() 호출하기

문제: CSV는 JSON이 아닙니다. 원시 CSV 문자열을 JSON.parse()에 전달하면 쉼표와 줄바꿈이 유효한 JSON 구문이 아니므로 SyntaxError가 발생합니다.

해결: 먼저 split() 또는 라이브러리를 사용하여 CSV를 JavaScript 객체로 파싱한 다음, JSON.stringify()를 사용하여 JSON을 생성하세요. 이미 유효한 JSON인 문자열에만 JSON.parse()를 호출하세요.

Before · JavaScript
After · JavaScript
const csv = 'name,email\n김민준,minjun@nexuslabs.io'
const data = JSON.parse(csv)
// SyntaxError: Unexpected token 'a'
const csv = 'name,email\n김민준,minjun@nexuslabs.io'
const lines = csv.trim().split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1).map(line =>
  Object.fromEntries(headers.map((h, i) => [h, line.split(',')[i]]))
)
const json = JSON.stringify(rows, null, 2)  // 유효한 JSON 문자열
JSON.stringify() 대신 toString() 사용하기

문제: JavaScript 객체에 toString()을 호출하면 실제 데이터 대신 쓸모없는 문자열 '[object Object]'가 반환됩니다. 이는 CSV-to-JSON 출력을 조용히 파괴합니다.

해결: JavaScript 객체를 JSON 문자열로 변환할 때는 항상 JSON.stringify()를 사용하세요. toString()은 기본 타입의 문자열 강제 변환을 위한 것이지 직렬화를 위한 것이 아닙니다.

Before · JavaScript
After · JavaScript
const row = { server: 'api-gateway', port: 8080 }
const output = row.toString()
// "[object Object]"  — 데이터가 사라짐
const row = { server: 'api-gateway', port: 8080 }
const output = JSON.stringify(row, null, 2)
// '{"server":"api-gateway","port":8080}'
따옴표 처리된 필드를 고려하지 않고 쉼표로 분할하기

문제: 단순한 split(",")은 따옴표 처리된 필드 내에 쉼표가 있는 CSV 값에서 오작동합니다: "Widget, Large"는 하나가 아닌 두 개의 값이 됩니다.

해결: 직접 제어하지 않는 CSV 데이터에는 PapaParse나 csv-parse를 사용하세요. 수동으로 파싱해야 한다면 현재 위치가 따옴표 처리된 필드 안에 있는지 추적하는 상태 머신 파서를 구현하세요.

Before · JavaScript
After · JavaScript
const line = '"Widget, Large","Premium quality",29.99'
const values = line.split(',')
// ["\"Widget", " Large\"", "\"Premium quality\"", "29.99"]
// 3개가 아닌 4개 값 — 첫 번째 필드가 잘못 분할됨
import Papa from 'papaparse'
const { data } = Papa.parse('"Widget, Large","Premium quality",29.99')
// data[0] = ["Widget, Large", "Premium quality", "29.99"]
// 3개 값, 올바르게 파싱됨
모든 CSV 값이 문자열임을 잊어버리기

문제: 타입 강제 변환 없이는 port: "8080"이 숫자 대신 문자열로 JSON에 남습니다. 숫자 타입을 기대하는 다운스트림 시스템이 데이터를 거부하거나 잘못 처리합니다.

해결: 매핑 단계에서 명시적인 타입 강제 변환을 적용하거나, dynamicTyping: true와 함께 PapaParse를 사용하세요. 어떤 필드가 숫자여야 하는지 항상 의도적으로 결정하세요.

Before · JavaScript
After · JavaScript
const row = { port: '8443', debug: 'true', workers: '4' }
JSON.stringify(row)
// {"port":"8443","debug":"true","workers":"4"}  — 모두 문자열
const row = {
  port: Number('8443'),           // 8443
  debug: 'true' === 'true',      // true
  workers: Number('4'),           // 4
}
JSON.stringify(row)
// {"port":8443,"debug":true,"workers":4}  — 올바른 타입

수동 파싱 대 라이브러리 — 빠른 비교

방법
보기 좋은 출력
유효한 JSON
커스텀 타입
스트리밍
설치 필요
JSON.stringify()
✓ (space 사용 시)
✓ toJSON()/replacer 활용
아니요 (내장)
JSON.parse()
N/A (파싱)
✓ (유효성 검사)
✓ reviver 활용
아니요 (내장)
csv-parse (Node.js)
✗ (객체 반환)
✗ (JSON 아님)
npm install
PapaParse
✗ (객체 반환)
✗ (JSON 아님)
npm install
csvtojson
✓ colParser 활용
npm install
jq (CLI)
N/A
시스템 설치
Miller (CLI)
N/A
시스템 설치

CSV 형식을 직접 제어하고 따옴표 처리된 필드가 없다고 알고 있는 간단한 스크립트의 경우, 내장된 split()+ JSON.stringify() 방식은 의존성 없이 잘 작동합니다. 사용자가 업로드한 CSV 파일을 처리하는 운영 시스템에서는 브라우저에서 PapaParse를, Node.js에서는 csv-parse를 사용하세요 — 두 라이브러리 모두 RFC 4180을 올바르게 처리하고 스트리밍을 지원합니다. csvtojson 패키지는 파싱과 직렬화를 단일 호출로 처리하는 JSON을 직접 출력하는 유일한 라이브러리입니다. 붙여넣기에서 결과까지 가장 빠른 경로가 필요할 때는 CSV to JSON 변환기가 별도 설정 없이 브라우저에서 모든 것을 처리합니다.

자주 묻는 질문

라이브러리 없이 JavaScript에서 CSV를 JSON으로 변환하려면 어떻게 하나요?

CSV 문자열을 줄바꿈 문자로 분할하여 행을 가져오고, 첫 번째 행에서 split(",")으로 헤더를 추출한 다음, 나머지 행을 해당 헤더를 키로 하는 객체로 변환합니다. 마지막으로 JSON.stringify(array, null, 2)를 호출하면 형식이 잘 갖춰진 JSON 문자열을 얻을 수 있습니다. 이 방식은 형식을 직접 제어할 수 있고 따옴표로 감싸진 필드나 내장된 쉼표가 없는 단순한 CSV 파일에 잘 작동합니다. 스프레드시트 내보내기나 외부 시스템 데이터의 경우, 따옴표 처리된 필드와 여러 줄로 된 값이 단순 분할기를 망가뜨릴 수 있으므로 PapaParse나 csv-parse를 사용하세요. 브라우저에서 처리하는 매우 작은 파일에는 이 의존성 없는 방식이 이상적입니다.

JavaScript
const csv = `name,email,department
김민준,minjun@nexuslabs.io,Engineering
이서연,seoyeon@nexuslabs.io,Product`

const lines = csv.trim().split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1).map(line => {
  const values = line.split(',')
  return Object.fromEntries(headers.map((h, i) => [h.trim(), values[i]?.trim()]))
})

console.log(JSON.stringify(rows, null, 2))
// [
//   { "name": "김민준", "email": "minjun@nexuslabs.io", "department": "Engineering" },
//   { "name": "이서연", "email": "seoyeon@nexuslabs.io", "department": "Product" }
// ]

JSON.stringify()와 toString()의 차이점은 무엇인가요?

일반 객체에 toString()을 호출하면 실제 데이터에 대해 아무런 정보도 담지 않는 쓸모없는 문자열 "[object Object]"가 반환됩니다. JSON.stringify()는 중첩된 객체와 배열을 포함하여 모든 키와 값을 올바르게 직렬화한 유효한 JSON 문자열을 생성합니다. CSV에서 파생된 행처럼 JavaScript 객체를 JSON 문자열로 변환해야 할 때는 항상 JSON.stringify()를 사용하세요. 역방향 작업 — JSON 문자열에서 살아있는 JavaScript 객체를 재구성하려면 — JSON.stringify()의 정확한 역함수인 JSON.parse()를 사용하세요. 이 두 함수는 완전한 왕복 쌍을 이룹니다: 데이터를 저장하거나 전송할 때는 stringify, 소비할 때는 parse를 사용합니다.

JavaScript
const row = { name: '김민준', role: 'Engineer' }

console.log(row.toString())       // "[object Object]"
console.log(JSON.stringify(row))   // '{"name":"김민준","role":"Engineer"}'

쉼표나 따옴표가 포함된 CSV 필드는 어떻게 처리하나요?

RFC 4180에 따르면 쉼표, 줄바꿈, 큰따옴표를 포함하는 필드는 큰따옴표로 감싸야 하며, 내장된 따옴표는 두 번 써서 이스케이프합니다 (따옴표로 감싸진 필드 안의 ""는 리터럴 따옴표 하나를 나타냅니다). 수동 split(",")은 이런 파일에서 오작동합니다 — 필드 경계가 더 이상 단순한 쉼표가 아니기 때문입니다. 실제 운영 데이터에는 PapaParse나 csv-parse를 사용하거나, 현재 위치가 따옴표로 감싸진 필드 안에 있는지 추적하는 상태 머신 파서를 직접 구현하세요. 올바른 상태 머신을 처음부터 작성하는 것은 생각보다 까다롭습니다: 필드 시작 부분의 따옴표, 필드 중간의 이스케이프된 따옴표, 따옴표 필드 안의 줄바꿈, 다양한 줄 끝 규칙(CRLF 대 LF) 등을 모두 처리해야 합니다. 간단한 CSV 데이터 이외에는 잘 검증된 라이브러리를 사용하세요.

JavaScript
// PapaParse는 RFC 4180을 올바르게 처리합니다
import Papa from 'papaparse'

const csv = `product,description,price
"Widget, Large","A big ""widget""",29.99
Bolt,Standard bolt,1.50`

const { data } = Papa.parse(csv, { header: true })
console.log(JSON.stringify(data, null, 2))
// description 필드에 올바르게 포함됨: A big "widget"

브라우저에서 직접 CSV를 JSON으로 변환할 수 있나요?

JSON.stringify()와 JSON.parse() 모두 모든 브라우저 엔진에 내장되어 있습니다. CSV 파싱 단계에서는 단순한 파일에 줄바꿈과 쉼표로 분할하거나, CDN을 통해 PapaParse를 포함할 수 있습니다 (Node.js 의존성 없음). 전체 변환이 서버 왕복 없이 클라이언트 측에서 이루어지며, 이는 파일 개인 정보 보호에 유용합니다 — 원시 CSV 데이터가 사용자의 기기를 절대 벗어나지 않습니다. 사용자가 <input type="file"> 요소를 통해 CSV 파일을 업로드하면, File API의 file.text() 메서드는 파일 내용을 문자열로 반환하는 Promise를 반환하며, 이를 변환 함수에 전달할 수 있습니다. 이로써 대시보드, 개발자 도구, 백엔드 없이 업로드된 데이터를 처리해야 하는 앱에서 완전한 브라우저 내 CSV-to-JSON 변환이 가능합니다.

JavaScript
// 브라우저: 사용자가 CSV 파일을 업로드
const fileInput = document.querySelector('input[type="file"]')
fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0]
  const text = await file.text()
  const lines = text.trim().split('\n')
  const headers = lines[0].split(',')
  const rows = lines.slice(1).map(line =>
    Object.fromEntries(headers.map((h, i) => [h.trim(), line.split(',')[i]?.trim()]))
  )
  console.log(JSON.stringify(rows, null, 2))
})

CSV에서 숫자와 불리언 값을 문자열 대신 올바른 타입으로 파싱하려면 어떻게 하나요?

CSV에는 타입 시스템이 없습니다 — 모든 필드는 문자열입니다. 매핑 단계에서 직접 값을 강제 변환해야 합니다. Number()나 parseFloat()으로 숫자 패턴을 확인하고, "true"/"false"는 불리언으로 변환하며, 빈 문자열은 null로 처리하세요. 숫자처럼 보이지만 문자열로 유지해야 하는 필드에 주의하세요: "07302" 같은 우편번호는 Number()로 강제 변환하면 앞의 0이 사라지고, 숫자 문자가 포함된 전화번호나 제품 코드도 마찬가지로 취약합니다. csvtojson 라이브러리는 colParser 옵션을 통해 자동 타입 강제 변환을 지원하며, 열별 변환 함수를 지정하고 문제가 되는 열의 자동 감지를 재정의할 수 있습니다. PapaParse의 dynamicTyping 옵션은 모든 열에 동일한 강제 변환을 전역적으로 적용합니다.

JavaScript
function coerceValue(val) {
  if (val === '') return null
  if (val === 'true') return true
  if (val === 'false') return false
  const num = Number(val)
  if (!isNaN(num) && val.trim() !== '') return num
  return val
}

// CSV-to-object 매핑 시 적용
const row = { port: coerceValue('8443'), debug: coerceValue('true'), host: coerceValue('api.internal') }
// { port: 8443, debug: true, host: "api.internal" }

Node.js에서 CSV-to-JSON 출력을 파일에 쓰려면 어떻게 하나요?

JSON.stringify() 출력과 함께 fs.writeFileSync()를 사용하세요. JSON.stringify의 세 번째 인수는 들여쓰기를 제어합니다 — 두 칸 들여쓰기는 2를, 탭은 "\t"를 전달하세요. 파일을 다시 읽으려면 JSON.parse(fs.readFileSync(path, "utf8"))를 사용하면 살아있는 JavaScript 객체 배열이 재구성됩니다. 비동기 컨텍스트(async 함수 또는 ES 모듈 최상위 레벨)에서 파일을 쓸 경우, 파일 쓰기가 완료될 때까지 이벤트 루프를 차단하지 않도록 fs.promises.writeFile()을 사용하는 것이 좋습니다. 몇 메가바이트를 초과하는 대용량 JSON 출력의 경우, 전체 문자열을 메모리에 생성하기 전에 WriteStream에 JSON 스트림을 파이프하는 것을 고려하세요.

JavaScript
import { writeFileSync, readFileSync } from 'node:fs'

// 쓰기
const jsonOutput = JSON.stringify(rows, null, 2)
writeFileSync('employees.json', jsonOutput, 'utf8')

// 다시 읽기
const parsed = JSON.parse(readFileSync('employees.json', 'utf8'))
console.log(Array.isArray(parsed))  // true
console.log(parsed[0].name)         // "김민준"

관련 도구

다른 언어로도 제공됩니다:Python
AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

SL
Sophie Laurent기술 검토자

Sophie is a full-stack developer focused on TypeScript across the entire stack — from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.