TechFeedTechFeed
Open Source

Zod 스키마 검증 실전 가이드 — TypeScript 런타임 타입 안전성

Zod 핵심 타입, z.infer 타입 추론, React Hook Form 폼 검증, API 응답 검증, 에러 메시지 커스터마이징, tRPC/Next.js 통합.

한 줄 요약: Zod는 TypeScript에서 런타임 스키마 검증과 정적 타입 추론을 동시에 처리하는 라이브러리다. 한 번 스키마를 정의하면 z.infer로 타입을 자동 생성하고, React Hook Form·tRPC·Next.js 서버 액션 등에서 바로 활용할 수 있다.

이 글이 필요한 사람
  • TypeScript 프로젝트에서 API 응답이나 폼 데이터를 안전하게 검증하고 싶은 개발자
  • 타입 정의와 검증 로직을 따로 유지하는 번거로움을 없애고 싶은 경우
  • React Hook Form에서 Zod로 폼 검증을 구현하려는 경우
  • tRPC 또는 Next.js App Router 서버 액션에 Zod를 연동하려는 경우
  • 에러 메시지를 한국어로 커스터마이징하거나 필드별 세밀한 검증이 필요한 경우

Zod 기본 스키마 정의 — 핵심 타입 총정리

Zod 공식 문서(zod.dev)와 GitHub(github.com/colinhacks/zod)에서 전체 API를 확인할 수 있다. 설치는 npm install zod 한 줄이다. TypeScript 4.5 이상, strict: true 환경을 권장한다.

Zod의 모든 스키마는 z 네임스페이스에서 시작한다. 기본 원시 타입부터 복합 타입까지 일관된 방식으로 정의한다.

타입Zod 스키마주요 메서드
stringz.string()min, max, email, url, regex, trim
numberz.number()min, max, int, positive, nonnegative
booleanz.boolean()
objectz.object({})partial, required, pick, omit, extend
arrayz.array()min, max, nonempty, length
enumz.enum([])
unionz.union([])discriminatedUnion
optional.optional()nullable, nullish
literalz.literal()
recordz.record()
Zod 기본 타입 정의 예시
import { z } from 'zod'; // 원시 타입 const nameSchema = z.string().min(1, '이름을 입력해주세요').max(50); const ageSchema = z.number().int().min(0).max(150); const activeSchema = z.boolean(); // 검증 const result = nameSchema.safeParse(''); if (!result.success) { console.log(result.error.issues); // [{code: 'too_small', message: '이름을 입력해주세요', ...}] } // parse vs safeParse // parse: 실패 시 ZodError throw // safeParse: 항상 {success, data} 또는 {success, error} 반환 const parsed = nameSchema.parse('홍길동'); // => '홍길동' const safe = ageSchema.safeParse('스물다섯'); // => { success: false, error: ... }

z.infer — 스키마에서 TypeScript 타입 자동 추론

Zod의 가장 강력한 기능 중 하나다. 스키마를 정의하면 z.infer<typeof schema>로 TypeScript 타입을 자동 생성한다. 타입 정의와 검증 로직을 따로 유지할 필요가 없어진다.

interfacetype을 별도로 작성하지 않아도 되고, 스키마를 수정하면 타입도 자동으로 따라온다. 타입과 검증 로직이 어긋나는 버그를 원천 차단한다.

z.infer로 타입 자동 생성
import { z } from 'zod'; const UserSchema = z.object({ id: z.number().int().positive(), name: z.string().min(1).max(100), email: z.string().email(), role: z.enum(['admin', 'user', 'guest']), createdAt: z.string().datetime(), profile: z.object({ bio: z.string().optional(), avatarUrl: z.string().url().optional(), }).optional(), }); // 자동 타입 추론 — interface를 따로 작성할 필요 없음 type User = z.infer<typeof UserSchema>; // User 타입: // { // id: number; // name: string; // email: string; // role: "admin" | "user" | "guest"; // createdAt: string; // profile?: { bio?: string; avatarUrl?: string; } | undefined; // } function processUser(user: User) { console.log(user.role); // 타입 완전 지원 }

폼 검증 실전 — React Hook Form + Zod

@hookform/resolvers 패키지의 zodResolver를 사용하면 React Hook Form과 Zod를 연결할 수 있다. 폼 스키마 하나로 타입 추론, 실시간 검증, 에러 메시지 표시를 모두 처리한다.

설치: npm install react-hook-form @hookform/resolvers zod

React Hook Form + zodResolver 연동
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const signupSchema = z.object({ username: z.string().min(3, '3자 이상 입력해주세요').max(20), email: z.string().email('올바른 이메일 형식이 아닙니다'), password: z.string().min(8, '비밀번호는 8자 이상이어야 합니다'), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: '비밀번호가 일치하지 않습니다', path: ['confirmPassword'], }); type SignupFormData = z.infer<typeof signupSchema>; export function SignupForm() { const { register, handleSubmit, formState: { errors } } = useForm<SignupFormData>({ resolver: zodResolver(signupSchema), }); const onSubmit = (data: SignupFormData) => { // data는 이미 검증된 상태 — 타입도 완전히 추론됨 console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('email')} placeholder="이메일" /> {errors.email && <p>{errors.email.message}</p>} <input {...register('password')} type="password" /> {errors.password && <p>{errors.password.message}</p>} <button type="submit">가입</button> </form> ); }
.refine()과 .superRefine(): 필드 간 의존 검증(비밀번호 확인 등)은 .refine()으로 처리한다. 여러 조건을 순차적으로 검사하거나 이슈를 직접 추가해야 할 때는 .superRefine()을 사용한다.

API 응답 검증 — 런타임 타입 안전성 확보

TypeScript는 컴파일 타임 타입만 검사한다. 외부 API 응답은 런타임에서 예상과 다른 형태가 올 수 있다. Zod로 API 응답 스키마를 정의하면 런타임 불일치를 즉시 감지하고 명확한 에러 메시지를 얻는다.

특히 써드파티 API, 레거시 백엔드, CDN의 JSON 응답처럼 구조를 신뢰할 수 없는 경우에 필수적이다.

fetch API 응답 Zod 검증
import { z } from 'zod'; const PostSchema = z.object({ id: z.number(), title: z.string(), body: z.string(), userId: z.number(), tags: z.array(z.string()).optional(), }); const PostListSchema = z.array(PostSchema); type Post = z.infer<typeof PostSchema>; async function fetchPosts(): Promise<Post[]> { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const json = await res.json(); // 런타임 검증 — 구조가 맞지 않으면 즉시 에러 const result = PostListSchema.safeParse(json); if (!result.success) { // result.error.format()으로 필드별 에러 확인 console.error('API 응답 형식 오류:', result.error.format()); throw new Error('API response validation failed'); } return result.data; // 완전히 타입 안전한 Post[] }

에러 메시지 커스터마이징과 한국어 처리

Zod의 에러 메시지는 두 가지 방법으로 커스터마이징할 수 있다. 스키마 메서드에 직접 문자열을 넣는 방식이 가장 간단하고, 전역 에러 맵을 설정하면 프로젝트 전체에 일괄 적용할 수 있다.

에러 객체의 .format()은 필드별 트리 구조로 에러를 정리하고, .flatten()fieldErrorsformErrors로 분리된 평탄한 구조를 반환한다. React Hook Form 없이 서버 액션에서 직접 에러를 처리할 때는 .flatten()이 편리하다.

에러 메시지 커스터마이징
import { z } from 'zod'; // 방법 1: 각 메서드에 직접 메시지 설정 const productSchema = z.object({ name: z.string({ required_error: '상품명은 필수입니다', invalid_type_error: '상품명은 문자열이어야 합니다', }).min(1, '상품명을 입력해주세요'), price: z.number({ required_error: '가격은 필수입니다', }).positive('가격은 0보다 커야 합니다'), stock: z.number().int().nonnegative('재고는 음수일 수 없습니다'), }); // 에러 구조 확인 const result = productSchema.safeParse({ name: '', price: -1000, stock: -5 }); if (!result.success) { // flatten() — 필드별 에러 배열 const flat = result.error.flatten(); console.log(flat.fieldErrors); // { name: ['상품명을 입력해주세요'], price: ['가격은 0보다 커야 합니다'], stock: ['재고는 음수일 수 없습니다'] } }
Next.js 서버 액션에서 Zod 검증
'use server'; import { z } from 'zod'; const ContactSchema = z.object({ name: z.string().min(1, '이름을 입력해주세요'), email: z.string().email('올바른 이메일 주소를 입력해주세요'), message: z.string().min(10, '메시지는 10자 이상 입력해주세요').max(1000), }); export async function submitContact(formData: FormData) { const raw = { name: formData.get('name'), email: formData.get('email'), message: formData.get('message'), }; const result = ContactSchema.safeParse(raw); if (!result.success) { return { success: false, errors: result.error.flatten().fieldErrors, }; } // result.data는 타입 안전 await saveContact(result.data); return { success: true }; }
tRPC와 Zod: tRPC는 내부적으로 Zod를 입력/출력 검증에 사용한다. input(UserSchema)으로 선언하면 클라이언트에서도 동일한 타입이 추론되어 엔드투엔드 타입 안전성을 확보할 수 있다. 별도 설정 없이 즉시 연동된다.
ZodTypeScript검증스키마React Hook FormtRPC

관련 포스트

오픈소스 기여 시작 가이드 — 첫 PR까지2026-02-262026년 주목할 오픈소스 프로젝트 10선2026-03-09오픈소스 라이선스 완벽 가이드2026-03-10오픈소스에 기여하는 실전 가이드 — 첫 PR부터 메인테이너까지2026-03-11