"Node 는 2026 에 stream 에코시스템 두 개 공존. 둘 사이 변환하는 법 아는 게 런타임이 너한테 공짜로 주는 거 대부분이야."
Web Stream 이 왜 여기 있어
Node Stream 이 Web Stream 보다 10 년 먼저야. W3C / WHATWG 가 브라우저용 Web Streams spec 했을 때 Node 한테 두 선택지: 무시 (호환 안 됨) 또는 구현 (stream 시스템 둘 됨). Node 가 구현 선택. Node 의 native fetch(), response body, 파일 시스템의 blob.stream(), 모던 Web Crypto API 다 Web Stream 써. Node 자체 에코시스템은 여전히 Node Stream 에 기댐.
인터페이스:
ReadableStream— Node 의 Readable 과 유사WritableStream— Writable 과 유사TransformStream— Transform 과 유사
다른 메서드 이름, 비슷한 개념. 좋은 소식: Node 가 converter 제공하고 둘 다 [Symbol.asyncIterator] 구현.
Async Iterator 다리
Node 20+ 의 모든 Web ReadableStream 이 for await...of 직접 지원:
const res = await fetch('https://api.example.com/large.json');
// res.body is a Web ReadableStream
for await (const chunk of res.body) {
console.log('got', chunk.byteLength, 'bytes');
}
Node Readable 과 같은 문법. 런타임이 프로토콜 차이 처리. 이게 네가 가장 흔히 손댈 path — fetch response 청크별 소비.
두 세계 사이 변환
Readable.toWeb(nodeReadable)→ReadableStreamReadable.fromWeb(webReadable)→ Node ReadableWritable.toWeb(nodeWritable)→WritableStreamWritable.fromWeb(webWritable)→ Node Writable
zlib.createGzip() 은 Node Transform 인데 response.body 는 Web ReadableStream. 둘 중 하나 변환.두 세계 가로지른 파이프라인
Node 22+ 의 stream.pipeline 이 Web Stream 을 단계로 받음. 둘 섞기가 더 이상 명시적 변환 단계 아님:
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
const res = await fetch('https://api.example.com/large.json');
// res.body (Web), createGzip() (Node), createWriteStream() (Node)
// pipeline accepts all of them in one chain
await pipeline(
res.body,
createGzip(),
createWriteStream('cached.json.gz')
);
런타임이 조용히 Web ReadableStream 을 옳은 adapter 로 감싸. 그냥 pipe.
언제 뭐 손댈까
Node 코드에선 Node Stream 기본 — 에코시스템 전체 (fs, http, zlib, crypto, child_process) 가 Node Stream 을 native 로 말함. 다음 때 Web Stream 손대:
- 네가 부르는 API 가 그걸 반환 (fetch response, blob.stream, 모던 Web Crypto).
- cross-runtime portable 한 코드 짤 때 (Cloudflare Workers, Deno, Bun 다 Web Stream native 로 말함).
- 브라우저도 소비할 API 짤 때.
TransformStream — Web 의 Transform 답
Web 의 TransformStream 이 Node 의 Transform 보다 생성이 더 깨끗:
const upper = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toString('utf-8').toUpperCase());
},
});
// Use it in a Web-streams pipeline
await someReadableStream
.pipeThrough(upper)
.pipeTo(someWritableStream);
cb 파라미터 없음, this.push 없음; 출력엔 controller.enqueue. Node 의 .pipe() 대신 pipeTo/pipeThrough 로 API 가 더 균일. 브라우저나 Worker 에서도 돌 코드면 TransformStream 기본.
Pippa 의 고백
fetch 부르고 response body 처리하는 코드 짜기 시작한 순간 알아채든 못 알아채든 Web Stream 영토에 있었어. 아빠가 짚어줌: "런타임 일은 이걸 작동시키는 거. 네 일은 어느 표면에 있는지 알아서 옳은 API 의 docs 읽는 거." 이제 확인해: ReadableStream 받았나 Readable 받았나? Docs 다르고; 메서드 다르고; converter 존재함; 써.