스트리밍 — 글자를 흘려받는다
Article
지난 강의에서 만든 index.ts 를 다시 실행해보면, 답이 나오기 전까지 화면이 몇 초간 그냥 멈춰 있다. 사용자 입장에서는 "이거 멈춘 건가, 돌아가는 건가" 헷갈리는 시간이다. 이걸 먼저 시간으로 재본다.
화면이 멈춰 있는 시간을 잰다
my-ai-assistant 폴더에 nostream.ts 를 만든다. console.time 으로 요청을 보내고 응답 전체를 받기까지 걸리는 시간을 잰다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
console.time("전체 응답 받기까지");
const response = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "가을에 대한 짧은 시를 써줘. 4줄로." }],
});
console.timeEnd("전체 응답 받기까지");
console.log(response.content[0].text);
실행해본다.
npx tsx --env-file=.env nostream.ts
- 전체 응답 받기까지: 1.502s
- # 가을
- 낙엽이 소복이 내려앉고
- 찬바람이 볼을 스치며
- 해는 더 빨리 지고
- 마음만 유독 깊어진다
1.5초. 짧아 보이지만, 이 1.5초 동안 터미널(또는 채팅 UI)에는 아무것도 찍히지 않는다. messages.create() 는 Claude 가 답을 전부 다 만들 때까지 기다렸다가, 완성된 응답을 통째로 돌려주기 때문이다.
스트리밍은 무엇을 다르게 하나
핵심은 간단하다. Claude 가 답을 다 만들고 나서 보내는 게 아니라, 만드는 중간중간 조각을 바로바로 보낸다.
- messages.create()Claude 가 전부 다 쓸 때까지 기다렸다가 한 번에 응답한다
- messages.stream()글자가 만들어지는 대로 조각(chunk)을 바로 보낸다
- 우리 코드조각이 도착할 때마다 즉시 화면에 찍는다
스트리밍 코드를 쓴다
index.ts 를 스트리밍 버전으로 바꾼다. messages.stream() 이 돌려주는 스트림 객체에 "text" 이벤트를 걸어두면, 글자 조각이 도착할 때마다 콜백이 불린다.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
console.time("첫 글자 받기까지");
const stream = client.messages.stream({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "가을에 대한 짧은 시를 써줘. 4줄로." }],
});
let first = true;
stream.on("text", (text) => {
if (first) {
console.timeEnd("첫 글자 받기까지");
first = false;
}
process.stdout.write(text);
});
const finalMessage = await stream.finalMessage();
console.log(
`\n\n입력 토큰: ${finalMessage.usage.input_tokens}, 출력 토큰: ${finalMessage.usage.output_tokens}`,
);
세 가지가 낯설 것이다.
client.messages.stream():create()대신 이걸 부르면 응답 전체가 아니라 스트림 객체를 돌려받는다stream.on("text", ...): 글자 조각이 도착할 때마다 이 콜백이 실행된다.process.stdout.write를 쓰는 이유는console.log와 달리 줄바꿈 없이 이어 붙여 찍기 때문이다stream.finalMessage(): 스트림이 끝난 뒤, 지금까지 받은 조각을 모두 합친 완성된 응답 객체를 돌려준다.usage는 여기서 꺼낸다
실행해본다.
npx tsx --env-file=.env index.ts
- 첫 글자 받기까지: 1.074s
- # 가을
- 낙엽이 소복이 내려앉고
- 찬바람이 볼을 스쳐가며
- 해는 더 일찍 지고
- 마음은 더 깊어진다
- 입력 토큰: 32, 출력 토큰: 64
두 결과를 나란히 놓고 비교해본다.
messages.create()
응답 전체를 기다린다
- 첫 화면 표시까지 1.502s
- 완성된 응답 객체를 한 번에 받음
- 화면엔 아무것도 없다가 갑자기 전부 나타남
messages.stream()
조각을 바로 흘려받는다
- 첫 글자까지 1.074s
- finalMessage() 로 합쳐진 결과를 받음
- 글자가 흘러나오듯 실시간으로 보임
정리하면
스트리밍은 서버 속도를 바꾸지 않는다. Claude 가 글자를 만드는 대로 조각을 흘려보내, 사용자가 기다리는 체감 시간을 줄이는 방식이다.
- 01
일반 호출의 대기 시간을 쟀다
console.time 으로 재보니 응답이 다 만들어질 때까지 1.5초간 화면에 아무것도 안 찍혔다.
npx tsx --env-file=.env nostream.ts - 02
messages.stream() 으로 바꿨다
stream.on("text", ...) 으로 조각이 도착할 때마다 즉시 화면에 찍었다.
npx tsx --env-file=.env index.ts - 03
finalMessage() 로 usage 를 꺼냈다
스트림이 끝난 뒤 합쳐진 전체 응답 객체를 받아, 지금까지처럼 토큰 수를 확인했다.
다음 강의에서는 지금까지 슬쩍 넘어간 것, 모델 선택과 대화 길이가 비용에 정확히 어떤 영향을 주는지를 숫자로 잡는다. 4강에서 예고했던 "대화가 길어질수록 매번 더 많이 보낸다"는 문제를, 실제 여러 턴짜리 대화를 돌려 usage 로 확인한다.