본문으로 건너뛰기

AOT 컴파일

Zod는 스키마를 사전에 컴파일해 표준 파서보다 몇 배 빠르게 실행되는 평탄하고 반복문이 없는 검증기로 만들 수 있습니다. 결과와 오류는 동일합니다.

import * as z from "zod";

const Player = z.object({
username: z.string(),
bio: z.string(),
xp: z.number(),
// ...20 more properties...
});

const CompiledPlayer = z.compile(Player);

Player와 똑같이 사용하세요.

Player.parse({ ... });
CompiledPlayer.parse({ ... }); // ~9x faster

CompiledPlayer와 같은 컴파일된 스키마도 다른 스키마와 마찬가지로 Zod 스키마입니다. 컴파일된 스키마에만 적용되는 특별한 규칙은 없습니다.

  • 같은 메서드: .parse(), .safeParse(), .extend(), .optional()
  • 동일하게 추론되는 입력 및 출력 타입
  • 같은 이슈와 오류 메시지

완벽한 동작 일치를 보장하기 위해 Zod의 전체 테스트 스위트는 일반 모드와 전역 자동 컴파일 활성화 모드에서 각각 한 번씩 실행됩니다.

객체와 튜플 같은 컨테이너가 가장 큰 이점을 얻습니다. 컴파일이 런타임의 키별 순회를 JS 엔진에서 최적화할 수 있는 평탄하고 반복문이 없는 검증 로직으로 바꾸기 때문입니다.

공통 나노초 축에서 파싱당 시간을 비교한 차트. 표준 파서는 회색 막대, 컴파일된 시간은 그 안의 파란 막대입니다. 객체 10개의 배열은 377ns에서 68ns(5.5배), 키 20개의 객체는 301ns에서 38ns(7.8배), 문자열 10개의 배열은 241ns에서 33ns(7.3배), 객체 3개의 유니온은 190ns에서 36ns(5.3배), 요소 3개의 튜플은 119ns에서 33ns(3.6배), 키 5개의 엄격한 객체는 117ns에서 32ns(3.7배), 판별 유니온은 92ns에서 27ns(3.4배), 키 5개의 객체는 76ns에서 28ns(2.8배)이며 컴파일 시 최대 7.8배 빠릅니다공통 나노초 축에서 파싱당 시간을 비교한 차트. 표준 파서는 회색 막대, 컴파일된 시간은 그 안의 파란 막대입니다. 객체 10개의 배열은 377ns에서 68ns(5.5배), 키 20개의 객체는 301ns에서 38ns(7.8배), 문자열 10개의 배열은 241ns에서 33ns(7.3배), 객체 3개의 유니온은 190ns에서 36ns(5.3배), 요소 3개의 튜플은 119ns에서 33ns(3.6배), 키 5개의 엄격한 객체는 117ns에서 32ns(3.7배), 판별 유니온은 92ns에서 27ns(3.4배), 키 5개의 객체는 76ns에서 28ns(2.8배)이며 컴파일 시 최대 7.8배 빠릅니다
파싱당 시간, 표준 파서와 컴파일된 파서 비교 — 낮을수록 좋음(벤치마크)

컴파일을 활성화하는 방법은 두 가지입니다.

z.compile()

단일 스키마를 컴파일하고 컴파일된 복사본을 반환합니다. 원본 스키마는 변경되지 않습니다.

스키마를 파생하는 메서드(.refine(), .extend(), .optional(), .meta() 등)는 컴파일되지 않은 스키마를 반환합니다. 중간 스키마가 아닌 최종 스키마를 컴파일하세요.

// ❌ the .refine() result is not compiled
const schema = z.compile(z.string()).refine((val) => val.length > 1);

// ✅ compile last
const schema2 = z.compile(z.string().refine((val) => val.length > 1));

import "zod/compile"

컴파일을 전역으로 활성화합니다. 이 가져오기 이후에 생성한 모든 스키마는 처음 파싱할 때 자동으로 컴파일됩니다.

import "zod/compile"; // must come before modules that define schemas
import * as z from "zod";

const schema = z.object({ name: z.string() });
schema.parse({ name: "ok" }); // compiled on first parse

컴파일은 지연 방식이므로 실제로 파싱에 사용하는 스키마만 컴파일됩니다.

Node.js CLI 플래그로도 사용할 수 있으며, 이를 통해 어떤 모듈이 스키마를 정의하기 전에 반드시 실행되도록 할 수 있습니다.

node --import zod/compile app.js   # ESM
node --require zod/compile app.cjs # CommonJS

또는 preloadbunfig.toml이나 nub.jsonc에 설정하세요.

nub.jsonc
{
"preload": ["zod/compile"]
}

이 가져오기는 라이브러리가 아니라 애플리케이션을 위한 것입니다.

작동 원리

내부적으로 z.compile()은 전체 스키마를 한 번 순회하고, 표준 런타임 검증기보다 훨씬 빠르게 입력을 검증할 수 있도록 고도로 최적화된 평탄하고 반복문이 없는 JavaScript 코드를 생성합니다. 이 코드는 new Function()을 통해 실행되며, 이는 사실상 더 강력한 eval입니다. 이렇게 실행된 코드는 빠른 경로 검증기로 동작합니다. 스키마는 이를 사용해 유효성을 "빠르게 검사"하고, 검증에 실패하면 상세한 오류 정보를 제공하기 위해 일반 런타임 로직으로 돌아갑니다.

../snippets/how-z-compile-works.mdx

입력이 유효하지 않으면 대체 경로에서 컴파일되지 않은 스키마를 실행하므로, 오류도 컴파일되지 않은 스키마의 오류입니다. 이에 따른 결과는 두 가지입니다.

  • 유효하지 않은 입력에서는 빠른 경로와 대체 경로가 모두 실행되므로 컴파일해도 실패 처리가 빨라지지 않습니다.
  • 세부 검증과 변환은 유효한 입력에서 한 번, 유효하지 않은 입력에서는 최대 두 번 실행됩니다.

지원하지 않는 스키마

일부 기능은 컴파일할 수 없거나 컴파일의 이점을 얻지 못합니다. 이런 경우 z.compile()은 컴파일을 중단하고 원본 스키마를 변경 없이 반환합니다.

const Schema = z.string().refine(async (val) => isAvailable(val));

z.compile(Schema); // returns Schema itself, uncompiled
  • async 세부 검증, 변환 및 검사
  • z.xor()
  • 재귀 스키마
  • z.coerce.*
  • 사용자 지정 when이 있는 검사
  • 콜백을 전달한 .catch()(상수를 전달한 .catch(value)는 정상적으로 컴파일됨)

객체, 배열, 튜플, 레코드 또는 교차 타입 안에서는 주변 구조가 컴파일된 상태로 유지되는 동안 지원하지 않는 하위 스키마만 표준 파서에서 실행됩니다. 지원하지 않는 멤버가 있는 유니온, .catch() 콜백 또는 하위 트리 어디에든 비동기 요소가 있으면 전체 스키마가 대체 경로로 전환됩니다.

인코딩(z.encode(), 코덱의 "backward" 방향)과 비동기 파싱은 항상 표준 파서를 사용합니다.

대체 경로를 사용하는 대신 예외를 던지게 하려면 strict를 전달하세요. 예를 들어 빈번하게 실행되는 경로의 스키마가 실제로 컴파일되었는지 확인할 수 있습니다.

z.compile(Schema, { strict: true }); // throws ZodCompileAsyncError

비동기 스키마에는 ZodCompileAsyncError가, 그 밖의 경우에는 ZodCompileUnsupportedError가 발생합니다. 둘 다 strict에서만 발생합니다.

콘텐츠 보안 정책

컴파일은 new Function을 사용하며, 이는 eval을 허용하지 않는 CSP 환경에서 사용할 수 없습니다. jitless가 설정되면 전역 모드는 비활성화됩니다.

z.config({ jitless: true });

z.compile()을 직접 호출하는 것은 명시적으로 컴파일을 활성화하는 것이므로 jitless와 관계없이 코드 생성을 시도합니다. 환경이 new Function을 거부하면 다른 이유로 컴파일할 수 없을 때와 마찬가지로 컴파일되지 않은 스키마를 반환합니다.

번들 크기

컴파일러의 코드량이 많으므로 z.compile() 또는 "zod/compile"로 호출하면 번들에 포함됩니다. gzip 압축 기준 약 7KB, 최소화 기준 28KB가 추가됩니다. z.compile()을 호출하거나 zod/compile을 가져오지 않는 번들에는 아무 비용도 없으며, 번들링 중 완전히 트리 셰이킹됩니다.

번들(키 4개의 객체 스키마)컴파일러 없음컴파일러 포함
Zod24.1 KB31.1 KB
Zod Mini4.6 KB13.2 KB

벤치마크

스키마가 복잡할수록 이점이 커집니다. 여기서는 표준 파서에 가장 유리한 조건인 단순 반복문 안에서 각 스키마를 단독으로 측정하므로, 페이지 상단 차트보다 성능 향상 폭이 작습니다(벤치마크).

스키마속도 향상
객체, 키 5개1.8x
객체, 키 10개2.2x
객체, 키 20개5.0x
객체, 키 50개10.2x
튜플, 항목 1개2.2x
튜플, 항목 3개2.5x
튜플, 항목 5개3.0x
튜플, 항목 10개3.7x

Moltar 벤치마크 픽스처에서 컴파일된 Zod와 컴파일되지 않은 Zod를 다른 라이브러리와 비교한 결과입니다. parseSafe 범주는 알 수 없는 키가 제거된 새 객체를 반환합니다.

Moltar 벤치마크 픽스처에서 초당 연산 수를 나타낸 막대 차트. parseSafe 범주: 컴파일된 Zod 4 47.5M, typia 45.3M, Zod 4 11.6M, valibot 1.8M, effect 1.7M, Zod 3 1.2M, arktype 152k, yup 121kMoltar 벤치마크 픽스처에서 초당 연산 수를 나타낸 막대 차트. parseSafe 범주: 컴파일된 Zod 4 47.5M, typia 45.3M, Zod 4 11.6M, valibot 1.8M, effect 1.7M, Zod 3 1.2M, arktype 152k, yup 121k
Moltar 벤치마크 픽스처의 처리량(parseSafe: 알 수 없는 키를 제거한 새 객체 반환) — 높을수록 좋음(벤치마크)

assertLoose 범주는 불리언을 반환하고 알 수 없는 키를 허용합니다. Zod는 이를 z.validate()로 실행합니다.

Moltar 벤치마크 픽스처에서 초당 연산 수를 나타낸 막대 차트. assertLoose 범주: typia 74.9M, arktype 66.2M, 컴파일된 Zod 4 60.6M, Zod 4 6.5M, valibot 1.9M, effect 1.7M, Zod 3 1.2M, yup 124kMoltar 벤치마크 픽스처에서 초당 연산 수를 나타낸 막대 차트. assertLoose 범주: typia 74.9M, arktype 66.2M, 컴파일된 Zod 4 60.6M, Zod 4 6.5M, valibot 1.9M, effect 1.7M, Zod 3 1.2M, yup 124k
Moltar 벤치마크 픽스처의 처리량(assertLoose: 불리언을 반환하고 알 수 없는 키를 허용) — 높을수록 좋음(벤치마크)