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 기본 스키마 정의 — 핵심 타입 총정리 — 프로젝트 구조 다이어그램
Zod 스키마 검증 실전 가이드 — TypeScript 런타임 타입 안전성 — 프로젝트 구조 다이어그램 (출처: 공식 문서 및 벤치마크 데이터 기반)
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 — 스키마에서 TypeScript 타입 자동 추론 — 기능 비교 차트
Zod 스키마 검증 실전 가이드 — TypeScript 런타임 타입 안전성 — 기능 비교 차트 (출처: 공식 문서 및 벤치마크 데이터 기반)
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 응답처럼 구조를 신뢰할 수 없는 경우에 필수적이다.

폼 검증 실전 — React Hook Form + Zod — 생태계 맵 시각화
Zod 스키마 검증 실전 가이드 — TypeScript 런타임 타입 안전성 — 생태계 맵 시각화 (출처: 공식 문서 및 벤치마크 데이터 기반)
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

관련 포스트

ElysiaJS 실전 튜토리얼 — Bun 기반 TypeScript API 서버, JWT 인증, Swagger 문서화, Docker 배포2026-04-25Temporal.io 워크플로우 튜토리얼 — TypeScript로 분산 작업 큐와 장기 실행 프로세스 구현2026-04-19오픈소스 기여 시작 가이드 — 첫 PR까지2026-02-262026년 주목할 오픈소스 프로젝트 10선2026-03-09