CSV 쉼표로 자르면 열이 밀리는 이유 — 따옴표 규칙

엑셀에서 받은 CSV를 코드에서 쉼표로 잘랐더니 어떤 행만 열이 하나씩 밀려 있는 분을 위한 글입니다. 결론부터 말하면 CSV의 쉼표가 전부 열 구분자는 아니고, 큰따옴표 안에 있는 쉼표는 그냥 글자입니다.
CSV는 값 안에 쉼표·큰따옴표·줄바꿈이 들어갈 수 있고, 그럴 때만 쓰는 인용 규칙이 따로 있습니다. 이 규칙 세 줄만 알면 밀린 열이 왜 밀렸는지 바로 짚어낼 수 있습니다.
요약 ① 값 안에 쉼표·큰따옴표·줄바꿈이 있으면 그 값 전체를 큰따옴표로 감쌉니다. ② 감싼 값 안의 큰따옴표는 두 번 연속으로 적어 이스케이프합니다.
""는 따옴표 한 글자입니다. ③ 따옴표 안의 쉼표와 줄바꿈은 구분자가 아니라 값이라,split(",")이나 줄 단위 읽기로는 잘라낼 수 없습니다.
열이 밀리는 건 파일이 깨져서가 아닙니다
상품 목록 CSV가 이렇게 생겼다고 해 보겠습니다.
name,memo,price
"연필, 2B","겉면에 ""HB"" 표기",1200
둘째 줄에는 쉼표가 세 개 있습니다. 그래서 split(",") 은 조각을 네 개 만들지만, 실제 열은 name·memo·price 세 개입니다. "연필 과 2B" 로 쪼개진 순간부터 그 행의 값이 한 칸씩 밀립니다.
줄 단위로 읽을 때도 같은 일이 생깁니다.
"노트","가로 210mm
세로 297mm",3500
이건 파일에서 두 줄로 보이지만 레코드는 하나입니다. 줄 수만 세면 행이 하나 늘어난 것으로 잡히고, 그 뒤 데이터가 통째로 어긋납니다.
CSV의 인용 규칙 — 세 줄이 전부입니다
CSV는 오래 쓰인 형식인데도 정식 규격 없이 퍼졌습니다. 2005년 10월에 나온 RFC 4180이 그 관행을 문서로 정리하고 text/csv MIME 타입을 등록했습니다. 문서 스스로도 "이 형식은 매우 흔한데도 한 번도 공식적으로 문서화된 적이 없다"고 적고 있습니다.
여기 적힌 인용 규칙은 이렇습니다.
- 각 필드는 큰따옴표로 감쌀 수도, 감싸지 않을 수도 있습니다. 다만 감싸지 않은 필드 안에는 큰따옴표가 올 수 없습니다.
- 줄바꿈·큰따옴표·쉼표가 들어간 필드는 큰따옴표로 감싸야 합니다.
- 감싼 필드 안에 큰따옴표가 나오면, 앞에 큰따옴표를 하나 더 붙여 이스케이프합니다.
그래서 "" 는 따옴표 두 개가 아니라 따옴표 한 글자입니다. "겉면에 ""HB"" 표기" 를 규칙대로 읽으면 값은 겉면에 "HB" 표기 가 됩니다.
한 가지 더 있습니다. RFC 4180은 "공백도 필드의 일부이며 무시해서는 안 된다"고 못 박습니다. a, b 의 둘째 값은 b 가 아니라 앞에 공백이 하나 붙은 b 입니다. 스프레드시트가 알아서 정리해 주는 것처럼 보여도, 규칙상으로는 남아 있는 값입니다.
도구 없이 직접 파싱해 보는 방법
핵심은 따옴표 안인지 밖인지 상태 하나만 들고 문자를 하나씩 훑는 것입니다. 정규식으로 한 번에 자르려 들면 연속된 "" 에서 금방 막힙니다.
읽는 규칙은 네 가지입니다.
- 따옴표 밖에서
"를 만나면 인용 구역에 들어갑니다. - 인용 구역 안에서
"를 만났는데 다음 글자도"이면, 값에 따옴표 한 글자를 넣고 두 글자를 건너뜁니다. - 인용 구역 안에서
"뒤에 다른 글자가 오면 인용 구역을 빠져나옵니다. - 인용 구역 밖의 쉼표만 열 구분이고, 밖의 줄바꿈만 행 구분입니다. 안쪽이면 전부 그냥 글자입니다.
브라우저 개발자 도구(대개 F12)의 콘솔에 아래를 붙여 넣으면, 이 규칙 그대로 동작하는 파서를 바로 만들 수 있습니다.
function parseCSV(text, delim) {
const rows = []; let row = []; let field = ""; let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQuotes) {
if (c === '"') {
if (text[i + 1] === '"') { field += '"'; i++; } // "" → " 한 글자
else inQuotes = false;
} else field += c; // 안쪽 쉼표·줄바꿈은 값
} else if (c === '"') inQuotes = true;
else if (c === delim) { row.push(field); field = ""; }
else if (c === "\n") { row.push(field); rows.push(row); row = []; field = ""; }
else if (c !== "\r") field += c;
}
row.push(field); rows.push(row);
return rows;
}
파일 끝이 줄바꿈으로 끝나면 이 함수는 빈 값 하나만 든 행을 마지막에 하나 더 만듭니다. 행 수를 셀 때는 그 행을 빼고 세면 됩니다.
문제는 CSV를 확인할 때마다 이 함수를 다시 붙여 넣고, 나온 배열을 눈으로 세어 열이 맞는지 대조해야 한다는 점입니다. 첫 행을 키로 바꿔 JSON으로 만들려면 코드가 더 붙습니다.
저희 도구로 확인하기. CSV 입력란에 붙여 넣기만 하면 같은 인용 규칙으로 파싱해, 첫 행을 키로 쓴 JSON 배열을 곧바로 보여 줍니다.
설치·회원가입 없이 브라우저 안에서 바로 변환되며, 붙여 넣은 CSV는 서버로 전송되지 않습니다.

변환 결과에서 자주 막히는 지점
| 상황 | 어떻게 처리되나 |
|---|---|
| 값 안의 쉼표 | 큰따옴표로 감싼 구간이면 글자로 남습니다 |
값 안의 "" | 큰따옴표 한 글자로 합쳐집니다 |
| 값 안의 줄바꿈 | 한 값 안에 유지되고 JSON에서는 \n 으로 보입니다 |
| 숫자처럼 보이는 값 | 전부 문자열로 나옵니다("1200") |
| 열이 모자란 행 | 빠진 키는 빈 문자열로 채워집니다 |
| 헤더 칸이 비어 있음 | col1·col2 같은 이름이 대신 붙습니다 |
값이 전부 문자열로 나오는 건 CSV 자체에 자료형이 없기 때문입니다. 파일에는 1200 이라는 글자만 적혀 있을 뿐, 그게 숫자인지 상품 코드인지 적어 둘 자리가 없습니다. 숫자로 써야 한다면 받는 쪽에서 변환하면 됩니다.
구분자가 쉼표가 아닌 파일도 흔합니다. 도구의 구분자 항목에서 쉼표·탭·세미콜론·파이프 중에 고를 수 있고, 구분자만 바뀔 뿐 인용 규칙은 똑같이 적용됩니다.
첫 행이 제목이 아니라 데이터인 파일이라면 첫 행을 헤더(키)로 사용 체크를 해제하면 됩니다. 그러면 객체 배열 대신 행마다 배열이 들어간 2차원 배열로 나옵니다.
정리
- CSV의 쉼표가 전부 구분자는 아닙니다. 큰따옴표로 감싼 구간 안의 쉼표는 값의 일부입니다.
- 값에 쉼표·큰따옴표·줄바꿈이 들어가면 그 값을 큰따옴표로 감싸고, 안쪽 큰따옴표는 두 번 연속으로 적습니다.
- 값 안 줄바꿈 때문에 파일의 줄 수와 레코드 수는 다를 수 있습니다. 줄 단위로 읽으면 행이 어긋납니다.
- 직접 파싱할 때는 정규식 대신 "따옴표 안인가 밖인가" 상태 하나를 들고 문자를 훑으면 됩니다.
- 공백은 필드의 일부로 봅니다.
a, b의 둘째 값에는 앞 공백이 남습니다. - 변환된 값은 모두 문자열입니다. 자료형은 CSV가 담지 못하는 정보입니다.
설치·회원가입 없이 브라우저 안에서 바로 변환되며, 붙여 넣은 CSV는 서버로 전송되지 않습니다.
자주 묻는 질문
왜 따옴표를 두 번 적으면 한 개가 되나요?
감싸는 따옴표와 값 안의 따옴표를 구별할 방법이 필요하기 때문입니다. RFC 4180은 감싼 필드 안에 큰따옴표가 나오면 앞에 큰따옴표를 하나 더 붙여 이스케이프하도록 정해 두었습니다. 읽는 쪽은 인용 구역 안에서 " 를 만났을 때 다음 글자가 또 " 인지만 보고, 맞으면 값에 한 글자를 넣고 아니면 인용 구역을 닫습니다.
값 안에 줄바꿈이 있는 CSV는 잘못 만든 파일인가요?
아닙니다. 규칙상 허용되는 형태이며, 줄바꿈이 든 값은 큰따옴표로 감싸도록 정해져 있습니다. 메모나 주소처럼 여러 줄인 값에서 흔히 나옵니다. 파일을 텍스트 편집기로 열어 그 값이 따옴표로 감싸여 있는지 확인해 보면, 파일이 잘못된 것인지 읽는 쪽이 줄 단위로 읽고 있는 것인지 구분됩니다.
세미콜론이나 탭으로 나뉜 파일도 같은 규칙인가요?
구분자만 다를 뿐 인용 규칙은 그대로 씁니다. 값 안에 그 구분자나 큰따옴표, 줄바꿈이 들어가면 큰따옴표로 감싸고 안쪽 따옴표는 두 번 적습니다. 도구에서는 구분자 항목을 쉼표에서 탭·세미콜론·파이프로 바꿔 같은 방식으로 변환할 수 있습니다.
붙여 넣은 CSV가 서버로 전송되나요?
아니요. 이 도구의 변환은 전부 브라우저 안에서 처리되며, 입력한 CSV나 변환 결과는 서버로 전송되거나 저장되지 않습니다. 입력란에 내용을 넣는 즉시 같은 화면에서 계산되고, 결과는 "JSON 복사" 버튼으로 클립보드에 담아 쓰면 됩니다.
