LLM API를 직접 붙여서 챗봇을 만들면 처음에 생각보다 신경 쓸 게 많다. 스트리밍 처리, 메시지 상태 관리, 에러 핸들링, 입력 제어... 하나씩 구현하다 보면 실제 기능보다 인프라 코드가 더 많아진다.
Vercel AI SDK는 이 부분을 많이 추상화해준다. 서버사이드 streamText와 클라이언트 useChat이 핵심이고, 설정이 별로 없다.
설치
npm install ai @ai-sdk/anthropic
OpenAI를 쓴다면 @ai-sdk/openai를 설치하면 된다. SDK가 여러 프로바이더를 동일한 인터페이스로 지원해서, 나중에 모델을 바꾸더라도 코드 변경이 최소화된다.
서버 API 라우트
App Router 기준으로 app/api/chat/route.ts를 만든다.
import { anthropic } from '@ai-sdk/anthropic';
import { streamText } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic('claude-haiku-4-5-20251001'),
messages,
system: '당신은 친절한 개발자 어시스턴트입니다.',
});
return result.toDataStreamResponse();
}
toDataStreamResponse()가 스트리밍 응답 형식으로 만들어준다. ReadableStream이나 SSE를 직접 다루지 않아도 된다.
API 키는 환경변수로만 넣으면 SDK가 알아서 읽어간다.
ANTHROPIC_API_KEY=sk-ant-...
클라이언트 컴포넌트
'use client';
import { useChat } from 'ai/react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
});
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<div className="flex-1 overflow-y-auto space-y-4">
{messages.map((m) => (
<div key={m.id} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-xs px-4 py-2 rounded-2xl text-sm ${
m.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100'
}`}>
{m.content}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2 mt-4">
<input
value={input}
onChange={handleInputChange}
placeholder="메시지를 입력하세요"
className="flex-1 border rounded-full px-4 py-2 text-sm"
/>
<button
type="submit"
disabled={isLoading}
className="px-5 py-2 bg-blue-500 text-white rounded-full text-sm disabled:opacity-50"
>
전송
</button>
</form>
</div>
);
}
useChat이 메시지 목록, 입력값, 폼 핸들링, 스트리밍 수신을 전부 처리한다. 직접 fetch 호출이나 상태 관리를 할 필요가 없다.
시스템 프롬프트 조정
용도에 따라 system을 바꾸면 동작이 달라진다.
const result = streamText({
model: anthropic('claude-haiku-4-5-20251001'),
messages,
system: '코드 리뷰 전문가입니다. 버그와 개선점을 간결하게 지적합니다.',
temperature: 0.2,
maxTokens: 2048,
});
코드 관련 작업이라면 temperature를 낮추는 게 일관된 결과를 낸다.
대화 내역 저장
useChat은 기본적으로 메모리 상태로만 대화를 관리한다. 새로고침하면 초기화된다. 영구 저장이 필요하면 onFinish에서 DB에 쓰고, 초기 메시지를 initialMessages로 넘겨준다.
const { messages } = useChat({
initialMessages: savedMessages,
onFinish: (message) => {
saveToDatabase(message);
},
});
Prisma로 메시지를 저장하는 경우 role과 content만 저장하면 다음 세션에서 그대로 복원할 수 있다.
Vercel 배포 시 주의사항
스트리밍 응답의 최대 처리 시간이 기본 10초로 제한된다. LLM 응답이 길면 중간에 잘리는 경우가 있다. route.ts에 maxDuration을 명시적으로 설정하면 된다.
export const maxDuration = 60;
Hobby 플랜은 최대 60초, Pro 플랜은 300초까지 설정할 수 있다.
기본 동작하는 챗봇을 만드는 데 실제로 필요한 코드는 60줄 안팎이다. SDK가 스트리밍 처리와 메시지 상태 관리를 대신해주니까, 나머지는 전부 UI 꾸미는 시간이다.