TypeScript로 프로젝트를 개발할 때, 데이터 유효성 검증은 필수적인 작업이다. 특히 클라이언트와 서버 간의 데이터 통신, 사용자가 입력한 폼 데이터, API 응답 데이터를 검증하는 과정에서 이 작업은 매우 중요하다.
TypeScript는 컴파일 시점에서의 타입은 검증하지만, 런타임에서 발생하는 에러는 방지하기 어려워 별도의 데이터 검증 라이브러리가 필요하다. 이때 사용할 수 있는 것이 Zod이다.
💡Zod란?
Zod는 TypeScript 기반의 스키마 선언 및 데이터 검증 라이브러리로, 데이터가 특정 형식과 구조를 따르는지 검증하기 위해 설계되었다.
Zod는 데이터 유효성 검증과 타입 안전성을 강화하기 위해 만들어졌다. Zod를 사용하면 TypeScript와 함께 타입 정의와 데이터 검증을 손쉽게 수행할 수 있다.
다른 데이터 검증 라이브러리인 Yup과 비슷해 보일 수 있지만, Zod는 타입스크립트와 더 깊이 통합되어, 데이터 스키마를 정의하면서 동시에 TypeScript 타입을 자동으로 생성할 수 있다. 이를 통해 타입 안전성과 데이터 검증을 동시에 만족할 수 있다는 점이 장점이다.
스키마 검증
스키마 검증은 데이터가 예상되는 형식과 조건을 만족하는지 확인하는 과정이다. 예를 들어, 사용자가 제출한 데이터가 의도하지 않은 형식이나 값일 경우, 런타임 오류, 보안 취약점, 데이터 손상 등의 문제가 발생할 수 있다. 스키마 검증을 통해 애플리케이션이 정해진 규칙에 맞는 데이터만 처리하도록 함으로써 이러한 문제를 예방할 수 있다.
Zod의 주요 기능
1. 기본 데이터 타입 검사
Zod는 기본적인 데이터 타입인 string, number, boolean 등에 대한 유효성 검사를 지원한다. 각 타입별로 간단하고 명확한 API가 제공되어, 스키마 정의와 유효성 검사가 직관적이다. 예를 들어, Zod.string()을 사용하면 문자열 타입을 검사하는 스키마를 정의할 수 있고, Zod.number()로 숫자 타입 검사를 쉽게 설정할 수 있다. 이는 데이터의 기본 형태를 검증하는 첫 단계로서, 데이터 무결성을 확보하는 데 큰 도움이 된다.
2. 복합 데이터 타입 검사
Zod는 객체, 배열, 튜플, 유니온, 교차 타입 등 다양한 복합 데이터 타입 검사를 지원한다. 예를 들어, 객체 타입의 유효성을 검사하려면 Zod.object()를 사용해 스키마를 정의할 수 있다. 또한, Zod.array()를 통해 배열의 각 요소를 특정 타입으로 제한할 수 있으며, Zod.tuple()로 정해진 순서의 타입을 요구하는 튜플도 정의 가능하다. Zod.union()과 Zod.intersection()을 통해 유니온 타입(여러 타입 중 하나)과 교차 타입(여러 타입이 모두 포함된 형태)의 데이터도 검증할 수 있어, 복잡한 데이터 구조에 대한 유효성 검사도 세밀하게 처리할 수 있다.
3. 조건부 유효성 검사
Zod는 특정 값의 범위나 조건을 제한할 수 있는 조건부 유효성 검사를 지원한다. 예를 들어, 숫자 스키마에서 .min()과 .max()를 사용해 숫자의 범위를 제한할 수 있으며, 문자열 스키마에서 .length()를 이용해 문자열의 길이를 제한하는 등 특정 조건에 따라 검사를 설정할 수 있다. 이와 같은 설정은 숫자 범위 제한이나 특정 형식의 문자열만을 허용하는 등 데이터의 세부적인 유효성 조건을 추가해야 할 때 유용하다.
4. 커스터마이징 가능한 에러 메시지
Zod는 유효성 검사가 실패할 때 사용자 정의 오류 메시지를 설정할 수 있다. 각 스키마 메서드에 .message()를 추가하면, 검사 실패 시 사용자가 정의한 메시지가 출력된다. 이와 같이 오류 메시지를 설정하면 사용자에게 유용하고 이해하기 쉬운 피드백을 제공할 수 있어, 데이터 검증의 정확성을 더욱 강화할 수 있다. 이는 특히 사용자 입력 폼 검증 시 유용하게 사용된다.
5. 런타임 타입 안전성
Zod는 런타임에서도 타입 유효성 검사를 수행하여, TypeScript의 컴파일 타임 검사로 잡아내지 못하는 타입 오류를 예방할 수 있다. TypeScript는 컴파일 단계에서 타입 검사를 진행하지만, API 응답 등 외부 데이터를 다룰 때는 런타임에서의 타입 검사도 필요할 수 있다. Zod의 런타임 타입 검사를 통해 외부로부터의 데이터 유입 시 타입 불일치를 방지하고, 예기치 않은 오류를 줄일 수 있어 더욱 안전한 애플리케이션을 구축할 수 있다.
https://github.com/colinhacks/zod#introduction
Zod에 대한 자세한 내용은 GitHub와 공식 문서를 참고한다.
💡Zod 설치하기
Zod는 npm, yarn, pnpm을 통해 설치할 수 있다. 또한, TypeScript를 기본으로 지원하므로 별도의 타입 패키지는 필요하지 않다.
# npm
npm install zod
# yarn
yarn add zod
# pnpm
pnpm add zod
💡Zod의 사용
Zod의 기본적인 사용 방법을 살펴보도록 하자.
다음 예제에서는 다양한 스키마를 정의하고 데이터를 검증하는 방법을 설명한다.
기본 타입 스키마 정의
Zod는 문자열, 숫자, 불리언과 같은 기본 데이터 타입을 쉽게 정의할 수 있다.
import { z } from 'zod';
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
// 데이터 검증
stringSchema.parse("Isaac"); // 통과
numberSchema.parse(123); // 통과
booleanSchema.parse(true); // 통과
// 유효성 검사 실패
stringSchema.parse(123); // 에러 발생: "Expected string, received number"
위 예제에서는 stringSchema, numberSchema, booleanSchema라는 기본 타입 검증 스키마를 정의했다. parse 메서드는 데이터가 스키마에 맞지 않을 경우 예외를 발생시킨다.
객체 스키마 정의
객체 형태의 데이터를 검증하기 위해 z.object()를 사용하여 각 속성의 스키마를 설정할 수 있다.
const userSchema = z.object({
name: z.string(),
age: z.number().min(20), // 나이는 20세 이상이어야 함
email: z.string().email() // 올바른 이메일 형식이어야 함
});
// 데이터 검증
userSchema.parse({
name: "Isaac",
age: 25,
email: "isaac@example.com"
});
위 예제에서는 userSchema로 ID, 이름, 이메일을 포함한 사용자 객체를 검증한다.
z.string().min(3)은 이름의 길이가 최소 3자 이상이어야 한다는 제한을 걸었으며, z.string().email()은 이메일 형식을 검증한다. 그리고 검증 실패 시 유효성 검사 에러가 발생하게 된다.
유니온 타입과 배열 정의
배열은 z.array()를 통해, 여러 타입을 포함하는 경우에는 z.union()을 통해 정의할 수 있다.
const stringArraySchema = z.array(z.string());
const unionSchema = z.union([z.string(), z.number()]);
// 데이터 검증
stringArraySchema.parse(["a", "b", "c"]); // 통과
unionSchema.parse("text"); // 통과
unionSchema.parse(123); // 통과
위 예제에서는 stringArraySchema로 문자열 배열을 정의하였고, unionSchema는 문자열이나 숫자 중 하나를 받을 수 있도록 했다.
타입 안전성과 자동 완성
Zod는 TypeScript의 타입 추론 기능을 활용해 데이터 타입을 자동으로 생성할 수 있다.
z.infer를 사용하여 스키마 기반으로 TypeScript 타입을 자동 생성할 수 있다.
const userSchema = z.object({
id: z.number(),
username: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof userSchema>;
const validUser = userSchema.parse({
id: 1,
username: 'isaac',
email: 'isaac@example.com',
});
validUser.id; // number 타입
validUser.username; // string 타입
위와 같이 parse 메서드로 객체를 검증할 때 TypeScript는 객체의 타입을 추론하므로 코드의 안정성과 가독성을 높일 수 있다.
에러 핸들링
Zod를 사용하면 검증 오류가 발생했을 때 상세한 오류 메시지를 쉽게 확인할 수 있다.
try {
const invalidUser = userSchema.parse({
id: 1,
username: 'do', // 최소 3자 이상이어야 함
email: 'bad-email', // 유효하지 않은 이메일 형식
});
} catch (error) {
console.error('검증 오류:', error.message);
}
💡Zod의 활용
폼 데이터 검증하기
React Hook Form과 함께 Zod를 사용하면 폼 데이터의 유효성 검사를 더욱 간편하게 처리할 수 있다. 이를 위해 @hookform/resolvers 패키지를 사용하여 Zod 스키마와 연결하도록 한다.
아래는 Zod를 이용해 유효성 검증 스키마를 작성하고 React Hook Form에서 사용하는 예제이다.
패키지 설치
React Hook Form과 Zod resolver를 설치한다.
npm install react-hook-form @hookform/resolvers
MyForm.tsx
MyForm.tsx 파일은 Zod 스키마를 이용해 사용자 입력을 검증하는 React 폼 컴포넌트이다.
폼이 제출되기 전에 각 필드가 올바른 형식인지 검증해 잘못된 데이터가 서버로 전송되는 것을 방지할 수 있다.
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// Zod 스키마 정의
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
age: z.number().min(20, "You must be at least 20 years old"),
email: z.string().email("Invalid email address"),
});
type FormData = z.infer<typeof formSchema>;
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name:</label>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>Age:</label>
<input type="number" {...register("age")} />
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<label>Email:</label>
<input type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
export default MyForm;
먼저 필요한 라이브러리를 가져오도록 한다. useForm은 react-hook-form에서 폼을 관리하는 훅으로, 여기서는 zodResolver와 함께 사용하여 폼 데이터 검증을 위한 스키마로 정의할 수 있다.
그리고 formSchema라는 Zod 스키마를 정의하였다. name, age, email 세 개의 필드를 포함하며, 각 필드에는 특정한 유효성 검증 조건을 설정하였다. 이 스키마를 바탕으로 FormData 타입을 자동으로 추론하여, TypeScript 프로젝트에서 타입 안전성을 강화할 수 있다.
useForm 훅에서 zodResolver(formSchema)를 통해 Zod 스키마를 적용하면, register 함수가 이 스키마를 기준으로 데이터를 검증하게 된다. errors 객체에는 검증 실패 시 각 필드별 에러 메시지가 담긴다. onSubmit 함수는 폼이 제출될 때 실행되며, 데이터가 콘솔에 출력한다.
이제 실제 폼 필드를 정의하도록 한다. register("name")와 같이 register 함수를 통해 각 필드를 Zod 스키마와 연결한다. 에러가 있는 경우 errors 객체에 접근하여, 에러 메시지를 화면에 표시한다. 예를 들어, 이름이 비어 있으면 "Name is required" 메시지가 나타나게 된다.
Zod를 통한 API 응답 검증하기
apiValidation.ts
API 요청을 통해 받아온 데이터를 Zod로 검증하면 예상치 못한 데이터 구조로 인한 오류를 줄일 수 있다.
아래는 Axios와 Zod를 함께 사용하여 API 응답을 검증하는 예제이다.
import axios from 'axios';
import { z } from 'zod';
// Zod 스키마 정의
const postSchema = z.object({
id: z.number(),
title: z.string(),
body: z.string(),
});
// API 호출 및 검증
async function fetchPost(postId: number) {
try {
const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}`);
const postData = postSchema.parse(response.data);
console.log(postData);
} catch (error) {
console.error("Invalid data format:", error);
}
}
fetchPost(1);
먼저 axios와 Zod를 가져오고, API 응답 데이터의 구조에 맞춰 postSchema 스키마를 정의한다. 여기서는 id 필드를 숫자로, title과 body 필드를 문자열로 설정하였다. 이는 API 호출로 받은 데이터가 정확히 이러한 구조를 가져야 함을 의미한다.
fetchPost 함수는 axios.get으로 API 요청을 보내고 응답 데이터를 받아온다. 받아온 response.data가 postSchema.parse에 전달되면, 이 데이터가 정의된 스키마에 맞는지 검증하게 된다. 만약 데이터가 맞지 않으면 catch 블록에서 에러가 발생하며, 오류 메시지를 출력한다.
이 방법을 통해 API에서 반환되는 데이터가 예상한 형식을 따르지 않는 경우에도 오류를 사전에 처리하여 애플리케이션의 안정성을 높일 수 있다.
이와 같이 Zod 라이브러리를 사용하면 TypeScript의 데이터 검증과 타입 정의를 강화하여, 개발 중에 데이터의 정확한 타입과 형태를 보장할 수 있으며, 오류 메시지 및 디버깅에 용이하게 활용할 수 있다.
참고 자료
Having Fun with TypeScript: Zod, Dagang Wei, 2024.03.12.
Exploring Zod: A Comprehensive Guide to Powerful Data Validation in JavaScript/TypeScript, Rasit Colakel, 2023.06.11.
Master schema validation in TypeScript with Zod, Domenico Colandrea, 2023.11.29.