💻 프로그래밍

Zod

TypeScript-first Schema Validation

TypeScript-first 스키마 검증 라이브러리. 런타임 검증과 정적 타입 추론을 동시에 제공하여 타입 안전한 애플리케이션 개발 지원.

📖 상세 설명

Zod는 TypeScript를 위해 설계된 스키마 선언 및 검증 라이브러리입니다. 스키마를 한 번 정의하면 런타임 데이터 검증과 TypeScript 타입 추론을 동시에 얻을 수 있어, 타입 정의를 중복으로 작성할 필요가 없습니다. "DRY(Don't Repeat Yourself)" 원칙을 따르면서도 완전한 타입 안전성을 보장하는 것이 Zod의 핵심 철학입니다.

Zod의 가장 큰 장점은 타입 추론과 런타임 검증의 완벽한 통합입니다. TypeScript의 타입 시스템은 컴파일 타임에만 작동하여 외부에서 들어오는 데이터(API 응답, 사용자 입력, 환경 변수 등)를 검증할 수 없습니다. Zod는 이 간극을 메워주어, 스키마로 정의한 구조가 런타임에도 검증되고 동시에 TypeScript 타입으로도 추론됩니다. z.infer를 사용하면 스키마에서 타입을 자동으로 추출할 수 있습니다.

Yup, Joi와 같은 다른 검증 라이브러리와 비교했을 때, Zod는 TypeScript 친화성에서 독보적입니다. Yup은 JavaScript 생태계에서 먼저 등장했고 타입 지원이 나중에 추가되었으며, Joi는 Node.js 환경에 최적화되어 브라우저 번들 사이즈가 큽니다. 반면 Zod는 처음부터 TypeScript를 위해 설계되어 타입 추론이 자연스럽고, 의존성이 없어(zero dependencies) 트리쉐이킹이 효율적이며, 번들 사이즈도 작습니다.

실무에서 Zod는 다양한 영역에 활용됩니다. React Hook Form, Formik 등과 통합하여 폼 검증에 사용하고, tRPC와 결합하여 end-to-end 타입 안전한 API를 구축합니다. 또한 API 응답 검증, 환경 변수 파싱, 설정 파일 검증 등에서 런타임 안전성을 보장합니다. Next.js, Remix 같은 풀스택 프레임워크에서 서버와 클라이언트 간 데이터 검증의 표준으로 자리잡고 있습니다.

💻 코드 예제

// 1. 기본 스키마 정의
import { z } from 'zod';

// 원시 타입 스키마
const stringSchema = z.string();
const numberSchema = z.number().positive().int();
const booleanSchema = z.boolean();

// 객체 스키마 정의
const UserSchema = z.object({
    id: z.number(),
    name: z.string().min(2, '이름은 2자 이상이어야 합니다'),
    email: z.string().email('올바른 이메일 형식이 아닙니다'),
    age: z.number().min(0).max(150).optional(),
    role: z.enum(['admin', 'user', 'guest']),
    createdAt: z.date().default(() => new Date()),
});

// 2. 타입 추론 - 스키마에서 TypeScript 타입 자동 추출
type User = z.infer<typeof UserSchema>;
// 결과: { id: number; name: string; email: string; age?: number; role: 'admin' | 'user' | 'guest'; createdAt: Date; }

// 3. 데이터 검증 (parse vs safeParse)
const userData = {
    id: 1,
    name: '김개발',
    email: 'dev@example.com',
    role: 'user'
};

// parse - 실패 시 예외 발생
try {
    const user = UserSchema.parse(userData);
    console.log('검증 성공:', user.name);
} catch (error) {
    if (error instanceof z.ZodError) {
        console.error('검증 실패:', error.errors);
    }
}

// safeParse - 결과를 객체로 반환 (권장)
const result = UserSchema.safeParse(userData);
if (result.success) {
    console.log('사용자:', result.data.email);
} else {
    console.error('오류:', result.error.flatten());
}

// 4. 고급 스키마 패턴
// API 응답 스키마
const ApiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
    z.object({
        success: z.boolean(),
        data: dataSchema,
        message: z.string().optional(),
        timestamp: z.string().datetime(),
    });

const UserListResponse = ApiResponseSchema(z.array(UserSchema));
type UserListResponse = z.infer<typeof UserListResponse>;

// 5. 폼 검증 (React Hook Form 연동)
const LoginFormSchema = z.object({
    email: z.string().email('올바른 이메일을 입력하세요'),
    password: z.string()
        .min(8, '비밀번호는 8자 이상이어야 합니다')
        .regex(/[A-Z]/, '대문자를 포함해야 합니다')
        .regex(/[0-9]/, '숫자를 포함해야 합니다'),
    rememberMe: z.boolean().default(false),
});

// 6. 변환(transform)과 정제(refine)
const ProductSchema = z.object({
    name: z.string().transform(s => s.trim()),
    price: z.string().transform(Number).pipe(z.number().positive()),
    quantity: z.coerce.number().int().min(1),
}).refine(
    data => data.quantity <= 100,
    { message: '한 번에 100개까지만 주문 가능합니다' }
);

🗣️ 실무에서 이렇게 말해요

💬 회의에서 - API 응답 검증 전략
"외부 API 응답을 그냥 any로 받아서 쓰다가 런타임 에러가 계속 발생하고 있어요. Zod 스키마를 도입해서 API 응답을 검증하면 어떨까요? safeParse로 검증하고, 실패하면 에러 리포팅하고 폴백 데이터를 반환하는 방식으로요. 스키마 정의하면 타입도 자동으로 나오니까 타입 정의 중복도 없어집니다."
💬 코드 리뷰에서 - 스키마 설계
"이 폼 스키마 설계 좋은데, password와 confirmPassword 일치 검증은 각 필드 레벨이 아니라 refine으로 객체 레벨에서 해야 해요. 그리고 에러 메시지에 path 옵션 넣어서 어떤 필드 오류인지 명확히 해주세요. flatten() 쓰면 프론트에서 에러 표시하기 훨씬 편해집니다."
💬 면접에서 - Zod vs 다른 라이브러리
"Yup도 사용해봤지만 Zod를 선호합니다. Yup은 타입 추론이 완벽하지 않아서 as Type 캐스팅을 자주 써야 했어요. Zod는 z.infer로 스키마에서 타입이 완벽하게 추론되고, tRPC와 조합하면 클라이언트-서버 간 end-to-end 타입 안전성을 얻을 수 있어서 풀스택 TypeScript 프로젝트에 최적입니다."

⚠️ 주의사항

📦
번들 사이즈 고려

Zod는 약 12KB(gzipped)로 작은 편이지만, 클라이언트 번들에 포함될 경우 영향을 고려하세요. 서버 사이드 검증만 필요하다면 클라이언트 번들에서 제외하고, 클라이언트에서는 경량 검증 로직을 사용하는 것도 방법입니다. 트리쉐이킹이 잘 되므로 사용하는 기능만 import하세요.

💬
에러 메시지 커스터마이징

기본 에러 메시지가 사용자 친화적이지 않을 수 있습니다. 각 검증 규칙에 명시적으로 한글 메시지를 지정하거나, z.setErrorMap()으로 전역 에러 맵을 설정하세요. 다국어 지원이 필요하면 i18n 라이브러리와 연동하여 동적으로 메시지를 생성할 수 있습니다.

성능 최적화

대용량 배열이나 깊은 중첩 객체 검증 시 성능 이슈가 발생할 수 있습니다. 반복되는 검증에는 스키마를 미리 생성해두고 재사용하세요. 실시간 입력 검증에는 debounce를 적용하고, 필요한 필드만 검증하는 partial schema를 활용하면 성능을 개선할 수 있습니다.

🔗 관련 용어

📚 더 배우기