복잡한 CSV 데이터를 안전하게 파싱하는 방법

CSV 데이터를 JavaScript에서 파싱할 때, split(',') 한 줄로 해결하려 했다가 망해본 적 있지 않은가?

특히 다음과 같은 경우:


name,age,desc
john,25,"funny, tall, smart"

문제: "funny, tall, smart"는 하나의 셀인데, 쉼표 때문에 3개의 컬럼으로 나뉘어버림!

이 글에서는 쉼표, 줄바꿈, 큰따옴표가 섞인 CSV 데이터를 객체로 변환하는 완전한 처리 과정을 소개한다.


🧾 예제 데이터

CSV 파일에는 다음과 같은 데이터가 있다고 가정한다:


name,age,intro
john,25,"funny, tall"
amy,30,"kind, smart"


🔄 처리 흐름

✅ 1. e.target.result로부터 CSV 텍스트를 가져오기

파일 리더를 통해 읽은 결과는 단순 문자열이다:

const raw = e.target.result;

예시:

"name,age,intro\r\njohn,25,\"funny, tall\"\r\namy,30,\"kind, smart\"\r\n"

✅ 2. 줄 단위로 나누기

const items = raw.split("\r\n");

결과:

[
  "name,age,intro",
  "john,25,\"funny, tall\"",
  "amy,30,\"kind, smart\""
]

✅ 3. 큰따옴표 안의 쉼표를 임시로 치환

CSV의 특성상 큰따옴표로 감싼 부분 안의 쉼표는 분리 기준이 되면 안 된다.

items.map((item) =>
  item.replace(/"[^"]*"/g, (match) => match.replace(/,/g, "##comma##"))
);

처리 결과 예시:

"john,25,\"funny##comma## tall\""

✅ 4. 쉼표로 split (이제는 안전하게 가능)

.map((item) => item.split(","))

이제 각 줄은 안전하게 배열로 바뀐다:

["john", "25", "\"funny##comma## tall\""]

✅ 5. 빈 줄 제거

.filter((arr) => !arr.every((element) => element === ""))

의미 없는 빈 줄은 제거한다.


✅ 6. 임시 토큰 복원 (##comma## → ,)

.map((item) => item.map((col) => col.replace(/##comma##/g, ",")))

최종적으로 다시 원래의 쉼표를 되돌린다:

["john", "25", "\"funny, tall\""]

🧠 객체 변환 단계

✅ 7. 헤더 추출

const headers = items[0]; // ["name", "age", "intro"]
items.splice(0, 1); // 데이터에서 헤더 제거

✅ 8. 데이터 배열을 객체 배열로 변환

const data = items.map((item) =>
  item.reduce((acc, col, i) => ({ ...acc, [headers[i]]: col }), {})
);

예시 결과:

[
  { name: "john", age: "25", intro: "funny, tall" },
  { name: "amy", age: "30", intro: "kind, smart" }
]

✨ 최종 코드

const items = e.target.result
  .split("\r\n")
  .map((item) =>
    item.replace(/"[^"]*"/g, (match) => match.replace(/,/g, "##comma##"))
  )
  .map((item) => item.split(","))
  .filter((arr) => !arr.every((element) => element === ""))
  .map((item) => item.map((col) => col.replace(/##comma##/g, ",")));

const headers = items[0];
items.splice(0, 1);

const data = items.map((item) =>
  item.reduce((acc, col, i) => ({ ...acc, [headers[i]]: col }), {})
);

🔚 마무리

이 방식은 매우 간단하지만 강력하다.

  • CSV에 쉼표 포함된 문자열이 있어도 안전하게 처리 가능
  • 추가 라이브러리 없이 순수 JavaScript로 구현 가능

📌 팁: 더 복잡한 CSV라면?

  • \n, \r, 셀 안의 줄바꿈 등 복잡한 CSV는 PapaParse 같은 라이브러리를 쓰는 것도 고려하자.
  • 대용량일 땐 성능상 reduce보다 전통적인 for 루프도 고려해볼 것.

📦 추가된 섹션: 줄바꿈 호환성 처리

🪓 줄바꿈 호환성 처리

지금까지는 다음과 같이 줄을 나눴다:

const items = raw.split("\r\n");

이 방식은 Windows에서 만든 CSV 파일(\r\n)에는 잘 작동하지만, macOS나 Linux에서 만든 파일은 줄바꿈 문자가 \n 또는 \r로만 되어 있을 수 있어서 줄이 제대로 분리되지 않을 수 있다.


✅ 해결 방법 1: 모든 OS 호환 정규식 사용

const items = raw.split(/\r\n|\n|\r/);

이 방식은 다음을 모두 처리할 수 있다:

운영체제줄바꿈 문자설명
Windows\r\n가장 일반적인 케이스
macOS\n 또는 \r구버전 mac은 \r, 신버전은 \n
Linux\n리눅스 계열 대부분

✅ 해결 방법 2: 간결한 정규식 (\r?\n)

실제로 대부분의 경우에는 이 간단한 정규식이면 충분하다:

const items = raw.split(/\r?\n/);
  • \r?\n\n 앞에 \r이 있을 수도 있고 없을 수도 있다는 의미
  • Windows의 \r\n, Linux/macOS의 \n을 모두 처리 가능

단, 구형 macOS의 \r만 줄바꿈으로 쓰인 파일은 잡아내지 못한다.


✨ 정리

정규식범위추천 용도
/\r?\n/Windows + Linux/macOS일반적인 경우에 충분
`/\r\n\n\r/`모든 줄바꿈 완전 지원극한 호환성이 필요한 경우

🔄 최종 코드에서 적용 예시

기존 코드에서 이 부분만 바꾸면 된다:

- const items = e.target.result.split("\r\n")
+ const items = e.target.result.split(/\r?\n/)

또는 더 강력하게:

const items = e.target.result.split(/\r\n|\n|\r/);

줄바꿈이 안 맞아서 split이 안 되면, 그 이후 모든 파싱 로직이 무너질 수 있다. 실제 운영 코드라면 항상 OS 간 줄바꿈 차이를 고려한 정규식을 쓰는 걸 추천한다.