CSV는 아무도 좋아하지 않지만 모두가 사용하는 형식입니다. 1970년대부터 존재해 왔고, 지구상의 모든 스프레드시트 앱의 기본 내보내기 형식이며, 지금 이 순간 데이터 파이프라인 어딘가에 분명 있을 것입니다. 형식은 사소해 보입니다 — 쉼표로 구분된 값들 — 하지만 올바르게 파싱하는 것은 놀랍도록 틀리기 쉽습니다. 실제로 어떻게 작동하는지, 어디서 복잡해지는지, Python과 JavaScript에서 실수 없이 처리하는 방법을 살펴보겠습니다.
CSV란 실제로 무엇인가
CSV는 쉼표로 구분된 값(Comma-Separated Values)의 약자입니다. CSV 파일은 일반 텍스트입니다 — 이진 인코딩 없음, 메타데이터 없음, 행과 열 이외의 구조 없음. 각 줄은 레코드이고, 줄 내의 각 값은 구분자(일반적으로 쉼표)로 구분되며, 첫 번째 행은 관례적으로 열 이름이 있는 헤더 행입니다. 실제 CSV가 어떻게 생겼는지 확인해 봅시다:
order_id,customer,product,quantity,unit_price,shipped
1001,Alice Nguyen,USB-C Hub,2,34.99,true
1002,Bob Martinez,Mechanical Keyboard,1,129.00,false
1003,Alice Nguyen,HDMI Cable,3,12.50,true
1004,Carol Smith,Webcam 1080p,1,79.95,false그게 전부입니다 — 6개 열, 4개 데이터 행, 그리고 헤더. 꺾쇠 괄호도, 중괄호도, 따옴표도 필요 없습니다(아직은).
이 단순함이 CSV가 지속되는 이유입니다: 지구상의 거의 모든 도구가 이것을 열고, 읽고, 생성할 수 있습니다.
Excel, Google Sheets, PostgreSQL의 COPY 명령어, pandas, R의 read.csv() — 모두
기본적으로 CSV를 지원합니다. 절충점은 형식에 구조가 거의 없다는 것입니다: 유형 없음, 중첩 없음, 스키마 없음.
모든 값은 당신이 결정하기 전까지는 문자열입니다.
RFC 4180 — 실제로 적용되지 않는 표준
명세가 있습니다: RFC 4180, 2005년 발행. CRLF 줄 끝, 큰따옴표 이스케이핑, 내장된 쉼표 처리 방법 등을 정의합니다. 그런데 RFC 4180은 정보 제공용이지 표준이 아닙니다. 이미 일반적인 관행을 설명하는 것이지, 법으로 정하는 것이 아닙니다. 아무도 따를 의무가 없으며, 실제로 정확히 따르는 사람은 거의 없습니다.
결과는 CSV 방언 혼란입니다. CRLF 대신 LF를 사용하는 파일, 첫 번째 행이 헤더인지 아닌지 모르는 파일, 끝에 새 줄이 있는 파일과 없는 파일, Excel에서 UTF-8 BOM을 앞에 붙인 파일. CSV에 관한 Wikipedia 문서에 실제로 달라지는 모든 것들이 잘 정리되어 있습니다. 가장 안전한 접근법: 직접 생성하지 않은 CSV 파일에 대해 아무것도 가정하지 말고, 항상 쉼표로 단순히 분리하는 대신 제대로 된 파서를 사용하세요.
인용 규칙: 값 안에 쉼표가 나타날 때
여기서 "쉼표로 분리"가 무너집니다. 값에 쉼표가 포함되어 있으면 어떻게 될까요? 또는 줄바꿈? 또는 큰따옴표? RFC 4180과 대부분의 실제 파서는 필드를 큰따옴표로 감싸서 처리합니다. 전체 규칙은 다음과 같습니다:
- 필드에 쉼표, 줄바꿈, 또는 큰따옴표가 포함된 경우, 전체 필드를 큰따옴표로 감싸기
- 필드에 큰따옴표가 포함된 경우, 두 배로 이스케이핑(
"") - 인용된 필드는 여러 줄에 걸쳐 있을 수 있습니다 — 줄바꿈은 값의 일부가 됩니다
- 따옴표 안의 공백은 유의미하며 보존되어야 합니다
order_id,customer,notes,unit_price
1005,David Lee,"Wants gift wrapping, express shipping",45.00
1006,Emma Brown,"Said: ""please handle with care""",89.99
1007,Frank Wu,"Address:
123 Main St
Apt 4B",15.50이 예시에서: David의 notes에 쉼표가 포함되어 있어 인용됩니다. Emma의 노트에
큰따옴표가 포함되어 있어 외부 따옴표 안에서 두 배로 됩니다. Frank의 주소는 여러 줄에 걸쳐 있습니다 —
줄 바꿈은 값의 일부입니다. 어떤 언어에서든 단순한 line.split(',')는 이 세 가지를 모두
완전히 망가뜨립니다. 이것이 진짜 파서가 필요한 이유입니다.
split으로는 올바르게 처리할 수 없습니다.구분자 변형: 탭, 세미콜론, 파이프
이름에도 불구하고, CSV는 쉼표를 사용할 필요가 없습니다. 여러 일반적인 변형이 다른 구분자를 사용하며, 실무에서 모두 만날 수 있습니다:
- TSV(탭으로 구분된 값) —
\t를 구분자로 사용합니다. 생물정보학(BLAST 출력, VCF 파일), 데이터베이스 내보내기, 값 자체에 쉼표가 자주 포함되는 곳에서 일반적입니다 - 세미콜론으로 구분 — 쉼표가 소수점 구분자인 로케일(독일, 프랑스, EU 대부분)에서 Excel의 기본값입니다. 유럽 동료의 CSV를 열었을 때 하나의 거대한 열이 생기는 이유가 이것입니다
- 파이프로 구분 —
|를 사용합니다. 메인프레임 시스템에서 탭이 제거될 수 있는 레거시 은행 및 보험 데이터 내보내기에서 일반적입니다 - 고정 너비 — 기술적으로 CSV는 아니지만 종종 같은 범주로 묶입니다. 열은 구분자 대신 고정 너비로 패딩됩니다
대부분의 CSV 파서를 사용하면 구분자를 명시적으로 지정할 수 있습니다. 다른 사람이 사용할 CSV를 작성할 때는 구분자를 문서화하세요. 직접 생성하지 않은 CSV를 읽을 때는 가정하기 전에 처음 몇 줄을 확인하세요. CSV 포매터는 비표준 구분자가 있는 파일을 검사하고 다시 형식화하는 데 도움이 됩니다.
Python에서 CSV 파싱
Python의 표준 라이브러리에는
csv 모듈이
포함되어 있으며, 정말 좋습니다. 가장 자주 사용할 두 클래스는 행을 리스트로 접근하는 csv.reader와
행을 딕셔너리로 접근하는 csv.DictReader입니다(거의 항상 원하는 것).
import csv
# csv.reader — each row is a list of strings
with open('orders.csv', 'r', encoding='utf-8', newline='') as f:
reader = csv.reader(f)
header = next(reader) # consume the header row
for row in reader:
order_id, customer, product, quantity, price, shipped = row
print(f"Order {order_id}: {quantity}x {product} for {customer}")
# csv.DictReader — each row is an OrderedDict keyed by the header
with open('orders.csv', 'r', encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
for row in reader:
if row['shipped'] == 'false':
print(f"Pending: {row['order_id']} — {row['customer']}")항상 포함해야 할 두 가지: encoding='utf-8'(또는 파일이 실제로 사용하는 인코딩)과
newline=''. 두 번째가 중요합니다 — csv 모듈은 자체적으로 줄바꿈을 처리하며
원시 바이트가 필요합니다. newline=''이 없으면 Windows에서 빈 줄이 추가로 생길 수 있습니다.
쓰기도 csv.writer를 사용하면 마찬가지로 간단합니다:
import csv
orders = [
{'order_id': 1008, 'customer': 'Grace Kim', 'product': 'Laptop Stand', 'quantity': 1, 'unit_price': 49.99, 'shipped': False},
{'order_id': 1009, 'customer': 'Henry Park', 'product': 'USB-C Hub', 'quantity': 2, 'unit_price': 34.99, 'shipped': True},
]
with open('new_orders.csv', 'w', encoding='utf-8', newline='') as f:
fieldnames = ['order_id', 'customer', 'product', 'quantity', 'unit_price', 'shipped']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(orders)
# To use a different delimiter (e.g. tab-separated)
with open('new_orders.tsv', 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f, delimiter='\t')
writer.writerow(['order_id', 'customer', 'product'])
writer.writerow([1008, 'Grace Kim', 'Laptop Stand'])csv 모듈은 모든 인용을 자동으로 처리합니다 — 필드에 쉼표나
줄바꿈이 포함되어 있으면 생각하지 않아도 큰따옴표로 감쌉니다. 그게 바로 라이브러리를 사용하는 이유입니다.
JavaScript / Node.js에서 CSV 파싱
JavaScript에는 내장 CSV 파서가 없습니다. 다음과 같이 하고 싶은 유혹이 있습니다:
// ❌ Don't do this — breaks immediately on quoted fields
const rows = csvText.split('\n').map(line => line.split(','));이것은 값에 쉼표, 따옴표 안에 줄바꿈, 또는 이스케이프된 큰따옴표가 있는 순간 실패합니다. 실제 사용에는 라이브러리를 사용하세요. PapaParse가 최선입니다 — 빠르고, 모든 엣지 케이스를 처리하며, 브라우저와 Node.js 모두에서 작동하고, 대형 파일을 위한 스트리밍을 지원합니다.
import Papa from 'papaparse';
import fs from 'fs';
// Parse a CSV string
const csvText = fs.readFileSync('orders.csv', 'utf8');
const result = Papa.parse(csvText, {
header: true, // first row becomes object keys
skipEmptyLines: true, // ignore blank lines at end of file
dynamicTyping: true, // converts "true"/"false" to booleans, numbers to numbers
});
console.log(result.data);
// [
// { order_id: 1001, customer: 'Alice Nguyen', product: 'USB-C Hub', quantity: 2, unit_price: 34.99, shipped: true },
// { order_id: 1002, customer: 'Bob Martinez', product: 'Mechanical Keyboard', quantity: 1, unit_price: 129, shipped: false },
// ...
// ]
// Check for parse errors
if (result.errors.length > 0) {
console.error('Parse errors:', result.errors);
}
// Generate CSV from an array of objects
const orders = [
{ order_id: 1008, customer: 'Grace Kim', product: 'Laptop Stand', quantity: 1, unit_price: 49.99 },
{ order_id: 1009, customer: 'Henry Park', product: 'USB-C Hub', quantity: 2, unit_price: 34.99 },
];
const csvOutput = Papa.unparse(orders);
fs.writeFileSync('export.csv', csvOutput, 'utf8');dynamicTyping 옵션은 편리하지만 알아둘 가치가 있습니다 — "34.99"같은 것을
자동으로 숫자로 변환합니다. 보통 원하는 것이지만, order_id같은 필드가 CSV에서 숫자이지만
문자열로 원할 경우 놀랍게 느껴질 수 있습니다. 엄격한 문자열 출력이 필요하다면 비활성화하세요.
CSV와 다른 형식 간의 빠른 변환에는 CSV를 JSON으로 도구가 코드 없이 브라우저에서 대부분의 일반적인 경우를 처리합니다 — 일회성 데이터 변환에 유용합니다. XML 기반 시스템에 데이터를 공급해야 하는 경우 CSV를 XML로도 있습니다.
반드시 마주칠 일반적인 함정
좋은 파서를 사용해도 CSV에는 잘 알려진 위험 요소가 있습니다. 가장 자주 나타나는 것들입니다:
- Excel의 BOM 바이트. Excel이 CSV를 "UTF-8"로 내보낼 때, 종종 UTF-8 BOM(
EF BB BF, 문자로\ufeff)을 앞에 붙입니다. 이 때문에 첫 번째 열 헤더가order_id대신order_id처럼 보입니다. Python에서는utf-8대신encoding='utf-8-sig'로 파일을 열어 자동으로 제거하세요. PapaParse는 투명하게 처리합니다. - CRLF vs LF 줄 끝. RFC 4180은 CRLF(
\r\n)를 지정하지만, Unix 도구는 LF(\n)를 생성하고 오래된 Mac 파일은 CR(\r)만 사용합니다. 이것이 Python의csv모듈에newline=''이 필요한 이유입니다 — 세 가지를 모두 내부적으로 처리합니다. 원시 바이트를 읽고 수동으로 분리한다면\r을 명시적으로 제거해야 합니다. - 인코딩 문제. CSV는 자체 인코딩을 선언할 방법이 없습니다 — HTML의
<meta charset>이나 XML의<?xml encoding="..."?>과 달리. Excel은 종종 UTF-8 대신 Windows-1252(CP1252)로 파일을 저장하여 악센트 문자를 망가뜨립니다.é대신é같은 문자가 보인다면 UTF-8 파일이 Latin-1로 디코딩되고 있거나 그 반대인 경우입니다. 파일을 생성하는 사람과 항상 인코딩을 별도로 확인하세요. - 날짜처럼 보이는 숫자. Excel은 열거나 저장할 때
1-2나03/04같은 값을 자동으로 날짜로 변환합니다. 제품 코드나 버전 번호를 내보내는 경우, Excel에서 작은따옴표를 앞에 붙여 이를 방지하세요('1-2) — 또는 파일을 생성하는 사람에게 그렇게 하도록 알려주세요. - 끝에 쉼표. 일부 내보내기 도구는 모든 줄 끝에 쉼표를 추가하여 가상의 빈 열을 만듭니다. 견고한 파서는 이를 무시하지만, 단순한 split은 추가로 빈 문자열 요소를 만듭니다.
이상해 보이는 파일을 다루고 있다면, CSV 검증기가 파일이 올바른 형식인지 빠르게 알려주고 처리 전에 인코딩이나 구조 문제를 표시합니다.
CSV vs JSON vs Excel — 언제 무엇을 사용할까
이 세 형식은 실제로 많이 겹치지만, 각각 명확한 강점이 있습니다:
- CSV를 사용하세요 시스템 간에 평탄하고 표형식 데이터를 이동할 때 — 데이터베이스 내보내기, 분석 파이프라인, 스프레드시트 가져오기, 대량 데이터 로딩. 보편적으로 지원되고, 크기가 작으며, git에서 쉽게 diff할 수 있습니다. 제약: 평탄합니다. 중첩 없음, 유형 없음, 관계 없음.
- JSON을 사용하세요 데이터가 계층적이거나 스키마가 중요할 때. 여러 항목이 있는 주문, 중첩 객체가 있는 설정 파일, API 응답 — 이것들은 자연스럽게 JSON입니다. CSV는 데이터를 정규화 해제하거나 자체 중첩 규칙을 발명하게 만듭니다. JSON 명세는 명확하고 모호하지 않습니다; 형식은 유형(숫자, 불리언, null, 배열, 객체)을 보존합니다.
- Excel(.xlsx)을 사용하세요 출력이 기계가 아닌 사람을 위한 것일 때. 형식, 수식, 여러 시트, 차트 — 비즈니스 사용자가 최종 소비자라면 Excel이 종종 올바른 선택입니다. 시스템 간 교환 형식으로는 절대 사용하지 마세요. OOXML 명세는 방대하며 버전 간에 불안정합니다.
실용적인 경험칙이 있습니다: 데이터를 스프레드시트에서 자연스럽게 볼 것 같다면 CSV가 아마 괜찮습니다. 트리나 코드 에디터에서 볼 것 같다면 JSON을 사용하세요. 필터링하고 정렬할 비즈니스 관계자에게 보낸다면 Excel을 사용하세요.
마무리
CSV는 설계상 단순하고 실제로는 놀랍도록 까다롭습니다. 형식에는 강제된 표준이 없고,
여러 구분자 변형으로 제공되며, 모든 수동 파싱 시도를 망가뜨리는 인용 규칙에 의존합니다.
해결책은 항상 같습니다: 진짜 파서를 사용하세요(Python의 내장 csv 모듈 또는 JavaScript의 PapaParse),
항상 인코딩을 명시적으로 지정하고, Excel의 BOM 바이트를 주의하세요. 신뢰할 수 있는 파서가 있으면
CSV는 실제로 훌륭한 형식입니다 — 생성하기 빠르고, 텍스트 에디터에서 쉽게 검사할 수 있으며, 어디서나
지원됩니다. 일상적인 CSV 파일 작업에는
CSV를 JSON으로, CSV 포매터,
CSV 검증기 도구가 코드 한 줄 없이 일반적인 작업을 처리합니다.