Back

Zod 4 마이그레이션 완전 가이드: 달라진 점, 성능 개선, 새 기능 총정리

최근 3년간 TypeScript 쓰면서 Zod 안 써본 사람 있나요? API 라우트, 폼 밸리데이션, tRPC 엔드포인트, 환경변수 파싱까지. Zod는 말 그대로 어디에나 있죠. 그런데 Zod 4가 나오면서 거의 모든 게 바뀌어 버렸어요.

숫자부터 볼게요. 문자열 파싱 14배, 배열 파싱 7배, 코어 번들 2.3배 축소, TypeScript 컴파일 속도 최대 10배 향상. 근데 이 성능 향상의 대가로 breaking changes가 한 트럭이에요. 버전만 올리면 CI가 빨간불로 뒤덮여요.

이 글에서는 달라진 점 하나하나 다 짚어볼게요. before/after 코드 전부 보여드리고, 팀 전체가 일주일간 스키마 에러 디버깅하는 상황을 피하는 마이그레이션 전략까지 정리했어요.

왜 이렇게까지 바뀐 걸까

Zod 3은 TypeScript 타입 시스템이 아직 덜 성숙하고, 서버사이드 밸리데이션 라이브러리의 번들 사이즈가 크게 문제되지 않던 시절에 설계된 거예요. 그런데 생태계가 진화하면서 상황이 달라졌죠. 콜드 스타트가 중요한 서버리스, 사이즈 제한이 빡빡한 Edge 런타임, 스키마 수천 개짜리 모노레포… 이런 환경에서 Zod 3의 아키텍처가 한계를 드러내기 시작한 거예요.

Zod 4는 세 가지 근본적인 문제를 해결하려고 처음부터 다시 만들었어요:

  1. 번들 사이즈: Zod 3의 메서드 체이닝 API는 트리쉐이킹이 사실상 불가능했어요. import 하나가 라이브러리 전체를 끌고 왔죠.
  2. 파싱 성능: 밸리데이션 파이프라인에 불필요한 객체 생성과 프로토타입 체인 룩업 오버헤드가 있었어요.
  3. TypeScript 컴파일 속도: 복잡한 Zod 스키마가 엄청난 양의 타입 인스턴스를 만들어서 큰 코드베이스에서 tsc가 기어 다녔어요.

근본적으로 해결하려면 API를 깨는 수밖에 없었죠. 하나씩 까볼게요.

Breaking Change #1: 에러 파라미터 통합

이게 제일 많은 코드를 깨뜨릴 변경이에요. Zod 3에서는 에러 메시지를 커스터마이징하는 방법이 세 가지나 있었거든요:

// ❌ Zod 3 — 파라미터가 세 종류 const schema = z.string({ required_error: "이름은 필수예요", invalid_type_error: "이름은 문자열이어야 해요", }); const email = z.string().email({ message: "이메일 형식이 올바르지 않아요" }); const age = z.number({ errorMap: (issue, ctx) => { if (issue.code === "too_small") return { message: "18세 이상이어야 해요" }; return { message: ctx.defaultError }; }, });

Zod 4에서는 error 하나로 전부 통합됐어요:

// ✅ Zod 4 — 통합된 error 파라미터 const schema = z.string({ error: "이름은 필수예요", // 단순 문자열 }); const email = z.string().email({ error: "이메일 형식이 올바르지 않아요", // 어디서나 같은 패턴 }); const age = z.number({ error: (issue) => { // 복잡한 로직은 함수 형태 if (issue.code === "too_small") return "18세 이상이어야 해요"; return "나이가 올바르지 않아요"; }, });

message 속성은 이제 모든 메서드에서 deprecated 됐어요. required_error, invalid_type_error, errorMap은 완전히 삭제. 전부 error 파라미터 하나로 통일하면 돼요. 공식 코드모드가 대부분 자동으로 처리해주긴 해요:

npx @zod/codemod --transform v3-to-v4 ./src

다만 커스텀 errorMap은 직접 확인해야 해요. 함수 시그니처가 (issue, ctx) => { message: string }에서 (issue) => string으로 바뀌었거든요.

Breaking Change #2: 탑레벨 포맷 밸리데이터

Zod 3에서는 문자열 포맷 검증을 메서드 체이닝으로 했어요. Zod 4에서는 자주 쓰는 것들이 탑레벨 함수로 승격됐어요:

// ❌ Zod 3 — 메서드 체이닝 const emailSchema = z.string().email(); const uuidSchema = z.string().uuid(); const urlSchema = z.string().url(); // ✅ Zod 4 — 탑레벨 함수 const emailSchema = z.email(); const uuidSchema = z.uuid(); const urlSchema = z.url();

왜 이렇게 바꿨냐면요, z.string().email()은 이메일 검증만 필요해도 ZodString 클래스 전체를 끌고 와요. 탑레벨 함수 방식이면 번들러가 안 쓰는 코드를 제대로 떨궈낼 수 있거든요.

중요: 메서드 체이닝 방식(z.string().email())은 Zod 4에서 아직 동작하지만 공식적으로 deprecated 됐어요. 당장 삭제되진 않으니 점진적으로 바꿔도 되지만, 향후 메이저 버전에서 제거될 거예요.

참고로 z.string().ip()z.string().cidr()완전히 삭제됐어요. 각각 z.ipv4(), z.ipv6(), z.cidrv4(), z.cidrv6()로 대체해야 해요. z.uuid()도 RFC 9562/4122 variant bits까지 검증하는 더 엄격한 방식으로 바뀌었고, 느슨한 패턴이 필요하면 새로 추가된 z.guid()를 쓰면 돼요.

Breaking Change #3: Coercion 입력 타입이 unknown으로 변경

z.coerce 네임스페이스는 Zod 4에서 그대로 있어요. 달라진 건 모든 coerced 스키마의 입력 타입unknown으로 바뀐 거예요:

const schema = z.coerce.string(); type SchemaInput = z.input<typeof schema>; // Zod 3: string // Zod 4: unknown

coercion이 실제로 하는 일을 생각해보면 당연한 변경이죠. 아무거나 받아서 변환을 시도하니까요. 다만 TypeScript가 입력 타입을 더 이상 좁혀주지 않으니까, 그 동작에 의존하던 코드에서 타입 에러가 터질 수 있어요.

z.coerce.number(), z.coerce.string() 같은 API 자체는 이전과 동일하게 동작해요. 바뀐 건 추론되는 입력 타입뿐이에요.

Breaking Change #4: Optional + Default 동작 변경

이게 은근히 위험한 변경이에요. Zod 3에서 .default() 또는 .catch()가 있는 스키마에 .optional()을 붙이면 빈 속성을 무시했는데, Zod 4에서는 기본값이 항상 적용되거든요:

const schema = z.object({ theme: z.string().default("light").optional(), }); // Zod 3: { theme: undefined } → { theme: undefined } ← 빈 속성 무시 // Zod 4: { theme: undefined } → { theme: "light" } ← 기본값 적용

동작이 예측 가능해지긴 했지만, undefined 여부로 "미제공"을 구분하던 코드는 터져요.

.default() 관련 변경이 하나 더 있어요. 기본값이 이제 출력 타입과 일치해야 해요. Zod 3에서는 기본값이 입력 타입과 매치되고 스키마를 통해 파싱됐는데, Zod 4에서는 파싱을 건너뛰고 기본값을 바로 반환해요:

// Zod 3: 기본값이 입력 타입과 매치, 파싱됨 const schema = z.string() .transform(val => val.length) .default("tuna"); // string 입력 → 파싱 → 4 schema.parse(undefined); // => 4 // Zod 4: 기본값이 출력 타입과 매치, 바로 반환 const schema = z.string() .transform(val => val.length) .default(0); // number 출력, 바로 반환 schema.parse(undefined); // => 0

예전 동작("프리파스 기본값")을 쓰려면 새로 추가된 .prefault()를 사용하면 돼요:

// ✅ Zod 4: .prefault()로 이전 .default() 동작 재현 const schema = z.string() .transform(val => val.length) .prefault("tuna"); // string이 transform을 거쳐 파싱됨 schema.parse(undefined); // => 4

Breaking Change #5: TypeScript Strict 모드 필수

Zod 4는 tsconfig.jsonstrict: true가 있어야 해요. Strict 모드 없이 사용하면 타입 에러가 나요:

{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext" } }

TypeScript 5.5 이상이 필요해요. 구버전 TS라면 TypeScript부터 업그레이드하세요.

새 기능: @zod/mini

Edge 런타임이나 서버리스 환경에서 번들 사이즈가 중요하다면 이게 핵이에요. Zod의 코어 밸리데이션을 훨씬 작은 사이즈로 제공하거든요:

import { z } from "@zod/mini"; const UserSchema = z.object({ name: z.string().check(z.minLength(1)), email: z.string().check(z.email()), role: z.enum(["admin", "user", "viewer"]), });
기능zod@zod/mini
코어 번들~13KB gzip~5.5KB gzip
.transform()
.pipe()
JSON Schema 생성

밸리데이션만 하고 변환은 안 하는 API 라우트 핸들러라면, @zod/mini 쓰는 게 당연히 이득이죠.

새 기능: 빌트인 JSON Schema 변환

더 이상 zod-to-json-schema 설치 안 해도 돼요. Zod 4에서 네이티브 JSON Schema 생성을 지원해요:

const UserSchema = z.object({ id: z.number().int().positive(), name: z.string().min(1).max(255), email: z.email(), role: z.enum(["admin", "editor", "viewer"]), }); const jsonSchema = UserSchema.toJSONSchema();

역방향도 돼요:

const schema = z.fromJSONSchema({ type: "object", properties: { name: { type: "string" }, age: { type: "integer", minimum: 0 }, }, required: ["name"], });

OpenAPI 스펙, JSON Schema 기반 폼 생성기, AI Tool 정의(MCP, function calling)에서 Zod↔JSON Schema 변환이 필요할 때 완전 편해져요.

새 기능: 스키마 메타데이터

스키마에 강타입 메타데이터를 붙일 수 있게 됐어요:

const NameSchema = z.string().min(1).max(100).meta({ label: "이름", placeholder: "홍길동", helpText: "실명을 입력하세요.", }); const meta = NameSchema.meta(); // → { label: "이름", placeholder: "홍길동", ... }

스키마 기반으로 폼을 자동 생성하는 패턴을 만들 수 있어요. 메타데이터는 .optional(), .array(), .transform() 같은 스키마 연산 후에도 보존돼요.

새 기능: 다국어 에러

밸리데이션 에러 메시지를 로케일별로 자동 번역하는 시스템이 생겼어요:

import { z } from "zod"; import { ko } from "@zod/locales/ko"; z.config({ locale: ko }); const result = z.string().min(5).safeParse("안녕"); // result.error.issues[0].message → "5자 이상이어야 합니다"

다국어 지원 때문에 모든 스키마에 커스텀 에러맵 감싸던 시절은 끝났어요.

새 기능: 템플릿 리터럴 타입

Zod 4에서 z.templateLiteral()이 추가됐어요. 특정 패턴을 따르는 문자열을 검증할 수 있어요:

const hexColor = z.templateLiteral([ z.literal("#"), z.string().regex(/^[0-9a-fA-F]{6}$/), ]); hexColor.parse("#ff00aa"); // ✅ hexColor.parse("red"); // ❌ type HexColor = z.infer<typeof hexColor>; // => `#${string}`

CSS 값, 시맨틱 버전 문자열, API 엔드포인트 패턴 같은 구조화된 문자열 포맷을 TypeScript 타입 추론까지 포함해서 검증할 수 있어요.

성능 벤치마크

Zod 4의 성능 향상은 점진적 개선이 아니라 수준이 다른 변화예요:

항목Zod 3Zod 4개선 폭
문자열 파싱1.0x14배1,300%
배열 파싱1.0x7배600%
객체 파싱1.0x6.5배550%
번들 사이즈~31KB gzip~13KB gzip2.3배 축소
TS 타입 인스턴스1.0x최대 10배 감소900%

TypeScript 컴파일 속도 개선이 특히 체감이 커요. 스키마 수백 개짜리 모노레포에서 tsc47초에서 5초로 줄었다는 벤치마크도 있어요.

마이그레이션 전략: 안전하게 가는 법

한 번에 다 바꾸지 마세요. 단계별로 가는 게 좋아요.

1단계: 사전 준비

  1. TypeScript strict 모드 켜져 있는지 확인
  2. 코드모드 dry-run 돌려보기:
npx @zod/codemod --transform v3-to-v4 --dry-run ./src
  1. 코드베이스에서 errorMap 검색해서 수동 마이그레이션 필요한 곳 파악
  2. .optional().default() 패턴 확인 (v4에서 동작 다름)
  3. TypeScript 5.5+ 인지 확인

2단계: 코드모드 실행

npx @zod/codemod --transform v3-to-v4 ./src git diff --stat

3단계: 수동 수정

# 남아있는 errorMap 찾기 grep -rn "errorMap" --include="*.ts" --include="*.tsx" ./src # z.coerce 패턴 찾기 grep -rn "z\.coerce\." --include="*.ts" --include="*.tsx" ./src # .optional().default() 체인 찾기 grep -rn "\.optional()\.default\|\.default(.*).optional" --include="*.ts" ./src

4단계: 테스트

  1. 에러 메시지 포맷 변경: 특정 에러 메시지를 assert하는 테스트가 깨짐
  2. Coercion 입력 타입 변경: z.coerce.* 입력 타입이 unknown으로 바뀌면서 새로운 타입 에러가 생길 수 있음
  3. Optional+Default: 기본값이 있는 옵셔널 필드의 undefined 체크가 달라짐

5단계: 새 기능 점진 도입

  • zod-to-json-schema.toJSONSchema()로 교체
  • z.registry()로 스키마 메타데이터 관리
  • 밸리데이션 전용 스키마 @zod/mini로 전환
  • z.templateLiteral()로 구조화된 문자열 검증

흔한 함정들

함정 1: Peer dependency 충돌

Zod 3에 의존하는 라이브러리(tRPC 구버전, react-hook-form 어댑터 등)가 Zod 4를 peer dependency로 안 받아줄 수 있어요. 업그레이드 전에 호환성 확인하세요:

npm ls zod

2026년 3월 기준 대부분의 주요 라이브러리가 Zod 4를 지원해요: tRPC v11+, @tanstack/react-form, react-hook-form v8 + @hookform/resolvers v4+.

함정 2: z.input / z.output 타입 변경

z.infer<typeof schema>는 달라진 게 없어요. 하지만 z.input이나 z.output을 쓰고 있었다면 default와 transform 관련 타입이 달라질 수 있어요.

함정 3: Discriminated union 에러 구조 변경

z.discriminatedUnion()은 아직 동작하지만 내부적으로 최적화됐어요. discriminated union 실패 시 에러 구조(issue code 등)에 의존하는 코드가 있다면 테스트가 필요해요.

함정 4: .passthrough(), .strict(), .strip()이 deprecated

객체 스키마는 여전히 기본적으로 알 수 없는 키를 strip해요. 하지만 .passthrough(), .strict(), .strip() 전부 deprecated 됐어요. Zod 4에서는 .catchall()로 미지의 키 처리를 명시적으로 하라는 거예요:

// ❌ Zod 4에서 deprecated const loose = z.object({ name: z.string() }).passthrough(); const strict = z.object({ name: z.string() }).strict(); // ✅ Zod 4 권장 const loose = z.object({ name: z.string() }).catchall(z.unknown()); const strict = z.object({ name: z.string() }).catchall(z.never());

이 메서드들을 많이 쓰고 있다면 deprecation 경고가 뜰 거예요. 마이그레이션 계획을 세워두세요.

지금 업그레이드해야 할까

지금 하세요:

  • 새 프로젝트 시작할 때 (Zod 4로 바로 시작)
  • Zod 스키마 때문에 빌드 타임이 느려진 경우
  • Edge/서버리스에 배포하고 번들을 줄여야 하는 경우
  • JSON Schema 연동이 필요한 경우

잠깐 기다려도 돼요:

  • 핵심 의존성이 아직 Zod 3만 지원하는 경우
  • 커스텀 errorMap 함수가 수백 개인 경우
  • 마이그레이션에 투자할 여력이 없는 경우

마무리

Zod 4는 TypeScript 밸리데이션 역사상 가장 임팩트 있는 업데이트예요. Breaking changes는 많지만 하나하나 이유가 있어요. 전부 Zod를 더 빠르고, 더 작고, 더 TypeScript 네이티브하게 만들기 위한 거예요.

마이그레이션 경로는 깔끔해요: 코드모드 돌리고, 수동 수정하고, 테스트하고, 배포. 대부분의 프로젝트가 하루면 마이그레이션 끝나요. v4에 올라가면 @zod/mini, 네이티브 JSON Schema, 메타데이터, 템플릿 리터럴 타입, 다국어 에러까지 서드파티 없이 전부 쓸 수 있어요.

npx @zod/codemod --transform v3-to-v4 --dry-run ./src 부터 돌려보세요. 얼마나 잡아주는지 확인하고, 나머지는 grep 돌려서 수정하면 돼요.

ZodTypeScriptvalidationmigrationschemaJavaScriptNode.jsdeveloper tools

관련 도구 둘러보기

Pockit의 무료 개발자 도구를 사용해 보세요