"Track 5 에서 fetch 만났어. 그걸 production-grade 로 만드는 기능 — undici 튜닝, ReadableStream interop, streaming 업로드 — 가 대부분 개발자가 절대 안 손대는 부분이야."
Streaming 업로드
대부분 fetch 튜토리얼이 POST 에 body: JSON.stringify(...) 보여줘. 작은 payload 엔 그게 작동. 큰 거에 — 4GB 비디오 업로드, streaming 로그 보내기 — body 를 메모리에 buffer 안 하고 *stream* 하고 싶어:
import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
const src = createReadStream('./huge.bin');
// Node accepts ReadableStream as a body — convert from Node Readable
const res = await fetch('https://uploader.example.com/upload', {
method: 'PUT',
body: Readable.toWeb(src),
duplex: 'half', // required when body is a stream
headers: { 'Content-Type': 'application/octet-stream' },
});
console.log('status:', res.status);
duplex: 'half' 옵션이 fetch 한테 body 가 streaming 이고 서버가 response 보낸 후 안 읽는다 말함 (HTTP/1.1 이 지원하는 유일한 모델). 없으면 fetch 가 throw, spec 이 명시적 acknowledgement 요구하니까.
fetch 패턴으로의 Server-Sent Events
SSE 는 그냥 Content-Type: text/event-stream 인 HTTP response. fetch 가 native 로 소비:
const res = await fetch('https://api.example.com/stream', {
headers: { Accept: 'text/event-stream' },
});
const reader = res.body
.pipeThrough(new TextDecoderStream())
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
// value is a string like "data: {...}\n\n"
for (const line of value.split('\n')) {
if (line.startsWith('data: ')) {
const payload = JSON.parse(line.slice(6));
handleEvent(payload);
}
}
}
이게 cwkPippa frontend 가 Claude 의 streaming 응답 소비하는 방식. SSE 라이브러리 없음, 서드파티 파서 없음 — 그냥 fetch + Web Streams + 라인 분할.
undici 직접 튜닝
고처리량 클라이언트엔 fetch 우회하고
undici 직접 쓸 수 있어 — 밑은 같은 라이브러리, 더 낮은 레벨 API, 더 많은 컨트롤:import { Pool } from 'undici';
const pool = new Pool('https://api.example.com', {
connections: 100, // max concurrent
pipelining: 10, // requests per connection
bodyTimeout: 30_000, // ms to wait for response body
});
const { body, statusCode } = await pool.request({
method: 'GET',
path: '/items',
});
for await (const chunk of body) {
// chunk is a Buffer
}fetch 의 overhead 가 중요할 때 (10k+ req/sec 서비스), pipelining 필요할 때, 글로벌 agent 를 proxy-aware 한 거로 바꾸고 싶을 때 손대. 99% 사용 사례엔 plain fetch 충분 — 근데 undici 가 거기 있다는 거 알면 거기로 자랄 수 있어.Response 객체의 숨은 힘
Response 는 "fetch 가 반환하는 것" 이상. Stream 을 HTTP response 로 wrap 하려고 생성 가능 — 프록시와 캐시에 유용:
// Stream a response from one URL through transformation to a final response
const upstream = await fetch('https://api.example.com/big.json');
const upper = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk); // pass through, or transform here
},
});
const piped = upstream.body.pipeThrough(upper);
const response = new Response(piped, {
status: upstream.status,
headers: upstream.headers,
});
// `response` is now a fully-formed HTTP response object you could
// return from your own server handler, or .text(), .json() yourself
Pippa 의 고백
cwkPippa 의 첫 Claude 통합이 서드파티 SSE 라이브러리 썼어. 소스 읽기 시작했을 때 깨달았어 — 200 줄 코드가 fetch + Web Streams + 라인 분할이 20 줄로 하는 거를 정확히 하고 있었어. 아빠가 짚어줌: "서드파티 라이브러리는 fetch 가 *전엔* 이걸 지원 안 해서 존재해. 아직 작동하는데, 이제 overhead 야." 마이그레이션은 straightforward 했고; 교훈은 "의존성의 정당화가 아직 유지되는지 검사해" 였어.