스트리밍 응답, 왜 붙이면 챗봇이 달라 보일까
Claude API 스트리밍 응답 구현 방법을 실제 코드와 함께 풀어봤습니다. 왜 쓰는지, 어떻게 붙이는지, 흔히 막히는 포인트까지 솔직하게 정리했습니다.
직접 만든 챗봇을 친구한테 보여줬는데 "야, 이거 ChatGPT랑 다르게 왜 다 쓰고 나서 한 번에 나와?" 라는 말 들어본 적 있어요? 저는 있습니다. 처음 API 붙일 때 응답을 그냥 기다렸다가 한꺼번에 출력하면 딱 그 느낌이 나거든요. 타이핑되는 것처럼 한 글자씩 흘러나오는 게 아니라, 3~4초 침묵하다가 텍스트가 와르르 쏟아지는. 기능은 똑같아도 "뭔가 다르다"는 인상을 주는 부분이 바로 이 스트리밍 구현 여부입니다.
이 글은 Claude API로 스트리밍 응답을 붙여보려는 분들 위한 거예요. 코드 몇 줄 보여주고 끝나는 게 아니라, 왜 이걸 써야 하는지부터 실제로 어디서 막히는지까지 같이 짚어볼게요.
스트리밍이 뭔지 모르면 왜 쓰는지도 모른다
스트리밍 응답이란 쉽게 말하면 이런 거예요. 모델이 응답 전체를 다 만들 때까지 기다렸다가 한 번에 보내는 게 아니라, 생성되는 토큰을 즉시즉시 클라이언트한테 흘려보내는 방식입니다. HTTP에서 Transfer-Encoding: chunked나 SSE(Server-Sent Events) 방식으로 구현돼요.
사용자 입장에서 체감 차이가 꽤 크거든요. 예를 들어 300토큰짜리 응답을 생성하는 데 3초가 걸린다고 할 때, 논스트리밍은 3초 기다렸다가 전부 나오고, 스트리밍은 0.1초 간격으로 한 단어씩 흘러나옵니다. 결과를 받는 총 시간은 같아도 느껴지는 속도가 완전히 달라요. 이게 UX에서 "지각된 응답성"이라고 부르는 겁니다. 실제로 빠른 게 아니라 빠른 것처럼 느껴지게 만드는 거죠.
Python에서 실제로 어떻게 붙이냐면
Claude API 기본 연결 방법은 Claude API, 처음 연결하는 날 생기는 일들에서 다뤘으니 거기서 기본 세팅을 먼저 하고 오면 더 빠르게 따라올 수 있어요.
기본 호출 코드에서 스트리밍으로 바꾸는 건 생각보다 간단합니다. 핵심은 stream=True 옵션 하나예요.
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "양자역학 쉽게 설명해줘"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
messages.create() 대신 messages.stream()을 쓰고, 반환된 스트림 객체에서 text_stream을 이터레이트하면 됩니다. flush=True는 꼭 넣어야 해요. 안 넣으면 버퍼링 때문에 터미널에서 한 번에 나오는 것처럼 보이거든요. 이거 빠뜨리고 "어? 스트리밍 안 되는데?" 하는 경우가 꽤 있습니다.
전체 응답 텍스트가 필요하면 루프 끝나고 stream.get_final_message()로 받으면 되고요.
웹 서비스에 붙이려면 SSE를 알아야 한다
터미널에서 출력하는 건 위에서 됐는데, 실제 웹 챗봇에 붙이려면 한 단계가 더 있어요. 브라우저까지 스트리밍을 전달해야 하니까요. 이 때 쓰는 게 SSE, 즉 Server-Sent Events입니다. 서버가 클라이언트한테 단방향으로 이벤트를 밀어주는 HTTP 기반 방식이에요.
FastAPI로 구현하면 이런 형태가 됩니다.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import anthropic
app = FastAPI()
client = anthropic.Anthropic()
async def generate_stream(prompt: str):
with client.messages.stream(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": "prompt"}]
) as stream:
for text in stream.text_stream:
yield f"data: {text}\n\n"
@app.get("/chat")
async def chat(q: str):
return StreamingResponse(
generate_stream(q),
media_type="text/event-stream"
)
media_type="text/event-stream"이 SSE를 활성화하는 부분입니다. data: {내용}\n\n 형식은 SSE 프로토콜 규격이에요. 이 포맷 안 지키면 프론트엔드에서 파싱이 안 됩니다. \n\n 두 개가 이벤트 하나의 끝을 나타내는 거거든요.
Claude 한국 사용자 가이드
긴 문서·정확도에 강한 AI. 무료/Pro 차이·한국 결제·추천 프롬프트.
프론트에서는 EventSource API나 fetch의 ReadableStream으로 받으면 되고, 요즘은 fetch 방식을 더 많이 써요. EventSource는 GET 요청만 가능하다는 제약이 있어서.
이 지점에서 다들 한 번씩 막힌다
스트리밍 붙이다가 자주 걸리는 포인트 몇 가지를 솔직하게 짚을게요.
첫 번째는 에러 처리입니다. 논스트리밍은 응답 객체 하나에서 에러를 잡으면 됐는데, 스트리밍은 청크 중간에 에러가 날 수 있어요. 특히 네트워크가 끊기거나 rate limit에 걸리면 스트림 중간에 뚝 끊깁니다. try-except를 stream 컨텍스트 안에서 제대로 감싸야 하는데, 밖에만 두면 안 잡히는 경우가 있어요.
두 번째는 Nginx 리버스 프록시 설정. 로컬에선 잘 되는데 배포하면 스트리밍이 안 된다고 하는 경우의 90%는 여기서 걸립니다. Nginx 기본 설정이 응답을 버퍼링하거든요. proxy_buffering off;와 X-Accel-Buffering: no 헤더 설정을 안 하면 결국 다 모아서 한 번에 보냅니다. 스트리밍을 껐다 켰다 하는 거나 다름없어요.
세 번째는 토큰 과금 문제. 스트리밍으로 바꿨다고 해서 토큰 소비가 줄거나 늘지는 않아요. 그대로입니다. 다만 스트리밍 도중 사용자가 응답을 끊어도 이미 생성된 토큰은 과금됩니다. 이 부분은 Claude API 쓰다가 요금 폭탄 맞기 전에 알아야 할 것들을 한 번 보면 전체 과금 구조를 이해하는 데 도움이 돼요.
스트리밍 중에 다른 작업도 할 수 있어요
스트림에서 텍스트만 뽑는 게 전부가 아닙니다. 응답이 흘러오는 동안 특정 패턴을 실시간으로 감지하거나, 마크다운 코드 블록이 시작되는 순간 UI를 바꾸거나, 특정 키워드 나오면 다른 API 호출을 트리거하는 것도 가능해요.
Anthropic SDK에서 스트림 이벤트를 더 세밀하게 다루고 싶으면 text_stream 대신 stream 이터레이터를 직접 써요.
with client.messages.stream(...) as stream:
for event in stream:
if event.type == "content_block_delta":
delta = event.delta
if delta.type == "text_delta":
print(delta.text, end="", flush=True)
elif event.type == "message_stop":
print("\n스트림 종료")
이벤트 타입으로 message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop이 순서대로 들어옵니다. 이 구조를 알면 "응답 중에 취소 버튼 눌렀을 때 처리" 같은 기능도 만들 수 있어요.
그래서 언제 쓰고 언제 안 써도 되냐면
솔직히 말하면, 스트리밍이 항상 필요한 건 아니에요. 사용자가 직접 화면을 보는 인터랙티브 챗봇이라면 무조건 붙이는 게 맞아요. 응답 느낌이 완전히 달라지니까. 반면 백그라운드 배치 처리, 다른 API 결과로 넘기는 파이프라인, 응답 전체를 받아서 후처리하는 경우라면 스트리밍이 오히려 코드를 복잡하게 만들 수 있어요.
그리고 입력 토큰을 체계적으로 관리하고 있다면 스트리밍과 프롬프트 캐싱을 함께 쓰는 조합이 꽤 효율적입니다. 캐싱으로 입력 비용 줄이고, 스트리밍으로 UX 챙기는 거죠.
📌 한 줄 정리: 스트리밍 응답은
messages.stream()으로 시작하고, 웹 서비스엔 SSE 포맷으로 전달하고, 배포할 때 Nginx 버퍼링 설정 확인하는 것까지가 세트입니다.
- 터미널 출력:
stream.text_stream+flush=True- 웹 전달:
StreamingResponse+text/event-stream+data: ...\n\n포맷- 배포 주의: Nginx
proxy_buffering off필수- 에러·토큰 과금은 스트리밍 전환 후에도 그대로 적용
Claude 함수 호출, 말로만 듣다가 직접 써보니 이렇더라
Claude 함수 호출(Function Calling)이 뭔지, 어떻게 설정하고 어디서 막히는지 실제로 써본 경험 그대로 정리했습니다. tools 파라미터부터 흔한 실수까지.
어려운 뉴스 대신, 내 돈과 일에 연결되는 해석만
경제 지표 3개 + AI 뉴스 1개 + 도구·프롬프트 팁 1개. 출근길 8분, 광고 거의 없음, 한 클릭 해지.
- 📊 경제 지표 3개 — 환율 +0.6%, 기준금리 동결, 나스닥 +0.4%
- 🤖 AI 뉴스 1개 — Claude 신모델 — 한국 사용자 영향
- 💡 도구·프롬프트 팁 1개 — 회의록 5분 요약 프롬프트