react-hook-form & zod

Hoon·2024년 4월 13일
0

React

목록 보기
14/15
post-thumbnail

react-hook-form

react-hook-form : React 기반의 form 관리 라이브러리로, form 상태유효성 검사를 처리하기 위한 간편한 방법을 제공한다. 이를 통해 form component의 개발과 유지 보수를 용이하게 처리할 수 있다.

install

$ yarn add react-hook-form

기본 사용법

useForm hook을 사용하여 register 를 이용해 등록한다.

import { useForm } from "react-hook-form";

const { register, handleSubmit } = useForm<FormType>({ defaultValues: { email: '', password: ''}});

// validation 성공
const handleFormSubmit = (form: FormType) => {};

// validation 실패 
const handleFormError = (errors: FieldErrors<FormType>) => {};

<form onSubmit={handleSubmit(handleFormSubmit, handleFormError)}>
  	<input
      type="email"
      {...register("email", {
        pattern: {
          value: EMAIL_REGEX,
          message: "Email is invalid.",
        },
      })}
     />
    <input
      type="password"
      {...register("password", { required: "비밀번호는 필수" })}
    />
	<button type="submit" />
</form>

FormContext

간단한 입력 페이지같은경우 위의 하나의 컴포넌트내에서 useForm 으로도 충분하지만, 실제 비즈니스 로직을 작성하는경우 복잡한 form을 다루는 경우가 많기에. useFormContextFormProvider 를 사용해서 관리한다. (하위에서 값이 변경된다 하여도 전체 리렌더링 X)

import { FormProvider, useForm } from "react-hook-form";

const method = useForm<FormType>({
  defaultValues: { email: "", password: "" },
});

<FormProvider {...method} >
  {children}
</FormProvider>

위와 같이 선언하면, 하위 컴포넌트에서 아래와같이 사용할 수 있다.

// 타입을 명시해야 타입추론 가능
const { register } = useFormContext<FormType>();

watch, useWatch

기본적으로 react-hook-formstate로 관리하는것이 아닌 ref로 관리하므로 값이 변경되어도 리렌더링 시키지않는다.

watchuseWatch 를 사용하면 해당 값을 관찰하다가 리렌더링 시킬 수 있다.

import { useFormContext, useWatch } from "react-hook-form";

const { register, watch, control } = useFormContext<FormType>({
  defaultValues: { email: "", password: "" },
});

const email = watch('email');
// or
const email = useWatch({ control, name: 'email' });

여기서 둘의 차이점은 watchContext 내에서 사용할 경우, Context 전체를 리렌더링 시키고, useWatch 의 경우 해당 컴포넌트만 리렌더링 시킨다.

getValues, setValue

import { useFormContext } from "react-hook-form";

const { getValues, setValue } = useFormContext<FormType>();

// get
const email = getValues('email')

// set
const setEmail = (value: string) => {
	setValue('email', value);
};

getValues 를 사용하면 form에서 값을 가져올 수 있다. 하지만, 리렌더링이 되지않으므로 선언시점의 값만 가져올 수 있다.

setValue 를 사용하면 외부에서 form의 값을 변경해줄 수 있다.

Controller

이렇게 state 가 아닌 ref 를 통해서 상태를 관리하는 컴포넌트를 비제어 컴포넌트 라고 한다. 하지만, 기존에 만들어져있는 라이브러리를 사용한다던가, 회사의 디자인시스템을 사용할 경우 해당 컴포넌트가 제어 컴포넌트 라면 Controller 를 사용해서 react-hook-form 의 기능을 사용할 수 있다.

import { useFormContext } from "react-hook-form";

const { control } = useFormContext<FormType>();

<Controller
  control={control}
  name="email"
  render={({ field: { value, onChange } }) => (
    <CustomInput value={value} onChange={onChange} />
  )}
  />

useFieldArray

type FormType = {
	name: string;
  	friendList: {
    	name: string;
      	phoneNumber: string;
    }[]
};

위와 같이 배열을 관리하는 form 을 다뤄야 할 경우 useFieldArray 훅을 사용해서 아래와같이 관리할 수 있다.

import { useFormContext, useFieldArray } from "react-hook-form";

const { control } = useFormContext<FormType>();

const { fields, append, remove } = useFieldArray({ control, name: "friendList" });

// 추가
const handleFriendAdd = () => {
	append({ name: '', phoneNumber: '' });
}

// 제거
const handleFriendRemove = (idx: number) => {
	remove(idx);
}

return (
  <>
    {fields.map((item, idx) => (
      <div>
        <input {...register(`friendList.${idx}.name`)} />
        <input {...register(`friendList.${idx}.phoneNumber`)} />
      </div>
    ))}
    <button onClick={handleFriendAdd}>친구 추가</button>
    <button onClick={handleFriendRemove}>친구 삭제</button>
  </>
)

+++ Context 에서 동일한 배열에 대해 서로 다른 컴포넌트에서 useFieldArray 를 사용할 경우, append remove 와 같은 함수들이 다른 컴포넌트에선 적용이 안된다.
(ex : 입력 컴포넌트와 view 컴포넌트가 분리되어있고, 서로 동일한 useFieldArray를 사용한 경우)

errors

submit이 동작할 경우 미리 정의해둔 옵션에 따라서 validation check를 진행하게 되고, 통과했다면 첫번째 콜백이, 실패했다면 두번째 콜백이 실행된다. 이때 실패했을경우 formStateerrors 가 업데이트되어 아래와같이 error 처리를 진행할 수 있다.

import { useFormContext } from "react-hook-form";

const { register, handleSubmit, formState: { errors } } = useFormContext<FormType>();

// validation 성공
const handleFormSubmit = (form: FormType) => {};

// validation 실패 
const handleFormError = (errors: FieldErrors<FormType>) => {};

<form onSubmit={handleSubmit(handleFormSubmit)}>
  <input {...register("name", { required: "이름은 필수입니다." })} />
  {errors.name && <p>{errors.name.message}</p>}
  
  <button type="submit" />
</form>

zod

zod : 스키마 선언유효성 검사 라이브러리이다. typescript 의 유효성 검증은 컴파일시에만 발생하는데, zod 의 유효성 검증은 컴파일이 끝난 프로그램의 실행 시점에 발생한다.

intall

$ yarn add @hookform/resolvers
$ yarn add zod

schema, type 정의

zod 를 사용해 아래와 같이 schema 를 정의하고 type 으로 나타낼 수 있다.

import { z } from 'zod';

const ManSchema = z.object({
  name: z.string().min(1, { message: '이름을 추가해주세요' }),
  email: z.string().email(),
  age: z.number(),
  phoneNumber: z.string().optional()
});

type ManType = z.infer<typeof ManSchema>

또한 schema 정의시에 validation check 부분의 에러메시지도 작성할 수 있다.

react-hook-form 에 적용

작성한 schema를 react-hook-form 선언부에서 resolver 로 전달하게 되면, submit시에 해당 schema에 정의해둔대로 validation check를 진행하게 된다.

+++ (react-hook-form에서 ...register('email', { required: '...'}) 형식으로 추가하여도 무시된다.)

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

const method = useForm<ManType>({
  defaultValues,
  resolver: zodResolver(ManSchema),
});

validation check

react-hook-form의 submit을 하지않고도 해당 schema를 이용해 validation check를 진행해야하는 상황이 있을수도있다 (제출전 특정 케이스에서 필요할때)

try {
  ManSchema.parse(form);

  // ... 통과 로직 작성 ~
} catch (error) {
  // 통과 X
  if (error instanceof z.ZodError) {
    error.errors.forEach((err) => {
      // 에러 로직 작성
      setError(key, { message: err.message });
    });
  }
}

위와같이 정의한 schemaparse 함수를 통해 validation check를 진행하게 되고 실패시 에러를 반환한다.

refine, superRefine

schema 정의시에 다른값과 상호작용을 통해 validation check를 진행하는 케이스에는 refine 혹은 superRefine 을 통해 아래와 같이 처리할 수 있다.

const ManSchema = z
  .object({
    password: z.string(),
    passwordConfirm: z.string(),
  })
  .refine((value) => value.password === value.passwordConfirm, {
    message: '비밀번호가 일치하지 않습니다',
    path: ['passwordConfirm'],
  });

// or

const ManSchema = z
  .object({
    password: z.string(),
    passwordConfirm: z.string(),
  })
  .superRefine(({ password, passwordConfirm }, ctx) => {
    if (password !== passwordConfirm) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: '비밀번호가 일치하지 않습니다',
        path: ['passwordConfirm'],
      });
    }
  });

이렇게 react-hook-formzod 를 사용하면 복잡한 form 관리와 그에 대한 validation check 를 좀 더 쉽고 명확하게 작성할 수 있다.

profile
개발자 Hoon입니다

0개의 댓글