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

문제 상황

사용자가 업로드한 CSV 파일을 파싱해 DB에 저장하는 API를 개발했는데, 100만 행이 넘는 파일에서 JavaScript heap out of memory 에러가 발생했다. 기존 코드는 fs.readFile로 파일 전체를 읽고 처리하는 방식이었다.

const csv = require('csv-parser');
const fs = require('fs');

// 기존 방식 - 메모리 문제 발생
const data = fs.readFileSync('large-file.csv', 'utf8');
const rows = parse(data); // 전체 데이터를 메모리에 적재

해결 방법

Node.js의 스트림을 활용해 청크 단위로 데이터를 처리하도록 변경했다. 파일을 한 번에 읽지 않고 일정 크기씩 읽어가며 처리하는 방식이다.

const fs = require('fs');
const csv = require('csv-parser');
const { pipeline } = require('stream/promises');

async function processLargeCSV(filePath) {
  const results = [];
  const batchSize = 1000;
  
  return new Promise((resolve, reject) => {
    fs.createReadStream(filePath)
      .pipe(csv())
      .on('data', async (row) => {
        results.push(row);
        
        if (results.length >= batchSize) {
          // 배치 단위로 DB insert
          await insertBatch(results.splice(0, batchSize));
        }
      })
      .on('end', async () => {
        if (results.length > 0) {
          await insertBatch(results);
        }
        resolve();
      })
      .on('error', reject);
  });
}

결과

  • 메모리 사용량: 2GB → 200MB로 감소
  • 처리 시간: 큰 차이 없음 (오히려 배치 insert로 약간 개선)
  • 파일 크기 제한 해제 가능

배압(backpressure) 제어를 위해 stream.pause()stream.resume()을 활용하면 더욱 안정적인 처리가 가능하다. 대용량 데이터 처리 시 스트림은 선택이 아닌 필수였다.