Node.js 스트림으로 대용량 CSV 파싱 메모리 문제 해결
문제 상황
고객사 주문 데이터 마이그레이션 작업 중, 600MB 크기의 CSV 파일을 처리해야 했다. 기존 코드는 fs.readFile로 전체 파일을 메모리에 올린 후 파싱했는데, 프로덕션 환경에서 JavaScript heap out of memory 에러가 발생했다.
// 기존 코드 - 메모리 오버플로우 발생
const data = await fs.readFile('orders.csv', 'utf-8');
const rows = data.split('\n');
해결 방법
Node.js 스트림과 csv-parser 라이브러리를 조합하여 청크 단위로 처리하도록 변경했다. 스트림은 데이터를 작은 단위로 나눠 처리하므로 전체 파일을 메모리에 올릴 필요가 없다.
const fs = require('fs');
const csv = require('csv-parser');
const processCSV = (filePath) => {
return new Promise((resolve, reject) => {
const results = [];
let batchCount = 0;
fs.createReadStream(filePath)
.pipe(csv())
.on('data', async (row) => {
results.push(row);
// 1000개씩 배치 처리
if (results.length >= 1000) {
batchCount++;
await saveBatch(results.splice(0, 1000));
console.log(`Batch ${batchCount} processed`);
}
})
.on('end', async () => {
if (results.length > 0) {
await saveBatch(results);
}
resolve();
})
.on('error', reject);
});
};
결과
- 메모리 사용량: 1.2GB → 120MB
- 처리 시간: 큰 차이 없음 (약 45초)
- 안정성: 더 큰 파일도 처리 가능
배치 단위를 조정하면 메모리와 성능 사이 밸런스를 맞출 수 있다. 1000개 단위가 우리 케이스에서는 최적이었다.
추가 개선
pipeline API를 사용하면 에러 핸들링이 더 깔끔하다.
const { pipeline } = require('stream/promises');
await pipeline(
fs.createReadStream(filePath),
csv(),
async function* (source) {
for await (const row of source) {
yield processRow(row);
}
}
);
스트림은 Node.js의 핵심 기능이지만 실전에서 제대로 쓸 기회가 많지 않았다. 이번 작업으로 확실히 익힐 수 있었다.