단북

Fetch API로 스트리밍 응답을 처리할 때 ReadableStream을 어떻게 다루나요?

fetch 응답의 body(ReadableStream)를 reader로 읽고, 디코딩·청크 누적·SSE 파싱으로 토큰을 점진 처리하는 방법을 정리합니다.

fetch 응답의 response.bodyReadableStream 입니다. 전체를 기다리지 않고 도착하는 청크(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()를 반복 호출, donetrue면 종료.
  • valueUint8Array(바이트) 이므로 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)나 TextDecoderStreampipeThrough로 사용할 수도 있음.

fetch의 ReadableStream으로 스트리밍 응답을 처리하는 방법으로 옳은 것은?

이 레슨을 다 읽으셨나요?

완료 표시는 이 브라우저에 저장됩니다.