Fetch API로 스트리밍 응답을 처리할 때 ReadableStream을 어떻게 다루나요?
fetch 응답의 body(ReadableStream)를 reader로 읽고, 디코딩·청크 누적·SSE 파싱으로 토큰을 점진 처리하는 방법을 정리합니다.
fetch 응답의 response.body 는 ReadableStream 입니다. 전체를 기다리지 않고 도착하는 청크(chunk)를 순차적으로 읽어 화면에 점진 출력할 수 있습니다. LLM 토큰 스트리밍 처리의 기본입니다.
기본 패턴 (reader)
const res = await fetch('/api/chat', { method: 'POST', body, signal });
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read(); // 청크를 하나씩
if (done) break;
const text = decoder.decode(value, { stream: true }); // Uint8Array → 문자열
appendToUI(text); // 도착하는 대로 출력
}getReader()로 리더를 얻어read()를 반복 호출,done이true면 종료.value는Uint8Array(바이트) 이므로TextDecoder로 디코딩.{ stream: true }로 멀티바이트 문자가 청크 경계에서 잘리는 것을 안전 처리.
SSE 형식 파싱
LLM API는 보통 data: {...}\n\n 형태의 SSE를 보냅니다. 청크는 줄·이벤트 경계에 맞지 않게 도착할 수 있으므로 버퍼에 모아 경계 단위로 파싱합니다.
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split('\n\n'); // 이벤트 구분
buffer = parts.pop() ?? ''; // 마지막 미완성 조각은 남겨 둠
for (const part of parts) {
const line = part.replace(/^data: /, '');
if (line === '[DONE]') return;
const json = JSON.parse(line);
appendToUI(json.delta);
}
}주의할 점
- 부분 청크 처리 — 한 청크가 한 토큰/한 줄과 일치하지 않음. 반드시 버퍼링 후 경계로 파싱.
- 취소 —
signal로 중단 연결. 중단 시reader.cancel()도 고려. - 에러·종료 —
done과[DONE]신호 구분,try/catch로 네트워크 오류 처리, 끝나면 리더 해제. - 모던 대안 — 환경에 따라
for await (const chunk of res.body)(async iterator)나TextDecoderStream을pipeThrough로 사용할 수도 있음.
fetch의 ReadableStream으로 스트리밍 응답을 처리하는 방법으로 옳은 것은?
이 레슨을 다 읽으셨나요?
완료 표시는 이 브라우저에 저장됩니다.