k
korAI
고급 전체
🔥 고급2026-05-106~8분

스트리밍 UI에서 백프레셔·청크 버퍼링으로 UX 끊김 없애기

Anthropic SDK의 스트리밍 응답을 프로덕션 UI에 연결할 때 발생하는 백프레셔·드롭 문제를 청크 버퍼링과 속도 제어로 해결하는 실전 패턴을 다룬다.

streamingbackpressureproduction

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

로컬 데모에서는 매끄러운 스트리밍이 프로덕션에서 끊기는 이유는 세 가지다. ① 네트워크 지터로 청크가 몰려 도착하는 버스트 수신, ② React setState 호출이 초당 100회 이상 발생하는 리렌더 폭풍, ③ SSE 연결이 30초 Nginx 타임아웃에 걸리는 프록시 컷오프. 각 실패 모드를 계층별로 처리하지 않으면 사용자는 텍스트가 멈추거나 UI가 버벅이는 경험을 한다.

청크 버퍼링 + 속도 제어 구현

핵심 전략은 수신 스트림을 디커플링 버퍼로 받아, 일정 간격(16ms ≈ 60fps)으로 UI에 배출하는 것이다.

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

const client = new Anthropic();

async function streamWithBackpressure(
  prompt: string,
  onChunk: (text: string) => void
): Promise<void> {
  const buffer: string[] = [];
  let flushTimer: ReturnType<typeof setInterval> | null = null;
  let done = false;

  // 16ms(60fps) 간격으로 버퍼 배출
  flushTimer = setInterval(() => {
    if (buffer.length > 0) {
      onChunk(buffer.splice(0, buffer.length).join(""));
    }
    if (done && buffer.length === 0 && flushTimer) {
      clearInterval(flushTimer);
    }
  }, 16);

  const stream = client.messages.stream({
    model: "claude-opus-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: prompt }],
  });

  for await (const event of stream) {
    if (
      event.type === "content_block_delta" &&
      event.delta.type === "text_delta"
    ) {
      buffer.push(event.delta.text);
      // 버퍼가 500자 초과 시 즉시 플러시 (버스트 대응)
      if (buffer.join("").length > 500) {
        onChunk(buffer.splice(0, buffer.length).join(""));
      }
    }
  }
  done = true;
}

수치 기준: 버퍼 임계값 500자는 p95 기준 Claude 응답 청크 크기(평균 8~40자)를 고려한 값이다. 임계값을 낮추면 리렌더가 늘고, 높이면 지연이 늘어난다. 팀 환경에 맞게 A/B 테스트로 튜닝할 것.

프록시 타임아웃 및 재연결 처리

Nginx/ALB 타임아웃 대응: proxy_read_timeout 300s 설정이 없으면 기본 60초에 SSE가 끊긴다. 클라이언트 측에서는 EventSource 대신 fetch + ReadableStream을 사용해 재연결 로직을 직접 제어한다.

실패 모드별 트레이드오프:

| 문제 | 원인 | 해결책 | 부작용 | |------|------|--------|--------| | 리렌더 폭풍 | setState 과호출 | 16ms 버퍼 배출 | 최대 16ms 지연 추가 | | 버스트 드롭 | 네트워크 지터 | 500자 즉시 플러시 | 플러시 빈도 불규칙 | | 프록시 컷오프 | 타임아웃 설정 | keep-alive + 재연결 | 중복 렌더 위험 |

운영 체크리스트

  • [ ] Nginx proxy_read_timeout ≥ 300초 확인
  • [ ] 스트림 오류 시 stream.finalMessage() 로 부분 응답 수집 후 재시도
  • [ ] 클라이언트 연결 종료 시 서버 측 스트림 abort 처리 (비용 낭비 방지)
  • [ ] 버퍼 배출 간격(16ms)을 환경 변수로 외부화해 런타임 튜닝 가능하게
  • [ ] p95 첫 토큰 지연(TTFT) ≤ 800ms 모니터링 알림 설정