티스토리 뷰

TypeScript는 강력한 타입 시스템을 제공하지만, 이는 컴파일 타임에만 유효합니다. 실제 애플리케이션이 실행되는 런타임(Runtime) 환경에서 외부 API 호출 결과나 사용자 입력값은 언제든 우리가 정의한 타입과 다를 수 있습니다.

Zod는 이러한 간극을 메워주는 스키마 선언 및 유효성 검사 라이브러리입니다. Zod를 사용하면 데이터의 구조를 정의함과 동시에 실행 시점에 해당 데이터가 유효한지 검증할 수 있습니다.

1. 왜 Zod인가? (TypeScript의 한계)

TypeScript는 "이 변수는 이 타입일 것이다"라고 가정하고 코드를 짭니다. 하지만 외부에서 들어오는 데이터는 통제할 수 없습니다.

interface User {
  name: string;
  age: number;
}

// 만약 API 응답으로 { name: "John", age: "30" } (문자열 나이)이 온다면?
// TS는 에러를 내지 않지만, 실행 중에 age.toFixed() 등을 호출하면 런타임 에러가 발생합니다.
const userData: User = await fetch('/api/user').then(res => res.json());

Zod는 데이터를 "믿는" 대신 "검증"합니다.

2. Zod 기초: 스키마 정의 및 검증

Zod를 사용하면 먼저 스키마를 정의하고, 해당 스키마를 통해 데이터를 파싱(Parsing)합니다.

2.1. 기본 타입 정의

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),         // UUID 형식의 문자열
  name: z.string().min(2).max(20), // 2~20자 사이의 문자열
  email: z.string().email(),     // 이메일 형식
  age: z.number().int().positive(), // 양의 정수
  isPremium: z.boolean().default(false), // 기본값 설정
});

// 데이터 검증
const result = UserSchema.safeParse({
  id: "550e8400-e29b-41d4-a716-446655440000",
  name: "Kim",
  email: "test@example.com",
  age: 25,
});

if (result.success) {
  console.log("검증 성공:", result.data);
} else {
  console.error("검증 실패:", result.error.format());
}

3. infer를 이용한 TypeScript와의 연결

Zod의 가장 큰 장점 중 하나는 정의한 스키마로부터 TypeScript 타입을 자동으로 추출할 수 있다는 점입니다. 이를 통해 중복 선언을 방지할 수 있습니다.

// 스키마로부터 타입 추출
type UserType = z.infer<typeof UserSchema>;

// 이제 UserType은 아래와 동일하게 동작합니다.
/*
type UserType = {
  id: string;
  name: string;
  email: string;
  age: number;
  isPremium: boolean;
}
*/

4. 고급 검증 기법

단순한 타입 체크를 넘어 비즈니스 로직을 검증에 포함할 수 있습니다.

4.1. refine을 이용한 커스텀 검증

const PasswordSchema = z.string()
  .min(8)
  .refine((val) => /[0-9]/.test(val), {
    message: "비밀번호는 숫자를 포함해야 합니다.",
  });

const RegistrationSchema = z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "비밀번호가 일치하지 않습니다.",
  path: ["confirmPassword"], // 에러가 발생한 필드 지정
});

4.2. transform을 이용한 데이터 변환

데이터를 검증함과 동시에 원하는 형태로 가공할 수 있습니다.

const SearchSchema = z.object({
  query: z.string().trim().toLowerCase(), // 공백 제거 및 소문자 변환
  page: z.string().transform(Number),     // 문자열을 숫자로 변환
});

5. 실무 활용: Next.js API Routes 적용

API로 들어오는 데이터(request body)를 Zod로 가드(Guard)하는 패턴입니다.

import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';

const ContactSchema = z.object({
  email: z.string().email(),
  message: z.string().min(10, "메시지는 최소 10자 이상이어야 합니다."),
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') return res.status(405).end();

  const validation = ContactSchema.safeParse(req.body);

  if (!validation.success) {
    // 유효성 검사 실패 시 구체적인 에러 메시지 반환
    return res.status(400).json({ 
      error: "유효하지 않은 요청입니다.",
      details: validation.error.flatten().fieldErrors 
    });
  }

  // validation.data는 이제 완벽하게 타입이 추론된 안전한 데이터입니다.
  const { email, message } = validation.data;
  
  // DB 저장 로직...
  res.status(200).json({ success: true });
}

요약: Zod 도입의 이점

  1. 타입 안전성(Type Safety): TypeScript의 정적 타입과 런타임의 실제 데이터를 완벽하게 일치시킵니다.
  2. 코드 중복 제거: 스키마 하나로 검증 로직과 TS 타입을 동시에 관리합니다(z.infer).
  3. 가독성 높은 에러: 사용자에게 보여주기 적합한 상세한 에러 정보를 제공합니다.
  4. 선언적 코드: 복잡한 if 조건문 도배 없이 선언적으로 데이터 구조를 정의합니다.

Zod는 더 이상 선택이 아닌, 견고한 TypeScript 애플리케이션을 위한 필수 도구입니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함