블로그 목록
라이브러리인프라

Claude API로 스트리밍 챗봇 만들기 — Next.js App Router에서 직접 붙여보기

Vercel AI SDK를 쓰면 챗봇 구현이 금방 된다. 근데 내부 동작이 어떻게 되는지 모르면 문제가 생겼을 때 막막하다. 이번엔 SDK 없이 직접 Claude API를 붙여보면서 스트리밍이 어떻게 동작하는지 파악했다.

패키지 설치

npm install @anthropic-ai/sdk

API 키는 Anthropic 콘솔에서 발급받아서 환경변수로 관리한다.

# .env.local
ANTHROPIC_API_KEY=sk-ant-...

Route Handler 작성

App Router 기준으로 /api/chat Route Handler를 만든다.

// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

export async function POST(req: Request) {
  const { messages } = await req.json();

  const stream = await client.messages.stream({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages,
  });

  return new Response(stream.toReadableStream(), {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
    },
  });
}

stream.toReadableStream()이 핵심이다. SDK가 SSE 형식의 ReadableStream을 반환해줘서 그대로 Response에 넘기면 된다. Edge Runtime을 쓰면 초기 응답 지연이 줄어든다.

클라이언트에서 스트림 읽기

async function sendMessage(content: string) {
  const response = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      messages: [{ role: "user", content }],
    }),
  });

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split("\n");

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const data = JSON.parse(line.slice(6));
      if (data.type === "content_block_delta") {
        setOutput((prev) => prev + data.delta.text);
      }
    }
  }
}

SSE 파싱을 직접 하는 게 번거로워 보이지만, 구조 자체는 단순하다. data: 이후를 JSON으로 파싱하고 content_block_delta 타입이면 텍스트를 꺼내면 된다.

프롬프트 캐싱으로 비용 줄이기

긴 시스템 프롬프트를 매 요청마다 보내면 토큰 비용이 빠르게 쌓인다. Claude API에는 프롬프트 캐싱 기능이 있다.

const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: longSystemPrompt,
      cache_control: { type: "ephemeral" },
    },
  ],
  messages,
});

cache_control: { type: "ephemeral" }를 붙이면 해당 블록을 캐시한다. 유효시간은 5분이고, 캐시 히트 시 토큰 비용이 최대 90%까지 줄어든다. 챗봇처럼 같은 시스템 프롬프트를 계속 보내는 경우에 효과가 크다.

멀티턴 대화 관리

대화 히스토리는 클라이언트에서 관리해서 매 요청마다 전체를 넘기는 방식이 가장 단순하다.

const [messages, setMessages] = useState<{ role: string; content: string }[]>([]);

async function handleSend(userInput: string) {
  const updated = [...messages, { role: "user", content: userInput }];
  setMessages(updated);

  let reply = "";
  // ... 스트리밍 응답 누적 ...

  setMessages([...updated, { role: "assistant", content: reply }]);
}

컨텍스트가 너무 길어지면 오래된 메시지를 잘라내거나 요약해서 넘긴다. Claude 모델이 1M 토큰 컨텍스트를 지원하기 때문에 일반적인 챗봇 용도에서는 잘릴 일이 거의 없다.


직접 구현해보면 Vercel AI SDK가 어떤 부분을 추상화하는지 명확하게 보인다. 빠르게 프로토타입을 만들 때는 SDK가 편하고, 세밀하게 제어해야 하거나 커스텀 로직이 필요한 경우엔 직접 구현하는 게 낫다.