본문으로 건너뛰기
Advertisement

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 구문 그대로 사용
- 최고 성능 (컴파일 타임 최적화)
- 새로운 패러다임 (학습 곡선 있음)
Advertisement