TypeScript의 타입 시스템은 컴파일 타임에 오류를 잡지만, API 응답은 런타임에 도착합니다. Zod는 그 간극을 메웁니다. 런타임 데이터의 형태와 타입을 검증하고, 같은 스키마에서 TypeScript 타입을 추론합니다. 복잡한 JSON 구조의 Zod 스키마를 손으로 작성하는 건 오래 걸립니다. JSON to Zod 생성기는 임의의 JSON 샘플에서 완전한 편집 가능한 스키마를 몇 초 만에 만들어 줍니다.

Zod란

Zod는 TypeScript 우선 스키마 선언·검증 라이브러리입니다. any를 반환하는 JSON.parse()와 달리, Zod는:

  • 데이터가 예상되는 형태에 맞는지 검증
  • 스키마에서 TypeScript 타입을 추론(타입 정의 중복 없음)
  • 상세하고 구조화된 오류 메시지 생성
  • Node.js와 브라우저 모두에서 실행
import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest'])
})

type User = z.infer<typeof UserSchema>
// 동등: { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest' }

const result = UserSchema.safeParse(apiResponse)
if (result.success) {
  console.log(result.data.name) // string으로 타입 지정됨
} else {
  console.error(result.error.issues)
}

JSON to Zod: 변환 동작 방식

다음 JSON 샘플을 입력하면:

{
  "id": 42,
  "name": "Alice",
  "email": "[email protected]",
  "age": 28,
  "active": true,
  "tags": ["admin", "beta"],
  "address": {
    "street": "123 Main St",
    "city": "Portland",
    "zip": "97201"
  },
  "metadata": null
}

생성기는 다음을 만들어냅니다:

import { z } from 'zod'

const Schema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
  age: z.number(),
  active: z.boolean(),
  tags: z.array(z.string()),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string()
  }),
  metadata: z.null()
})

export type Schema = z.infer<typeof Schema>

이 스키마는 출발점입니다. 실제 비즈니스 규칙에 맞게 개선하세요(아래 섹션 참조).

Zod 검증을 사용하는 곳

API 응답 검증

// Zod 없이 — any 타입, 런타임 보장 없음
const user = (await fetch('/api/user').then(r => r.json())) as User

// Zod로 — 검증됨 + 타입 지정됨
const UserSchema = z.object({ id: z.number(), name: z.string() })

async function fetchUser(id: number) {
  const raw = await fetch(`/api/users/${id}`).then(r => r.json())
  return UserSchema.parse(raw) // 형태가 맞지 않으면 ZodError 발생
}

tRPC와 Next.js API 라우트

// tRPC 프로시저 입력 검증
import { z } from 'zod'
import { publicProcedure } from '../trpc'

const createUserInput = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user')
})

export const createUser = publicProcedure
  .input(createUserInput)
  .mutation(async ({ input }) => {
    // input은 { name: string; email: string; role: 'admin' | 'user' }로 타입 지정됨
    return db.users.create(input)
  })

React Hook Form으로 폼 검증

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const formSchema = z.object({
  username: z.string().min(3, '3자 이상 필요'),
  password: z.string().min(8, '8자 이상 필요'),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: "비밀번호가 일치하지 않습니다",
  path: ['confirmPassword']
})

type FormData = z.infer<typeof formSchema>

function SignupForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(formSchema)
  })
  // ...
}

환경 변수 검증

import { z } from 'zod'

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().int().positive().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  API_SECRET: z.string().min(32)
})

export const env = envSchema.parse(process.env)
// 필수 환경 변수가 누락되거나 잘못된 경우 시작 시 명확한 오류로 충돌

생성된 스키마 개선

생성기는 JSON 값에서 타입을 추론하지만, JSON으로는 모든 Zod 제약을 표현할 수 없습니다. 다음은 수동으로 추가하세요:

문자열 형식

// 생성된 것
email: z.string()

// 개선된 것
email: z.string().email()
url: z.string().url()
uuid: z.string().uuid()
isoDate: z.string().datetime()

숫자 제약

// 생성된 것
age: z.number()

// 개선된 것
age: z.number().int().min(0).max(150)
price: z.number().positive()
quantity: z.number().int().nonnegative()

선택적과 필수 필드

JSON 샘플은 존재하는 값만 보여줍니다. 필드가 없을 수 있는 경우:

// 생성된 것(샘플에 존재하는 필드)
nickname: z.string()

// 개선된 것
nickname: z.string().optional()        // undefined 허용
nickname: z.string().nullable()        // null 허용
nickname: z.string().nullish()         // null 또는 undefined 허용

유니온과 열거형

필드가 여러 타입을 가질 수 있는 경우:

// 샘플에서 status는 "active"
status: z.string()

// 모든 가능한 값을 확인한 후 개선
status: z.enum(['active', 'inactive', 'pending', 'archived'])

// 또는 다른 형태의 유니온
result: z.union([
  z.object({ success: z.literal(true), data: DataSchema }),
  z.object({ success: z.literal(false), error: z.string() })
])

혼합 타입 배열

// [1, "hello", true] 같은 혼합 배열
items: z.array(z.union([z.number(), z.string(), z.boolean()]))

JSON 샘플의 null 값 처리

JSON 필드가 null이면 생성기는 z.null()을 만들어냅니다. 실제로 null은 항상 null이 아니라 필드가 nullable임을 의미하는 경우가 많습니다:

// 생성된 것
metadata: z.null()

// 더 실용적
metadata: z.string().nullable()        // string | null
metadata: z.record(z.unknown()).nullable()  // object | null

parse vs safeParse

Zod는 다른 오류 처리를 가진 두 가지 파싱 메서드를 제공합니다:

메서드실패 시반환값
.parse(data)ZodError 발생검증된 데이터
.safeParse(data){ success: false, error } 반환{ success, data | error }

시작 시(환경 변수·설정)에는 .parse()를 사용하고 실패하면 충돌하도록 합니다. API 응답과 사용자 입력에는 오류를 우아하게 처리하고 싶은 .safeParse()를 사용합니다.

// parse — 실패 시 발생
const config = ConfigSchema.parse(process.env)

// safeParse — 오류 처리
const result = UserSchema.safeParse(apiResponse)
if (!result.success) {
  // result.error.issues에 구조화된 오류 세부 정보 포함
  return { error: result.error.format() }
}
// result.data는 완전히 타입 지정됨
return result.data

도구 사용하기

JSON에서 Zod 스키마를 즉시 생성하기 →

API 응답·Postman 컬렉션·Swagger 예제 등 임의의 JSON 객체를 붙여 넣으면 동작하는 Zod 스키마가 생성됩니다. 이것을 출발점으로 삼아 도메인별 제약을 추가하세요.

중첩된 객체·배열·null 값·혼합 타입 배열을 처리합니다. 출력에는 z.infer 타입 export가 포함되어 있어 별도의 인터페이스를 작성하지 않고도 TypeScript 타입을 얻을 수 있습니다.