블로그 목록
AINext.js

Vercel AI SDK로 스트리밍 챗봇 만들기 — Next.js App Router에서 LLM 연동

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로 메시지를 저장하는 경우 rolecontent만 저장하면 다음 세션에서 그대로 복원할 수 있다.

Vercel 배포 시 주의사항

스트리밍 응답의 최대 처리 시간이 기본 10초로 제한된다. LLM 응답이 길면 중간에 잘리는 경우가 있다. route.tsmaxDuration을 명시적으로 설정하면 된다.

export const maxDuration = 60;

Hobby 플랜은 최대 60초, Pro 플랜은 300초까지 설정할 수 있다.


기본 동작하는 챗봇을 만드는 데 실제로 필요한 코드는 60줄 안팎이다. SDK가 스트리밍 처리와 메시지 상태 관리를 대신해주니까, 나머지는 전부 UI 꾸미는 시간이다.