Back

Vercel AI SDK 완벽 가이드: Next.js로 프로덕션급 AI 채팅 앱 만들기

AI 기반 앱 만들기가 이렇게 쉬웠던 적이 없어요. Vercel AI SDK를 쓰면 Next.js 앱에서 정교한 스트리밍 채팅 인터페이스, 텍스트 완성 시스템, AI 지원 기능을 놀랄 만큼 적은 보일러플레이트로 만들 수 있습니다.

그런데 문제가 있어요: SDK가 빠르게 진화하고 있고, 문서는 버전별로 흩어져 있고, 대부분의 튜토리얼은 겉만 훑고 지나가죠. OpenAI, Anthropic 등 LLM 프로바이더를 웹 앱에 통합하려다가 스트리밍 복잡성, 토큰 관리, 상태 동기화에 허덕이셨다면—이 가이드가 딱이에요.

이 딥다이브 가이드에서는 처음부터 프로덕션 레디 AI 채팅 앱을 만들어봅니다. 기본 훅부터 도구 호출, 멀티 모델 라우팅, 레이트 리미팅 같은 고급 패턴까지 전부 다룰 거예요. 끝까지 읽으면 어떤 AI 기능이든 만들 수 있는 탄탄한 기반이 생깁니다.

📌 버전 안내: 이 가이드는 Vercel AI SDK v6.0+ (2025년 말 릴리즈) 기준입니다. 이전 버전 사용 시 일부 API가 다를 수 있어요. npm info ai version으로 설치된 버전을 확인하세요.


왜 Vercel AI SDK인가?

코드 들어가기 전에, Vercel AI SDK가 왜 React 앱에서 AI 통합 표준이 됐는지 알아봅시다.

스트리밍 문제

LLM API를 직접 호출하면 전체 생성이 끝나야 응답을 받아요. 500토큰 응답이면 5-10초 대기. 유저들은 이걸 싫어해요.

// 나이브한 접근법 - 최악의 UX const response = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: '양자 컴퓨팅 설명해줘' }], }); // 유저가 로딩 스피너 보면서 8초 기다림... console.log(response.choices[0].message.content);

스트리밍은 토큰이 생성되는 대로 보내서 이 문제를 해결해요. 그런데 React에서 제대로 된 스트리밍 구현은 생각보다 복잡합니다:

  • API에서 ReadableStream 관리하기
  • SSE(Server-Sent Events)나 newline-delimited JSON 파싱
  • 리렌더 폭포 없이 React 상태 업데이트
  • 취소용 abort 시그널 처리
  • 로딩, 에러, 완료 상태 관리
  • 클라이언트-서버 상태 동기화

Vercel AI SDK는 이 모든 걸 심플하고 선언적인 훅으로 추상화해줍니다.

어떤 프로바이더든 상관없이 동작

SDK의 가장 멋진 점은 어떤 AI 서비스를 쓰든 코드가 똑같다는 거예요:

import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; // 같은 함수 시그니처, 다른 프로바이더 const result = await generateText({ model: openai('gpt-4-turbo'), // 또는: model: anthropic('claude-3-opus'), // 또는: model: google('gemini-pro'), prompt: '양자 컴퓨팅 설명해줘', });

한 줄만 바꾸면 프로바이더 전환 가능. 리팩토링 필요 없어요.


핵심 개념: AI SDK 아키텍처

Vercel AI SDK는 세 개의 주요 패키지로 나뉩니다:

1. AI SDK Core (ai)

기반 패키지로 다음을 제공:

  • generateText() - 전체 결과로 텍스트 생성
  • streamText() - 텍스트 생성 스트리밍
  • generateObject() - 구조화된 JSON 생성
  • streamObject() - 구조화된 JSON 생성 스트리밍
  • embed() - 임베딩 생성
  • embedMany() - 배치 임베딩

2. AI SDK UI (@ai-sdk/react)

UI 구축용 React 훅:

  • useChat() - 전체 채팅 인터페이스 관리
  • useCompletion() - 단일 턴 텍스트 완성
  • useObject() - 구조화된 데이터 스트리밍
  • useAssistant() - OpenAI Assistants API 통합

3. 프로바이더 패키지

모델 구현체:

  • @ai-sdk/openai - OpenAI, Azure OpenAI
  • @ai-sdk/anthropic - Claude 모델
  • @ai-sdk/google - Gemini 모델
  • @ai-sdk/mistral - Mistral AI
  • @ai-sdk/amazon-bedrock - AWS Bedrock
  • 그 외 커뮤니티 프로바이더...

셋업: 프로젝트 부트스트랩

프로덕션 레디 프로젝트 구조를 만들어봅시다:

npx create-next-app@latest ai-chat-app --typescript --tailwind --app cd ai-chat-app # AI SDK 패키지 설치 npm install ai @ai-sdk/openai @ai-sdk/anthropic # 옵션: UI 컴포넌트 npm install @radix-ui/react-scroll-area lucide-react

환경 설정

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

프로젝트 구조

src/
├── app/
│   ├── api/
│   │   └── chat/
│   │       └── route.ts      # 채팅 API 엔드포인트
│   ├── page.tsx              # 메인 채팅 UI
│   └── layout.tsx
├── components/
│   ├── chat/
│   │   ├── ChatContainer.tsx
│   │   ├── MessageList.tsx
│   │   ├── MessageBubble.tsx
│   │   └── ChatInput.tsx
│   └── ui/
│       └── Button.tsx
├── lib/
│   ├── ai/
│   │   ├── models.ts         # 모델 설정
│   │   └── prompts.ts        # 시스템 프롬프트
│   └── utils.ts
└── types/
    └── chat.ts

채팅 API 만들기: 서버 사이드 구현

API 라우트에서 핵심 로직이 돌아가요. 제대로 된 채팅 엔드포인트를 만들어봅시다:

기본 채팅 라우트

// src/app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; export const runtime = 'edge'; // 낮은 레이턴시를 위한 엣지 런타임 export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo'), messages, system: `당신은 유용한 AI 어시스턴트입니다. 간결하고 명확하게 답변해주세요.`, }); return result.toDataStreamResponse(); }

기본 채팅은 이게 끝! 근데 프로덕션 앱은 더 많은 게 필요하죠...

프로덕션 레디 채팅 라우트

// src/app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { streamText, convertToModelMessages } from 'ai'; import { z } from 'zod'; export const runtime = 'edge'; export const maxDuration = 30; // 최대 실행 시간 // 요청 검증 스키마 const chatRequestSchema = z.object({ messages: z.array(z.object({ role: z.enum(['user', 'assistant', 'system']), content: z.string(), })), model: z.enum(['gpt-4-turbo', 'claude-3-opus']).default('gpt-4-turbo'), temperature: z.number().min(0).max(2).default(0.7), }); // 모델 레지스트리 const models = { 'gpt-4-turbo': openai('gpt-4-turbo'), 'claude-3-opus': anthropic('claude-3-opus-20240229'), }; const SYSTEM_PROMPT = `당신은 소프트웨어 개발 전문 AI 어시스턴트입니다. 가이드라인: - 정확하고 잘 구조화된 응답 제공 - 관련 있을 때 코드 예제 포함 - 사실적 주장 시 출처 인용 - 추측보다 불확실성 인정 - 간결하면서도 포괄적으로`; export async function POST(req: Request) { try { const body = await req.json(); const { messages, model, temperature } = chatRequestSchema.parse(body); // 레이트 리미팅 체크 (본인 로직 구현) const clientIP = req.headers.get('x-forwarded-for') || 'unknown'; // await checkRateLimit(clientIP); const result = streamText({ model: models[model], messages: await convertToModelMessages(messages), system: SYSTEM_PROMPT, temperature, maxTokens: 4096, // 클라이언트 연결 끊김용 abort 시그널 abortSignal: req.signal, // 로깅/분석용 콜백 onFinish: async ({ text, usage }) => { console.log(`완료: ${usage.totalTokens} 토큰`); // await logUsage(usage); }, }); return result.toDataStreamResponse(); } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify({ error: '잘못된 요청', details: error.errors }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } console.error('채팅 API 에러:', error); return new Response(JSON.stringify({ error: '내부 서버 에러' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } }

채팅 UI 만들기: useChat 훅 딥다이브

useChat 훅이 프론트엔드 구현의 핵심이에요:

기본 사용법

// src/app/page.tsx 'use client'; import { useChat } from '@ai-sdk/react'; export default function ChatPage() { const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat(); 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((message) => ( <div key={message.id} className={`p-4 rounded-lg ${ message.role === 'user' ? 'bg-blue-100 ml-auto max-w-[80%]' : 'bg-gray-100 mr-auto max-w-[80%]' }`} > <p className="whitespace-pre-wrap">{message.content}</p> </div> ))} </div> <form onSubmit={handleSubmit} className="flex gap-2 pt-4"> <input value={input} onChange={handleInputChange} placeholder="메시지를 입력하세요..." className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={isLoading} /> <button type="submit" disabled={isLoading || !input.trim()} className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50" > {isLoading ? '전송 중...' : '보내기'} </button> </form> </div> ); }

고급 useChat 설정

'use client'; import { useChat, Message } from '@ai-sdk/react'; import { useState, useCallback, useRef, useEffect } from 'react'; export default function AdvancedChatPage() { const [selectedModel, setSelectedModel] = useState('gpt-4-turbo'); const scrollRef = useRef<HTMLDivElement>(null); const { messages, input, setInput, handleInputChange, handleSubmit, isLoading, error, reload, stop, append, setMessages, } = useChat({ api: '/api/chat', // 각 요청에 추가 데이터 전송 body: { model: selectedModel, temperature: 0.7, }, // 초기 메시지 (예: DB에서) initialMessages: [], // 고유 ID 생성 generateId: () => crypto.randomUUID(), // 응답 스트리밍 시작 시 호출 onResponse: (response) => { if (!response.ok) { console.error('응답 에러:', response.status); } }, // 스트리밍 완료 시 호출 onFinish: (message) => { console.log('메시지 완료:', message.id); // DB 저장, 분석 등 }, // 에러 발생 시 호출 onError: (error) => { console.error('채팅 에러:', error); }, }); // 바닥으로 자동 스크롤 useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); // 프로그래밍 방식 메시지 추가 const sendQuickMessage = useCallback((content: string) => { append({ role: 'user', content }); }, [append]); // 채팅 기록 클리어 const clearChat = useCallback(() => { setMessages([]); }, [setMessages]); // 마지막 메시지 재시도 const retryLast = useCallback(() => { if (messages.length >= 2) { reload(); } }, [messages, reload]); return ( <div className="flex flex-col h-screen max-w-3xl mx-auto"> {/* 컨트롤이 있는 헤더 */} <header className="flex items-center justify-between p-4 border-b"> <h1 className="text-xl font-bold">AI 채팅</h1> <div className="flex items-center gap-2"> <select value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} className="p-2 border rounded" > <option value="gpt-4-turbo">GPT-4 Turbo</option> <option value="claude-3-opus">Claude 3 Opus</option> </select> <button onClick={clearChat} className="p-2 text-gray-500 hover:text-gray-700"> 초기화 </button> </div> </header> {/* 메시지 */} <div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-4"> {messages.length === 0 && ( <div className="text-center text-gray-500 mt-8"> <p className="text-lg mb-4">오늘 무엇을 도와드릴까요?</p> <div className="flex flex-wrap justify-center gap-2"> {['React Server Components 설명', '코드 디버깅', '함수 작성'].map((prompt) => ( <button key={prompt} onClick={() => sendQuickMessage(prompt)} className="px-4 py-2 bg-gray-100 rounded-full hover:bg-gray-200 text-sm" > {prompt} </button> ))} </div> </div> )} {messages.map((message) => ( <MessageBubble key={message.id} message={message} /> ))} {isLoading && ( <div className="flex items-center gap-2 text-gray-500"> <div className="animate-pulse"></div> <span>AI가 생각 중...</span> <button onClick={stop} className="text-red-500 text-sm"> 중지 </button> </div> )} {error && ( <div className="p-4 bg-red-50 text-red-600 rounded-lg"> <p>에러: {error.message}</p> <button onClick={retryLast} className="text-sm underline"> 재시도 </button> </div> )} </div> {/* 입력 */} <form onSubmit={handleSubmit} className="p-4 border-t"> <div className="flex gap-2"> <input value={input} onChange={handleInputChange} placeholder="메시지를 입력하세요..." className="flex-1 p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={isLoading} /> <button type="submit" disabled={isLoading || !input.trim()} className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors" > 보내기 </button> </div> </form> </div> ); } function MessageBubble({ message }: { message: Message }) { const isUser = message.role === 'user'; return ( <div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}> <div className={`max-w-[80%] p-4 rounded-2xl ${ isUser ? 'bg-blue-500 text-white rounded-br-md' : 'bg-gray-100 text-gray-900 rounded-bl-md' }`} > <p className="whitespace-pre-wrap">{message.content}</p> </div> </div> ); }

도구 호출: AI 능력 확장하기

현대 LLM의 가장 강력한 기능 중 하나가 도구 호출(함수 호출)이에요. AI SDK가 이걸 매끄럽게 만들어줍니다:

도구 정의하기

// src/lib/ai/tools.ts import { z } from 'zod'; import { tool } from 'ai'; export const weatherTool = tool({ description: '위치의 현재 날씨를 가져옵니다', parameters: z.object({ location: z.string().describe('도시 이름, 예: "서울"'), unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), }), execute: async ({ location, unit }) => { // 프로덕션에서는 실제 날씨 API 호출 const temp = Math.floor(Math.random() * 30) + 5; return { location, temperature: temp, unit, condition: '구름 조금', }; }, }); export const searchTool = tool({ description: '웹에서 최신 정보를 검색합니다', parameters: z.object({ query: z.string().describe('검색 쿼리'), }), execute: async ({ query }) => { // 프로덕션에서는 검색 API 사용 return { results: [ { title: `${query} 검색 결과`, url: 'https://example.com' }, ], }; }, }); export const calculatorTool = tool({ description: '수학 계산을 수행합니다', parameters: z.object({ expression: z.string().describe('수학 표현식, 예: "2 + 2 * 3"'), }), execute: async ({ expression }) => { try { // 경고: eval은 프로덕션에서 위험함 - 적절한 수학 파서 사용 const result = Function(`"use strict"; return (${expression})`)(); return { expression, result }; } catch { return { expression, error: '잘못된 표현식' }; } }, });

API 라우트에서 도구 사용하기

// src/app/api/chat/route.ts import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { weatherTool, searchTool, calculatorTool } from '@/lib/ai/tools'; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai('gpt-4-turbo'), messages, tools: { weather: weatherTool, search: searchTool, calculator: calculatorTool, }, // 순차적 다중 도구 호출 허용 maxSteps: 5, }); return result.toDataStreamResponse(); }

UI에서 도구 결과 처리하기

'use client'; import { useChat } from '@ai-sdk/react'; export default function ChatWithTools() { const { messages, input, handleInputChange, handleSubmit } = useChat(); return ( <div> {messages.map((message) => ( <div key={message.id}> {message.role === 'user' && <UserMessage content={message.content} />} {message.role === 'assistant' && ( <div> {/* 도구 호출 표시 */} {message.toolInvocations?.map((tool) => ( <ToolCard key={tool.toolCallId} tool={tool} /> ))} {/* 텍스트 콘텐츠 표시 */} {message.content && <AssistantMessage content={message.content} />} </div> )} </div> ))} <form onSubmit={handleSubmit}> <input value={input} onChange={handleInputChange} /> <button type="submit">보내기</button> </form> </div> ); } function ToolCard({ tool }: { tool: any }) { return ( <div className="p-3 my-2 bg-purple-50 border border-purple-200 rounded-lg"> <div className="text-sm font-medium text-purple-700"> 🔧 {tool.toolName} </div> {tool.state === 'result' && ( <pre className="mt-2 text-xs bg-white p-2 rounded overflow-x-auto"> {JSON.stringify(tool.result, null, 2)} </pre> )} </div> ); }

구조화된 출력: generateObject와 useObject

때로는 AI가 텍스트가 아닌 구조화된 데이터를 반환해야 할 때가 있어요:

서버 사이드 구조화 생성

// src/app/api/analyze/route.ts import { openai } from '@ai-sdk/openai'; import { generateObject } from 'ai'; import { z } from 'zod'; const sentimentSchema = z.object({ sentiment: z.enum(['positive', 'negative', 'neutral']), confidence: z.number().min(0).max(1), keywords: z.array(z.string()), summary: z.string(), }); export async function POST(req: Request) { const { text } = await req.json(); const { object } = await generateObject({ model: openai('gpt-4-turbo'), schema: sentimentSchema, prompt: `이 텍스트의 감정을 분석해: "${text}"`, }); return Response.json(object); }

클라이언트 사이드 객체 스트리밍

'use client'; import { useObject } from '@ai-sdk/react'; import { z } from 'zod'; const recipeSchema = z.object({ name: z.string(), ingredients: z.array(z.object({ item: z.string(), amount: z.string(), })), steps: z.array(z.string()), prepTime: z.number(), cookTime: z.number(), }); export default function RecipeGenerator() { const { object, submit, isLoading } = useObject({ api: '/api/generate-recipe', schema: recipeSchema, }); return ( <div> <button onClick={() => submit({ dish: '초콜릿 케이크' })}> 레시피 생성 </button> {isLoading && <p>생성 중...</p>} {object && ( <div> <h2>{object.name}</h2> {object.ingredients && ( <ul> {object.ingredients.map((ing, i) => ( <li key={i}>{ing.amount} {ing.item}</li> ))} </ul> )} {object.steps && ( <ol> {object.steps.map((step, i) => ( <li key={i}>{step}</li> ))} </ol> )} </div> )} </div> ); }

프로덕션 패턴: 레이트 리미팅, 캐싱, 에러 핸들링

Upstash Redis로 레이트 리미팅

// src/lib/rate-limit.ts import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); export const rateLimiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, '1 m'), // 분당 10 요청 analytics: true, }); // API 라우트에서 사용 export async function POST(req: Request) { const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'; const { success, limit, reset, remaining } = await rateLimiter.limit(ip); if (!success) { return new Response('요청 한도 초과', { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), }, }); } // ... 채팅 로직 계속 }

자주 묻는 쿼리 응답 캐싱

// src/lib/cache.ts import { Redis } from '@upstash/redis'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import crypto from 'crypto'; const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }); function hashPrompt(prompt: string): string { return crypto.createHash('sha256').update(prompt).digest('hex'); } export async function getCachedOrGenerate(prompt: string): Promise<string> { const cacheKey = `ai:${hashPrompt(prompt)}`; // 캐시 확인 const cached = await redis.get<string>(cacheKey); if (cached) { console.log('캐시 히트!'); return cached; } // 새 응답 생성 const { text } = await generateText({ model: openai('gpt-4-turbo'), prompt, }); // 1시간 캐시 await redis.set(cacheKey, text, { ex: 3600 }); return text; }

포괄적인 에러 핸들링

// src/lib/ai/error-handling.ts import { AIError } from 'ai'; export function handleAIError(error: unknown): Response { console.error('AI 에러:', error); if (error instanceof AIError) { switch (error.code) { case 'rate_limit_exceeded': return new Response( JSON.stringify({ error: '요청 한도 초과', message: '잠시 후 다시 시도해주세요.', retryAfter: 60, }), { status: 429 } ); case 'context_length_exceeded': return new Response( JSON.stringify({ error: '메시지가 너무 깁니다', message: '메시지를 줄이거나 새 대화를 시작해주세요.', }), { status: 400 } ); case 'invalid_api_key': return new Response( JSON.stringify({ error: '설정 오류', message: 'AI 서비스가 일시적으로 사용 불가합니다.', }), { status: 503 } ); default: return new Response( JSON.stringify({ error: 'AI 에러', message: error.message, }), { status: 500 } ); } } return new Response( JSON.stringify({ error: '내부 에러', message: '예기치 않은 오류가 발생했습니다.', }), { status: 500 } ); }

멀티 모델 라우팅: 스마트 프로바이더 선택

프로덕션 앱에서는 태스크에 따라 다른 모델로 라우팅하고 싶을 수 있어요:

// src/lib/ai/router.ts import { openai } from '@ai-sdk/openai'; import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; import { streamText } from 'ai'; type TaskType = 'code' | 'creative' | 'analysis' | 'general'; function detectTaskType(message: string): TaskType { const codeKeywords = ['코드', '함수', '디버그', '구현', 'typescript', 'python']; const creativeKeywords = ['작성', '스토리', '창작', '상상', '시']; const analysisKeywords = ['분석', '비교', '평가', '연구', '설명']; const lowerMessage = message.toLowerCase(); if (codeKeywords.some(k => lowerMessage.includes(k))) return 'code'; if (creativeKeywords.some(k => lowerMessage.includes(k))) return 'creative'; if (analysisKeywords.some(k => lowerMessage.includes(k))) return 'analysis'; return 'general'; } function selectModel(taskType: TaskType) { switch (taskType) { case 'code': return anthropic('claude-3-opus-20240229'); // 코딩에 최고 case 'creative': return openai('gpt-4-turbo'); // 창작 글쓰기에 좋음 case 'analysis': return google('gemini-1.5-pro'); // 긴 컨텍스트 분석에 좋음 default: return openai('gpt-4-turbo'); // 범용 } } export async function routedChat(messages: any[]) { const lastUserMessage = messages.filter(m => m.role === 'user').pop(); const taskType = detectTaskType(lastUserMessage?.content ?? ''); const model = selectModel(taskType); console.log(`${taskType} 모델로 라우팅`); return streamText({ model, messages, }); }

배포 고려사항

Vercel 배포

// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { serverComponentsExternalPackages: ['@ai-sdk/openai'], }, }; module.exports = nextConfig;

Vercel 환경 변수

# Vercel Dashboard > Settings > Environment Variables에서 설정 OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... UPSTASH_REDIS_REST_URL=https://... UPSTASH_REDIS_REST_TOKEN=...

엣지 런타임 고려사항

  • 엣지 함수는 25MB 크기 제한
  • Node.js API 없음 (Web API 사용)
  • 제한된 실행 시간 (Vercel에서 30초)
  • 저지연 AI 응답에 완벽

결론

Vercel AI SDK는 AI 기반 앱 구축을 극적으로 단순화합니다. 다룬 내용:

  • 핵심 개념: 스트리밍, 프로바이더 아키텍처, 훅
  • useChat 딥다이브: 전체 설정, 상태 관리, UI 패턴
  • 도구 호출: 커스텀 함수로 AI 능력 확장
  • 구조화된 출력: 스키마로 타입 세이프 AI 응답
  • 프로덕션 패턴: 레이트 리미팅, 캐싱, 에러 핸들링
  • 멀티 모델 라우팅: 스마트 프로바이더 선택
  • 테스팅과 배포: 프로덕션 모범 사례

성공적인 AI 통합의 핵심은 올바른 도구 사용뿐 아니라—앱을 안정적이고, 성능 좋고, 사용자 친화적으로 만드는 패턴을 이해하는 거예요.

퀵 레퍼런스

// 기본 채팅 const { messages, input, handleSubmit, isLoading } = useChat(); // 옵션 설정 const chat = useChat({ api: '/api/chat', body: { model: 'gpt-4' }, onFinish: (message) => saveToDb(message), onError: (error) => toast.error(error.message), }); // 프로그래밍 제어 chat.append({ role: 'user', content: '안녕' }); chat.reload(); chat.stop(); chat.setMessages([]);

심플하게 시작해서, 필요에 따라 발전시키고, 토큰 사용량 모니터링 잊지 마세요—지갑이 고마워할 거예요.


Vercel AI SDK로 멋진 거 만들고 계신가요? 생태계가 빠르게 성장하고 있어서, 새 프로바이더와 기능이 계속 나오고 있어요. 최신 기능 따라가려면 공식 문서랑 GitHub 릴리즈 주시하세요.

vercel-ai-sdknextjsaichatgptstreamingreacttypescript

관련 도구 둘러보기

Pockit의 무료 개발자 도구를 사용해 보세요