본문 바로가기
카테고리 없음

Zod 스키마란? — TypeScript에서 데이터 검증까지 해결하기

by jungsunbeen 2026. 4. 1.

Zod 스키마란? — TypeScript에서 데이터 검증까지 해결하기

TypeScript를 사용하면서 "타입을 다 정의했는데 왜 런타임 오류가 나지?" 라는 경험, 한 번쯤 해봤을 것이다. 이 글은 그 문제를 해결해주는 Zod가 왜 생겨났고, 어떻게 쓰는지를 차근차근 살펴본다.


Zod가 필요할까?

TypeScript를 쓰는 이유 중 하나는 타입 안정성이다. 아래처럼 타입을 정의해두면 실수로 잘못된 값을 넣을 때 IDE가 바로 경고해준다.

type User = {
  name: string;
  age: number;
};

그런데 문제가 있다. TypeScript의 타입 체크는 컴파일 시점에만 작동한다.

예를 들어 API 응답으로 받아온 데이터가 있다고 해보자.

const data = await fetch("/api/user").then(res => res.json());

이 data는 TypeScript 입장에서 any나 다름없다. 실제로 서버가 어떤 값을 내려보내는지, TypeScript는 알 수 없다. 아래처럼 age가 숫자가 아닌 문자열로 내려와도 TypeScript는 아무 경고도 하지 않는다.

// 서버 응답
{
  "name": "john",
  "age": "20"  // ← string인데 아무도 막지 못함
}

TypeScript는 개발할 때의 도구이고, 실제 실행 중에는 어떠한 검증도 하지 않는다. 이 간극을 메워주는 게 바로 Zod다.


Zod란?

Zod는 한 문장으로 정리하면 이렇다.

데이터 구조를 정의하고, 런타임에서도 실제로 검증해주는 라이브러리

TypeScript의 타입은 컴파일 후 사라지지만, Zod의 스키마는 코드가 실행되는 순간에도 살아서 동작한다. 덕분에 "이 데이터가 내가 기대한 형태인지"를 실제 실행 시점에도 확인할 수 있다.

설치는 간단하다.

npm install zod

기본 사용법

1. 스키마 정의

import { z } from "zod";

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

TypeScript의 type을 정의하는 것과 비슷해 보이지만, 차이가 있다. 이 코드는 런타임에도 살아있는 객체다.

2. 데이터 검증

const data = { name: "john", age: 20 };

const result = UserSchema.parse(data);
// 통과하면 result는 { name: "john", age: 20 }

만약 데이터가 스키마와 맞지 않으면 Zod는 즉시 에러를 던진다.

const bad = { name: "john", age: "20" };

UserSchema.parse(bad);
// ZodError: age는 number여야 합니다.

에러를 던지는 대신 결과값으로 받고 싶다면 safeParse를 쓰면 된다.

const result = UserSchema.safeParse(bad);

if (!result.success) {
  console.log(result.error); // 에러 정보
} else {
  console.log(result.data);  // 검증된 데이터
}

safeParse는 try-catch 없이도 에러를 핸들링할 수 있어서 실무에서 훨씬 자주 쓰인다.

3. 타입 자동 생성

Zod의 가장 편리한 기능 중 하나다. 스키마에서 TypeScript 타입을 자동으로 뽑아낼 수 있다.

type User = z.infer<typeof UserSchema>;
// { name: string; age: number }

더 이상 type User와 UserSchema를 따로 관리하지 않아도 된다. 스키마 하나만 수정하면 타입도 자동으로 따라온다.


자주 쓰는 유효성 검사 옵션

Zod는 단순한 타입 체크 외에도 다양한 조건을 추가할 수 있다.

const SignupSchema = z.object({
  email: z.string().email(),           // 이메일 형식
  password: z.string().min(8),         // 최소 8자
  age: z.number().min(0).max(120),     // 0 ~ 120
  nickname: z.string().optional(),     // 있어도 되고 없어도 됨
});

API 요청 바디를 검증할 때 이런 식으로 쓰면 별도의 유효성 검사 로직 없이도 깔끔하게 처리할 수 있다.


실제로는 어디에 쓰나?

가장 많이 쓰이는 상황은 세 가지다.

첫째, API 응답 검증이다. 서버에서 받은 데이터가 예상한 형태인지 확인할 때 쓴다.

const response = await fetch("/api/user").then(res => res.json());
const user = UserSchema.parse(response);

둘째, 폼 입력 검증이다. React Hook Form 같은 라이브러리와 Zod를 함께 쓰면 폼 유효성 검사와 타입 추론을 한 번에 해결할 수 있다.

셋째, 환경 변수 검증이다. .env 파일의 값이 제대로 설정됐는지 앱 시작 시점에 확인하는 용도로도 많이 활용한다.

const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.string().transform(Number),
});

const env = EnvSchema.parse(process.env);

정리

TypeScript의 타입은 개발 중에만 작동하고, 런타임에서는 아무 보장도 해주지 않는다. Zod는 이 공백을 채워주는 도구다.

스키마를 한 번 정의하면 타입 추론, 런타임 검증, 유효성 검사까지 전부 따라온다. 코드가 줄어드는 건 덤이다.

처음엔 "그냥 TypeScript로 충분하지 않나?" 싶을 수 있는데, 외부 데이터를 한 번이라도 다뤄보면 Zod의 필요성을 바로 느끼게 된다.