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 안전하게 쓰는 법
- parser 라이브러리 사용 — 자체 split X. Python
csvmodule, Nodepapaparse, Java OpenCSV, Go encoding/csv - 인코딩 명시 — UTF-8 default, 한국어는 BOM 박힌 UTF-8
- BOM strip — parser 옵션 또는 pre-process
- CSV injection 방어 — 사용자 입력 export 시 위험 prefix escape
- 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 의 우편번호 손실
03722 → 3722. 앞 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 무시)참고 자료
- RFC 4180 (CSV 공식) — datatracker
- OWASP CSV Injection — OWASP
- papaparse — Node CSV parser
- Python csv module — Python docs
요약
- 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 단위 확인.