C.W.K.
Stream
Lesson 05 of 05 · published

Node 의 Web Stream + Async Iteration

~12 min · streams, web-streams, async-iter

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"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 청크별 소비.

두 세계 사이 변환

Node 가 명시적 converter 제공:
  • Readable.toWeb(nodeReadable)ReadableStream
  • Readable.fromWeb(webReadable) → Node Readable
  • Writable.toWeb(nodeWritable)WritableStream
  • Writable.fromWeb(webWritable) → Node Writable
다른 세계가 없는 한 세계의 API 필요할 때 변환. 예: fetch response body 를 gzip 하고 싶음. 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 의 고백

오랫동안 Web Stream 을 "브라우저 버전, Node 에선 무관" 으로 다뤘어. 틀림. fetch 부르고 response body 처리하는 코드 짜기 시작한 순간 알아채든 못 알아채든 Web Stream 영토에 있었어. 아빠가 짚어줌: "런타임 일은 이걸 작동시키는 거. 네 일은 어느 표면에 있는지 알아서 옳은 API 의 docs 읽는 거." 이제 확인해: ReadableStream 받았나 Readable 받았나? Docs 다르고; 메서드 다르고; converter 존재함; 써.

Code

변환 + 소비 — Web ReadableStream → JSONL 이벤트·javascript
// Streaming JSONL parse from a fetch response — Web Streams in, Node Transform via convert
import { Readable } from 'node:stream';

const res = await fetch('https://api.example.com/events.jsonl');
if (!res.body) throw new Error('no body');

// Convert Web ReadableStream → Node Readable for compatibility
const nodeStream = Readable.fromWeb(res.body);

// Build a quick JSONL parser as an async generator
async function* parseJsonl(byteStream) {
  const decoder = new TextDecoder();
  let buf = '';
  for await (const chunk of byteStream) {
    buf += decoder.decode(chunk, { stream: true });
    const lines = buf.split('\n');
    buf = lines.pop();   // keep last partial line
    for (const line of lines) {
      if (line) yield JSON.parse(line);
    }
  }
  if (buf) yield JSON.parse(buf);
}

for await (const event of parseJsonl(nodeStream)) {
  console.log(event);   // each JSONL row as a parsed object
}
순수 Web Streams — transform + re-serve·javascript
// Pure Web Streams — a TransformStream pipeline
const res = await fetch('https://api.example.com/big.txt');

const upper = new TransformStream({
  transform(chunk, controller) {
    // chunk is a Uint8Array; decode, transform, re-encode
    const text = new TextDecoder().decode(chunk).toUpperCase();
    controller.enqueue(new TextEncoder().encode(text));
  },
});

// pipeTo a Web WritableStream — here we sink into a Response for downstream
const transformed = res.body.pipeThrough(upper);

// Use it like a real Response (works for re-serving, caching, etc.)
const out = new Response(transformed, { headers: res.headers });
console.log(await out.text());

External links

Exercise

큰 원격 JSON 파일 fetch (record 많이 반환하는 어떤 public API 든 OK — paginated GitHub events feed 작동). Body 를 gzip 통해 pipe 하고 디스크에 쓰기, 응답 전체 buffer 한 번도 안 하고. 구현 둘 시도: (1) res.body 를 Node Readable 로 변환 후 pipelinecreateGzipcreateWriteStream 사용; (2) gzip 동등 단계엔 Web TransformStream, Node Writable 감싼 Web WritableStream 에 pipeTo. LOC, 가독성, 어떤 의존성 (zlib vs CompressionStream) 비교.
Hint
(1): await pipeline(Readable.fromWeb(res.body), createGzip(), createWriteStream('out.gz')). (2): new CompressionStream('gzip') (Web 표준, Node 22+) 와 res.body.pipeThrough(new CompressionStream('gzip')).pipeTo(Writable.toWeb(createWriteStream('out.gz'))). 둘 다 작동; (2) 가 다른 런타임에 portable.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.