Node.js 스트림으로 대용량 CSV 파싱 메모리 이슈 해결
문제 상황
고객사 데이터 마이그레이션 작업 중 200MB가 넘는 CSV 파일을 처리하다가 Node.js 프로세스가 메모리 부족으로 죽는 현상이 발생했다. fs.readFileSync로 전체 파일을 읽고 파싱하는 방식이었다.
const fs = require('fs');
const csv = require('csv-parser');
// 문제가 된 코드
const data = fs.readFileSync('large-data.csv', 'utf-8');
const rows = data.split('\n').map(row => parseRow(row));
해결 방법
Node.js의 Readable Stream을 활용해 파일을 청크 단위로 읽으면서 처리하도록 변경했다.
const fs = require('fs');
const csv = require('csv-parser');
const { pipeline } = require('stream');
let processedCount = 0;
const readStream = fs.createReadStream('large-data.csv')
.pipe(csv())
.on('data', async (row) => {
readStream.pause();
await processRow(row);
processedCount++;
readStream.resume();
})
.on('end', () => {
console.log(`처리 완료: ${processedCount}건`);
})
.on('error', (err) => {
console.error('에러 발생:', err);
});
개선 결과
- 메모리 사용량: 1.2GB → 80MB
- 처리 시간: 큰 차이 없음 (오히려 10% 정도 빨라짐)
- 안정성: OOM 에러 완전 해소
추가 최적화
DB insert를 배치로 처리하기 위해 버퍼링을 추가했다.
const BATCH_SIZE = 1000;
let buffer = [];
readStream.on('data', async (row) => {
buffer.push(row);
if (buffer.length >= BATCH_SIZE) {
readStream.pause();
await bulkInsert(buffer);
buffer = [];
readStream.resume();
}
});
스트림은 Node.js의 핵심 개념이지만 실제로 쓸 일이 없어서 미뤄뒀는데, 이번 기회에 제대로 이해하게 됐다. 대용량 데이터 처리에서는 필수적인 패턴인 것 같다.