| name | claude-api-streaming-frontend |
| description | Claude Messages API 스트리밍 응답을 React 프론트에서 받는 패턴. SSE 이벤트 처리, ReadableStream 파싱, useState throttle, AbortController 중단, 백엔드 프록시 권장, 프롬프트 캐싱, 에러 재시도까지 포함.
|
Claude API Streaming — Frontend Pattern
소스: https://platform.claude.com/docs/en/api/messages-streaming · https://platform.claude.com/docs/en/api/messages · https://platform.claude.com/docs/en/build-with-claude/prompt-caching · https://platform.claude.com/docs/en/api/errors · https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking · https://platform.claude.com/docs/en/about-claude/models/migration-guide · https://github.com/anthropics/anthropic-sdk-typescript · https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
검증일: 2026-08-12
버전 기준: Messages API anthropic-version: 2023-06-01, TypeScript SDK @anthropic-ai/sdk v0.116.0 (npm latest, 2026-08-12 확인), MDN SSE 표준
모델 기준: Claude Opus 5(claude-opus-5) / Sonnet 5(claude-sonnet-5) / Haiku 4.5(claude-haiku-4-5)
짝 스킬: frontend/chat-ui-pattern (메시지 리스트·virtuoso·스크롤 동작) / meta/dream-interpretation-prompt-engineering (system 프롬프트 설계)
언제 사용하나
- 채팅 UI에서 응답을 토큰 단위로 점진 렌더해야 할 때
- 긴 출력(요약·분석·코드 생성)에서 HTTP 타임아웃을 피하고 싶을 때 (
max_tokens 큰 요청)
- 사용자가 응답을 중간에 끊을 수 있어야 할 때
언제 쓰지 말까
- 단발성 짧은 응답(분류, 단일 단어 출력) —
stream: false 단순 호출이 더 단순
- 응답 전체가 도착해야만 후처리 가능한 경우(전체 JSON 파싱 필요) — SDK
finalMessage() 사용
1. 핵심 원칙 — 백엔드 프록시 권장
결론: 프로덕션에서는 반드시 백엔드 프록시. 클라이언트에서 API 키 직접 사용 금지.
| 방식 | 보안 | 용도 |
|---|
클라이언트 직접 호출 (dangerouslyAllowBrowser: true) | ❌ API 키 노출 | 로컬 프로토타입 한정 |
| 백엔드 프록시 (Next.js Route / Axum / Express / Spring) | ✅ 키 서버 보관 | 프로덕션 표준 |
주의: Anthropic SDK는 브라우저 사용 시 dangerouslyAllowBrowser: true 옵션을 명시해야 동작한다. 이름 그대로 위험 신호다. 키가 번들에 박혀 배포되거나 DevTools로 노출된다. 키 탈취 시 다크웹 reverse-proxy로 재판매되는 사례가 보고되어 있다 (OWASP API Top 10 2026).
2. SSE 응답 구조
event: message_start
data: {"type":"message_start","message":{...}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: ping
data: {"type":"ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":15}}
event: message_stop
data: {"type":"message_stop"}
포맷 규칙 (MDN SSE 표준):
- 각 줄은
field: value\n
- 한 메시지는 빈 줄(
\n\n)로 종료
- 데이터 줄은
data: 접두사
: 시작 라인은 주석 (keep-alive)
이벤트 흐름
message_start — 빈 content를 가진 Message 객체 1회
- content block 단위 (각 인덱스마다):
content_block_start 1회
content_block_delta N회
content_block_stop 1회
message_delta 1회 이상 — stop_reason, usage(누적)
message_stop 1회 (종료 신호)
ping 이벤트는 어디서든 끼어들 수 있음
error 이벤트는 200 응답 이후에도 발생 가능 — 별도 처리 필요
Delta 타입
delta.type | 발생 조건 | 누적 방법 |
|---|
text_delta | 일반 텍스트 응답 | delta.text 문자열 연결 |
input_json_delta | tool_use 입력 | delta.partial_json 연결 후 content_block_stop에서 JSON.parse |
thinking_delta | adaptive thinking | delta.thinking 연결 |
signature_delta | thinking 종료 직전 | delta.signature 1회 (무결성 검증용) |
주의: message_delta.usage의 토큰 카운트는 누적값이지 증분이 아니다.
주의 — 5 계열에서 thinking.display 기본값은 "omitted"다. Opus 5는 사고가 기본 ON이지만,
기본 설정에서는 thinking 블록이 빈 문자열로 스트리밍된다. 프론트 입장에서는
출력 시작 전 긴 정지로 보이므로, 추론 요약을 사용자에게 보여주려면 백엔드 요청에
thinking: { type: 'adaptive', display: 'summarized' }를 명시해야 한다.
display는 노출 여부만 바꾸며, 사고 자체는 어느 설정에서든 동일하게 수행·과금된다.
3. 프론트 패턴 A — 백엔드 프록시 + fetch ReadableStream
권장 패턴. 백엔드가 Anthropic으로 스트림을 받아 SSE를 그대로 클라이언트로 forward한다.
3-1. React 훅 (fetch + getReader)
import { useCallback, useRef, useState } from 'react';
interface UseClaudeStreamResult {
text: string;
isStreaming: boolean;
error: Error | null;
send: (prompt: string) => Promise<void>;
abort: () => void;
}
export function useClaudeStream(): UseClaudeStreamResult {
const [text, setText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState<Error | null>(null);
const controllerRef = useRef<AbortController | null>(null);
const abort = useCallback(() => {
controllerRef.current?.abort();
controllerRef.current = null;
}, []);
const send = useCallback( (: ) => {
();
controller = ();
controllerRef. = controller;
();
();
();
{
res = (, {
: ,
: { : },
: .({ prompt }),
: controller.,
});
(!res.) {
();
}
(!res.) {
();
}
reader = res..();
decoder = ();
buffer = ;
pending = ;
: | = ;
= () => {
rafId = ;
(pending) {
( prev + pending);
pending = ;
}
};
= () => {
pending += chunk;
(rafId === ) {
rafId = (flush);
}
};
() {
{ done, value } = reader.();
(done) ;
buffer += decoder.(value, { : });
: ;
((sepIdx = buffer.()) !== -) {
rawEvent = buffer.(, sepIdx);
buffer = buffer.(sepIdx + );
dataLines = rawEvent
.()
.( l.())
.( l.());
(dataLines. === ) ;
dataStr = dataLines.();
: ;
{
json = .(dataStr);
} {
;
}
evt = json {
: ;
?: { ?: ; ?: };
?: { : ; : };
};
(evt. === && evt.?. === ) {
(evt.. ?? );
} (evt. === ) {
(evt.?. ?? );
} (evt. === ) {
}
}
}
(rafId !== ) (rafId);
();
} (e) {
((e ). === ) {
} {
(e );
}
} {
();
controllerRef. = ;
}
}, [abort]);
{ text, isStreaming, error, send, abort };
}
3-2. 컴포넌트에서 unmount cleanup
import { useEffect } from 'react';
import { useClaudeStream } from '@/hooks/useClaudeStream';
export function ChatBox() {
const { text, isStreaming, error, send, abort } = useClaudeStream();
useEffect(() => {
return () => abort();
}, [abort]);
return (
<div>
<pre>{text}</pre>
{isStreaming ? (
<button onClick={abort}>중단</button>
) : (
<button onClick={() => send('한국 도덕교육에서 절제의 의미는?')}>전송</button>
)}
{error && <p>에러: {error.message}</p>}
</div>
);
}
4. 백엔드 프록시 변형
4-1. Next.js 15 Route Handler (Edge runtime)
import Anthropic from '@anthropic-ai/sdk';
export const runtime = 'edge';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
export async function POST(req: Request) {
const { prompt } = await req.json();
const upstream = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
system: [
{
type: 'text',
text: '당신은 도덕교육 전문가입니다. ...',
cache_control: { type: 'ephemeral' },
},
],
messages: [{ role: 'user', content: prompt }],
stream: true,
});
const encoder = new TextEncoder();
const stream = ({
() {
{
( event upstream) {
payload = ;
controller.(encoder.(payload));
}
controller.();
} (err) {
errPayload = ;
controller.(encoder.(errPayload));
controller.();
}
},
});
(stream, {
: {
: ,
: ,
: ,
},
});
}
4-2. Rust (Axum) — SSE 응답
use axum::response::sse::{Event, Sse};
use futures::stream::Stream;
4-3. Spring Boot WebFlux — SseEmitter / Flux
Flux<ServerSentEvent<String>> 반환 또는 SseEmitter 사용. 핵심은 동일하게 Anthropic 응답을 그대로 forward.
주의: 어떤 백엔드든 응답 헤더에 Content-Type: text/event-stream 과 Cache-Control: no-cache 를 반드시 설정한다. 프록시·CDN의 응답 버퍼링도 차단해야 한다 (Cloudflare·nginx).
5. 프론트 패턴 B — Anthropic SDK 직접 (브라우저, 비권장)
빠른 프로토타입 한정. 프로덕션 금지.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({
apiKey: import.meta.env.VITE_ANTHROPIC_API_KEY,
dangerouslyAllowBrowser: true,
});
const stream = client.messages.stream({
model: 'claude-sonnet-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
});
stream.on('text', (chunk) => {
});
stream.on('error', (err) => console.error(err));
stream.controller.abort();
SDK 이벤트 메서드:
.on('text', cb) — text_delta 자동 누적된 청크
.on('message', cb) — 최종 Message
.on('error', cb) — 에러
await stream.finalMessage() — 최종 누적 Message 반환
상세 레퍼런스 (예제·고급 패턴·흔한 실수) → references/REFERENCE.md