React-hook-form을 활용해 폼을 관리해보자.

HyunJoo·2023년 12월 11일
0
post-thumbnail

React-Hook-Form 라이브러리를 직접 사용해보고 공통 컴포넌트화 시키면서 알게 된 정보를 정리하려고 한다.

소개 : react-hook-form

라이브러리의 공식문서에 장점이 많이 나열되어 있다.
1. 유효성 검사 코드를 간편하게 작성할 수 있다.
1. 특히 유효성에 맞지 경우 error를 바로 확인할 수 있다.
2. state를 사용하지 않기 때문에 불필요한 리렌더링을 줄일 수도 있다.

'와~~~ 좋은 라이브러리에요' 가 아닌 이 라이브러리를 사용해서 UI 컴포넌트에 적용시키는 방법을 집중적으로 이야기하려고 한다.

💡typescript와 함께 설명이 되어 있습니다.

register

react-hook-form 라이브러리는 useForm 훅을 사용하는 것이 가장 쉬운 방식이다. useForm 훅이 리턴해주는 register 메서드를 input 엘리멘트의 prop로 내려주면 된다.

import { useForm } from 'react-hook-form';  
  
function SampleForm() {  
  const { register } = useForm();  
  return <input {...register('sample', { required: true })} />;
}  
  
export default SampleForm;

register : (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })

name : 위의 코드에서는 sample 고유한 이름을 등록했지만 name.firstName.0로 등록하여 {name: { firstName: [ 'value' ] }} 처럼 다수의 입력을 관리할 수도 있다.

RegisterOptions 으로 간단하게 유효성 검사를 할 수 있다.

자세한 내용은 공식문서를 참고하길 바란다.

UI 컴포넌트 적용

공식문서에는 React-SelectAntD and MUI로 설명이 되어 있지만 React-bootstrap으로 설명하겠다.

import { Form } from 'react-bootstrap';
import { useForm } from 'react-hook-form';  
  
function SampleForm() {  
  const { register } = useForm();  
  return <Form.Control {...register('sample')} />
}  
  
export default SampleForm;

너무 간단하게 사용할 수 있다. register 메서드의 리턴값을 풀어서 props로 넘겨주는 방식이다.

이것만으로는 입력값을 관리할 수는 없다.

입력값 확인

import { Form } from 'react-bootstrap';  
import { SubmitHandler, useForm } from 'react-hook-form';  
  
type Inputs = {  
  sample: string;  
};  
  
function SampleForm() {  
  const {  
    register,  
    handleSubmit,   
  } = useForm<Inputs>();  
  
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
  
  const onError: SubmitErrorHandler<Inputs> = (err) => console.log(err)
  
  return (  
    <form onSubmit={handleSubmit(onSubmit, onError)}>  
      <Form.Control {...register('sample', { required: true })} /> 
      <button type="submit">제출</button>  
    </form>  
  );  
}  
  
export default SampleForm;

갑자기 코드가 많이 늘었다. 겁먹지 말자.

form 엘리멘트의 onSubmit 어트리뷰트를 보자. useForm 훅이 리턴해주는 handleSubmit 메서드를 실행시켰다.

handleSubmit 메서드의 타입을 보자. (공식문서)

((data: Object, e?: Event) => Promise<void>, (errors: Object, e?: Event) => void) => Promise<void>

위 코드에서는 첫번째 인자로 onSubmit 함수를 넣었다. 두번째 인자는 옵션이지만 onError 함수를 넣었다.
sample의 값이 유효성을 통과하면 onSubmit 함수를 실행되고 통과하지 못하면 onError 함수가 실행된다.

이제 제출버튼을 누르면 값에 따라서 아래와 같은 오브젝트를 보여준다.

// onSubmit의 data (유효성을 통과하면)
{ sample: '입력값' }

// onError의 err (유효성을 통과하지 못하면)
{
  sample : {
    message: "",
    ref: input.form-control,
    type: "required"
  }
}

그럼 에러가 발생했을 때 onError 함수의 err를 state로 관리해서 사용하면 되는가?

아니다.

에러 표시하기

코드 중간 +로 추가한 코드를 표현했다.

import { Form } from 'react-bootstrap';  
import { SubmitHandler, useForm } from 'react-hook-form';  
  
type Inputs = {  
  sample: string;  
};  
  
function SampleForm() {  
  const {  
    register,  
    handleSubmit,   
+   formState: { errors }    
  } = useForm<Inputs>();  
  
  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
  
  const onError: SubmitErrorHandler<Inputs> = (err) => console.log(err)
  
  return (  
    <form onSubmit={handleSubmit(onSubmit)}>  
      <Form.Control {...register('sample', { required: true })} />  
+     {errors.sample && <span>{errors.sample.message}</span>} 
      <button type="submit">제출</button>
    </form>  
  );  
}  
  
export default SampleForm;

useForm이 리턴해주는 formState 오브젝트의 값 중 errors를 사용하여 에러를 표시할 수 있다.

erros 오브젝트는 다음과 같이 발생한다.

{
  sample : {
    message: "",
    ref: input.form-control,
    type: "required"
  }
}

어디서 많이 본 구조 아닌가? 맞다. onError 함수에서 콘솔에 찍힌 내용과 같다. 우리가 상태로 관리하는 것이 아닌 useForm 훅에서 리턴해주는 formState 오브젝트 안에 모두 담겨있다.

지금이야 입력이 sample 하나만 있지만 다수의 입력이 있다면 통과하지 못한 입력의 name만 나오게 되어 있다.

에러 컨트롤

에러는 언제 뜨게 되는가?

언제_에러가

제출 버튼을 누르고 (onSubmit 함수 실행 ) 이후에는 실시간으로 확인할 수 있다. onSubmit 함수 실행 전에는 errors 오브젝트는 빈 오브젝트이다.

렌더링은 정말 안되는가?

크롬의 리액트 개발자 도구를 이용해서 확인해보자. 비교군을 두기 위해 왼쪽이 일반적인 state로 관리하는 폼이고 오른쪽이 라이브러리를 사용한 폼이다. (좀 더 UI 답게 만들어보았다)

렌더가_안되요

일반 폼은 값을 입력할 때마다 렌더링이 발생한다. 하지만 React Hook FormonSubmit을 할 때만 발생한다.

도대체 왜?

리액트의 렌더링은 언제 발생하는가? state가 변경이 되면! 으로 쉽게 대답할 수 있다.
리액트에서 렌더링이 발생하지 않고 값만 변경하는 방법은 무엇이 있는가? useRef가 반환해주는 변경 가능한 ref 객체를 사용!

그렇다면 일반 폼이 렌더링 되는 이유는? state가 변경이 되어서!
그렇다면 React Hook Form은 왜 렌더링이 되지 않는가? useRef가 반환해주는 변경 가능한 ref 객체를 사용해서?

맞다. 왠지 감이 잡히지만 실제 코드를 보면 더 확실해진다.
아래 코드는 실제 코드와 엄청 다르다. 이해를 위해 간단하게 정리한 것이다.

// src/useForm.ts

// TFieldValues은 예시 코드의 Inputs를 제네릭으로 받은 것이다.
export function useForm<TFieldValues>(props) {
  const _formControl = useRef<UseFormReturn<TFieldValues> | undefined>();
  const [formState, updateFormState] = useState<TFieldValues>({
    errors : props.errors || {}
  })

  _formControl.current.formState = getProxyFormState(formState, control);
  return _formControl.current;
}

Custom Hook은 무언가를 return 하게 되어 있다. 마지막에 무엇을 리턴하였는가?

_formControl.current를 리턴하였다. _formControl은 useRef가 리턴하는 ref 객체이다.
그리고 errors 오브젝트는 formState 이라는 state 변수의 값이다.

위의 코드에서는 생략되었지만 입력의 값이 변경될 때는 state를 건드리지 않고 _formControl.current만 변경된다. 폼의 상태가 변경될 때 (updateFormState 함수가 호출될 때를 찾아보면 된다) 리렌더링이 발생하게 된다.

즉, 우리가 state를 통해서 값을 변경하던 것을 React Hook Form이 알아서 ref를 사용하게 해주는 것이다.

마치며

React Hook Form이 동작하는 방식은 정확하게 이해하지는 못했지만 적어도 왜 그렇게 하는지는 알게 되었다.
다음에는 register 메서드가 아닌 control 메서드와 Controller 컴포넌트를 사용해서 좀 더 세부적으로 값을 컨트롤 하는 방식을 알아보자.

profile
Front-End Engineer

0개의 댓글