•3 min read•조회 0
Zod로 런타임 타입 검증 마스터하기
TypeScript 프로젝트에서 Zod를 활용한 런타임 타입 검증과 API 응답 안전하게 다루는 실무 패턴을 알아봅니다.
정다운
TypeScript의 타입은 컴파일 타임에만 존재합니다. 런타임에 외부 데이터(API 응답, 폼 입력 등)를 검증하려면 별도의 도구가 필요한데, Zod가 이 문제를 우아하게 해결해줍니다.
왜 Zod인가?
// TypeScript 타입만으로는 런타임 안전성을 보장할 수 없습니다
interface User {
id: number;
name: string;
email: string;
}
// API 응답이 실제로 이 형태인지 알 수 없음
const user: User = await fetch('/api/user').then(r => r.json());
// 런타임 에러 가능성 🚨Zod는 스키마를 정의하면 런타임 검증과 TypeScript 타입을 동시에 얻을 수 있습니다.
기본 사용법
import { z } from 'zod';
// 스키마 정의
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
});
// 타입 자동 추론
type User = z.infer<typeof UserSchema>;
// { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest'; createdAt: string }
// 런타임 검증
const result = UserSchema.safeParse(apiResponse);
if (result.success) {
console.log(result.data); // 타입 안전!
} else {
console.error(result.error.issues); // 상세한 에러 정보
}실무 패턴 1: API 응답 검증
// api/schemas.ts
export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
z.object({
items: z.array(itemSchema),
total: z.number(),
page: z.number(),
pageSize: z.number(),
hasNext: z.boolean(),
});
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
avatar: z.string().url().nullable(),
});
export const UsersResponseSchema = PaginatedResponseSchema(UserSchema);
// api/users.ts
export async function getUsers(page: number) {
const response = await fetch(`/api/users?page=${page}`);
const data = await response.json();
const result = UsersResponseSchema.safeParse(data);
if (!result.success) {
// 에러 로깅 후 센트리 등으로 전송
console.error('API 응답 형식 오류:', result.error.flatten());
throw new Error('Invalid API response');
}
return result.data;
}실무 패턴 2: 폼 검증 (React Hook Form 연동)
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const SignupSchema = z.object({
email: z.string().email('올바른 이메일을 입력해주세요'),
password: z
.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.regex(/[A-Z]/, '대문자를 포함해야 합니다')
.regex(/[0-9]/, '숫자를 포함해야 합니다'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: '비밀번호가 일치하지 않습니다',
path: ['confirmPassword'],
});
type SignupForm = z.infer<typeof SignupSchema>;
function SignupPage() {
const { register, handleSubmit, formState: { errors } } = useForm<SignupForm>({
resolver: zodResolver(SignupSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
{/* ... */}
</form>
);
}실무 패턴 3: 환경 변수 검증
// env.ts
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000), // 문자열을 숫자로 변환
ENABLE_CACHE: z.coerce.boolean().default(true),
});
// 앱 시작 시 검증 - 잘못된 설정은 빨리 실패하는 게 좋습니다
export const env = EnvSchema.parse(process.env);
// 이제 타입 안전하게 사용
console.log(env.PORT); // number 타입
console.log(env.ENABLE_CACHE); // boolean 타입고급 팁: transform과 preprocess
// 날짜 문자열을 Date 객체로 변환
const EventSchema = z.object({
title: z.string(),
startDate: z.string().datetime().transform((str) => new Date(str)),
});
// 입력값 전처리 (공백 제거 등)
const TrimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
);
// nullable과 optional 구분
const ProfileSchema = z.object({
bio: z.string().nullable(), // null 허용, undefined 불가
website: z.string().optional(), // undefined 허용, null 불가
twitter: z.string().nullish(), // 둘 다 허용
});결론
Zod는 TypeScript 프로젝트의 런타임 안전성을 크게 높여줍니다. 특히 외부 데이터를 다루는 API 레이어, 폼 검증, 환경 변수 검증에서 적극 활용해보세요. "타입은 거짓말하지 않는다"는 확신을 런타임까지 가져갈 수 있습니다.
# 설치
npm install zod
# React Hook Form 연동 시
npm install @hookform/resolvers