최근에 잘 사용하고 있는 Zod
라이브러리의 사용법과, 왜 써야하는지에 대해 공부할 겸 적는 글이다.
자바스크립트는 런타임에 타입 검사가 수행되어 타입이 결정되는 동적 타입 언어이다.
컴파일 타임에 타입 검사가 이루어지지 않으므로, 런타임에서 다른 타입의 값이 할당될 경우 타입 에러가 발생할 수 있고 프로그램의 안정성이 떨어질 수 있다.
let foo = 42; // 숫자
console.log(foo); // 42
foo = "Hello"; // 문자열로 변경
console.log(foo); // Hello
이러한 점을 보완하기 위해 미리 타입을 선언하고, 컴파일 시점에 타입검사를 수행할 수 있는 타입스크립트가 등장하게 되었다.
TS는 정적 타입(static typing) 언어로, 변수의 타입이 컴파일 타임에 결정된다. 즉, 변수의 타입을 명시적으로 선언해야 하며, 타입에 맞지 않는 값을 할당할 경우 컴파일 시점에서 오류가 발생한다.
컴파일 시점에 타입 에러를 발견할 수 있는 TS의 사용으로 개발 단계에서 많은 에러를 줄이게 되었지만, 타입스크립트는 결국 JS로 변환되어 실행되기 때문에 런타임에서 발생하는 타입 에러까지는 잡아낼 수 없다는 한계가 있었다.
예를 들어 이렇게 정의해놓은 User 데이터를 응답으로 받아야 하는데,
interface User {
"id": 1,
"name": "홍길동",
"email": "hong@example.com"
}
이렇게 기존 User 데이터 타입과 다른 데이터를 받아올 경우 타입 에러가 발생하게 된다.
{
"id": "1", // string
"username": "홍길동" // 기존 name과 다른 필드
}
생각보다 이런 경우가 종종 발생하기 때문에, 프로그램의 안정성을 위해 더더욱 한번 더 검증하는 과정이 필요하다고 생각한다.
Zod는 주로 타입스크립트와 함께 사용되는 스키마 검증 라이브러리이다.
앞서 언급한 런타임에서 타입검사를 수행할수 없는 TS의 한계를 보완해, 런타임에서 스키마 검증을 통해 타입 안정성을 향상시킬수 있으며 작은 번들사이즈, 직관적 API 등을 통해 쉽게 사용할 수 있다.
Zod 공식문서에는 이러한 장점들을 소개하고 있다.
- Zero dependencies
(의존성 X)- Works in Node.js and all modern browsers
(Node.js 및 모든 최신 브라우저에서 사용 가능)- Tiny: 8kb minified + zipped
(8kb의 작은 사이즈)- Immutable: methods (e.g. .optional()) return a new instance
(불변성: 메서드(예: .optional())는 새로운 인스턴스를 반환)- Concise, chainable interface
(간결하고, 체이닝 가능한 인터페이스 제공)- Functional approach: parse, don't validate
(함수형 접근 : 검증이 아닌, 파싱)- Works with plain JavaScript too! You don't need to use TypeScript.
(순수 JS에서도 작동 - TS를 사용하지 않아도 됨)
공식문서에서 가져온 사용 예시이다.
import { z } from "zod";
const User = z.object({
username: z.string(),
});
User.parse({ username: "Ludwig" });
// extract the inferred type
type User = z.infer<typeof User>;
// { username: string }
정의한 스키마에서 타입을 추출하여 사용할 수 있으며,
z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
이메일, url 등 정말 다양한 형식의 유효성 검증이 가능하고, 추가 인자로 메세지를 넘겨 커스텀 에러 메세지를 지정할수도 있다.
zod의 parse
와 safeParse
는 데이터 검증을 위한 메서드이다.
두 메서드의 가장 큰 차이점은 '에러 처리 방식' 에 있다.
parse
데이터를 검증하고, 유효하지 않을 경우 에러를 발생시키며
검증에 성공할 경우 데이터를 반환한다.
const stringSchema = z.string();
stringSchema.parse("fish"); // => returns "fish"
stringSchema.parse(12); // throws error
safeParse
데이터를 검증하고, 유효하지 않을 경우 결과를 객체 형태로 반환 한다.
즉, 검증 실패시 에러를 발생시키지 않고 결과값을 객체로 받아보고 싶을 경우 사용한다.
stringSchema.safeParse(12);
// => { success: false; error: ZodError }
const result = stringSchema.safeParse("billie");
// => { success: true; data: 'billie' }
if (!result.success) {
// handle error then return
result.error;
} else {
// do something
result.data;
}
이 외에도 여러가지 기능이 있지만, 나는 유효성 검증이 많이 필요한 form
을 다룰 때에도 react-hook-form
라이브러리와 함께 사용할수 있다는 점도 너무 좋았다.
이런식으로 form 스키마를 작성한 뒤,
const formSchema = z.object({
email: z
.string()
.min(1, "이메일은 필수 입력사항입니다.")
.regex(emailRegex, "올바른 이메일 형식으로 입력해주세요."),
phone: z
.string()
.min(1, "휴대폰 번호는 필수 입력사항입니다.")
.regex(phoneRegex,
"하이픈 (-) 없이 숫자만 휴대폰 번호 형식에 맞게 입력해주세요.",
),
image: z
.instanceof(File)
.refine(
(file) => file && file.type.startsWith("image/"),
"이미지 파일만 업로드 가능합니다",
),
});
useForm의 resolver에 추가해주면 스키마 검증을 통해 유효하지 않은 값일 경우 바로 react-hook-form
의 에러 객체에 내가 지정한 에러 메세지가 할당된다.
const {
register,
handleSubmit,
...
} = useForm<Form>({
resolver: zodResolver(formSchema),
});
안정성 있는 프로그램과 유지보수를 용이하게 하기 위해, 이런 라이브러리들의 다양한 기능들을 공부해서 잘 활용하는것도 중요한 것 같다.