Zod 완벽 가이드 | TypeScript 스키마 검증·타입 안전성·Form·API·실전 활용
이 글의 핵심
Zod로 타입 안전한 검증을 구현하는 완벽 가이드입니다. 스키마 정의, Form 검증, API 검증, React Hook Form 통합까지 실전 예제로 정리했습니다.
실무 경험 공유: 수동 검증 로직을 Zod로 전환하면서, 런타임 에러가 80% 감소하고 코드 가독성이 크게 향상된 경험을 공유합니다.
들어가며: “런타임 에러가 자주 발생해요”
실무 문제 시나리오
시나리오 1: 타입과 런타임이 달라요
TypeScript는 컴파일 타임만 검증합니다. Zod는 런타임도 검증합니다.
시나리오 2: Form 검증이 복잡해요
수동 검증은 번거롭습니다. Zod는 선언적으로 검증합니다.
시나리오 3: API 응답 검증이 필요해요
타입 단언은 위험합니다. Zod는 안전하게 검증합니다.
1. Zod란?
핵심 특징
Zod는 TypeScript 우선 스키마 검증 라이브러리입니다.
주요 장점:
- 타입 안전성: 자동 타입 추론
- 런타임 검증: 안전한 검증
- Zero Dependencies: 의존성 없음
- 작은 크기: 8KB
- 직관적 API: 간단한 문법
2. 기본 사용
설치
npm install zod
기본 스키마
import { z } from 'zod';
// 기본 타입
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
// 검증
stringSchema.parse('hello'); // ✅ 'hello'
stringSchema.parse(123); // ❌ ZodError
// 안전한 검증
const result = stringSchema.safeParse('hello');
if (result.success) {
console.log(result.data); // 'hello'
} else {
console.error(result.error);
}
3. 객체 스키마
기본 객체
const userSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string().min(2).max(50),
age: z.number().min(0).max(120).optional(),
role: z.enum(['user', 'admin']),
});
type User = z.infer<typeof userSchema>;
// 검증
const user = userSchema.parse({
id: 1,
email: '[email protected]',
name: 'John',
role: 'user',
});
Nested 객체
const postSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
author: z.object({
id: z.number(),
name: z.string(),
}),
tags: z.array(z.string()),
metadata: z.record(z.string(), z.any()),
});
4. 고급 검증
커스텀 검증
const passwordSchema = z
.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.regex(/[A-Z]/, '대문자를 포함해야 합니다')
.regex(/[a-z]/, '소문자를 포함해야 합니다')
.regex(/[0-9]/, '숫자를 포함해야 합니다');
const signupSchema = z
.object({
email: z.string().email(),
password: passwordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: '비밀번호가 일치하지 않습니다',
path: ['confirmPassword'],
});
Transform
const dateSchema = z.string().transform((str) => new Date(str));
const userSchema = z.object({
name: z.string().transform((name) => name.trim().toLowerCase()),
age: z.string().transform((age) => parseInt(age, 10)),
});
const result = userSchema.parse({
name: ' JOHN ',
age: '30',
});
// { name: 'john', age: 30 }
5. React Hook Form 통합
설치
npm install react-hook-form @hookform/resolvers
Form 컴포넌트
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
email: z.string().email('유효한 이메일을 입력하세요'),
password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다'),
});
type FormData = z.infer<typeof formSchema>;
export default function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
console.log('Valid data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">로그인</button>
</form>
);
}
6. API 검증
Next.js API Route
// pages/api/users.ts
import { z } from 'zod';
import type { NextApiRequest, NextApiResponse } from 'next';
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
issues: result.error.issues,
});
}
const user = await db.user.create(result.data);
res.status(201).json(user);
}
7. 환경 변수 검증
// env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.string().transform((val) => parseInt(val, 10)),
});
export const env = envSchema.parse(process.env);
// 타입 안전하게 사용
console.log(env.PORT); // number
8. 실전 예제
복잡한 Form
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
});
const orderSchema = z.object({
items: z
.array(
z.object({
productId: z.number(),
quantity: z.number().min(1),
})
)
.min(1, '최소 1개 이상의 상품이 필요합니다'),
shippingAddress: addressSchema,
billingAddress: addressSchema.optional(),
paymentMethod: z.enum(['card', 'paypal', 'bank']),
agreeToTerms: z.literal(true, {
errorMap: () => ({ message: '약관에 동의해야 합니다' }),
}),
});
type Order = z.infer<typeof orderSchema>;
정리 및 체크리스트
핵심 요약
- Zod: TypeScript 스키마 검증
- 타입 안전성: 자동 타입 추론
- 런타임 검증: 안전한 검증
- React Hook Form: 완벽한 통합
- API 검증: 안전한 API
- 환경 변수: 타입 안전
구현 체크리스트
- Zod 설치
- 스키마 정의
- Form 검증 구현
- API 검증 구현
- 환경 변수 검증
- 에러 처리
- React Hook Form 통합
같이 보면 좋은 글
- tRPC 완벽 가이드
- Next.js App Router 가이드
- TypeScript 완벽 가이드
이 글에서 다루는 키워드
Zod, TypeScript, Validation, Schema, Type Safety, Form, API
자주 묻는 질문 (FAQ)
Q. Yup과 비교하면 어떤가요?
A. Zod가 TypeScript 지원이 더 좋고 타입 추론이 완벽합니다.
Q. Joi와 비교하면 어떤가요?
A. Zod가 TypeScript 친화적이고 더 가볍습니다.
Q. 성능은 어떤가요?
A. 매우 빠릅니다. 8KB로 작고 최적화되어 있습니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, tRPC, Next.js 등 많은 프로젝트에서 사용합니다.