17.2 런타임 타입 검증 — Zod 완전 정복과 Valibot/ArkType 비교
Zod — TypeScript-first 스키마 검증
npm install zod
기본 스키마
import { z } from 'zod'
// 기본 타입 스키마
const StringSchema = z.string()
const NumberSchema = z.number()
const BooleanSchema = z.boolean()
// 파싱 — 실패 시 ZodError throw
const name = StringSchema.parse('Alice') // 'Alice'
const age = NumberSchema.parse('25') // ZodError!
// 안전한 파싱 — 실패 시 success: false 반환
const result = NumberSchema.safeParse('not a number')
if (!result.success) {
console.log(result.error.issues)
// [{ code: 'invalid_type', expected: 'number', received: 'string' }]
}
객체 스키마
import { z } from 'zod'
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email('올바른 이메일 주소를 입력하세요.'),
name: z.string().min(2, '이름은 2자 이상이어야 합니다.').max(50),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user', 'guest']).default('user'),
tags: z.array(z.string()).max(10).optional(),
address: z.object({
city: z.string(),
country: z.string().length(2),
}).optional(),
createdAt: z.date().default(() => new Date()),
})
// 타입 추론
type User = z.infer<typeof UserSchema>
// {
// id: string;
// email: string;
// name: string;
// age?: number;
// role: 'admin' | 'user' | 'guest';
// ...
// }
// 파싱
const user = UserSchema.parse({
id: '550e8400-e29b-41d4-a716-446655440000',
email: 'alice@example.com',
name: 'Alice',
})
고급 Zod 패턴
import { z } from 'zod'
// 유니온 타입
const Shape = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('circle'), radius: z.number() }),
z.object({ kind: z.literal('rectangle'), width: z.number(), height: z.number() }),
])
// 변환 (transform)
const StringToNumber = z.string().transform((val) => parseInt(val, 10))
type StringToNumber = z.infer<typeof StringToNumber> // number
// 정제 (refine)
const PasswordSchema = z.string()
.min(8)
.refine(
(val) => /[A-Z]/.test(val),
{ message: '대문자를 포함해야 합니다.' }
)
.refine(
(val) => /[0-9]/.test(val),
{ message: '숫자를 포함해야 합니다.' }
)
// 교차 타입 (superRefine)
const ConfirmPasswordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '비밀번호가 일치하지 않습니다.',
path: ['confirmPassword'],
})
}
})
// 부분 스키마
const PartialUserSchema = UserSchema.partial() // 모두 optional
const RequiredUserSchema = UserSchema.required() // 모두 required
const PickedSchema = UserSchema.pick({ name: true, email: true })
const OmittedSchema = UserSchema.omit({ password: true })
// 확장
const AdminUserSchema = UserSchema.extend({
permissions: z.array(z.string()),
lastLoginAt: z.date().nullable(),
})
z.infer와 타입 공유
import { z } from 'zod'
// API 스키마 정의 (한 곳에서 관리)
export const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).optional(),
published: z.boolean().default(false),
})
// 자동으로 TypeScript 타입 생성
export type CreatePostDto = z.infer<typeof CreatePostSchema>
// 프론트엔드 — react-hook-form과 통합
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
function PostForm() {
const form = useForm<CreatePostDto>({
resolver: zodResolver(CreatePostSchema),
})
// ...
}
// 백엔드 — Express 미들웨어
function validateBody<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten().fieldErrors,
})
}
req.body = result.data
next()
}
}
app.post('/posts', validateBody(CreatePostSchema), createPostHandler)
Valibot — 경량 대안
npm install valibot
import * as v from 'valibot'
// Zod와 유사하지만 더 작은 번들 크기
const UserSchema = v.object({
id: v.string([v.uuid()]),
email: v.string([v.email()]),
name: v.string([v.minLength(2), v.maxLength(50)]),
age: v.optional(v.number([v.integer(), v.minValue(0)])),
})
type User = v.Output<typeof UserSchema>
const result = v.safeParse(UserSchema, { id: '...', email: '...', name: 'Alice' })
if (result.success) {
console.log(result.output)
}
고수 팁
Zod vs Valibot vs ArkType 선택 기준
Zod:
- 가장 넓은 생태계 (tRPC, react-hook-form, Prisma 통합)
- 문서와 커뮤니티 최고
- 번들 크기 13KB+ gzipped
Valibot:
- 트리 쉐이킹 최적화 (사용한 것만 번들)
- 엣지 런타임 (Cloudflare Workers 등)
- Zod와 유사한 API
ArkType:
- TypeScript 구문 그대로 사용
- 최고 성능 (컴파일 타임 최적화)
- 새로운 패러다임 (학습 곡선 있음)