7.4 KiB
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 |
|
|
|
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).