Node.js 스트림으로 대용량 CSV 파일 처리하기
문제 상황
관리자 페이지에서 CSV 파일을 업로드해 사용자 데이터를 일괄 등록하는 기능을 만들었다. 초기에는 fs.readFileSync로 파일 전체를 읽어 처리했는데, 100MB가 넘는 파일이 들어오면서 서버가 터지기 시작했다.
// 기존 방식 - 메모리에 전체 로드
const data = fs.readFileSync(filePath, 'utf8');
const rows = data.split('\n');
rows.forEach(row => {
// DB insert
});
스트림 기반 처리
Node.js의 스트림을 사용하면 파일을 청크 단위로 읽어 처리할 수 있다. csv-parser 라이브러리와 조합했다.
const fs = require('fs');
const csv = require('csv-parser');
function processCSV(filePath) {
return new Promise((resolve, reject) => {
let count = 0;
fs.createReadStream(filePath)
.pipe(csv())
.on('data', async (row) => {
await User.create(row);
count++;
})
.on('end', () => {
console.log(`${count} rows processed`);
resolve(count);
})
.on('error', reject);
});
}
배치 처리 추가
DB insert가 한 건씩 발생해 너무 느렸다. 배치로 묶어서 처리하도록 개선했다.
const BATCH_SIZE = 1000;
let batch = [];
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (row) => {
batch.push(row);
if (batch.length >= BATCH_SIZE) {
stream.pause();
User.bulkCreate(batch)
.then(() => {
batch = [];
stream.resume();
});
}
});
결과
- 메모리 사용량: 500MB → 50MB
- 200MB 파일 처리 시간: OOM 에러 → 2분 내외
- 서버 안정성 확보
스트림은 대용량 데이터 처리에 필수적이었다. backpressure 개념을 이해하고 pause/resume을 적절히 사용하는 게 핵심이었다.