
오늘은 특별한 이유가 있어서라기보다는, 그냥 React Hook Form을 공부해보려다가 자연스럽게 Zod라는 라이브러리까지 살펴보게 되었다. 구글링으로 간단히 알아봤는데, 문법이 굉장히 직관적이고 간단한 데다, TypeScript로는 잡을 수 없는 런타임 단계의 타입 에러까지 검출할 수 있다는 점이 인상 깊어서 이렇게 블로그에 정리하게 되었다.
Zod is a TypeScript-first schema declaration and validation library.
Zod의 설명은 공식 문서에서 확인할 수 있는데, 위 문장을 번역하면, "Zod는 TypeScript를 우선으로 고려한 스키마 선언 및 검증 라이브러리입니다."라는 뜻이다.
TypeScript를 사용하더라도, 결국 런타임에서 실행되는 것은 JavaScript이기 때문에 컴파일 시점에서의 에러는 잡아낼 수 있어도, 런타임에서 발생하는 에러까지는 완벽하게 제어할 수 없다. 예를 들어, TypeScript에서는 number 타입만 받도록 제한하는 것은 가능하지만, 숫자의 범위나 정수/실수 여부 같은 세부적인 제약은 걸기 어렵다.
이러한 TypeScript의 한계를 보완하기 위해 사용하는 것이 바로 Zod다.
스키마는 경험, 지식, 정보 등을 구조화하고 조직화하는 인지적 틀 또는 개념적 구조를 의미하는 고대 그리스어 "skhēma(형태, 모습)"에서 유래됐다고 한다. Zod에서의 스키마는 데이터의 형태 및 구조라고 생각하면 된다.
npm install zod
yarn add zod
pnpm add zod
문법이 워낙 간단하니, 바로 예시 코드로 사용법을 알아보자.
// humanForm.ts
import { z } from "zod";
export const userSchema = z.object({
name: z.string().min(2, "이름은 필수입니다."),
age: z.coerce.number().positive("나이는 양수여야 합니다."),
});
위 코드를 보면 알 수 있듯, 사용법이 매우 간단하고 문법도 직관적이어서 의미를 쉽게 추측할 수 있다.
간단히 설명하면,
z는 Zod의 모든 기능을 모아둔 객체이다.
z.object({})는 여러 정보를 객체 형태로 정의하는 함수이다.
name: z.string().min(2, "이름은 필수입니다.")는 name이 문자열(string)이어야 하고, 최소 2글자 이상이어야 한다는 뜻이다.
age: z.coerce.number().positive("나이는 양수여야 합니다.")는 age를 숫자(number) 타입으로 강제 변환하고, 양수만 허용한다는 의미이다.
import { useState } from "react";
import { userSchema } from "./schema";
const Main = () => {
const [name, setName] = useState("");
const [age, setAge] = useState("");
const [error, setError] = useState<string | null>(null);
const onSubmit = () => {
const result = userSchema.safeParse({
name,
age,
});
if (result.success) {
setError(null);
console.log("유효한 값:", result.data);
} else {
setError(result.error.issues[0].message);
console.log("유효성 오류:", result.error.issues);
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<label>
이름:
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
나이:
<input value={age} onChange={(e) => setAge(e.target.value)} />
</label>
<button>제출</button>
{error && <p>{error}</p>}
</form>
);
};
export default Main;
간단하게 입력 폼을 만들어 봤는데, 중요한 부분만 가져와서 설명해보겠다.
const onSubmit = () => {
const result = humanForm.safeParse({
name,
age,
});
if (result.success) {
setError(null);
console.log("유효한 값:", result.data);
} else {
setError(result.error.issues[0].message);
console.log("유효성 오류:", result.error.issues);
}
};
유효성 검사 역시 매우 간단하다. .parse 또는 .safeParse 메서드를 사용해서 진행하며,
위 코드 기준으로 result.success가 true이면, "유효한 값"이라는 메시지와 함께 data가 출력된다.

반대로 result.success가 false이면, "유효성 오류"라는 메시지와 함께 어떤 오류가 발생했는지 아주 자세한 정보가 출력된다.

.parse와 .safeParse의 차이점에 대해 궁금하다면, Zod 공식 문서를 참고해보는 것을 추천한다. 공식 문서가 아주 잘 정리되어 있어서 빠르게 이해할 수 있을 것이다.
Zod의 스키마를 기준으로 타입을 추론할 수도 있다. 이 기능을 사용하면 타입을 따로 작성할 필요가 없어져 코드가 훨씬 간결해진다.
import { z } from "zod";
import { userSchema } from "./schema";
// 타입 추론
type User = z.infer<typeof userSchema>;
위와 같이 z.infer<typeof 스키마>를 사용하면, 스키마를 기반으로 자동으로 타입이 생성된다. 덕분에 TypeScript의 type alias를 사용하는 것처럼 자연스럽고 편리하게 타입을 정의할 수 있다.
// 추론된 타입:
// type User = {
// name: string;
// age: number;
// }
const exampleUser: User = {
name: "전준연",
age: 18,
};
오늘은 우연히 알게 된 Zod에 대해 알아보고, 간단하게 사용도 해보았다. 사용법이나 문법이 매우 간단하면서도 TypeScript를 완벽하게 보완할 수 있다는 점이 특히 인상 깊었다. 앞으로 실제로 사용할 일이 생긴다면, 매우 유용하게 활용할 수 있을 것 같다.