본문 바로가기
SPA/React

[React] Zod를 이용하여 React Hook Form 사용하기

by J4J 2023. 12. 24.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 zod를 이용하여 react-hook-form 사용하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

관련 글

 

[React] react-hook-form을 이용하여 validation (유효성) 처리하기

 

 

반응형

 

 

Zod란?

 

이번 글에서는 react-hook-form에 다루기보다는 react-hook-form과 함께 사용해 볼 수 있는 zod에 대해 다뤄보도록 하겠습니다.

 

react-hook-form에 대해 더 궁금하신 분들은 위의 링크를 참고해주시면 될 것 같습니다.

 

 

 

zod는 validation 처리를 위해 사용할 수 있는 라이브러리로 여러 서비스들에서 쉽게 볼 수 있는 입력 값이 올바르게 되어있는지를 확인할 때 활용할 수 있습니다.

 

Zod 공식 문서를 확인해보면 zod는 다음과 같은 특징들을 가지고 있다고 소개하고 있습니다.

 

  • 타입 스크립트를 기반으로 생성된 라이브러리로 타입 스크립트 우선 선언을 수행
  • 의존되는 라이브러리가 없음
  • node와 모든 browser에서 동작 가능
  • 8kb 축소 및 압축으로 크기가 작음 
  • 불변성
  • 간결하며 연결성을 제공해 주는 인터페이스
  • 검증하지 않고 파싱을 하는 기능적인 접근 제공
  • 타입 스크립트가 아닌 순수 자바 스크립트에서도 동작

 

 

 

이런 zod는 react에서 사용할 땐 react-hook-form과 같이 곁들여져 사용되고는 합니다.

 

react-hook-form을 통해서 전체적인 form들을 관리해 주고 특정 form들에 매핑되어야 하는 값이 올바른지에 대한 유효성의 판단을 zod가 해주면서 효과적인 입력 값 검증 기능을 제공해 주기 때문입니다.

 

 

 

 

하지만 validation 처리를 수행할 때 zod 뿐만 아니라 더 많은 다양한 라이브러리들이 존재합니다.

 

대표적으로 yup과 joi가 존재하며 zod를 포함한 npm trend는 다음과 같습니다.

 

joi vs yup vs zod

 

 

 

이들에 대한 비교는 Zod 공식 문서 (Comparison)를 보면 더 많은 것을 확인해볼 수 있습니다.

 

비교에 대해 간단하게 얘기해보자면 타입 스크립트를 사용하고자 하면 joi는 정적 타입 추론을 제공하지 않기 때문에 yup과 zod를 사용하는 것이 좋습니다.

 

그러면 yup과 zod만 남게 되는데, 저는 지금까지 yup을 사용해 왔지만 최근에 zod를 활용하는 것으로 방향을 변경했습니다.

 

왜냐하면 yup에 비해 zod를 사용했을 때 더 쉽고 간편하게 활용할 수 있는 개발자 경험을 느낄 수 있었기 때문입니다.

 

단순하게 1개의 객체에 대해서 유효성을 검증하는 것은 차이가 거의 없지만 더 복잡한 유효성 검증을 하게 되면 yup보다 zod를 사용하는 것이 개발하는 사람도, 개발된 것을 보는 사람도 더 효과적이라고 판단되었습니다.

 

 

 

 

Zod 사용 환경 설정

 

zod만을 이용하여 사용해볼 수 있지만 위에서 얘기드린 것처럼 react에서는 react-hook-form과 함께 사용하는 것이 자주 발생되기 때문에 react-hook-form과 같이 사용하는 환경 설정을 하겠습니다.

 

환경 설정은 부가적인 설정 없이 단순하게 패키지만 설치해 주시면 됩니다.

 

 

 

[ 1. 패키지 설치 ]

 

$ npm install react-hook-form @hookform/resolvers zod

 

 

 

 

객체 유효성 검증

 

이번엔 react-hook-form과 zod를 어떻게 소스 코드로 작성해 볼 수 있는지 간단하게 작성해 보겠습니다.

 

가장 먼저 제일 흔한 케이스로 객체에 대한 유효성 검증하는 방법입니다.

 

다음과 같이 코드를 작성해 볼 수 있습니다.

 

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

interface Person {
    name: string;
    address: string;
    age: number;
}

const schema = z.object({
    name: z.string().min(1, { message: '이름은 필수값입니다.' }),
    address: z.string().min(1, { message: '주소는 필수값입니다.' }),
    age: z
        .number({
            invalid_type_error: '나이는 필수값입니다.',
        })
        .min(1, { message: '나이는 1살부터 입력이 가능합니다.' }),
});

export default function Object() {
    const {
        register,
        formState: { errors },
        handleSubmit,
    } = useForm<Person>({
        resolver: zodResolver(schema),
        defaultValues: {
            name: '',
            address: '',
            age: 0,
        },
    });

    return (
        <form onSubmit={handleSubmit((e) => console.log(e))}>
            <h2>Object Validation</h2>

            <div>
                <div>
                    <input type="text" placeholder="이름을 입력해주세요." {...register('name')} />
                    {errors.name && <p>{errors.name.message}</p>}
                </div>

                <div>
                    <input type="text" placeholder="주소를 입력해주세요." {...register('address')} />
                    {errors.address && <p>{errors.address.message}</p>}
                </div>

                <div>
                    <input
                        type="number"
                        placeholder="나이를 입력해주세요."
                        {...register('age', { valueAsNumber: true })}
                    />
                    {errors.age && <p>{errors.age.message}</p>}
                </div>
            </div>

            <button type="submit">submit</button>
        </form>
    );
}

 

 

 

코드를 작성하고 submit 버튼을 클릭하면 다음과 같이 올바르지 않은 입력 값에 대한 에러가 발생되는 것을 볼 수 있습니다.

 

객체 유효성 검증 화면

 

 

 

 

배열 유효성 검증

 

다음은 배열 유효성 검증입니다.

 

배열에 대한 유효성을 검증하고 싶을 땐 다음과 같이 소스 코드를 작성해볼 수 있습니다.

 

import { zodResolver } from '@hookform/resolvers/zod';
import { useFieldArray, useForm } from 'react-hook-form';
import { z } from 'zod';

interface Person {
    name: string;
    address: string;
    age: number;
}

interface UseForm {
    persons: Person[];
}

const schema = z.object({
    persons: z.array(
        z.object({
            name: z.string().min(1, { message: '이름은 필수값입니다.' }),
            address: z.string().min(1, { message: '주소는 필수값입니다.' }),
            age: z
                .number({
                    invalid_type_error: '나이는 필수값입니다.',
                })
                .min(1, { message: '나이는 1살부터 입력이 가능합니다.' }),
        }),
    ),
});

export default function Array() {
    const {
        register,
        formState: { errors },
        handleSubmit,
        control,
    } = useForm<UseForm>({
        resolver: zodResolver(schema),
        defaultValues: {
            persons: [],
        },
    });

    const { fields, append } = useFieldArray({
        control,
        name: 'persons',
    });

    return (
        <form onSubmit={handleSubmit((e) => console.log(e))}>
            <h2>Array Validation</h2>

            <div>
                {fields.map((_person, index) => (
                    <div key={index}>
                        <div>
                            <input
                                type="text"
                                placeholder="이름을 입력해주세요."
                                {...register(`persons.${index}.name`)}
                            />
                            {errors.persons?.[index]?.name && <p>{errors.persons?.[index]?.name?.message}</p>}
                        </div>

                        <div>
                            <input
                                type="text"
                                placeholder="주소를 입력해주세요."
                                {...register(`persons.${index}.address`)}
                            />
                            {errors.persons?.[index]?.address && <p>{errors.persons?.[index]?.address?.message}</p>}
                        </div>

                        <div>
                            <input
                                type="number"
                                placeholder="나이를 입력해주세요."
                                {...register(`persons.${index}.age`, { valueAsNumber: true })}
                            />
                            {errors.persons?.[index]?.age && <p>{errors.persons?.[index]?.age?.message}</p>}
                        </div>
                    </div>
                ))}
            </div>

            <button
                onClick={() =>
                    append({
                        name: '',
                        address: '',
                        age: 0,
                    })
                }
            >
                add
            </button>
            <button type="submit">submit</button>
        </form>
    );
}

 

 

 

그러면 다음과 같이 동적으로 추가되는 값들에 대한 유효성 검증을 한 번에 수행하는 것을 볼 수 있습니다.

 

배열 유효성 검증 화면

 

 

 

 

커스텀 유효성 검증 (1) - Refine

 

다음은 커스텀 유효성 검증입니다.

 

커스텀 유효성 검증은 zod에서 제공해 주는 검증 방식들인 min(), max() 등의 함수들을 활용하는 것이 아니라 개발자가 직접 필요한 검증 방식을 커스텀하는 것을 의미합니다.

 

그중 많이 볼 수 있는 것은 refine이 있습니다.

 

refine을 사용하게 되면 객체 내부의 값들 간의 관계를 이용한 유효성 검증을 수행할 수 있습니다.

 

예를 들면 비밀번호와 비밀번호 확인하는 값의 비교, 체크박스를 선택했을 때 값이 입력되었는지 검증 등이 있습니다.

 

 

 

이들 중 체크박스 선택 여부에 대한 검증을 간단하게 코드로 작성해 보겠습니다.

 

체크박스를 선택했을 때 값을 입력하지 않은 경우 에러 메시지가 나오는 코드는 다음과 같습니다.

 

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

interface School {
    name: string;
    address: string;
}

interface Person {
    name: string;
    age: number;
    hasSchool: boolean;
    school: School;
}

const schema = z
    .object({
        name: z.string().min(1, { message: 'person 이름은 필수값입니다.' }),
        age: z
            .number({
                invalid_type_error: 'person 나이는 필수값입니다.',
            })
            .min(1, { message: 'person 나이는 1살부터 입력이 가능합니다.' }),
        hasSchool: z.boolean(),
        school: z.object({
            name: z.string(),
            address: z.string(),
        }),
    })
    .refine((person) => (person.hasSchool ? person.school.name.length >= 1 : true), {
        message: 'school 이름은 필수값입니다.',
        path: ['school.name'],
    })
    .refine((person) => (person.hasSchool ? person.school.address.length >= 1 : true), {
        message: 'school 주소는 필수값입니다.',
        path: ['school.address'],
    });

export default function Refine() {
    const {
        register,
        formState: { errors },
        handleSubmit,
    } = useForm<Person>({
        resolver: zodResolver(schema),
        defaultValues: {
            name: '',
            age: 0,
            hasSchool: false,
            school: {
                name: '',
                address: '',
            },
        },
    });

    return (
        <form onSubmit={handleSubmit((e) => console.log(e))}>
            <h2>Refine Validation</h2>

            <div>
                <div>
                    <input type="text" placeholder="person 이름을 입력해주세요." {...register('name')} />
                    {errors.name && <p>{errors.name.message}</p>}
                </div>

                <div>
                    <input
                        type="number"
                        placeholder="person 나이를 입력해주세요."
                        {...register('age', { valueAsNumber: true })}
                    />
                    {errors.age && <p>{errors.age.message}</p>}
                </div>

                <div>
                    <label>
                        <span>school 입력 여부를 선택해주세요.</span>
                        <input type="checkbox" {...register('hasSchool')} />
                        {errors.hasSchool && <p>{errors.hasSchool.message}</p>}
                    </label>
                </div>
            </div>

            <div>
                <div>
                    <input type="text" placeholder="school 이름을 입력해주세요." {...register('school.name')} />
                    {errors.school?.name && <p>{errors.school.name.message}</p>}
                </div>

                <div>
                    <input type="text" placeholder="school 주소를 입력해주세요." {...register('school.address')} />
                    {errors.school?.address && <p>{errors.school.address.message}</p>}
                </div>
            </div>

            <button type="submit">submit</button>
        </form>
    );
}

 

 

 

 

코드를 위와 같이 작성하면 체크박스를 선택하지 않았을 때 다음과 같이 school 정보에 대한 유효성 검증이 수행되지 않습니다.

 

체크박스 미 선택 유효성 검증

 

 

 

하지만 체크박스를 선택하면 다음과 같이 school 정보 또한 유효성 검증 대상이 되는 것을 볼 수 있습니다.

 

체크박스 선택 유효성 검증

 

 

 

 

커스텀 유효성 검증 (2) - Transform

 

다음 커스텀 유효성 검증은 transform입니다.

 

transform은 말 그대로 변환하는 것을 의미하며 입력되는 값을 원하는 형태로 변경한 뒤 검증하고 싶을 때 활용할 수 있습니다.

 

예를 들어 다음과 같이 소스 코드를 작성한다면 주소 값에 "서울시"라는 값을 입력하더라도 transform 처리가 이루어져 값이 입력되지 않은 것으로 인식됩니다.

 

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

interface Person {
    name: string;
    address: string;
    age: number;
}

const schema = z.object({
    name: z.string().min(1, { message: '이름은 필수값입니다.' }),
    address: z
        .string()
        .transform((address) => address.replace('서울시', ''))
        .refine((address) => address.length >= 1, {
            message: '주소는 필수값입니다.',
        }),
    age: z
        .number({
            invalid_type_error: '나이는 필수값입니다.',
        })
        .min(1, { message: '나이는 1살부터 입력이 가능합니다.' }),
});

export default function Transform() {
    const {
        register,
        formState: { errors },
        handleSubmit,
    } = useForm<Person>({
        resolver: zodResolver(schema),
        defaultValues: {
            name: '',
            address: '',
            age: 0,
        },
    });

    return (
        <form onSubmit={handleSubmit((e) => console.log(e))}>
            <h2>Tramform Validation</h2>

            <div>
                <div>
                    <input type="text" placeholder="이름을 입력해주세요." {...register('name')} />
                    {errors.name && <p>{errors.name.message}</p>}
                </div>

                <div>
                    <input type="text" placeholder="주소를 입력해주세요." {...register('address')} />
                    {errors.address && <p>{errors.address.message}</p>}
                </div>

                <div>
                    <input
                        type="number"
                        placeholder="나이를 입력해주세요."
                        {...register('age', { valueAsNumber: true })}
                    />
                    {errors.age && <p>{errors.age.message}</p>}
                </div>
            </div>

            <button type="submit">submit</button>
        </form>
    );
}

 

 

 

 

Transform&nbsp; 유효성 검증

 

 

 

한 가지 인지해야 될 점은 transform은 말 그대로 값이 변환되는 것을 의미합니다.

 

즉, 유효성 검증이 통과되더라도 개발자가 확인되는 값은 transform이 처리된 이후의 값이니 참고하여 사용하시면 될 것 같습니다.

 

transform 유효성 검증 통과

 

 

 

 

 

 

 

 

이상으로 zod를 이용하여 react-hook-form 사용하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글