Files
2nd/10_Wiki/Topics/Coding/Frontend_Streams_API.md
T
2026-05-09 22:47:42 +09:00

7.4 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-streams-api Streams API — ReadableStream / TransformStream / pipeThrough Coding draft B conceptual 2026-05-09 2026-05-09
frontend
streams
vibe-coding
language applicable_to
TS
Frontend
Backend
Streams
ReadableStream
WritableStream
TransformStream
pipeThrough
backpressure

Streams API

Browser + Node + Deno + Bun 에 표준. ReadableStream → TransformStream → WritableStream. Fetch streaming, SSE, AI streaming, file 처리. Backpressure 자동.

📖 핵심 개념

  • ReadableStream: 데이터 출력.
  • WritableStream: 데이터 입력.
  • TransformStream: middle (변환).
  • Backpressure: consumer 가 느리면 producer 가 자동 멈춤.

💻 코드 패턴

Fetch streaming (browser)

const res = await fetch('/api/large');
const reader = res.body!.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log('chunk:', value.byteLength);
}

Text decoder

const res = await fetch('/api/sse');
const reader = res.body!
  .pipeThrough(new TextDecoderStream())
  .getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(value);  // string chunks
}

TransformStream (custom)

const upper = new TransformStream<string, string>({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  },
});

await fetch('/text')
  .then(r => r.body!)
  .then(s => s.pipeThrough(new TextDecoderStream()))
  .then(s => s.pipeThrough(upper))
  .then(s => s.pipeTo(new WritableStream({
    write(chunk) { console.log(chunk); }
  })));

LLM SSE streaming (fetch)

async function* streamLLM(prompt: string) {
  const res = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ prompt }),
    headers: { 'content-type': 'application/json' },
  });
  
  const reader = res.body!
    .pipeThrough(new TextDecoderStream())
    .getReader();
  
  let buffer = '';
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += value;
    
    let idx;
    while ((idx = buffer.indexOf('\n\n')) >= 0) {
      const event = buffer.slice(0, idx);
      buffer = buffer.slice(idx + 2);
      
      if (event.startsWith('data: ')) {
        const json = event.slice(6);
        if (json === '[DONE]') return;
        yield JSON.parse(json);
      }
    }
  }
}

// 사용
for await (const chunk of streamLLM('Hello')) {
  process.stdout.write(chunk.text);
}

Server-side streaming (Hono / Bun)

import { stream } from 'hono/streaming';

app.get('/stream', (c) => {
  return stream(c, async (stream) => {
    for (let i = 0; i < 100; i++) {
      await stream.writeln(`chunk ${i}`);
      await stream.sleep(100);
    }
  });
});

Manual ReadableStream

const rs = new ReadableStream({
  start(controller) {
    controller.enqueue('a');
    controller.enqueue('b');
    controller.close();
  },
});

const reader = rs.getReader();
const { value } = await reader.read();

Async iterator (Streams 도)

const rs = new ReadableStream({
  start(controller) {
    setInterval(() => controller.enqueue(Date.now()), 1000);
  },
});

// for await
for await (const v of rs) {
  console.log(v);
}

→ Modern browsers 가 stream 의 async iterator 지원.

File API (browser, large file)

const file = input.files![0];
const stream = file.stream();

const reader = stream.getReader();
let bytesRead = 0;
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  bytesRead += value.byteLength;
  console.log(`progress: ${bytesRead / file.size * 100}%`);
}

→ 큰 파일 — 메모리에 올리지 않고 stream.

DecompressionStream

const res = await fetch('/data.gz');
const decompressed = res.body!.pipeThrough(new DecompressionStream('gzip'));
const text = await new Response(decompressed).text();

CompressionStream

const text = 'Lorem ipsum...';
const compressed = new Blob([text]).stream()
  .pipeThrough(new CompressionStream('gzip'));

await fetch('/upload', { method: 'POST', body: compressed });

Web Worker + Stream (transferable)

const stream = new ReadableStream({...});
worker.postMessage({ stream }, [stream]);

→ Stream 도 transferable.

Cancel

const reader = stream.getReader();
// 중단
await reader.cancel('user cancelled');

// AbortController (fetch)
const ac = new AbortController();
fetch('/stream', { signal: ac.signal });
ac.abort();

Backpressure

const ws = new WritableStream({
  async write(chunk) {
    // 느린 처리
    await db.insert(chunk);
  },
}, new CountQueuingStrategy({ highWaterMark: 10 }));

await readable.pipeTo(ws);
// → 자동 backpressure: write 느리면 read 멈춤

Tee (split stream)

const [a, b] = readable.tee();

a.pipeTo(write1);
b.pipeTo(write2);

Error handling

const tx = new TransformStream({
  transform(chunk, controller) {
    try {
      controller.enqueue(JSON.parse(chunk));
    } catch (e) {
      controller.error(e);  // downstream 도 오류
    }
  },
});

await readable.pipeTo(write).catch(err => {
  console.error('stream error:', err);
});

Node.js (web stream)

import { Readable } from 'node:stream';

// Node stream → Web stream
const webStream = Readable.toWeb(nodeStream);

// Web stream → Node stream
const nodeStream = Readable.fromWeb(webStream);

React UI (streaming render)

function ChatMessage({ stream }: { stream: ReadableStream }) {
  const [text, setText] = useState('');
  
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const reader = stream.getReader();
      while (!cancelled) {
        const { done, value } = await reader.read();
        if (done) break;
        setText(t => t + value);
      }
    })();
    return () => { cancelled = true; };
  }, [stream]);
  
  return <p>{text}</p>;
}

Bun streams

const file = Bun.file('large.txt');
for await (const chunk of file.stream()) {
  // ...
}

Streaming SSR (React 19 / Next)

// Next.js
export default async function Page() {
  return (
    <Suspense fallback={<Spinner />}>
      <SlowComponent />
    </Suspense>
  );
}

→ 서버 가 HTML 을 stream. 빠른 first byte.

🤔 의사결정 기준

상황 추천
Fetch 큰 response ReadableStream
LLM streaming TextDecoderStream + 파싱
File upload 큰 File.stream()
변환 chain TransformStream
Compress / decompress Compression API
Server stream Hono / Bun stream
Worker 통신 Transferable stream

안티패턴

  • 모두 메모리에 (await res.text()): 큰 = OOM. Stream.
  • Backpressure 무시 (manual write loop): 메모리 폭주.
  • Cancel 안 함 (component unmount): 누수.
  • String concatenation in transform: copy 폭발.
  • Tee 후 한 쪽만 read: 다른 쪽 블락.
  • Error 무전파: 디버깅 어려움.
  • Node Buffer + Web Stream 혼동: type 깨짐.

🤖 LLM 활용 힌트

  • Browser + Node + Deno + Bun 표준.
  • TextDecoderStream / CompressionStream 가 freebie.
  • pipeThrough chain 으로 복잡 변환.
  • Backpressure 자동 (highWaterMark).

🔗 관련 문서