CSV와 RFC 4180: 쉼표·큰따옴표·줄바꿈은 어떻게 처리하나
셀 안의 쉼표와 줄바꿈 때문에 CSV 열이 밀리는 이유를 RFC 4180, IANA text/csv 등록, W3C 표 데이터 권고, Python·Go 문서로 정리했습니다. RFC 4180이 정하지 않은 것도 나눠 봅니다.
CSV는 “쉼표로 나눈 텍스트”라서 직접 만들기 쉬워 보입니다. 그런데 값 하나에 쉼표나 줄바꿈이 들어가는 순간 열이 밀리고 행이 쪼개집니다. 이럴 때 흔히 기준으로 드는 문서가 RFC 4180인데, 이 문서가 무엇을 정했고 무엇을 정하지 않았는지는 생각보다 덜 알려져 있습니다.
RFC 4180은 인터넷 표준이 아니다
RFC 4180은 2005년 10월에 나온 Informational RFC입니다. 문서 첫머리에 “어떤 종류의 인터넷 표준도 규정하지 않는다”고 적혀 있습니다. 2장은 공식 명세가 없다고 밝힌 뒤 “대부분의 구현이 따르는 것으로 보이는 형식”을 기록합니다. 이 RFC가 공식적으로 한 일은 text/csv 미디어 타입을 IANA에 등록한 것입니다.
2014년에는 RFC 7111이 CSV의 행·열·셀을 가리키는 URI 프래그먼트를 정의하면서 RFC 4180을 갱신했습니다. RFC 4180을 대체하려던 개정 초안 draft-shafranovich-rfc4180-bis는 IETF Datatracker에서 07판 이후 만료된 상태입니다(2026년 10월 3일 확인).
따옴표 규칙: 감싸고, 안쪽 따옴표는 두 번
RFC 4180 2장의 주요 규칙은 다음과 같습니다.
- 줄바꿈, 큰따옴표, 쉼표가 들어간 필드는 큰따옴표로 감싼다.
- 감싼 필드 안의 큰따옴표는 하나 더 붙여
""로 쓴다. - 감싸지 않은 필드에는 큰따옴표가 나올 수 없다.
- 공백도 필드의 일부이므로 무시하지 않는다. 마지막 필드 뒤에는 쉼표를 붙이지 않는다.
id,memo
1,"He said ""hi"", then left
line2"
화면에는 세 줄로 보이지만 헤더를 빼면 레코드는 하나입니다. 두 번째 필드가 큰따옴표로 감싸져 있으므로 그 안의 쉼표와 줄바꿈은 값의 일부입니다.
줄 단위로 자르면 안 되는 이유
RFC 4180은 레코드 구분자를 CRLF로 정하고, 마지막 레코드 뒤의 줄바꿈은 있어도 없어도 된다고 설명합니다. 문법을 정의한 ABNF는 큰따옴표로 감싼 필드 안에 CR과 LF가 들어가는 것도 허용합니다. 따라서 파일을 줄바꿈 기준으로 split하면 필드 안의 줄바꿈에서 레코드가 잘립니다. CSV를 읽으려면 현재 위치가 따옴표 안인지 추적하는 파서가 필요합니다.
줄바꿈 처리는 구현마다 다릅니다.
- Python의
csv모듈은 기본적으로 레코드 끝에\r\n을 씁니다. reader는lineterminator설정과 관계없이\r과\n을 모두 줄 끝으로 인식합니다. 공식 문서는 파일을newline=''로 열라고 안내합니다. 그러지 않으면 따옴표 안의 줄바꿈이 제대로 해석되지 않거나,\r\n을 쓰는 플랫폼에서\r이 하나 더 붙을 수 있습니다. - Go의
encoding/csv는 RFC 4180 형식을 지원하지만 Writer는 기본적으로 CRLF 대신 LF를 씁니다.UseCRLF로 바꿀 수 있습니다. Reader는 여러 줄 필드 안을 포함해\r\n을 모두\n으로 바꿉니다. - W3C의 2015년 권고안 「Model for Tabular Data and Metadata on the Web」은 행 끝에 CRLF를 권하면서 LF도 허용합니다. 이 부분이 RFC 4180의
text/csv정의와 다르다는 점은 문서 스스로 밝히고 있습니다.
Python 3.14에서 다음 코드를 실행하면 레코드 끝은 \r\n, 필드 안의 줄바꿈은 \n인 문자열이 나옵니다. ABNF상 감싼 필드 안에는 CR과 LF가 들어갈 수 있으므로 두 형태가 한 파일에 함께 나타날 수 있습니다.
import csv, io
buf = io.StringIO(newline="")
w = csv.writer(buf)
w.writerow(["id", "memo"])
w.writerow([1, 'He said "hi", then left\nline2'])
print(repr(buf.getvalue()))
# 'id,memo\r\n1,"He said ""hi"", then left\nline2"\r\n'
문자 집합: 문법은 ASCII, 등록은 UTF-8 간주를 권고
RFC 4180의 ABNF에서 필드 내용인 TEXTDATA는 %x20-21 / %x23-2B / %x2D-7E입니다. 쉼표와 큰따옴표를 뺀 출력 가능한 ASCII 문자만 들어가므로 한글은 이 문법 밖에 있습니다. RFC는 흔히 US-ASCII를 쓰지만 charset 파라미터로 다른 문자 집합도 쓸 수 있다고 설명합니다. W3C 권고안도 이 ASCII 제한은 실제로는 흔히 무시되며 국제적인 웹에서는 현실적이지 않다고 지적합니다.
현재 IANA의 text/csv 등록은 2014년 1월 17일에 갱신됐습니다. 이 등록은 charset 파라미터를 쓰도록 권고하고, 파라미터가 없으면 UTF-8로 간주하도록 권고합니다. W3C 권고안도 UTF-8을 권합니다.
파일 맨 앞의 BOM을 프로그램마다 다르게 처리하는 문제는 UTF-8 BOM 글에서 다뤘습니다. EUC-KR로 인코딩된 CSV라면 인코딩 변환 도구로 UTF-8로 바꿀 수 있습니다.
정리
- RFC 4180은 인터넷 표준 트랙 명세가 아닌 정보성 문서입니다. ABNF 문법을 제시하지만, 본문의 소문자
should와must는 RFC 2119의 대문자 요구 수준 키워드가 아니라 일반 문장입니다. - CSV를 쓸 때는 쉼표·큰따옴표·줄바꿈이 든 필드를 감싸고 안쪽 큰따옴표를 두 번 씁니다. 문자열을 직접 이어 붙이기보다 언어의 CSV 라이브러리를 쓰는 편이 안전합니다.
- CSV를 읽을 때는 줄 단위로 자르지 않습니다. 레코드 끝이 CRLF인지 LF인지는 파일을 만든 프로그램에 따라 다를 수 있습니다.
- 인코딩과 헤더 유무는 파일만 봐서는 알기 어려울 수 있습니다. HTTP로 제공한다면
text/csv; charset=utf-8; header=present처럼 미디어 타입 파라미터로 밝힐 수 있습니다.
참고한 명세와 문서
- RFC 4180 — Common Format and MIME Type for Comma-Separated Values (CSV) Files: 2장(형식과 ABNF), 3장(
text/csv등록) - RFC 7111 — URI Fragment Identifiers for the text/csv Media Type
- IANA: text/csv 미디어 타입 등록
- IETF Datatracker: draft-shafranovich-rfc4180-bis
- W3C: Model for Tabular Data and Metadata on the Web: 7.2·7.3절, 부록 B.1
- Python 표준 라이브러리: csv
- Go 표준 라이브러리: encoding/csv
링크와 내용은 2026년 10월 3일에 확인했습니다. 초안 상태와 언어별 문서는 바뀔 수 있습니다.