LLM 응답을 그대로 쓰려면 텍스트 파싱이 필요한 경우가 많다. JSON으로 출력해달라고 프롬프트에 적어도 포맷이 들쭉날쭉하거나, 응답 앞뒤에 설명 텍스트가 섞이기도 한다. 파싱 코드를 따로 짜고, 유효성 검사도 해야 한다.
Vercel AI SDK의 generateObject는 이 과정을 없애준다. Zod 스키마를 정의하면 LLM이 그 구조에 맞는 JSON을 반환한다. 직접 파싱하거나 검증할 필요가 없다.
설치
npm install ai @ai-sdk/anthropic zod
기본 사용법
import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const { object } = await generateObject({
model: anthropic('claude-haiku-4-5-20251001'),
schema: z.object({
title: z.string(),
summary: z.string(),
tags: z.array(z.string()),
difficulty: z.enum(['easy', 'medium', 'hard']),
}),
prompt: '다음 기술 블로그 글을 분석해주세요: [글 내용]',
});
console.log(object.title); // string으로 타입 추론
console.log(object.tags); // string[]
console.log(object.difficulty); // "easy" | "medium" | "hard"
object는 Zod 스키마에서 추론된 타입을 그대로 가진다. 캐스팅 없이 타입 안전하게 사용할 수 있다.
실용 예시 — 비정형 텍스트에서 정보 추출
자유 형식의 피드백 텍스트에서 구조화된 정보를 뽑는 경우다.
const feedbackSchema = z.object({
sentiment: z.enum(['positive', 'neutral', 'negative'])
.describe('전반적인 피드백 감정 톤'),
score: z.number().min(1).max(5)
.describe('만족도 점수 (1-5)'),
issues: z.array(z.string())
.describe('언급된 문제점 목록'),
suggestions: z.array(z.string())
.describe('개선 제안 목록'),
});
export async function analyzeFeedback(text: string) {
const { object } = await generateObject({
model: anthropic('claude-haiku-4-5-20251001'),
schema: feedbackSchema,
prompt: `다음 사용자 피드백을 분석해주세요:\n\n${text}`,
});
return object;
}
결과물을 DB에 저장하거나 다른 API로 넘길 수 있는 형태로 바로 받을 수 있다.
스트리밍 버전 — streamObject
응답이 긴 경우엔 streamObject를 쓰면 부분적으로 받아서 처리할 수 있다.
import { streamObject } from 'ai';
const { partialObjectStream } = streamObject({
model: anthropic('claude-haiku-4-5-20251001'),
schema: z.object({
recommendations: z.array(
z.object({
title: z.string(),
reason: z.string(),
})
),
}),
prompt: '주니어 개발자에게 추천할 책 5권을 알려주세요.',
});
for await (const partial of partialObjectStream) {
// 응답이 생성되면서 배열이 점진적으로 채워짐
console.log(partial.recommendations);
}
partialObjectStream은 스키마 구조가 점진적으로 채워진 상태를 내보낸다. UI에 실시간으로 반영할 때 유용하다.
Next.js API 라우트에서 쓰기
// app/api/analyze/route.ts
import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const schema = z.object({
category: z.enum(['bug', 'feature', 'question', 'other']),
priority: z.enum(['low', 'medium', 'high']),
summary: z.string().max(100),
});
export async function POST(req: Request) {
const { text } = await req.json();
const { object } = await generateObject({
model: anthropic('claude-haiku-4-5-20251001'),
schema,
prompt: `다음 GitHub 이슈를 분류해주세요:\n\n${text}`,
});
return Response.json(object);
}
이슈 자동 분류, 문서 태깅, 콘텐츠 분석 같은 작업을 간단한 API 엔드포인트로 만들 수 있다.
스키마 설계 팁
z.enum()으로 가능한 값 범위를 제한하면 LLM이 임의의 값을 만들어내는 걸 막을 수 있다..describe('...')로 각 필드의 의미를 LLM에 전달하면 정확도가 올라간다.- 없을 수 있는 필드는
.optional()을 붙인다. 명확하지 않은 값에 LLM이 억지로 뭔가를 채우는 걸 방지한다. - 배열 항목도
z.string()보다z.object({...})로 구조를 구체화할수록 결과가 더 일관된다.
텍스트 입력을 받아서 구조화된 데이터로 만들어야 하는 상황이라면 generateObject가 파싱 코드를 대신해준다. 스키마 한 번 정의해두면 타입도 자동으로 따라오니까, 실제 서비스에 LLM을 붙일 때 검토해볼 만한 패턴이다.