Zod 완벽 가이드 | TypeScript 스키마 검증·타입 안전성·Form·API·실전 활용

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 등 많은 프로젝트에서 사용합니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3