중첩된 컴포넌트에서 React Hook Form 현명하게 사용하기

modi·2023년 5월 3일
3

React

목록 보기
1/2
post-thumbnail

리액트로 캡스톤 프로젝트를 진행하며 react-hook-form을 도입하게 되었다!

기존에 노마드코더의 리액트 강의(다 듣지도 않았음..)에서 react-hook-form을 알게 되고, 이런 편리한 기능을 가진 라이브러리가 있다니!! 하며 놀란 기억이 있다. 그동안 프로젝트에서 사용하지 못하다가, 이번 캡스톤 프로젝트에서 혼자 프론트엔드 개발을 진행하게 되어 react-hook-form을 도입했다.

그래서 왜 또 굳이굳이 라이브러리를 도입하는데? 그냥 useState로 상태관리 열심히 하면 되잖아

맞다. 그냥 상태관리 빡세게(?) 하면 되지.... 역시 답은 하드코딩이지!! 그럼에도 해당 기술을 도입하게 된 이유는 이런 과정이 매우 귀찮기 때문이다.

리액트에서 form 상태 관리

나의 첫 번째 form 상태 관리

리액트 form 상태 관리에 어려움을 느낀 건 나의 첫 번째(!) 프론트엔드 프로젝트인 에코노삡 프로젝트였다. 책의 정보를 form 형태로 받아야 했는데, 정보가 무려 9가지나 된다.

사실 1차원적인 해결방법은 어렵지 않다. useState hook을 활용해 9개의 상태를 선언해주면 된다. 다만 form을 만들 때 우리는 상태만 필요한 것이 아니라는 점....

우리 모두 단순한 로그인에 사용되는 form을 상상해보자!

  • 아이디 input의 value
  • 비밀번호 input의 value
  • 아이디 input의 이벤트 함수
  • 비밀번호 input의 이벤트 함수
  • 아이디 input 유효성 검사에 따른 에러 처리: 아이디 입력 안 함, 아이디가 너무 짧음,...
  • 비밀번호 input 유효성 검사에 따른 에러 처리: 비밀번호 입력 안 함, 비밀번호가 너무 짧음, ...
  • submit 이벤트 에러 처리: 입력한 아이디와 비밀번호가 일치하지 않음, ...

input이 단 두 가지 뿐인데 해야 할 일이 많고 반복적이다.
상태가 9가지인 에코노삡의 코드는 개발을 진행한 내가 봐도 이해하기 어려웠다.

난 분명 공통되는 로직과 컴포넌트를 재사용하고 싶어서 리액트를 사용했는데 이슈가 해결되지 않았다. 컴포넌트의 가독성이 매우 낮고 복잡해 유지보수가 어려웠다. 리액트의 장점을 다 죽이고 있었다.

나의 첫 번째 프로젝트는 이렇게 끝났다. Javascript도 겉만 핥고 4개월 동안 리액트 학습과 동시에 진행한 프로젝트라 아쉬움이 컸다. 또한 프로젝트를 완성하고 싶은 욕심에 아예 처음부터 리팩토링을 시작했다.

처음과 리팩토링 후의 변화

  • 여러 input의 상태를 하나의 객체 state(inputInfo)에서 관리함
  • input의 이벤트 함수(e) => {setInputInfo({... inputInfo, new: e.target.value}로 통일
  • 리팩토링 전처럼 input마다 에러 처리는 따로 하지 않았지만 api 통신 후 alert 기능을 활용해 성공/실패 여부를 알림

문제점

  • 줄인다고 줄였지만 코드가 너무 길다
  • 컴포넌트 리렌더링이 잦다 (리액트는 state의 값이 변할 때마다 리렌더링이 발생함)
  • 처음보단 코드를 읽기 쉬워지고 재사용성을 고려해 컴포넌트를 설계했지만, 그럼에도 상태관리는 너무 귀찮다...
  • 이대로 이번에 회피한 에러 처리까지 하면 머리가 터질 것 같았다!!

form과 다시 만나다

이번 캡스톤 프로젝트은 form이 많이 사용된다. 그리고 디자인 단계부터 컴포넌트 재사용성을 고려해 반복되는 디자인이 특징이다. 수많은 form을 보고 이번 프로젝트에 react-hook-form을 도입하기로 결정했다.

react-hook-form이 뭔데?

react-hook-form은...

  • 사용하기 쉬운 유효성 검사를 통해 성능이 우수하고 유연하며 확장가능한 form을 제공한다
  • 비제어 컴포넌트 방식을 사용함으로써 사용자의 입력 혹은 form이나 애플리케이션의 루트에서 변경되는 다른 form 값으로 인해 발생하는 리렌더링의 양을 줄여준다
  • 리렌더링 횟수와 유효성 검사 계산최소화하고 마운팅 속도를 높인다
  • 작성해야 하는 코드의 양을 줄여준다
  • <form> 전체를 다시 렌더링하지 않고도 개별 <input> 입력form 상태 업데이트를 구독할 수 있다

컴포넌트를 살펴보며 react-hook-form 알아보자

아래의 코드는 emailusername을 입력받아 콘솔에 출력한다.

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

const Example = () => {
  const { handleSubmit, register, formState: { errors } } = useForm();
  const onSubmit = values => console.log(values);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="email"
        {...register("email", {
          required: "Required",
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: "invalid email address"
          }
        })}
      />
      {errors.email && errors.email.message}

      <input
        {...register("username", {
          validate: value => value !== "admin" || "Nice try!"
        })}
      />
      {errors.username && errors.username.message}

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

먼저 useForm 훅을 사용한다. configuration 옵션으로 modedefaultValues를 넘길 수 있다.

mode로 validation 전략을 설정할 수 있다. onSubmit, onChange, onBlur, all 등의 옵션이 있는데, mode가 onChange일 경우 다수의 리렌더링으로 성능에 영향을 끼칠 수 있어 주의해야 한다.

defaultValues로 form의 기본 값을 제공할 수 있다.

register

register을 사용해 input 또는 select 요소를 등록하고 유효성 검사 규칙을 적용할 수 있다.

<input
	type="email"
	{...register("email", {
      pattern: {
      value:
        /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
      message: "이메일 형식에 맞지 않습니다.",
    },
  })}
/>;

register의 첫 번째 매개변수는 name이다. 해당 필드를 다루는 key 값으로 필수이며 고유해야 한다. name은 숫자로 시작하거나 숫자를 key 이름으로 사용할 수 없다.

register의 두 번재 매개변수는 option 객체다. 해당 객체에는 유효성 검사를 위한 프로퍼티들이 들어갈 수 있다. 유효성 검사를 위한 value와 더불어 message로 구성된 객체를 사용해, 에러에 대한 구체적인 메세지를 제공할 수 있다.

formState

에러에 대한 정보는 formState 객체의 errors에 들어있다. 에러가 존재하지 않는다면 해당 객체는 비어있다.

이제 react-hook-form의 기본적인 동작 방법은 알게 되었다. 이제 프로젝트에 적용해보자!

form의 디자인 및 기능은 다음과 같다

  • label: input에 대한 설명
  • input
  • input에 입력된 값이 유효성 검사를 통과하지 못했을 경우, input의 border-colorred로 변함 + input 아래에 red 문장 나타남
  • submit 했을 때 필수임에도 입력되지 않은 input의 경우에도, input의 border-colorred로 변함 + input 아래에 red 문장 나타남
  • input 유효성 검사에 대한 설명이 필요한 경우, input 아래에 blue 문장 나타남

Form.jsx

useForm 훅을 매번 필요할 때마다 사용하는 것이 귀찮을 것 같아서 재사용이 가능한 Form.jsx 컴포넌트를 만들었다. 코드는 아래와 같다.

먼저 useForm 훅을 사용해 methods 객체를 생성했다. 필요한 프로퍼티만 선언하지 않고, 모든 함수와 객체를 리턴 받은 이유는 <FormProvider>를 사용하기 위해서다.

⭐️ FormProvider와 useFormContext ⭐️

useFormContext 훅을 사용하면 form context에 액세스할 수 있다. 이 훅은 context를 프로퍼티로 전달하는 것이 불편한 중첩된 구조에서 사용하기 위한 것이다.

useFormContext를 사용하기 위해서는 <FormProvider> 컴포넌트로 form을 감싸 주어야 한다.

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} > // pass all methods into the context
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <NestedInput />
        <input type="submit" />
      </form>
    </FormProvider>
  );
}

function NestedInput() {
  const { register } = useFormContext(); // retrieve all hook methods
  return <input {...register("test")} />;
}

LabeledInput.jsx

useFormContext 훅을 사용해 registergetValues를 props로 전달받지 않고 form context에 액세스할 수 있었다.

SignIn.jsx

Form.jsx를 사용해 로그인 페이지를 아래의 코드로 간단히 구현할 수 있다.

더불어 나는 기존에 prop type checking을 귀찮다는 이유로 하지 않았는데, 컴포넌트의 재사용을 쉽게 하기 위해서는 필요성을 깨닫고 PropTypes를 사용했다.

Form.jsx의 propTypes는 아래와 같다. arrayOfexact 기능을 사용해 객체로 이루어진 배열에 대한 type도 지정할 수 있었다.

Form.propTypes = {
  onSubmit: PropTypes.func.isRequired,
  onError: PropTypes.func,
  defaultValues: PropTypes.object.isRequired,
  inputInformations: PropTypes.arrayOf(
    PropTypes.exact({
      id: PropTypes.string.isRequired,
      label: PropTypes.string.isRequired,
      type: PropTypes.string.isRequired,
      required: PropTypes.object,
      requireMessage: PropTypes.string,
    })
  ).isRequired,
};

결론

react-hook-form을 이용해 form의 상태를 보다 쉽게 관리할 수 있었다. 또한 코드의 가독성이 기존의 방식보다 뛰어나다. 특히 에러 처리가 매우 쉬웠다. 웹 어플리케이션의 퍼포먼스뿐만 아니라 코드 작성이 편리하다. <FormProvider>useFormContext를 활용해 재사용할 수 있게 컴포넌트화도 가능하다. form을 많이 사용하는 경우 강추하고 싶다!

자세한 코드는 github에서 확인할 수 있다.

참고자료

react-hook-form 공식 홈페이지
react-hook-form 을 활용해 효과적으로 폼 관리하기
Typechecking With PropTypes

profile
FE 개발자 취업 준비의 준비생

0개의 댓글