Node.js Streams — 대용량 데이터를 메모리 폭발 없이 처리하는 법
Readable, Writable, Transform + backpressure의 동작 원리
// ❌ 10GB 파일을 메모리에 전부 올린다
const data = fs.readFileSync('huge.csv');
// ✅ chunk 단위로 흘려보낸다 — 메모리 ~64KB 유지
fs.createReadStream('huge.csv')
.pipe(transformStream)
.pipe(fs.createWriteStream('output.csv'));
4가지 스트림 타입
Readable — 데이터를 읽어들이는 소스 (fs.createReadStream, http request)
Writable — 데이터를 쓰는 목적지 (fs.createWriteStream, http response)
Transform — 데이터를 변환하면서 통과 (zlib.createGzip, csv-parser)
Duplex — 읽기+쓰기 모두 (net.Socket, WebSocket)
Backpressure — 소비자가 느리면 생산자를 멈춘다
writable.write(chunk)가 false를 반환하면 내부 버퍼가 가득 찼다는 뜻이다. 이때 Readable은 읽기를 멈추고(pause), Writable의 'drain' 이벤트를 기다린다.
.pipe()가 이걸 자동으로 해준다. 수동으로 하려면:
readable.on('data', (chunk) => {
const ok = writable.write(chunk);
if (!ok) {
readable.pause(); // 생산 멈춤
writable.once('drain', () => {
readable.resume(); // 소비자 준비 → 생산 재개
});
}
});
backpressure 없이 빠른 Readable + 느린 Writable을 연결하면, 내부 버퍼가 계속 커져서 결국 메모리가 터진다.
pipeline() — 에러 처리까지 자동
const { pipeline } = require('stream/promises');
await pipeline(
fs.createReadStream('huge.csv'),
zlib.createGzip(),
fs.createWriteStream('huge.csv.gz')
);
// 에러 발생 시 모든 스트림을 자동으로 정리(destroy)
.pipe()는 에러 전파를 안 해서 스트림이 누수될 수 있다. pipeline()이 이걸 해결한다.
핵심 포인트
Stream은 데이터를 chunk 단위로 처리 — 전체를 메모리에 올리지 않는다
pipe()로 Readable → Transform → Writable 체이닝
write()가 false 반환 = 버퍼 가득 참 → pause/drain으로 backpressure 처리
pipeline()이 pipe()보다 안전 — 에러 시 모든 스트림 자동 destroy