한 줄 정의
z.compile()은 스키마를 미리 컴파일해, new Function()으로 만든 평평하고 루프 없는 JS 검증 함수로 바꾸는 AOT(ahead-of-time) 최적화 API다.
컴파일된 스키마는 기존 스키마와 똑같이 .parse() / .safeParse()를 쓴다. 인터페이스는 그대로고, 내부 실행 경로만 빨라진다.
작동 원리
컴파일은 스키마의 구조를 읽어, typeof 검사와 프로퍼티 읽기만으로 이루어진 직선형 코드를 new Function()으로 생성한다. 분기도 루프도 없이 "JavaScript가 표현할 수 있는 가장 빠른 로직"으로 짜인 이 코드가 파싱의 핫패스가 된다.
이 컴파일된 경로는 통과/실패 여부만 빠르게 가른다. 입력이 막히면 컴파일된 함수는 INVALID 심벌을 돌려주고, 그 즉시 기존 런타임 파서로 폴백한다 — 필드별 issues 같은 세밀한 에러 정보는 느리지만 정확한 원래 경로에서 만든다. 그래서 결과와 에러는 컴파일 여부와 무관하게 항상 동일하다.
모든 스키마가 컴파일되는 건 아니다. 비동기 refinement, z.xor(), 재귀 스키마, z.coerce.*, 커스텀 조건부 체크는 컴파일 대상에서 빠지고 일반 경로로 돈다. new Function()을 쓰는 만큼 CSP가 unsafe-eval을 막는 환경에서는 z.config({ jitless: true })로 컴파일 자체를 꺼야 한다.
쓰는 법
직접 컴파일 — 이미 만든 스키마를 z.compile()에 넘기면 컴파일된 버전을 돌려준다. 원본 스키마는 그대로 두고 컴파일된 것만 핫패스에 쓴다.
import { z } from 'zod'
const Player = z.object({
username: z.string(),
bio: z.string(),
xp: z.number(),
})
const CompiledPlayer = z.compile(Player)
CompiledPlayer.parse({ username: 'a', bio: 'b', xp: 1 })
전역 모드 — zod/compile을 임포트하면, 그 뒤로 만드는 모든 스키마가 첫 파싱 시점에 자동으로 컴파일된다. 스키마마다 z.compile()을 일일이 부르지 않아도 된다.
import 'zod/compile'
import * as z from 'zod'
실전 예제
요청마다 도는 API 핸들러처럼, 같은 스키마로 수천 번 반복 파싱하는 자리가 컴파일의 표적이다. 모듈 스코프에서 한 번 컴파일해 재사용한다.
import { z } from 'zod'
const Point = z.object({
x: z.number(),
y: z.number(),
})
// 모듈 로드 시 한 번 컴파일 — 요청마다 다시 만들지 않는다
export const CompiledPoint = z.compile(Point)
import { CompiledPoint } from './schema'
export function handlePing(body: unknown) {
const r = CompiledPoint.safeParse(body)
if (!r.success) return { ok: false, issues: r.error.issues }
return { ok: true, point: r.data } // { x: number; y: number }
}
쓰는 이유
파싱은 대개 무시할 만큼 싸지만, 요청량이 큰 API 경계나 대량의 배열/유니온을 반복 검증하는 자리에서는 그 자체가 병목이 된다. z.compile()은 스키마 정의나 타입 추론 방식을 하나도 바꾸지 않고 실행 속도만 끌어올린다는 점이 핵심이다 — 20키 객체 기준 약 8배, 유니온 기준 약 5배 같은 수치가 이 트레이드오프 없는 성격에서 나온다. 다만 컴파일 대상에 못 들어가는 스키마 형태(재귀·coerce·비동기 refinement)가 존재하고, new Function()을 쓰는 만큼 엄격한 CSP 환경에서는 jitless 설정으로 끄는 선택지가 따로 있다는 것까지 알아야 이 기능을 제대로 켜고 끌 수 있다.
Reference