Zod v4 완벽 가이드 - 타입 검증의 모든 것

Zod v4는 타입 스키마 정의와 검증을 위한 강력한 도구로, TypeScript 생태계에서 큰 인기를 끌고 있습니다.

간단하게 사용법을 알아보겠습니다.

1: Zod v4 핵심 개념과 기본 사용법

Zod는 "런타임에서 타입을 검증하는 라이브러리"로, TypeScript의 타입 시스템을 런타임으로 확장합니다.

TypeScript는 컴파일 타임에만 타입을 검사하지만, Zod는 런타임에서 데이터의 유효성을 보장합니다.

예를 들어 API 응답이나 사용자 입력은 런타임에 결정되므로 Zod로 검증해야 안전합니다.

  • 타입 스키마 정의: z.string(), z.number() 같은 기본 타입부터 시작해 복잡한 객체 구조까지 정의 가능.
  • 파싱(parse) vs. 안전 파싱(safeParse):
    • parse()는 검증 실패 시 에러를 던집니다.
    • safeParse()success: boolean과 함께 결과를 반환해 에러를 graceful하게 처리합니다.

실전 예제

import { z } from "zod";

// 기본 스키마 정의
const UserSchema = z.object({
  name: z.string().min(3, "이름은 3자 이상이어야 합니다"),
  age: z.number().positive("나이는 양수여야 합니다"),
});

// 검증 실행
const result = UserSchema.safeParse({ name: "Li", age: -1 });

if (!result.success) {
  console.log(result.error.format());
  /* 출력:
  {
    name: { _errors: ["이름은 3자 이상이어야 합니다"] },
    age: { _errors: ["나이는 양수여야 합니다"] }
  }
  */
}

Q&A

  1. Q: TypeScript 인터페이스만 사용하면 어떤 한계가 있나요?
    A: 인터페이스는 컴파일 타임에만 유효하며, 실제 런타임 데이터(예: API 응답)의 형식을 보장하지 않습니다.

  2. Q: safeParse 대신 parse를 사용할 때의 위험성은?
    A: parse는 검증 실패 시 에러를 throw하므로, try-catch로 처리하지 않으면 애플리케이션이 크래시될 수 있습니다.

연습 문제
email: stringisVerified: boolean을 가진 ProfileSchema를 작성하고, 잘못된 데이터로 safeParse를 테스트해 보세요.

정답

const ProfileSchema = z.object({
  email: z.string().email("유효한 이메일이 아닙니다"),
  isVerified: z.boolean(),
});

const testResult = ProfileSchema.safeParse({
  email: "not-an-email",
  isVerified: "yes" // 잘못된 타입
});

console.log(testResult.error?.format());
// { email: { _errors: [...] }, isVerified: { _errors: [...] } }

2: 고급 검증과 커스텀 로직

Zod는 조건부 검증, 커스텀 유효성 검사, 변환(transform) 등을 지원합니다.

  • .refine(): 조건부 검증 (예: 비밀번호 복잡도)

  • .transform(): 데이터 파싱 중 변환 (예: 문자열 → 숫자)

  • 조건부 필드: .refine()으로 복잡한 로직 추가.

    const PasswordSchema = z.string().refine(
      (val) => val.length >= 8 && /[A-Z]/.test(val),
      "비밀번호는 8자 이상, 대문자 포함 필요"
    );
    
  • transform: 데이터 파싱 중 변환.

    const StringToNumber = z.string().transform((val) => parseInt(val));
    

실전 예제

const DiscountSchema = z.object({
  code: z.string(),
  expiresAt: z.date().refine((date) => date > new Date()),
}).transform((data) => ({ ...data, isActive: true }));

Q&A

  1. Q: refinetransform의 차이는?
    A: refine은 검증만 수행하고, transform은 데이터 자체를 변환합니다.

  2. Q: 비동기 검증은 어떻게 하나요?
    A: .refine() 대신 **.refine(async () => {...})**을 사용합니다.

연습 문제
startDateendDate를 가진 스키마를 작성하고, endDatestartDate 이후인지 검증하세요.

정답

const EventSchema = z.object({
  startDate: z.date(),
  endDate: z.date(),
}).refine(
  (data) => data.endDate > data.startDate,
  "종료일은 시작일 이후여야 합니다"
);

EventSchema.parse({
  startDate: new Date("2023-01-01"),
  endDate: new Date("2022-12-31") // 에러 발생
});

3: Zod와 프론트엔드/백엔드 통합

  • 폼 검증: React Hook Form과 Zod 결합 (예: @hookform/resolvers).

  • API 검증: Next.js, Express에서 요청 데이터 검증.

실전 예제 (React Hook Form)

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema),
});

Q&A

  1. Q: API 응답을 Zod로 검증하는 이유?
    A: 클라이언트가 예상하지 못한 데이터 형식으로 인한 버그를 방지할 수 있습니다.

  2. Q: Zod 스키마 → Swagger 자동 생성 방법?
    A: zod-to-openapi 라이브러리를 사용합니다.

연습 문제
Next.js API 라우트에서 POST /api/users 요청의 body를 Zod로 검증하는 미들웨어를 작성하세요.

정답

// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from "next";

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const result = UserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json(result.error.format());
  }
  // 성공 로직
}

4: 에러 핸들링과 디버깅

  • 에러 메시지 커스터마이징: .min(5, { message: "너무 짧습니다!" })
const Schema = z.object({
  username: z.string({
    required_error: "사용자명은 필수입니다",
    invalid_type_error: "문자열이어야 합니다",
  }),
});
  • 에러 형식: ZodError 객체의 format() 메서드로 사용자 친화적 메시지 생성.
try {
  Schema.parse({});
} catch (err) {
  if (err instanceof z.ZodError) {
    // 1. 전체 에러 객체
    console.log(err.errors);

    // 2. 사용자 친화적 형식
    console.log(err.flatten());
  }
}

최종 통합 과제

도전 과제

  1. 회원가입 폼 스키마를 작성하세요. (이메일, 비밀번호, 비밀번호 확인 일치 검증).
const SignUpSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine(
  (data) => data.password === data.confirmPassword,
  "비밀번호가 일치하지 않습니다"
);
  1. NestJS 또는 Next.js 프로젝트에 Zod를 통합해 API 요청 검증을 구현하세요.
// pages/api/signup.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const result = SignUpSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      success: false,
      errors: result.error.flatten(),
    });
  }
  // DB 저장 로직
}

Zod의 장점

  • 런타임 안전성: TypeScript만으로는 불가능한 검증 가능
  • 개발자 경험: 직관적인 API 디자인과 풍부한 커스터마이징
  • 생태계: React Hook Form, tRPC 등과의 원활한 통합

이상입니다.