프로젝트를 Vercel AI SDK v4에서 v5로 올리려다가 useChat 붙은 파일에서 컴파일 에러가 좌르륵 떴다. message.content가 없어지고, input이랑 handleSubmit도 훅에서 사라졌다.
v5부터 chat message 구조 자체가 바뀐 탓이다. 한 메시지는 이제 텍스트 한 덩어리가 아니라 parts 배열이다. 텍스트, 툴 호출, 리즈닝이 각각 파트로 들어가는 형태다.
프론트엔드 — messages는 이제 parts 배열이다
'use client';
import { useState } from 'react';
import { useChat } from '@ai-sdk/react';
export default function Chat() {
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState('');
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<b>{m.role}:</b>
{m.parts.map((part, i) => {
if (part.type === 'text') return <span key={i}>{part.text}</span>;
if (part.type === 'reasoning') return <em key={i}>{part.text}</em>;
return null;
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}
message.content를 그대로 꺼내던 v4 코드는 다 안 먹는다. parts.map으로 파트 타입별로 렌더링해야 한다. 툴 호출 결과나 리즈닝 블록이 별도 파트로 분리돼서 UI 분기가 오히려 깔끔해졌다.
input이랑 handleSubmit도 훅에서 뺐다. 어차피 컴포넌트에서 로컬 state로 관리하는 게 자연스러운데, 그걸 훅에 안 두는 방향으로 바뀐 거다. 전송은 sendMessage({ text }) 한 줄이면 된다.
서버 — UIMessage[]를 ModelMessage[]로 변환해 넘긴다
API 라우트에도 변화가 있다. 클라이언트에서 UIMessage 배열이 오는데, LLM은 그걸 그대로 못 먹는다. convertToModelMessages로 바꿔서 넘겨야 한다.
// app/api/chat/route.ts
import { streamText, convertToModelMessages, UIMessage } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: anthropic('claude-sonnet-4-5'),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
두 개가 세트다. 입력은 convertToModelMessages로 UIMessage → ModelMessage, 응답은 toUIMessageStreamResponse()로 UIMessage 스트림. 이걸 안 맞추면 클라이언트에 파트가 안 들어와 화면이 그냥 빈다.
마이그레이션할 때 걸리는 것들
- v4의
message.content를 참조하던 곳은 전부parts순회로 바꿔야 한다. 텍스트만 뽑고 싶으면parts.filter(p => p.type === 'text').map(p => p.text).join(''). - 툴 호출 렌더링이 편해졌다.
part.type이'tool-이름'형태라 툴별로 UI 컴포넌트를 따로 분기하기 쉽다. - API 라우트 응답은
toUIMessageStreamResponse()로 통일. v4의toDataStreamResponse()는 사라졌다.
새 구조가 언뜻 번거로워 보이는데, 툴 호출 붙은 챗봇을 한 번이라도 만들어보면 파트 단위로 나뉜 게 훨씬 낫다. 텍스트, 툴, 리즈닝을 자연스럽게 별도 컴포넌트로 뽑을 수 있어서, 하나의 문자열 안에 다 우겨넣던 v4 시절보다 UI 짜기가 편해진다. useChat 붙인 앱을 v5로 올릴 계획이라면 프론트/API 양쪽 다 메시지 구조부터 손봐야 한다.