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"
파일 리더를 통해 읽은 결과는 단순 문자열이다:
const raw = e.target.result;
예시:
"name,age,intro\r\njohn,25,\"funny, tall\"\r\namy,30,\"kind, smart\"\r\n"
const items = raw.split("\r\n");
결과:
[
"name,age,intro",
"john,25,\"funny, tall\"",
"amy,30,\"kind, smart\""
]
CSV의 특성상 큰따옴표로 감싼 부분 안의 쉼표는 분리 기준이 되면 안 된다.
items.map((item) =>
item.replace(/"[^"]*"/g, (match) => match.replace(/,/g, "##comma##"))
);
처리 결과 예시:
"john,25,\"funny##comma## tall\""
.map((item) => item.split(","))
이제 각 줄은 안전하게 배열로 바뀐다:
["john", "25", "\"funny##comma## tall\""]
.filter((arr) => !arr.every((element) => element === ""))
의미 없는 빈 줄은 제거한다.
.map((item) => item.map((col) => col.replace(/##comma##/g, ",")))
최종적으로 다시 원래의 쉼표를 되돌린다:
["john", "25", "\"funny, tall\""]
const headers = items[0]; // ["name", "age", "intro"]
items.splice(0, 1); // 데이터에서 헤더 제거
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 }), {})
);
이 방식은 매우 간단하지만 강력하다.
지금까지는 다음과 같이 줄을 나눴다:
const items = raw.split("\r\n");
이 방식은 Windows에서 만든 CSV 파일(\r\n)에는 잘 작동하지만, macOS나 Linux에서 만든 파일은 줄바꿈 문자가 \n 또는 \r로만 되어 있을 수 있어서 줄이 제대로 분리되지 않을 수 있다.
const items = raw.split(/\r\n|\n|\r/);
이 방식은 다음을 모두 처리할 수 있다:
운영체제 | 줄바꿈 문자 | 설명 |
---|---|---|
Windows | \r\n | 가장 일반적인 케이스 |
macOS | \n 또는 \r | 구버전 mac은 \r, 신버전은 \n |
Linux | \n | 리눅스 계열 대부분 |
실제로 대부분의 경우에는 이 간단한 정규식이면 충분하다:
const items = raw.split(/\r?\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 간 줄바꿈 차이를 고려한 정규식을 쓰는 걸 추천한다.