라이브러리로 어떤 문제를 해결하나요? - zod 편
zod 로 어떤 문제를 해결할 수 있나요?
![thumbnail](/_next/image?url=%2Fimages%2FusingZod%2Fthumbnail.png&w=3840&q=75)
라이브러리로 어떤 문제를 해결하나요
는 다음과 같은 주제를 다루고 있어요.
- 라이브러리에 대한 설명
- 라이브러리를 통해 어떤 문제를 해결할 수 있는지?
요약
Zod를 사용하면 데이터 검증 문제를 쉽게 해결할 수 있습니다.
TypeScript와 Zod를 함께 사용하면 컴파일 타임과 런타임에서 모두 타입 안전성을 확보할 수 있습니다.
이를 통해 코드를 더 안전하고 유지보수하기 쉽게 만들 수 있습니다.
api response value 검증, form 검증과 같이 데이터 검증이 필요한 상황에서 Zod 를 활용해보세요!
Zod는 왜 사용하나요?
Zod 홈페이지에서는 TypeScript-first schema validation with static type inference 라고 소개되어 있습니다.
이를 해석하면 정적 유형 추론을 통한 타입스크립트 우선 스키마 유효성 검사입니다.
데이터 검증은 프론트엔드 개발에서 매우 중요한 과제입니다.
신뢰할 수 없는 데이터를 처리하거나, 예상치 못한 형식의 데이터가 들어오면 애플리케이션이 오작동할 수 있습니다.
이런 문제를 해결하기 위해 사용하는 라이브러리 중 하나가 바로 Zod입니다.
유효성 검증을 직접 구현하면 어떤가요?
interface User {
id: string;
name: string;
age: number;
roles: ("ADMIN" | "USER" | "GUEST")[];
}
// 유효성 검증 함수
function validateUser(user: any) {
if (typeof user.id !== "string") {
throw new Error("Invalid id");
}
if (typeof user.name !== "string" || user.name.length < 1) {
throw new Error("Invalid name");
}
if (typeof user.age !== "number" || user.age < 18 || user.age > 100) {
throw new Error("Invalid age: Must be an integer between 18 and 100");
}
if (
!Array.isArray(user.roles) ||
!user.roles.every((role) => ["ADMIN", "USER", "GUEST"].includes(role))
) {
throw new Error("Invalid roles");
}
}
User interface 몇 개의 필드를 검증하는데도 상당한 코드가 필요합니다.
또한, 각 필드의 유효 조건을 한눈에 파악하기 어렵다는 단점이 있습니다.
현재 4개의 필드만 있어도 이런 상황인데, 필드 수가 더 많아진다면 파악이 더욱 어려워질 것입니다.
Zod를 사용하면 어떤가요?
import { z } from "zod";
// User 스키마 정의
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, "Name is required"),
age: z
.number()
.int()
.min(18, "Must be at least 18 years old")
.max(100, "Age must be less than or equal to 100"),
roles: z.array(z.enum(["ADMIN", "USER", "GUEST"])),
});
// 타입 추론
type User = z.infer<typeof UserSchema>;
Zod를 사용하면 위와 같은 문제를 해결할 수 있습니다.
Zod를 통해 유효성 검증 스키마를 작성하고, 타입을 추론하며, 유효성 검증까지 직관적으로 확인할 수 있습니다.
직접 코드를 작성하는 것보다 훨씬 효율적인 방법으로 보입니다.
Zod를 사용하면 유효성 검증을 보다 효과적으로 할 수 있다는 것을 알게 되었습니다.
그렇다면 이러한 유효성 검증
은 언제 사용하는 것이 좋을까요? 🤔
앞서 신뢰할 수 없는 데이터를 처리하거나 예상치 못한 형식의 데이터가 들어오면 애플리케이션이 오작동할 수 있습니다.
라고 이야기했습니다.
그렇다면 신뢰할 수 없는 데이터를 처리하는 경우와 예상치 못한 형식의 데이터가 들어오는 상황은 언제일까요?
이 두 상황이 어떤 연관 관계가 있는지 지금부터 살펴보겠습니다.
예상치 못한 형식의 데이터가 들어온 경우
fetch only
화면에서 보여주는 대부분의 요소는 서버로부터 전달 받은 데이터입니다.
우리는 이러한 데이터를 fetch 를 통해 가져옵니다.
이러한 fetch api 를 typescript
와 함께 쓰면 다음과 같습니다.
interface User {
id: number;
name: string;
email: string;
}
const fetchUsers = async (): Promise<User[]> => {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data: User[] = await response.json();
return data;
};
특별하게 문제가 있어보이진 않아보입니다.
하지만 아래 이 코드에서 우리는 예상치 못한 데이터가 들어올 수 있다는 점을 알 수 있습니다.
const data: User[] = await response.json();
호출한 데이터가 User[] 형태일 것이라고 예상
해서 타입을 적용했습니다.
하지만 이는 예상
에 불과하기 때문에, 실제로는 예상한 타입과 다른 데이터가 들어올 수 있습니다.
이런 경우, 다른 데이터가 들어왔다는 것을 런타임
에서만 알 수 있습니다.
이를 런타임에서만 알 수 있다는 건 개발자가 의도하지 않은 시나리오가 사용자에게 노출된다는 의미입니다.
이 때, 우리는 유효성 검증을 사용하여 문제를 해결할 수 있습니다.
fetch with zod
먼저, fetch 를 zod 와 함께 사용한 예제를 살펴보겠습니다.
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// Zod 스키마에서 유추된 타입 정의
type User = z.infer<typeof UserSchema>;
// Zod 배열 스키마 정의
const UsersSchema = z.array(UserSchema);
const fetchUsers = async (): Promise<User[]> => {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const data = await response.json();
// Zod로 데이터 검증
const parsedData = UsersSchema.safeParse(data);
if (!parsedData.success) {
throw new Error("Response data is not of type User[]");
}
return parsedData.data;
};
전달받은 데이터가 User[]
인지 확인하기 위해 Zod를 사용하여 유효성 검증을 진행했습니다.
만약 User[]
가 아닌 데이터가 들어온다면, UsersSchema.safeParse(data)
를 통해 이를 인지할 수 있습니다.
개발자는 이 상황을 코드에서 제어할 수 있게 됩니다.
이로 인해 런타임에서 예상치 못한 에러가 발생하는 대신,
잘못된 에러가 발생하더라도 앱이 정상적으로 동작할 수 있도록 기본값 할당과 같이 대응하는 등
개발자가 직접 대응할 수 있다는 점이 매우 큰 장점이라고 생각합니다.
예상치 못한 형식의 데이터가 들어온 경우 zod 가 어떤 도움을 주나요?
zod 를
유효성 검증
을 통해 이 상황을제어
할 수 있습니다.
>기본값 할당
과 같은 대응이 기본적인 예시입니다.
신뢰할 수 없는 데이터를 처리하는 경우
그리고 사용자가 폼에 입력한 데이터
가 예상과 다를 수 있습니다.
이번에는 사용자 입력 폼
을 처리할 때에 zod 가 어떤 도움을 주는지 살펴보겠습니다.
원활한 사용자 폼 처리를 위해 react-hook-form 라이브러리를 사용하겠습니다 :D
react-hook-form only
react-hook-form 을 통해 사용자가 입력하는 데이터를 검증해보겠습니다.
import { useForm } from "react-hook-form";
interface User {
name: string;
age: number;
}
const UserForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<User>({
defaultValues: {
name: "",
age: 18,
},
});
const onSubmit = (data: User) => {
console.log("Valid form data:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name</label>
<input
{...register("name", {
required: "Name is required",
minLength: {
value: 1,
message: "Name must be at least 1 character",
},
})}
/>
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>Age</label>
<input
type="number"
{...register("age", {
required: "Age is required",
min: { value: 18, message: "Must be at least 18 years old" },
max: {
value: 100,
message: "Age must be less than or equal to 100",
},
})}
/>
{errors.age && <p>{errors.age.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
};
export default UserForm;
충분히 이해할 수 있는 코드입니다.
name 필드는 항상 존재해야 하며, 최소 길이는 1입니다.
age 필드도 항상 존재해야 하며, 최소 나이는 18세, 최대 나이는 100세입니다.
지금은 2개의 필드만을 다루고 있어 문제가 없어 보이지만,
필드가 5~6개 이상이라면 이 폼에서 다루고 있는 필드의 검증 로직을 파악하기 쉽지 않을 것 같습니다.
이 때, 우리는 zod를 사용하여 더 직관적인 코드를 구성할 수 있습니다.
react-hook-form with zod
먼저, react-hook-form 을 zod 와 함께 사용한 예제를 살펴보겠습니다.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// User 스키마 정의
const UserSchema = z.object({
name: z.string().min(1, "Name is required"),
age: z
.number()
.int()
.min(18, "Must be at least 18 years old")
.max(100, "Age must be less than or equal to 100"),
});
type User = z.infer<typeof UserSchema>;
const UserForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<User>({
resolver: zodResolver(UserSchema),
});
const onSubmit = (data: User) => {
console.log("Valid form data:", 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", { valueAsNumber: true })} />
{errors.age && <p>{errors.age.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
};
export default UserForm;
Zod 스키마를 정의할 때, 검증 로직과 에러 메시지를 함께 정의합니다.
이렇게 하면 React Hook Form만 사용할 때보다 더 직관적인 코드를 작성할 수 있습니다.
필드가 5~6개 혹은 그 이상이더라도 각 필드에 어떤 검증 로직이 있는지 쉽게 이해할 수 있습니다.
많은 양의 폼 데이터를 처리해야 할 때,
react-hook-form과 zod를 함께 사용하면 시너지 효과가 뛰어납니다.
각 필드의 검증 로직과 에러 메시지를 쉽게 파악할 수 있다는 점이 매우 큰 장점이라고 생각합니다.
그래서 Zod 는 어떤 문제를 해결하나요?
Zod를 사용하면 데이터 검증 문제를 쉽게 해결할 수 있습니다.
TypeScript와 Zod를 함께 사용하면 컴파일 타임과 런타임에서 모두 타입 안전성을 확보할 수 있습니다.
api response value 검증, form 검증과 같이 데이터 검증이 필요한 상황에서 Zod 를 활용해보세요!