스트리밍 UI에서 백프레셔·청크 버퍼링으로 UX 끊김 없애기
Anthropic SDK의 스트리밍 응답을 프로덕션 UI에 연결할 때 발생하는 백프레셔·드롭 문제를 청크 버퍼링과 속도 제어로 해결하는 실전 패턴을 다룬다.
왜 스트리밍 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 모니터링 알림 설정