react-hook-form

bible_k_·2023년 10월 3일
0
post-thumbnail

input들로 구성된 form을 만들 때는 기본적으로 각 input의 상태값을 관리해줘야하는데, 내가 이번에 구현한 페이지에서는 select까지 포함해서 input이 총 46개가 들어갔다.
useState를 사용한다면 각 input에 1:1로 대응되도록 state를 만들지 않고 객체 형태로 state로 관리한다고 해도 복잡한 건 마찬가지이다. 더불어 서버에 보내기 전 데이터 가공과 유효성 검사까지 진행하려면 꽤나 지겨워진다. 그래서 폼 관리를 간편하게 만드는 라이브러리인 React Hook Form을 통해 폼을 구현했다.

react-hook-form 의 장점

  • 간결하고 간편한 사용: React Hook Form은 코드를 간결하게 작성할 수 있도록 설계되어 있다. 폼 로직을 단순화하고 관리하기 쉽게 만들어준다.
  • 렌더링 최적화: React Hook Form은 렌더링을 최적화하기 위해 사용자 입력에 대한 리렌더링을 최소화한다. 이는 성능 향상에 도움이 된다.
  • 상태 관리: React Hook Form은 내부적으로 폼 상태를 관리하므로, 별도의 상태 관리 라이브러리를 사용하지 않아도 된다.
  • 다양한 입력 유효성 검사 지원: 다양한 유효성 검사를 수행할 수 있으며, Yup, Joi, zod 등과 통합하여 사용할 수 있다.
  • 커스텀 훅 및 컴포넌트와 통합: React Hook Form은 커스텀 훅과 컴포넌트와 쉽게 통합된다. 컴포넌트 간의 재사용성을 향상시키고 코드를 모듈화할 수 있다.
  • 서버로의 데이터 전송: 폼 데이터를 서버로 쉽게 전송할 수 있다.

TypeScript + react-hook-form

나는 타입스크립트와 함께 사용했고 공식문서에서 기본적인 사용법을 확인할 수 있다.

https://www.react-hook-form.com/ts/

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>
  );
}

register 메서드를 통해 input을 훅폼에 등록한다. register는 객체를 반환하기 때문에 스프레드 문법을 사용해서 넣어줘야하고, register의 인자로 input name을 넣어줘야한다.

위 예제에서는 확인할 수 없지만 register를 등록할 때
<input {...register(inputName)} />
inputName을 string 그 자체로 넣지 않는 이상 정확히 미리 정의한 formValue타입과 일치하지 않는다면 타입오류가 발생하기 때문에 꼼꼼하게 타입을 지정해줘야한다.

Input Name -> Submit Result
register("firstName") -> {firstName: 'value'}
register("name.firstName") -> {name: { firstName: 'value' }}
register("name.firstName.0") -> {name: { firstName: [ 'value' ] }}

<input {...register(inputName as keyof InputTypes, {
특히 formValue 객체의 뎁스가 깊고, map을 돌리면서 변수에 일부 조작하여 inputName을 넣어줘야하여 타입을 지정하기 어려운 상황이라면 위와 같이 타입 단언을 사용할 수 있다. (타입 단언은 TypeScript 컴파일러에게 해당 값이 원하는 타입임을 알려주는 데 사용된다.)

하지만 타입 단언을 사용하면 컴파일러의 타입 검사를 우회하게 되므로 잘못된 타입 단언은 런타임 오류를 유발할 수 있다. 때문에 타입 정의를 개선하거나 제네릭 타입을 활용하여 타입 안전성을 확보할 수 있는지 먼저 검토하고 필요한 경우에 사용해야 한다.

다양한 메서드

장점에서 설명했듯이 훅폼의 메서드를 통해 간단하게 validation을 적용할 수 있고 원하는 처리를 간단하게 해줄 수 있다.

import * as React from "react"
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit, resetField, } = useForm<FormValues>();
 
  const handleClick = () => resetField("firstName")


  return (
    <form>
      <input {...register("firstName", { required: true })} />
      <button type="button" onClick={handleClick}>
        Reset
      </button>
    </form>
  )
}

간단하게 resetField 메서드로 예를 들자면 말 그래도 field를 리셋해준다. 나는 등록된 이미지를 삭제할 때 사용했다.

https://react-hook-form.com/docs/useform
이외에도 공식문서에서 원하는 메서드를 쉽게 찾아볼 수 있다.

https://react-hook-form.com/docs/useform/register#options
특히 validation과 관련된 부분은 register option 부분에서 상세히 설명되어있다.

고민했던 부분1.

하나의 테이블에 input이 8개가 들어가는 테이블이 있었다. 그래서 각 input마다 error message를 표기하는 것은 ui상 적절하지 않았고, 그렇다고 submit 콜백함수에서 모든 필드를 수동으로 체크하는 것도 비효율 적이라고 판단했다. 무엇보다 8개의 input 중 하나라도 error가 있을 시 실시간으로 error message가 표시되도록 관리하고자 했다. 하지만 errors는 FormState의 객체로된 프로퍼티이기 때문에 useEffect를 통해 실시간 상태 변경이 불가능하다. 내부 값은 변경되어도 객체 그 자체는 불변하기 때문.

해결방법.

errors가 속한 FormState를 useEffect의 의존성 배열에 넣는 것이다. 단점은 errors외에 다른 FromState의 프로퍼티들이 변경될 때에도 상태변경이 일어난다. 하지만 콘솔 찍으며 계속 사용해본 결과 실제 에러가 발생했을 때 거의 상태변경이 일어나지 않았기 때문에 딱히 신경쓰일만한 정도는 아니었다. 이로써 테이블 formState에 상태변경이 생겼을 때 해당 input들에 error가 있는지 확인하고 에러메시지 state를 설정하는 방식으로 진행했다.

페북 다니는 개발자님이 옛날옛적 나와 같은 고민을 issued에 남겨주셨었었당 덕분에 해결!
https://github.com/react-hook-form/react-hook-form/issues/3455

const Table: React.FC<TableProps> = ({ tableResource, register, formState }) => {
  const [tableErrorMessage, setTableErrorMessage] = useState("");
  const { title, subtitle, theadList, tbody, unit, registerName, required } =
    tableResource;
  const { errors } = formState;

  //테이블 전체의 에러를 하나의 state로 관리
  useEffect(() => {
    for (let name of registerName) {
      const tableName = name.split(".")[0];
      const error = errors[tableName as keyof InputTypes];
      if (error) {
        setTableErrorMessage("필수 입력란을 작성해주세요.");
        break;
      }
      setTableErrorMessage("");
    }
  }, [formState]);

고민했던 부분2.

validation을 걸어놨는데 한번 submit을 하기 전까지는 실시간으로 에러메시지가 노출이 되지 않았다. 한번 submit후에야 즉각적으로 에러체킹이 되었다.

해결.

onChange모드로 설정하면 처음부터 value가 change 될 때마다 실시간 validation이 이루어진다.

const FormUploader: React.FC<FormUploaderProps> = () => {
  const { handleSubmit, watch, register, formState, resetField } = useForm<InputTypes>({
    mode: "onChange", //실시간 validation을 위해 onChange 모드 설정
  });

속성 활용

required - 공란으로 제출시 설정한 에러메시지를 errors.{fieldname}.message에서 가져와 활용할 수 있다.
pattern - 정규식을 통해 validation을 진행할 수 있다. 이 친구도 에러메시지 설정 가능하다.
valueAsNumber - input은 기본적으로 string으로 value가 저장된다. type이 number인 경우 이 속성을 사용하여 number타입으로 변환하여 저장할 수 있다.
setValueAs - 임의로 입력값을 조작하여 저장할 수 있다.

고민했던 부분3.

valueAsNumber가 계속 이상했다. boolean 값을 받는 친구인데, boolean 쓰면 에러 뜨고!! 공식문서에서도 원인은 못찾았지만 내가 예상하는 원인은 pattern과 함께 못쓰는 것 같다. pattern이랑 같이 쓰면 둘 중에 하나가 에러남!

해결방법.

내가 구현하고자 했던 것은 정규식을 활용한 validation과 number type으로 변환이었다. 위에 썼듯이 pattern과 valueAsNumber은 동시에 사용이 안되니 valueAsNumber은 대신 setValueAs를 이용했다. value를 인자로 받고 원하는 처리를 한 후 저장할 값을 return 하면 된다.

      <input
        id={id}
        type={type}
        className={twMerge(
          "block w-full h-[44px] p-[10px] border border-gray-300 rounded-lg bg-gray-50 text-[12px] text-gray-900 outline-none",
          errors[id] && "border-red-400",
        )}
        placeholder={placeholder}
        {...register(id, {
          required: required && "필수 입력란을 작성해주세요.",
          pattern: {
            value: validationRule || /.*/,
            message: "입력 형식이 올바르지 않습니다.",
          },
          setValueAs: (value) => handleSetValue(value, type),
        })}
      />
profile
후론트엔드 개발자

0개의 댓글