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

💬 댓글