React-Hook-Form 파헤치기 (with yup)

sikkzz·2024년 6월 9일
0

React

목록 보기
11/12
post-thumbnail

🖐️ 시작하며

프로젝트를 제작하면서 생각보다 많은 양의 form들을 겪는 일이 빈번하게 생길 거라고 생각합니다.

저 또한 많은 form들을 개발해서 사용하고 있었는데 일반적인 state를 사용한 제어 컴포넌트로 form을 관리하다 보니 각 form들의 value값과 형식이 다 다르기에 재사용 가능한 로직으로 통일해서 사용하기 어려움이 있었습니다.

무엇보다 제어 컴포넌트의 가장 큰 단점인 리렌더링의 문제점을 해결하지 못하고 있었습니다. form의 리렌더링이 다른 컴포넌트에 영향가지 않도록 최대한 코드를 분리해서 작업하다보니 파일 수가 너무 많아짐을 경험했고 form을 입력할때마다 생기는 리렌더링 최적화에 어려움을 겪었습니다.

form을 일관성 있게 관리하면서 리렌더링 최적화까지 할 수 있는 방법이 없을까 하다가 알게된 라이브러리가 바로 react-hook-form이었습니다.

제어 컴포넌트, 비제어 컴포넌트 관련 내용은 해당 글을 참고하시기 바랍니다.

React-Hook-Form

React-Hook-Form은 React 기반 폼 관리 라이브러리입니다. 다른 폼 관리 라이브러리로는 Formik가 있습니다. npm trends로 확인해보면 React-Hook-Form이 2년정도 늦게 나왔음에도 불구하고 훨씬 많은 개발자들이 채택하고 있는 라이브러리입니다.

다음과 같은 장점들을 가지고 있습니다.

  • 간결하고 직관적인 Hook 기반 API
  • 리렌더링 및 Virtual DOM 업데이트를 최소화함으로써 높은 성능 지원
  • 내장 유효성 검사 및 외부 라이브러리를 통한 편리한 유효성 검사

React-Hook-Form은 기본적으로 비제어 컴포넌트 방식으로 구현되어 있기 때문에 렌더링 이슈에 대해서 자유롭습니다. 또한 context API 형태로 사용이 가능하기에 form의 데이터와 상태를 Provider 아래에서 props drilling 없이 사용할 수 있습니다.

자세한 내용 및 장점은 각 함수들을 살펴보면서 더 알아보겠습니다.

React-Hook-Form 공식 문서에서 제공하는 기본 예제 코드는 다음과 같습니다.

import { useForm, SubmitHandler } from "react-hook-form"

type Inputs = {
  example: string
  exampleRequired: string
}

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Inputs>()
  
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input defaultValue="test" {...register("example")} />
      <input {...register("exampleRequired", { required: true })} />
      {errors.exampleRequired && <span>This field is required</span>}
      <input type="submit" />
    </form>
  )
}

React-Hook-Form 라이브러리 코드를 확인해보면서 주요 함수들에 대해 알아보겠습니다.

코드는 React-Hook-Form 깃허브에서 확인할 수 있습니다.

useForm

useForm은 form 양식을 쉽게 관리할 수 있는 hook입니다.

props type

다음과 같은 props type을 가집니다.

export type UseFormProps<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
> = Partial<{
  mode: Mode;
  disabled: boolean;
  reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
  defaultValues: DefaultValues<TFieldValues> | AsyncDefaultValues<TFieldValues>;
  values: TFieldValues;
  errors: FieldErrors<TFieldValues>;
  resetOptions: Parameters<UseFormReset<TFieldValues>>[1];
  resolver: Resolver<TFieldValues, TContext>;
  context: TContext;
  shouldFocusError: boolean;
  shouldUnregister: boolean;
  shouldUseNativeValidation: boolean;
  progressive: boolean;
  criteriaMode: CriteriaMode;
  delayError: number;
}>;

주로 많이 사용하는 props들에 대해 간단히 설명해드리겠습니다.

mode

mode는 동작 모드 설정과 유효성 검사 방법을 지정합니다. onBlur, onChange, onSubmit 등이 있습니다. 다음과 같이 설정 가능합니다.

const { register } = useForm({
	mode: "onSubmit" // onChange, onBlur, onSubmit, all, onTouched
})

onBlur는 입력 필드의 유효성 검사가 입력 필드가 포커스를 잃을 때만 수행됩니다. input을 수정하고 있을 때는 유효성 검사가 이루어지지 않지만 다른 곳을 클릭하거나 input에서 focus가 벗어나면 유효성 검사가 실행됩니다.

onChange는 입력 필드의 값이 변경될 때마다 유효성 검사가 수행됩니다. 사용자 입력에 맞춰서 오류를 표시하거나 숨길때 유용합니다. 다만 사용자가 입력 및 삭제 할 때 마다 리렌더링이 발생합니다.

onSubmit(기본값)는 submit할 때만 유효성 검사가 일어납니다.

reValidateMode

reValidateMode는 첫 유효성 검사 이후 수행되는 유효성 검사에 대한 옵션입니다. onChange, onBlur, onSubmit 등이 있으며 동작 방식은 위와 동일합니다.

기본값으로는 onChange가 설정되어 있어서 따로 설정을 안하면 첫 유효성 검사 이후에는 onChange 옵션으로 적용되어 글자 입력마다 유효성 검사가 일어납니다.

defaultValues

defaultValues는 value들의 초기값을 설정할 수 있습니다. 렌더링시 적용되고 이후 value값은 set을 통해 수정이 가능합니다.

예를 들어 이름과 생일을 받는 form을 설정해준다 했을 때 다음 코드와 같이 초기값을 설정할 수 있습니다.

const { register } = useForm({ 
  defaultValues: { name: "홍길동", birthDay: "2000.00,00" }, 
});

resolver

resolver는 비동기 유효성 검사 수행을 위해 사용합니다. 폼 제출 시 실행되는 유효성 검사 함수를 정의하며 Promise를 통해 비동기적으로 유효성 검사를 실행하고 결과를 반환합니다.

주로 yup 라이브러리를 사용해서 유효성 검사를 진행합니다. yupResolver를 통해 input 값을 제출하기 전에 유효성 검사를 진행하게 됩니다.

yup 라이브러리는 JS 기반 객체 스키마 유효성 검사 라이브러리입니다.

import { yupResolver } from "@hookform/resolvers/yup";
import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form";

import * as yup from "yup";

const schema = yup.object({
  test: yup.string().required("텍스트를 입력해주세요"),
});

const App = () => {
	const { register } = useForm({ resolver: yupResolver(schema) });
  
  	const onSubmit: SubmitHandler<Inputs> = (data) => {
    	console.log(data);
  	};
  
  return (
      <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("test")} />
          <button type="submit">제출</button>

          {errors.test && <p style={{ color: "red" }}>{errors.test.message}</p>}
      </form>
    </div>
}

이렇게 작성 후 코드를 실행시켜보면 input을 비웠을때 작성한 schema의 유효성 검사가 일어나는걸 확인할 수 있습니다.

return type

다음과 같은 return type을 가집니다.

export type UseFormReturn<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues extends FieldValues | undefined = undefined,
> = {
  watch: UseFormWatch<TFieldValues>;
  getValues: UseFormGetValues<TFieldValues>;
  getFieldState: UseFormGetFieldState<TFieldValues>;
  setError: UseFormSetError<TFieldValues>;
  clearErrors: UseFormClearErrors<TFieldValues>;
  setValue: UseFormSetValue<TFieldValues>;
  trigger: UseFormTrigger<TFieldValues>;
  formState: FormState<TFieldValues>;
  resetField: UseFormResetField<TFieldValues>;
  reset: UseFormReset<TFieldValues>;
  handleSubmit: UseFormHandleSubmit<TFieldValues, TTransformedValues>;
  unregister: UseFormUnregister<TFieldValues>;
  control: Control<TFieldValues, TContext>;
  register: UseFormRegister<TFieldValues>;
  setFocus: UseFormSetFocus<TFieldValues>;
};

마찬가지로 주요 사용할 함수만 알아보겠습니다.

register

register는 form 입력 또는 선택 요소를 등록하거나 유효성 검사 규칙 적용 등에 사용합니다.

첫 번째 파라미터는 해당 필드에 대한 key값을, 두 번쨰 파라미터는 option 객체를 받습니다. option은 다음 옵션들이 있습니다.

ref

React element ref랑 동일한 기능을 합니다.

<input {...register("test")} />

required

양식 제출 시 입력 값이 필수일지를 결정하는 boolean 필드입니다. 오류 메시지를 반환하는 문자열을 할당할 수 있습니다.

<input
  {...register("test", {
    required: true
  })}
/>

maxLength

입력값의 최대 길이를 정하는 필드입니다.

<input
  {...register("test", {
      maxLength: 2
  })}
/>

minLength

입력값의 최소 길이를 정하는 필드입니다.

<input
  {...register("test", {
    minLength: 2
  })}
/>

max

입력값의 최대값을 정하는 필드입니다.

<input
  type="number"
  {...register('test', {
    max: 3
  })}
/>

min

입력값의 최소값을 정하는 필드입니다.

<input
  type="number"
  {...register("test", {
    min: 3
  })}
/>

pattern

입력값에 대한 정규식 패턴을 지정하는 필드입니다. 유효성 검사에 많이 사용됩니다.

<input
  {...register("test", {
    pattern: {
      value: /^[0-9]*$/,
      message: "숫자만 입력 가능합니다."
    }
  })}
/>

validate

콜벡 함수를 인자로 받아 유효성 검사를 하거나 콜백 함수 개체를 전달해서 모든 유효성 검사를 할 수 있는 필드입니다. required나 pattern의 유효성 검사와는 별개로 자체적으로 실행됩니다.

<input
  {...register("test", {
    // value에 대한 validate 예시
    validate: (value, formValues) => value === '1'
  })}
/>

<input
  {...register("test1", {
    validate: {
      // test1 필드에 하위 validate 적용
      positive: v => parseInt(v) > 0,
      lessThanTen: v => parseInt(v) < 10,
      validateNumber: (_, values) =>
        !!(values.number1 + values.number2), 
      checkUrl: async () => await fetch(),
    }
  })}
/>

valueAsNumber

숫자 value만 반환하게 하는 필드입니다. 에러시 NaN을 리턴합니다. validation 이전에 valueAs가 먼저 검증되며 input type number에만 사용 가능합니다.

<input
  type="number"
  {...register("test", {
    valueAsNumber: true,
  })}
/>

valueAsDate

Date 객체만 반환하게 하는 필드입니다. 에러시 Invalid Date를 리턴합니다. 동일하게 validation 이전에 valueAs가 먼저 검증됩니다.

<input
  type="date"
  {...register("test", {
    valueAsDate: true,
  })}
/>

이외에도 disabled, onChange, onBlur, value 등 옵션이 있습니다.

다음 코드와 같이 input에 register를 등록해 사용할 수 있습니다.

// react-hook-form version >=7 
const App = () => {
  const { register } = useForm();
  
  const onSubmit = (data) => {
    console.log(data);
  };
  
  return (
      <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("test", {
          	required: true,
          	pattern: {
            	value: /^[0-9]*$/,
                message: "숫자만 입력 가능합니다.",
            } 
          )} />
          <button type="submit">제출</button>
      </form>
    </div>
}

참고로 react-hook-form 6버전까지는 ref에 등록해 사용했지만 7버전 부터는 위 코드와 같이 spread 연산자를 사용해 등록합니다. 아래 코드는 6버전 register 등록 코드입니다.

// react-hook-form version =< 6
const App = () => {
  const { onChange, onBlur, name, ref } = register("test");
  
  return (
      <form onSubmit={handleSubmit(onSubmit)}>
          <input
    		onChange={onChange}
      		onBlur={onBlur}
      		name={name}
      		ref={ref}
    	  />
          <button type="submit">제출</button>
      </form>
    </div>
}

formState

formState에는 form 양식 상태에 대한 전체 정보가 포함되어 있습니다.

isDirty

useForm에서 defaultValue와 입력 값에 대한 변경이 있는지를 구분하는 boolean 필드입니다.

defaultValue와 변경 value 값이 같은지 여부를 반환합니다.

const {
  formState: { isDirty, dirtyFields },
  setValue,
} = useForm({ defaultValues: { test: "" } });

// isDirty: true -> test value 값이 change로 변경되었기 때문에 true
setValue('test', 'change')
 
// isDirty: false -> test value 값을 ""로 변경했지만 defaultValue와 동일하기에 false
setValue('test', '')

touchedFields

사용자가 상호작용한 모든 입력을 포함하는 객체입니다.

defaultValues

useForm에 의해 설정된 초기값을 나타냅니다.

isLoading

비동기 초기값을 받아올 경우 true, 그렇지 않으면 false를 반환하는 필드입니다.

isValid

양식에 유효성 검증 오류가 없을 경우 true, 그렇지 않으면 false를 반환하는 필드입니다.

errors

필드에 대한 에러 정보가 들어있는 객체입니다.

handleSubmit

handleSubmit은 submit 이벤트가 발생했을 때 form 태그 onSubmit 이벤트로 사용합니다. 유효성 검사 통과시 데이터를 넘길 수 있습니다.

import React from "react"
import { useForm, SubmitHandler } from "react-hook-form"

type FormValues = {
  firstName: string
  lastName: string
  email: string
}

export default function App() {
  const { register, handleSubmit } = useForm<FormValues>()
  const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input type="email" {...register("email")} />

      <input type="submit" />
    </form>
  )
}

reset

전체 양식 상태, 필드 참조 등을 초기화합니다. 인자에 따라서 전체 초기화, 특정 필드 초기화 등이 가능합니다.

reset(); // 전체 양식 초기화

reset({ test: "abc" }); // test 필드를 abc defaultValue로 초기화

trigger

form 양식 또는 유효성 검사를 수동으로 실행시켜줍니다.

trigger() // 모든 필드에 대한 유효성 검사 실행

trigger("test") // test 필드에 대해 유효성 검사 실행

trigger(["test"]) // test 배열 필드에 대해 유효성 검사 실행
import React from "react"
import { useForm } from "react-hook-form"

type FormInputs = {
  firstName: string
  lastName: string
}

export default function App() {
  const {
    register,
    trigger,
    formState: { errors },
  } = useForm<FormInputs>()

  return (
    <form>
      <input {...register("firstName", { required: true })} />
      <input {...register("lastName", { required: true })} />
      <button
        type="button"
        onClick={() => {
          trigger("lastName") // lastName 필드 유효성 검사 실행
        }}
      >
        Trigger
      </button>
      <button
        type="button"
        onClick={() => {
          trigger(["firstName", "lastName"]) // firstName, lastName 필드 유효성 검사 실행
        }}
      >
        Trigger Multiple
      </button>
      <button
        type="button"
        onClick={() => {
          trigger() // 전체 필드 유효성 검사 실행
        }}
      >
        Trigger All
      </button>
    </form>
  )
}

control

Controller 컴포넌트 내부에서 사용되는 옵션입니다. Controller 컴포넌트에 control props를 전달하면 form을 제어 컴포넌트 형식으로 사용할 수 있습니다.

일반적인 input 활용 비제어 컴포넌트에 사용하기 보다는 주로 커스텀 컴포넌트나 외부 UI 라이브러리들과 함께 사용할 때 사용합니다. dropdown, calendar등의 컴포넌트를 react-hook-form으로 구성할때나 AntD, MUI 등 UI 라이브러리에 적용시킬 때 주로 사용합니다.

공식 문서에서 제공하는 예제 코드는 다음과 같습니다.

import React from "react"
import { useForm, Controller } from "react-hook-form"
import { TextField } from "@material-ui/core"

type FormInputs = {
  firstName: string
}

// controller
function App() {
  const { control, handleSubmit } = useForm<FormInputs>()
  
  const onSubmit = (data: FormInputs) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        as={TextField}
        name="firstName"
        control={control}
        defaultValue=""
      />

      <input type="submit" />
    </form>
  )
}

// register
const App = () => {
	const { register } = useForm();
  
  	const onSubmit = (data) => {
    	console.log(data);
  	};
  
  return (
      <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("test", {
          	required: true,
          	pattern: {
            	value: /^[0-9]*$/,
                message: "숫자만 입력 가능합니다.",
            } 
          )} />
          <button type="submit">제출</button>
      </form>
    </div>
}

기존에 useForm의 register를 사용할 때와 코드를 비교해보면 한눈에 차이점을 파악하실 수 있습니다.

input 대신 MUI 라이브러리의 TextFiled를, register에서 사용하던 option field들을 Controller 컴포넌트의 control props를 통해 전달해준다는 차이점이 있습니다.

react-hook-form에서는 control 사용법을 useController hook을 사용하거나 Controller 컴포넌트를 직접 사용하는 방식 2가지 방법을 제공합니다.

재사용 가능한 hook으로 사용하냐, 일반적인 컴포넌트로 사용하냐 차이가 존재합니다. 자세한 내용은 useController hook, Controller 컴포넌트와 함께 더 알아보겠습니다.

이외에도 useForm에 사용되는 많은 옵션들이 있지만 자세한건 공식 문서에서 확인하시기 바랍니다. 주로 사용할 옵션들만 간단히 알아봤습니다.

useController

useController는 Controller 컴포넌트 대신 재사용 가능한 hook으로 사용할 수 있게 제공하는 hook입니다. 주로 재사용 가능한 제어 컴포넌트에 많이 사용합니다.

props type

다음과 같은 props type을 가집니다.

name

양식에 대한 고유 key 값입니다. register 양식에서 test를 key 값으로 사용하던 방식과 동일합니다.

<input {...register("test")} />

<Controller name="test" />

control

useForm에서 사용하던 register와 동일한 역할을 합니다. 마찬가지로 여러 option field들을 가지고 있습니다.

<input {...register} />

<Controller control={control} />

defaultValue

useForm에서 사용하던 defaultValue와 동일합니다. 초기값을 정할 수 있으며 null이나 빈 문자열로 사용해야 합니다. undefined 형식은 사용할 수 없습니다.

rules

useForm에서의 register 옵션과 동일한 형식의 유효성 검사를 제공합니다. required, max, min등 위에서 다룬 register와 동일하게 유효성 검증이 가능합니다.

return type

다음과 같은 return type을 가집니다.

onChange

입력값들을 library로 보내는 역할을 합니다. 값이 undefined일 수 없으며 formState를 업데이트 해주는 역할을 합니다

onBlur

입력의 onBlur 이벤트를 library로 보내는 역할을 합니다.

value

컴포넌트의 value(값)입니다.

name

key값으로 사용되는 고유값입니다.

ref

form의 입력과 연결하기 위해 사용하는 ref입니다.

공식 문서에서 제공하는 controller의 예제 코드는 다음과 같습니다.

import ReactDatePicker from "react-datepicker"
import { TextField } from "@material-ui/core"
import { useForm, Controller } from "react-hook-form"

type FormValues = {
  ReactDatepicker: string
}

function App() {
  const { handleSubmit, control } = useForm<FormValues>()

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        control={control}
        name="ReactDatepicker"
        render={({ field: { onChange, onBlur, value, ref } }) => (
          <ReactDatePicker
            onChange={onChange} 
            onBlur={onBlur} 
            selected={value}
          />
        )}
      />

      <input type="submit" />
    </form>
  )
}

useFormContext

useFormContext는 각 form 요소들의 props drilling을 줄이기 위해 react-hook-form에서 context api를 사용하게 해주는 hook입니다.

공식 문서에서 제공하는 사용법은 다음과 같습니다.

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

export default function App() {
  const methods = useForm()
  const onSubmit = (data) => console.log(data)

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <TestInput />
        <input type="submit" />
      </form>
    </FormProvider>
  )
}

function TestInput() {
  const { register } = useFormContext() 
  return <input {...register("test")} />
}

사용법은 일반적인 useContext 사용법과 동일하며 FormProvider로 요소를 감싸주고 하위 컴포넌트에서 useFormContext 호출을 통해 사용 가능합니다.

🔚 마치며

우선 react-hook-form 라이브러리에 대해 자세히 알아보았습니다. 이렇게 이론적인 내용만 보다보니 확 와닿지 않는 부분들이 클거라 생각합니다.

다음에는 이 react-hook-form을 프로젝트에 직접 적용시켜서 개선한 form에 대해 알아보겠씁니다.

감사합니다.

참조

react-hook-form 공식 문서
https://react-hook-form.com/

react-hook-form npm 문서
https://www.npmjs.com/package/react-hook-form?activeTab=code

react-hook-form 공식 github
https://github.com/react-hook-form/react-hook-form

profile
FE Developer

0개의 댓글