mailgithub

라이브러리로 어떤 문제를 해결하나요? - zod 편

zod 로 어떤 문제를 해결할 수 있나요?

thumbnail

라이브러리로 어떤 문제를 해결하나요 는 다음과 같은 주제를 다루고 있어요.

  1. 라이브러리에 대한 설명
  2. 라이브러리를 통해 어떤 문제를 해결할 수 있는지?

요약

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 를 활용해보세요!