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 변환

설치·회원가입 없이 브라우저 안에서 바로 변환되며, 붙여 넣은 CSV는 서버로 전송되지 않습니다.

변환 결과에서 자주 막히는 지점

상황어떻게 처리되나
값 안의 쉼표큰따옴표로 감싼 구간이면 글자로 남습니다
값 안의 ""큰따옴표 한 글자로 합쳐집니다
값 안의 줄바꿈한 값 안에 유지되고 JSON에서는 \n 으로 보입니다
숫자처럼 보이는 값전부 문자열로 나옵니다("1200")
열이 모자란 행빠진 키는 빈 문자열로 채워집니다
헤더 칸이 비어 있음col1·col2 같은 이름이 대신 붙습니다

값이 전부 문자열로 나오는 건 CSV 자체에 자료형이 없기 때문입니다. 파일에는 1200 이라는 글자만 적혀 있을 뿐, 그게 숫자인지 상품 코드인지 적어 둘 자리가 없습니다. 숫자로 써야 한다면 받는 쪽에서 변환하면 됩니다.

구분자가 쉼표가 아닌 파일도 흔합니다. 도구의 구분자 항목에서 쉼표·탭·세미콜론·파이프 중에 고를 수 있고, 구분자만 바뀔 뿐 인용 규칙은 똑같이 적용됩니다.

첫 행이 제목이 아니라 데이터인 파일이라면 첫 행을 헤더(키)로 사용 체크를 해제하면 됩니다. 그러면 객체 배열 대신 행마다 배열이 들어간 2차원 배열로 나옵니다.

정리

  • CSV의 쉼표가 전부 구분자는 아닙니다. 큰따옴표로 감싼 구간 안의 쉼표는 값의 일부입니다.
  • 값에 쉼표·큰따옴표·줄바꿈이 들어가면 그 값을 큰따옴표로 감싸고, 안쪽 큰따옴표는 두 번 연속으로 적습니다.
  • 값 안 줄바꿈 때문에 파일의 줄 수와 레코드 수는 다를 수 있습니다. 줄 단위로 읽으면 행이 어긋납니다.
  • 직접 파싱할 때는 정규식 대신 "따옴표 안인가 밖인가" 상태 하나를 들고 문자를 훑으면 됩니다.
  • 공백은 필드의 일부로 봅니다. a, b 의 둘째 값에는 앞 공백이 남습니다.
  • 변환된 값은 모두 문자열입니다. 자료형은 CSV가 담지 못하는 정보입니다.
CSV → JSON 변환

설치·회원가입 없이 브라우저 안에서 바로 변환되며, 붙여 넣은 CSV는 서버로 전송되지 않습니다.

자주 묻는 질문

왜 따옴표를 두 번 적으면 한 개가 되나요?

감싸는 따옴표와 값 안의 따옴표를 구별할 방법이 필요하기 때문입니다. RFC 4180은 감싼 필드 안에 큰따옴표가 나오면 앞에 큰따옴표를 하나 더 붙여 이스케이프하도록 정해 두었습니다. 읽는 쪽은 인용 구역 안에서 " 를 만났을 때 다음 글자가 또 " 인지만 보고, 맞으면 값에 한 글자를 넣고 아니면 인용 구역을 닫습니다.

값 안에 줄바꿈이 있는 CSV는 잘못 만든 파일인가요?

아닙니다. 규칙상 허용되는 형태이며, 줄바꿈이 든 값은 큰따옴표로 감싸도록 정해져 있습니다. 메모나 주소처럼 여러 줄인 값에서 흔히 나옵니다. 파일을 텍스트 편집기로 열어 그 값이 따옴표로 감싸여 있는지 확인해 보면, 파일이 잘못된 것인지 읽는 쪽이 줄 단위로 읽고 있는 것인지 구분됩니다.

세미콜론이나 탭으로 나뉜 파일도 같은 규칙인가요?

구분자만 다를 뿐 인용 규칙은 그대로 씁니다. 값 안에 그 구분자나 큰따옴표, 줄바꿈이 들어가면 큰따옴표로 감싸고 안쪽 따옴표는 두 번 적습니다. 도구에서는 구분자 항목을 쉼표에서 탭·세미콜론·파이프로 바꿔 같은 방식으로 변환할 수 있습니다.

붙여 넣은 CSV가 서버로 전송되나요?

아니요. 이 도구의 변환은 전부 브라우저 안에서 처리되며, 입력한 CSV나 변환 결과는 서버로 전송되거나 저장되지 않습니다. 입력란에 내용을 넣는 즉시 같은 화면에서 계산되고, 결과는 "JSON 복사" 버튼으로 클립보드에 담아 쓰면 됩니다.

가격 보기카톡 무료 상담