Node.js 스트림으로 대용량 CSV 파싱 최적화

문제 상황

사용자가 업로드한 CSV 파일을 파싱해서 DB에 저장하는 API를 구현했다. 초기에는 fs.readFileSync로 파일을 전부 읽어서 처리했는데, 150MB 파일이 올라오면서 JavaScript heap out of memory 에러가 발생했다.

// 기존 코드 (문제)
const data = fs.readFileSync(filePath, 'utf8');
const rows = data.split('\n');
rows.forEach(row => {
  // 파싱 및 DB 저장
});

해결: Stream 기반 처리

Node.js의 Stream API를 활용해 청크 단위로 처리하도록 변경했다. csv-parser 라이브러리와 조합해서 사용했다.

const fs = require('fs');
const csvParser = require('csv-parser');
const { promisify } = require('util');
const pipeline = promisify(require('stream').pipeline);

async function processCSV(filePath) {
  const results = [];
  const batchSize = 1000;
  
  await pipeline(
    fs.createReadStream(filePath),
    csvParser(),
    async function* (source) {
      let batch = [];
      for await (const row of source) {
        batch.push(row);
        if (batch.length >= batchSize) {
          await saveBatch(batch);
          batch = [];
        }
      }
      if (batch.length > 0) {
        await saveBatch(batch);
      }
    }
  );
}

결과

  • 메모리 사용량: ~800MB → ~80MB
  • 150MB 파일 처리 시간: 23초 → 18초
  • OOM 에러 해결

배치 단위로 DB에 저장하면서 bulk insert를 활용한 것도 속도 개선에 기여했다. Stream은 메모리 효율뿐 아니라 backpressure 제어도 자동으로 처리해주기 때문에 안정성도 높아졌다.

참고사항

Node.js 10부터는 stream.pipeline을 사용하면 에러 핸들링이 더 간결해진다. async generator를 Transform stream으로 사용할 수 있는 것도 편리했다.