k
korAI
고급 전체
🔥 고급2026-09-066~8분

스트리밍 UI에서 백프레셔와 청크 버퍼링으로 사용자 경험 안정화하기

Claude Streaming API를 프로덕션에 붙일 때 흔히 간과되는 백프레셔 문제와 청크 드롭을 방지하는 버퍼링 전략을 수치와 함께 설명한다.

streamingperformanceui

왜 스트리밍 UI는 프로덕션에서 자주 깨지는가

스트리밍 응답은 체감 지연을 줄이지만, 네트워크 계층과 UI 렌더 루프 사이 속도 불일치(백프레셔)가 발생하면 청크 유실·중복 렌더·레이아웃 쉬프트로 이어진다. 실측 기준으로 Claude claude-opus-4-5 는 피크 시 약 80~120 토큰/초를 생성하는데, React setState를 청크마다 호출하면 60fps 기준 16ms 예산을 초과해 평균 프레임 드롭이 40% 이상 발생한다.

핵심 전략: 시간 기반 청크 병합(Time-Window Batching)

청크를 수신하는 즉시 DOM에 반영하지 않고, 16~32ms 윈도우로 누적 후 한 번에 업데이트한다. 이 방식은 setState 호출을 최대 90% 줄이면서도 사용자가 느끼는 타이핑 효과는 유지한다.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

async function streamWithBatching(
  onChunk: (text: string) => void
): Promise<void> {
  let buffer = "";
  let flushTimer: ReturnType<typeof setTimeout> | null = null;
  const FLUSH_INTERVAL_MS = 24; // ~2프레임

  const flush = () => {
    if (buffer.length > 0) {
      onChunk(buffer);
      buffer = "";
    }
    flushTimer = null;
  };

  const stream = await client.messages.stream({
    model: "claude-opus-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "장문 보고서를 작성해줘." }],
  });

  for await (const event of stream) {
    if (
      event.type === "content_block_delta" &&
      event.delta.type === "text_delta"
    ) {
      buffer += event.delta.text;
      if (!flushTimer) {
        flushTimer = setTimeout(flush, FLUSH_INTERVAL_MS);
      }
    }
  }

  // 스트림 종료 후 잔여 버퍼 처리
  if (flushTimer) clearTimeout(flushTimer);
  flush();
}

실패 모드와 운영 체크리스트

실패 모드

  • FLUSH_INTERVAL_MS를 50ms 이상으로 설정하면 타이핑 애니메이션이 끊기는 것처럼 보여 UX 점수 하락
  • 스트림 도중 사용자가 페이지를 이탈하면 타이머가 메모리에 잔류 → AbortController로 반드시 정리
  • SSE 프록시(Next.js API Route 등)를 거칠 경우 프록시 버퍼가 추가되어 실제 지연이 2배로 증가할 수 있음

트레이드오프

  • 버퍼 윈도우 ↑ → CPU 사용률 ↓, 체감 지연 ↑
  • 버퍼 윈도우 ↓ → 즉각적 반응, 렌더 비용 ↑
  • 권장값: 일반 텍스트 24ms, 코드 블록 렌더러 포함 시 32ms

운영 체크리스트

  • [ ] stream.finalMessage()로 finish_reason 검증 (stop vs max_tokens)
  • [ ] 스트림 연결 타임아웃 30초 설정, 재연결 시 누적 텍스트 복원 로직 구현
  • [ ] 서버 → 클라이언트 구간 p99 지연 모니터링 (목표: <200ms)
  • [ ] AbortController 연동으로 컴포넌트 언마운트 시 스트림 즉시 해제