게시일: 2026년 7월 16일
Vercel AI SDK로 기본 제공 프롬프트 API 사용에서는 @browser-ai/core로 구동되는 generateText(), streamText(), 하이브리드 코드, Output.object()를 사용한 구조화된 출력이라는 네 가지 핵심 생성 프리미티브를 살펴봤습니다. 이번에는 더 상호작용적인 것을 빌드합니다. 프롬프트 API를 사용할 수 없는 경우 클라우드 모델로 자동 대체되는 완전한 스트리밍 채팅 UI로, 브라우저에서 완전히 실행됩니다.
빌드할 항목
다음을 실행하는 React 채팅 인터페이스
- 다중 턴 대화를 스트리밍하기 위해 Vercel AI SDK의
useChat후크를 사용합니다. - 백엔드 서버 없이 브라우저에서 모델 루프를 실행합니다.
- 프롬프트 API를 사용할 수 없는 경우 Gemini 2.5 Flash로 자동 대체됩니다.
- 스트리밍 중에 불완전한 토큰을 처리하여 어시스턴트 대답을 GitHub 스타일 마크다운으로 렌더링합니다.
- 첫 번째 토큰을 기다리는 동안 '생각 중…' 반짝임이 표시됩니다.
- 위로 스크롤한 경우 아래로 스크롤 버튼과 함께 새 메시지로 자동 스크롤됩니다.
추가 종속 항목
ai, @browser-ai/core, @ai-sdk/google 외에도 채팅 UI에는 React, AI SDK의 React 바인딩, 몇 가지 Markdown 패키지가 필요합니다.
npm install react react-dom @ai-sdk/react
npm install react-markdown remark-gfm harden-react-markdown
npm install -D @types/react @types/react-dom
UI의 경우 Tailwind CSS, 일부 구성요소 유틸리티, 아이콘용 Lucide도 추가합니다.
npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react
투표 구성: 다중 항목 빌드
프로젝트에 이미 index.html이 있습니다. chat.html를 두 번째 진입점으로 추가하고 Vite가 두 항목을 모두 빌드하도록 구성합니다.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: { alias: { '@': resolve(__dirname, './src') } },
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
chat: resolve(__dirname, 'chat.html'),
},
},
},
});
chat.html는 <div id="root"> 및 스크립트 태그만 있는 최소한의 요소입니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Built-in AI Chatbot</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/chat.tsx"></script>
</body>
</html>
자동 모델 선택
챗봇에서 앱은 이 선택을 자동으로 수행합니다. 먼저 내장 모델을 시도하고 프롬프트 API를 사용할 수 없는 경우 클라우드로 대체합니다.
이는 React가 마운트되기 전 모듈 로드 시간에 실행되므로 사용자가 첫 번째 메시지를 입력할 때쯤이면 에이전트가 준비됩니다.
const agentPromise: Promise<ToolLoopAgent> = (async () => {
const builtIn = browserAI();
let model: any = builtIn;
if (typeof builtIn.availability === 'function') {
const availability = await builtIn.availability();
if (availability === 'unavailable') {
const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
model = createGoogleGenerativeAI({ apiKey })('gemini-2.5-flash');
} else if (availability === 'downloadable') {
await builtIn.createSessionWithProgress(() => {});
}
}
return new ToolLoopAgent({ model, instructions: 'You are a helpful assistant.' });
})();
새로운 부분은 ToolLoopAgent입니다. 이는 모든 모델 위에서 멀티턴 대화 루프를 관리하는 AI SDK 추상화입니다. 모델과 시스템 프롬프트를 가져와 내부적으로 앞뒤로 처리합니다.
useChat을 사용하도록 에이전트 연결
@ai-sdk/react 패키지의 useChat 후크는 일반적으로 HTTP 엔드포인트와 통신합니다.
브라우저 측 추론의 경우 DirectChatTransport를 대신 사용하세요. 서버가 관여하지 않고 브라우저에서만 ToolLoopAgent 루프를 실행합니다.
const transport = useMemo(() => new DirectChatTransport({ agent }), [agent]);
const { messages, sendMessage, status, stop } = useChat({ transport });
DirectChatTransport은 대화 상태를 보유하므로 안정적인 참조여야 합니다. 따라서 useMemo이 중요합니다. 렌더링할 때마다 다시 만들면 대화가 재설정됩니다.
useChat를 사용하면 다음과 같은 이점이 있습니다.
Messages: 전체 대화가UIMessage[]로 표시되며, 각 대화에는role와parts배열이 있습니다.sendMessage({ text }): 새 사용자 턴을 전송하고 응답 스트리밍을 시작합니다.Status:'idle'|'submitted'|'streaming'|'error'Stop: 진행 중인 생성을 취소합니다.
메일 렌더링
각 메시지에는 parts 배열이 있습니다. 이 챗봇에서는 type:
'text' 부분만 신경 쓰면 됩니다. 사용자 메시지는 오른쪽 정렬된 풍선으로 표시되고 어시스턴트 메시지는 아이콘과 함께 왼쪽 정렬됩니다.
const ChatMessage = ({ message, isStreaming }: { message: UIMessage; isStreaming: boolean }) => {
const isUser = message.role === 'user';
const textParts = message.parts.map((part, i) => {
if (part.type !== 'text') return null;
if (isUser) return <span key={i}>{part.text}</span>;
return <Response key={i} parseIncompleteMarkdown={isStreaming}>{part.text}</Response>;
});
if (isUser) {
return (
<div className="flex flex-col items-end gap-2 animate-fade-up">
<MessageContent className="w-fit max-w-[min(80%,56ch)] ...">
{textParts}
</MessageContent>
</div>
);
}
return (
<div className="flex items-start gap-3">
<AIIcon />
<MessageContent className="text-[13px] leading-[1.65]">{textParts}</MessageContent>
</div>
);
};
MessageContent 및 Response은 AI 요소입니다.
npm에서 설치하는 대신 프로젝트에 복사하는 shadcn 스타일 소스 구성요소입니다. Response는 GitHub 맞춤형 마크다운 (표, 작업 목록, 취소선)을 위해 remark-gfm로, AI 출력의 링크와 이미지를 정리하기 위해 harden-react-markdown로 react-markdown를 래핑합니다.
메시지가 아직 스트리밍되는 동안 parseIncompleteMarkdown 속성은 true입니다. 스트리밍 중에 모델이 **bold를 작성하지만 아직 **를 닫지 않아 리터럴 별표로 렌더링되는 매달린 토큰이 남을 수 있습니다. parseIncompleteMarkdown는 열려 있는 **, __, `, ~~를 닫고 매달린 [ 링크 시작을 잘라내므로 렌더링된 출력이 모든 증분 청크에서 깨끗하게 유지됩니다.
'생각 중…' 상태
메시지를 전송한 후 첫 번째 토큰을 수신하기까지의 시간은 status이며 'submitted'입니다. 이 기간 동안 앱은 애니메이션으로 된 반짝임을 표시합니다.
{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
<ThinkingMessage />
)}
messages.at(-1)?.role !== 'assistant' 조건은 어시스턴트 메시지가 스트리밍되기 시작하면 시머가 다시 표시되지 않도록 합니다.
ThinkingMessage는 Shimmer 구성요소를 사용합니다. background-clip: text를 사용하여 이동하는 그라데이션이 적용된 <span>로, '생각 중…' 텍스트에 스위핑 강조 효과를 제공합니다.
자동 스크롤
새 콘텐츠가 도착하면 앱이 하단으로 스크롤되지만 사용자가 이미 하단에 있는 경우에만 스크롤됩니다. 대화 중에 읽고 있는 내용에서 스크롤하면 짜증이 날 수 있습니다.
const [isAtBottom, setIsAtBottom] = useState(true);
useEffect(() => {
if (isAtBottom) endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, status, isAtBottom]);
const handleScroll = () => {
const el = containerRef.current;
if (!el) return;
setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 50);
};
isAtBottom가 false이면 플로팅 하단으로 스크롤 버튼이 표시되고 사용자가 하단으로 돌아가면 사라집니다.
입력 영역
텍스트 영역은 입력할 때마다 높이를 auto로 재설정한 다음 scrollHeight로 설정하여 자동으로 크기가 조절됩니다. Enter(Shift+Enter 아님)를 누르면 제출되고, 대답이 스트리밍되는 동안 보내기 버튼이 stop()를 호출하는 중지 버튼으로 대체됩니다.
<textarea
onInput={(e) => {
const el = e.currentTarget;
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (input.trim() && !isStreaming) {
sendMessage({ text: input });
setInput('');
}
}
}}
/>;
{
isStreaming ? (
<Button variant="outline" onClick={stop}>
Stop
</Button>
) : (
<Button type="submit" disabled={!input.trim()}>
Send
</Button>
);
}
로드 상태로 마운트
agentPromise는 비동기이므로 Chat을 렌더링하기 전에 기다립니다. App 래퍼는 프로미스를 처리하고 그동안 스피너를 표시합니다.
function App() {
const [agent, setAgent] = (useState < ToolLoopAgent) | (null > null);
useEffect(() => {
agentPromise.then(setAgent);
}, []);
if (!agent) {
return (
<div className="flex h-dvh items-center justify-center">
<Loader size={20} />
</div>
);
}
return <Chat agent={agent} />;
}
에이전트가 해결되면(내장 모델이 즉시 부팅되거나 모델 다운로드를 기다리는 경우) 스피너가 사라지고 채팅 UI가 마운트됩니다.
데모
실시간 데모는 브라우저에서 완전히 실행되는 전 기능을 갖춘 챗봇입니다. 메시지를 입력하고 Enter 키를 누릅니다. 프롬프트 API를 사용할 수 있는 경우 네트워크 요청 없이 기기 내 모델에서 직접 대답을 스트리밍합니다. 브라우저에서 프롬프트 API를 지원하지 않으면 자동으로 Gemini 2.5 Flash로 대체됩니다. 목록으로 설명하거나, 코드 스니펫을 작성하거나, 마크다운 서식을 사용해 달라고 요청해 보세요. 대답은 서식이 지정된 코드 블록, 표, 인라인 코드로 기본적으로 렌더링됩니다.

결론
이 두 도움말을 통해 원시 생성 기본 요소부터 세련된 스트리밍 채팅 인터페이스에 이르기까지 Vercel AI SDK가 브라우저의 내장 프롬프트 API로 지원하는 모든 기능을 살펴보았습니다.
Vercel AI SDK로 기본 제공 프롬프트 API 사용에서는 스트리밍되지 않는 텍스트 생성과 스트리밍되는 텍스트 생성에 generateText() 및 streamText()를 사용하는 방법, Output.object()로 구조화된 JSON 출력을 요청하는 방법, 생성 로직을 변경하지 않고 런타임에 기본 제공 모델과 클라우드 제공업체 중에서 선택하는 하이브리드 코드를 작성하는 방법을 알아봤습니다.
이 문서에서는 동일한 빌딩 블록을 가져와 완전한 React UI로 래핑했습니다. 대화 루프를 관리하는 ToolLoopAgent, 브라우저에서 직접 응답을 스트리밍하는 DirectChatTransport가 있는 useChat, 마크다운 응답이 도착하면 깔끔하게 렌더링하는 AI Elements 구성요소 등이 있습니다. 프롬프트 API를 사용할 수 없는 경우 자동 클라우드 대체도 제공됩니다.
결과적으로 백엔드가 필요 없이 브라우저에서 완전히 작동하는 두 데모가 생성됩니다.
- Vercel AI SDK 데모와 함께 기본 제공 프롬프트 API 사용: 텍스트 생성, 스트리밍, 구조화된 출력을 나란히 표시
- 프롬프트 API 데모와 함께 Vercel AI SDK UI 및 AI 요소 사용: 마크다운 렌더링 및 어두운 모드가 있는 완전한 스트리밍 채팅 봇