본문으로 건너뛰기
yutils

CSV 는 왜 항상 깨질까?

CSV 는 가장 단순해 보이는 포맷 — 쉼표와 줄. 그러나 필드 안 쉼표·따옴표·줄바꿈, BOM, Excel 의 인코딩 선택, dialect 차이가 production 에서 가장 자주 깨지는 포맷으로 만든다.

약 8분 읽기

CSV 가 가장 단순한 포맷 같다 — 쉼표로 칼럼 분리, 줄바꿈으로 행 분리. 그러나 실무에서 가장 자주 깨진다. 필드 안에 쉼표가 들어가면? 따옴표 안에 따옴표는? 멀티라인 텍스트는? Excel 이 박는 BOM 은? 한국어 Excel 의 CP949 인코딩은? 이 가이드는 CSV 의 RFC 4180 표준, 흔한 함정, 그리고 실무 디버깅 패턴을 정리한다.

CSV 의 "표준" — RFC 4180

2005 년에야 등장한 informal standard. 그 전까지 모든 도구가 자체 규칙. 핵심:

1. 각 줄 = 하나의 record
2. 쉼표 (,) 가 field 구분자
3. 줄바꿈 = CRLF
4. 필드 안에 , 또는 " 또는 줄바꿈 있으면 " 로 감싼다
5. " 안의 " 는 "" 로 escape

예:
name,note,city
"Lee, Sumi","She said ""hi""","Seoul"
"Park","Multi
line text","Busan"

그러나 RFC 4180 은 informal. Excel / Google Sheets / 각 언어 라이브 러리가 미묘하게 다른 동작.

Edge case 1 — 필드 안 쉼표

Bad:
name,address
Kim Sumi,Seoul, Gangnam, 123      ← 4 필드?

RFC 4180 — quote 필요:
name,address
Kim Sumi,"Seoul, Gangnam, 123"     ← 2 필드 ✓

Naive split (line.split(',')) 은 quote 안 쉼표 모름. 반드시 RFC 4180 parser 사용. CSV ↔ JSON papaparse 라이브러리 사용 — RFC 4180 정합.

Edge case 2 — 따옴표 안 따옴표

Original: She said "hi"

CSV escape (RFC 4180):
name,quote
"Lee","She said ""hi"""

           ^^^^^^^^^^^^^
           외부 "..." 가 field 경계
           내부 "" 가 single " 로 디코드

일부 도구는 \" escape — 표준 X. CSV 의 escape 는 오직 "".

Edge case 3 — 멀티라인 필드

name,description
"Yu","Line 1
Line 2
Line 3"
"Lee","Single line"

2 개 record. 줄 수 가 record 수와 다름. wc -l 같은 line-based 도구가 잘못된 결과 — record 수는 parser 만 정확.

왜 멀티라인이 위험한가

  • line-based 도구 (grep / sed / awk) 가 record 경계 혼동
  • DB import 시 BULK INSERT 가 줄 단위 read → 잘림
  • quote 안 자주 박히는 사람 입력 (주소·설명) 이 위험원

해결 — pre-process 로 멀티라인 escape (\n 문자로) 또는 JSONL 같은 줄 단위 포맷 사용.

Edge case 4 — BOM (Byte Order Mark)

Excel for Windows 가 CSV 저장 시 UTF-8 BOM (EF BB BF) 앞에 박음:

raw bytes:
EF BB BF 6E 61 6D 65 2C 76 61 6C 75 65 0A
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       BOM  "name,value\n"

parser 결과:
첫 컬럼 이름이 "\uFEFFname" 또는 "name" 으로 깨짐
                ^ BOM 이 첫 글자가 됨

→ DB import 시 "Column 'name' not found"
→ 모든 row 의 첫 컬럼 잘못

해결:

  • BOM 인식 parser 사용 — papaparse 의 default 가 strip BOM
  • Excel 저장 옵션 — "UTF-8 (no BOM)" 또는 LibreOffice
  • shell — sed '1s/^\xEF\xBB\xBF//' file.csv

Edge case 5 — Excel 의 한국어 인코딩 (CP949)

한국어 Windows Excel 의 default 인코딩이 UTF-8 이 아닌 CP949 ( Microsoft 의 EUC-KR 확장). 영어권에서 만든 도구가 UTF-8 가정 → 한국어 모두 깨짐:

한국 Excel 저장:
EC A0 A4 EC 9B 90  ← UTF-8 "직원" 이면
B5 60 B0 F8        ← CP949 "직원" 이면 (다른 byte!)

UTF-8 가정 parser 가 CP949 byte 읽으면:
"직원" → "직���" (mojibake)

해결:

  • Excel 의 "다른 이름으로 저장" → "CSV UTF-8" 옵션 (Excel 2016+ 에서 가능)
  • parser 에 인코딩 명시 — Python의 encoding="cp949"
  • BOM 으로 인코딩 추측 — UTF-8 BOM 있으면 UTF-8 안전

Edge case 6 — Excel 의 자동 형변환

Excel 이 CSV 를 열면서 멋대로 변환:

CSV:                Excel 열기:
"0123",,,           → 123 (앞 0 손실, 우편번호 망함)
"05/22",,,          → 5월 22일 날짜 (날짜처럼 보이면 변환)
"1.234e3",,,        → 1234 (과학표기 해석)
"01/02/2026",,,     → 2026-01-02 또는 2026-02-01 (locale 따라)
"=SUM(1+1)",,,      → 2 (수식 실행! → CSV injection 공격)

해결:

  • 앞에 single quote ('): '0123 → text 강제
  • 탭 (TAB) 으로 separator 사용 (TSV) — Excel 이 덜 자동 변환
  • CSV injection — =, +, -, @ 로 시작하는 셀은 사용자 입력에서 제거 또는 escape

CSV Injection 공격

사용자 입력 필드:
=HYPERLINK("http://evil.com/", "Click me")

CSV 다운로드 후 Excel 에서 열면 클릭 가능한 phishing 링크 자동 생성.

=cmd|'/C calc'!A1
→ Windows Excel 일부 버전에서 calc.exe 실행 (이전에는)

OWASP — 사용자 입력 CSV export 시 =, +,-, @ 시작 셀에 single quote prefix.

Edge case 7 — 헤더 유무

헤더 있음:
name,age
Alice,30
Bob,25

헤더 없음:
Alice,30
Bob,25

같은 데이터지만 parser 가 다르게 해석. RFC 4180 은 헤더 optional — 파일 자체에는 표시 X. 보통 약속:

  • 모든 값이 string 이면 헤더 있을 확률 ↑
  • 첫 줄 컬럼 수 ≠ 다른 줄 → 헤더 다르게 표기?
  • magic file 또는 sidecar README 로 명시

도구 옵션 — papaparse 의 header: true 강제. CSV ↔ JSON 가 first-row-as-header 토글 제공.

Dialect — TSV / PSV / 그리고 더

  • TSV (Tab-Separated) — separator 가 tab. 필드 안 tab 거의 없어 quote 부담 ↓
  • PSV (Pipe-Separated) — separator 가 |. SQL 출력에 자주
  • SSV (Semicolon) — separator 가 ;. 유럽 (소수점이 , 라 conflict 회피)
  • Fixed-width — separator 없이 고정 폭 컬럼. legacy mainframe

독일 / 프랑스 Excel 은 default 가 ; — 같은 CSV 가 나라마다 다르게 보임.

실용 — CSV 안전하게 쓰는 법

  1. parser 라이브러리 사용 — 자체 split X. Python csv module, Node papaparse, Java OpenCSV, Go encoding/csv
  2. 인코딩 명시 — UTF-8 default, 한국어는 BOM 박힌 UTF-8
  3. BOM strip — parser 옵션 또는 pre-process
  4. CSV injection 방어 — 사용자 입력 export 시 위험 prefix escape
  5. JSON 또는 JSONL 검토 — type 보존·중첩·줄 단위 안전이 필요하면 JSON

디버깅 시 — CSV ↔ JSON 로 JSON 변환. 각 row 가 어떻게 parsing 되는지 즉시 확인.

흔한 함정

1. line.split(',')

가장 흔한 안티패턴. quote 안 쉼표·escape·BOM 모두 무시.

2. 한국어 Excel 의 CP949 trap

UTF-8 가정 → 모자이크. 인코딩 명시 또는 Excel UTF-8 옵션.

3. Excel 의 우편번호 손실

037223722. 앞 0 손실. ="03722" 또는 text 형식 사전 지정.

4. 큰 CSV 의 메모리 폭주

100 MB CSV 를 전체 메모리 로드 → OOM. streaming parser (papaparse step 옵션, Python csv.reader 의 iterator) 사용.

5. trailing newline 의 빈 row

name,age
Alice,30
Bob,25
        ← 빈 줄

일부 parser: 3 row + empty row
다른 parser: 2 row (trailing newline 무시)

참고 자료

요약

  • CSV 는 informal RFC 4180. 도구마다 미묘하게 다른 dialect.
  • 핵심 7 가지 함정 — 필드 안 쉼표 / 따옴표 escape / 멀티라인 / BOM / 인코딩 (CP949) / Excel 자동 변환 / 헤더 모호성
  • CSV injection — =, +, -, @ 시작 셀은 Excel 수식. OWASP escape 권장.
  • dialect — TSV / PSV / SSV (유럽) / fixed-width. 같은 ".csv" 확장자라도 다른 separator.
  • 안전 — parser 라이브러리 + 인코딩 명시 + BOM strip + injection escape + streaming (큰 파일).
  • 더 복잡한 데이터 (중첩·type) 는 JSON / JSONL. CSV 의 한계 인정.
  • 디버깅 — CSV ↔ JSON 로 JSON 변환 후 row 단위 확인.
가이드 목록으로