react-hook-form 우아하게 사용하기(form 컴포넌트화)

HappyFrog·2023년 12월 19일
3

react-hook-form

목록 보기
3/3
post-thumbnail

기초적인 사용법에 대해서는 다루지 않습니다. 해당 사항을 찾으시는 경우 다른 레퍼런스나 공식문서를 참고 부탁드립니다

출발점

이전 포스팅에서 useController를 활용하여 재사용성 있는 제어 컴포넌트를 만든적이 있다. 해당 제어 컴포넌트를 사용해서 DX를 올릴 수 있었지만 여러 form을 만들다 보니 다음과 같은 문제점에 직면하게 되었다.

  • 첫째: form 별로 중복성 높은 코드가 자주 작성 되었다. 예를 들면, Input 별로 control을 일일히 prop으로 넘겨 주었어야 하는 등 유사한 코드를 중복해서 작성하는 일이 꽤 많이 생겼다

  • 둘째: 관심사 분리가 제대로 되지 않아 가독성이 많이 저하되는 문제가 발생 하였다. 예를 들어 Input 컴포넌트에 일일히 control prop을 전달하고, rules를 정의하며 Input 컴포넌트의 내에서 에러를 핸들링 하다보니 Input 컴포넌트의 고유 관심사(?)라고 할 수 있는 단순 데이터 입력 측면 보다는 복잡해져서 가독성이 많이 저하 되었고 이는 결국 form이 복잡해 질수록 DX를 저하시키는 요인이 되었다.

서론이 많이 길었지만 결국 개선을 해야 겠다고 마음먹은 요인은 정리하자면 가독성 개선중복성 개선이다

Form의 컴포넌트화

방법에 대해서 고민 하며 공식문서를 찾아보다 advanced usage를 보고 아이디어를 얻게 되었다(공식문서).
즉 form 자체를 컴포넌트화 하여 child들에게 control을 넘겨주는 방식으로 빼어서 해당하는 form의 컨텍스트를 전달하는 로직을 분리하여 관심사를 Form 커스텀 컴포넌트로 넘겨주는 방식이라고 할 수 있다

커스텀 Form 컴포넌트 코드

"use client"; // next.js(v13이상)에서 react이면 제거
import React, { DOMAttributes } from "react";
import { Controller } from "react-hook-form";
import type {
  FieldPath,
  FieldValues,
  UseControllerProps,
} from "react-hook-form";

type TForm<
  T extends FieldValues = FieldValues,
  K extends FieldPath<T> = FieldPath<T>
> = {
  onSubmit: DOMAttributes<HTMLFormElement>["onSubmit"];
  ruleDefinitions?: Record<keyof T, UseControllerProps<T, K>["rules"]>;
  control: UseControllerProps<T, K>["control"];
};

const Form = <
  T extends FieldValues = FieldValues,
  K extends FieldPath<T> = FieldPath<T>
>({
  children,
  onSubmit,
  ruleDefinitions,
  control,
  ...props // form(html tag)에 대한 속성들
}: React.PropsWithChildren<
  TForm<T, K> & React.FormHTMLAttributes<HTMLFormElement>
>) => {
  return (
    <form onSubmit={onSubmit} {...props}>
      {React.Children.map(children, (child) => {
        // typeguard
        if (!child) return null;
        if (!React.isValidElement(child)) return null;
	
        // props에 name 속성이 포함된 경우만 Controller
        // 그 외에(ex: 버튼)는 child 렌더링
        return child.props.name ? (
          <Controller
            control={control}
            name={child.props.name}
            rules={ruleDefinitions?.[child.props.name]}
            render={({ field }) => {
              return React.cloneElement(child, { ...field, ...child.props });
            }}
          />
        ) : (
          child
        );
      })}
    </form>
  );
};

export default Form;

이렇게 만든 위의 Form 컴포넌트는 child가 name prop(즉, 입력과 관련된 컴포넌트)인 경우 Controller 컴포넌트를 통해 렌더링 되며, 제출 버튼 같이 입력과 관계가 없어 Controller가 필요하지 않은 경우 해당 컴포넌트 그대로 렌더링 하게 된다.

만든 Form 컴포넌트의 사용

Form을 만들고 나면 사용은 간단하다

<Form
  control={control}
  onSubmit={handleSubmit(onSubmit)}
  ruleDefinitions={{ test: { required: "필수 데이터 입니다"} }}
>
   <InputText name="test" />
   <span>{errors.test?.message}</span>
   <button type="submit">submit</button>
</Form>

이전과 비교해보면 Input 컴포넌트를 Controller로 감싸던가, 커스텀 컴포넌트로 따로 만들지 않아도 간단히 작성이 가능하며, react-hook-form의 컨텍스트를 관리하는 관심사는 Form 컴포넌트에서 가져가 Input에 직접 작성하지 않아도 되어 보다 가독성이 올라갔다고 할 수 있다.

정리

UseController 커스텀 훅을 사용하는 것도 나쁘지 않은 방법이다. 하지만 위와 같이 form 구성이 복잡해서 고통을 받는다면 이 포스트의 방법도 좋을 것이라 생각한다

profile
성장하고 싶은 긍정 개구리

0개의 댓글