Node.js 스트림으로 대용량 CSV 파싱 최적화하기
문제 상황
데이터 마이그레이션 작업 중 800MB 크기의 CSV 파일을 파싱하는 과정에서 heap out of memory 에러가 발생했다. 기존 코드는 fs.readFileSync로 전체 파일을 메모리에 올린 후 처리하는 방식이었다.
const data = fs.readFileSync('large.csv', 'utf-8');
const rows = data.split('\n'); // 메모리 부족 발생
스트림 기반 접근
Node.js의 스트림을 활용해 파일을 청크 단위로 읽고 처리하도록 변경했다. csv-parser 라이브러리와 조합했다.
const fs = require('fs');
const csv = require('csv-parser');
const { Transform } = require('stream');
const processRow = new Transform({
objectMode: true,
transform(row, encoding, callback) {
// 데이터 가공
const processed = {
id: row.id,
email: row.email.toLowerCase(),
created_at: new Date(row.timestamp)
};
callback(null, processed);
}
});
fs.createReadStream('large.csv')
.pipe(csv())
.pipe(processRow)
.on('data', async (row) => {
await db.insert(row);
})
.on('end', () => {
console.log('완료');
});
배압(Backpressure) 처리
초기에는 DB 삽입 속도보다 파싱 속도가 빨라 메모리가 다시 쌓이는 문제가 있었다. pause()와 resume()으로 배압을 제어했다.
const stream = fs.createReadStream('large.csv')
.pipe(csv())
.on('data', async (row) => {
stream.pause();
await db.insert(row);
stream.resume();
});
결과
- 메모리 사용량: 1.2GB → 50MB
- 처리 시간: 45초 → 38초
- 안정성 확보로 더 큰 파일도 처리 가능
스트림은 Node.js의 핵심 강점 중 하나다. 대용량 데이터 처리 시 선택이 아닌 필수였다.