Zod가 Required를 검사하지 않는다

Jemin·2024년 6월 19일
3

트러블슈팅

목록 보기
11/11
post-thumbnail

서론

노마드코더 NextJS 강의의 코드 챌린지를 하다가 input 속성인 required를 개발자 도구로 제거하고 submit을 해봤는데 그대로 통과하는 문제가 발생했다.

GPT 답변
Input 컴포넌트의 required 속성 확인:

React Hook Form과 Zod를 함께 사용할 때, Zod 스키마에서 필수로 설정된 필드가 실제로 필수로 인식되도록 required 속성을 Input 컴포넌트에 전달해야 합니다.

음... 이건 딱 봐도 아니지 않을까?

GPT는 무시하고 일단 강의에서 혹시 같은 문제를 발견한 사람이 있나 좀 찾아봤는데, 아무도 없었다. 아무래도 초심자들이 많이 들어서 그런가 이런걸 의심을 안하는 모양이다.

React-Hook-Form + Zod를 사용해서 클라이언트에서 먼저 입력값에 대한 검사를 하는데, 강의를 그대로 따라와서 딱히 문제가 될만한 코드는 보이지 않는다.

문제 원인을 찾아보자

클라이언트에서는 Zod로 만든 Schema를 react-hook-form에 주입해서 입력값을 검사하고 있다.

"use client";

export default function AddProduct() {
  
  	...
  
    const {
        register,
        handleSubmit,
        setValue,
        setError,
        formState: { errors },
    } = useForm<ProductType>({
        resolver: zodResolver(productSchema), // zod schema 주입
    });


    const onSubmit = handleSubmit(async (data: ProductType) => {
      
        ...

        const formData = new FormData();
        formData.append("title", data.title);
        formData.append("price", data.price + "");
        formData.append("description", data.description);
        formData.append("photo", data.photo);
        const errors = await uploadProduct(formData);
        if (errors) {
            for (const [field, message] of Object.entries(errors.fieldErrors)) {
                setError(field as keyof ProductType, {
                    message: message.join(", "),
                });
            }
        }
    });

    const onValid = async () => {
        await onSubmit();
    };

    return (
        <div>
            <form action={onValid} className="flex flex-col gap-5 p-5">
                ...
            </form>
        </div>
    );
}

서버에서도 productSchema라는 Zod로 만든 Schema로 검사한다.

"use server";

export async function uploadProduct(formData: FormData) {
    const data = {
        photo: formData.get("photo"),
        title: formData.get("title"),
        price: formData.get("price"),
        description: formData.get("description"),
    };

    const result = productSchema.safeParse(data); // 유효성 검사

    if (!result.success) {
        return result.error.flatten();
    } else {
        ...
    }
}

클라이언트, 서버 둘 다 아무런 값을 입력하지 않아도 검사에 통과시켜버리는데, 공통적으로 사용하는 것이 Zod로 만든 Schema다.

그렇다면, Schema 파일을 확인해보자.

import z from "zod";

export const productSchema = z.object({
    id: z.coerce.number().optional(),
    photo: z.string({
        required_error: "Photo is required",
    }),
    title: z.string({ required_error: "Title is required" }),
    description: z.string({ required_error: "Description is required" }),
    price: z.coerce.number({ required_error: "Price is requried" }),
});

export type ProductType = z.infer<typeof productSchema>;

보기에는 문제가 없는 것 같지만, 아무래도 Schema가 문제인 것 같아서 조금 찾아봤다.

nonempty

Zod에서 nonempty라는 메서드를 제공한다고 하는데, 이름부터가 비어있는지 확인하는 검증 같아서 적용해보았다.

nonempty는 deprecated 되었다고 한다...

github에 등록된 이슈

z.string() validates empty strings #2466

github에 등록된 이슈가 지금 문제와 같은데, 이슈에서 가져온 아래 댓글에서 깨달음을 얻을 수 있었다.

scotttrinh commented
나는 Zod의 목적이 무엇인지에 대한 일반적인 오해라고 생각합니다. Zod는 TypeScript를 사용하여 시스템을 보호하기 위해 노력하고 있습니다. 따라서 이러한 경우에는 "필수"와 "문자 열"은 예를 들어 HTML 형태의 특별한 컨셉과 아무 관련이 없습니다.

먼저 Zod 공식 문서를 들어가보면 TypeScript-first schema validation with static type inference 라고 쓰여있는 것을 볼 수 있다. 타입스크립트 스키마를 우선으로 검증을 수행한다는 뜻이다.

그러므로 위 댓글에서 말한 것 처럼 Zod의 string() 으로 required를 검사하는 것은 Zod의 목적과는 다르다는 뜻이고, 빈 문자열인 "" 도 string 타입이기 때문에 검증에 통과한다고 말하고 있다.

위 코드에서 handleSubmit이 전달받는 data는 input 태그를 통해서 입력되는 값이라, input type 속성을 설정해주면 아무것도 입력하지 않아도 action에서 해당 값을 string은 "", number는 0으로 전달해준다. 그렇기에 타입스크립트 입장에서는 아무런 문제가 되지 않아 그대로 통과시켜주는 것이다.

해당 댓글에서 trim()min()을 사용해서 required를 검사하는 솔루션도 알려준다. 덕분에 아래와 같이 Schema를 수정해서 문제를 해결할 수 있었다.

import z from "zod";

export const productSchema = z.object({
    id: z.coerce.number().optional(),
    photo: z.string({
        required_error: "Photo is required",
    }),
    title: z.string().trim().min(1, "Title is required"),
    description: z.string().trim().min(1, "Description is required"),
    price: z.coerce.number().min(1, "Price is requried"),
});

export type ProductType = z.infer<typeof productSchema>;

number(), string() 과 같은 메서드는 타입에 대한 검사만하고 required 검사는 trim(), min()을 같이 사용해서 검사하는 것으로 변경했다.

photo값은 input으로 받는 것이 아닌 react-hook-form의 setValue로 직접 입력해주기 때문에, 문제가 되지 않았던 것으로 보인다.

마무리

문제는 해결했지만, 그렇다면 왜 string()메서드 안에 required_error라는 값이 있는 건지 궁금해서 조금 더 찾아보았다.

zod schema not triggering required_error for empty strings

스택 오버플로우에서도 유사한 문제의 질문이 있는데, 아래 코드에서 required_error에 작성한 메시지가 왜 노출되지 않느냐는 질문이다.

export const Schema = z.object({
  text: z
    .string({ required_error: "required error message" }),
});

required_error는 아무래도 값이 null이나 undefined일때 발생하는 것 같다.

아무래도 required_error라는 이름으로 필수입력 검증을 해줄 것 처럼 보여서 오해하는 사람이 많은 것 같다..

profile
경험은 일어난 무엇이 아니라, 그 일어난 일로 무엇을 하느냐이다.

0개의 댓글