react-hook-form API 정리 & react-hook-form을 리액트 컴포넌트에도 사용하기

김은호·2023년 1월 26일
6

Intro

개인 프로젝트를 진행하면서 input의 렌더링 퍼포먼스를 높이기 위해 react-hook-form을 사용하였다. input을 생각보다 많이 사용해서 common Input Component를 만들어 사용하려는 과정에서 react-hook-form을 적용하려니 어려움을 겪었다.

이참에 수박 겉핥기식으로 알고있는 react-hook-form을 깊게 다루어 앞으로의 활용 능력을 키울려고 글을 작성한다.

react-hook-form

React Hook Form relies on an uncontrolled form, which is the reason why the register function captures ref and the controlled component has its re-rendering scope with Controller or useController. This approach reduces the amount of re-rendering that occurs due to a user typing in an input or other form values changing at the root of your form or applications.
// https://react-hook-form.com/faqs

간단히 정리하면 react-hook-form의 register 함수가 ref를 캡쳐하는 방식의 비제어 컴포넌트 방식을 이용하여 input의 리렌더링 횟수를 줄여주고, 제어 컴포넌트는 react-hook-fork의 controller / useController에서 리렌더링되는 영역을 지정해준다.

제어 컴포넌트 vs 비제어 컴포넌트: https://url.kr/k1oyf5

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

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);
   
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName", { required: true, maxLength: 20 })} />
      <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
      <input type="number" {...register("age", { min: 18, max: 99 })} />
      <input type="submit" />
    </form>
  );
}

위 코드는 react-hook-form 공식 사이트에 나와있는 예제 코드이다. 특징을 보면 input에 value property가 없는데, 기존에 state를 사용할 때는 value를 주어야 값이 업데이트가 되어 실시간으로 state를 볼 수 있는 방식과는 차이가 있다.

그 이유는 공식 사이트에서 설명한대로 ref를 토대로한 비제어 컴포넌트 방식으로 설계되었기 때문이다.

기존의 useState를 사용한 form 제어(제어 컴포넌트 방식)

react-hook-form을 사용하지 않는 많은 사람들이 사용하는 방식이라고 생각된다.

const [text,setText] = useState("");
return <input type="text" value={input} onChange={(e)=>setText(e.target.value)}/>

input이 하나일 때는 괜찮지만 여러 개라면 setter 함수의 이름을 정하는 것부터 난관이다.

useForm

useForm parameters

form을 쉽게 관리하는 API

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

interface IForm {
  occupation: string;
  id: string;
  name: string;
  pwd: string;
  email: string;
  phone: string;
}

function ReactHookForm() {
  const {
    register,
    formState: {errors},
    watch,
    reset,
    handleSubmit,
    getValues,
    setError,
    setFocus
  } = useForm<IForm>({
    mode: "onChange",
    defaultValues: {
      occupation: "student",
      id: "",
      name: "",
      pwd: "",
      email: "",
      phone: "",
    },
  });
  return ();

}

파라미터로 여러 옵션을 넣을 수 있는데, 가장 많이 사용하는 것은 mode와 defaultValues이다.

  • mode: 유저가 submit을 하기 전에 설정한 모드로 Validation 검사를 한다.
  • defaultValues: form 값에 대한 default 값을 지정, 동기/비동기 모두 지원한다.
    -> defaultValues: async () => fetch('/api-endpoint');

useForm Return Values

export type UseFormReturn<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
    register: UseFormRegister<TFieldValues>;
    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>;
    unregister: UseFormUnregister<TFieldValues>;
    control: Control<TFieldValues, TContext>;
    setFocus: UseFormSetFocus<TFieldValues>;
};

register

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

input or select 태그를 등록해서 유효성 검사 등을 활용할 수 있도록 한다.

파라미터로 name과 option을 받는데, name은 input이나 select element를 다루기 위한 key값으로 반드시 필요하다. useForm의 interface로 등록하는 것이 바로 이 name이다.
option은 Validation 검사를 위한 추가 옵션인데, required: true, minLength: 5 등의 여러 옵션을 지정할 수 있다.

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

register을 spread로 하는 것은 곧 input에 register의 반환값을 다 넣는 다는 것인데, 코드로 살펴보면 다음과 같다.

const { onChange, onBlur, name, ref } = register('firstName'); 
        
<input 
  onChange={onChange} // assign onChange event 
  onBlur={onBlur} // assign onBlur event
  name={name} // assign name prop
  ref={ref} // assign ref prop
/>
// 위와 같다.
<input {...register('firstName')} />

register로 onChange, onBlur 이벤트와 ref를 전달하여 비제어 컴포넌트 방식으로 실시간으로 form의 state를 확인할 수 있는 것 같다.

formState

form의 전체 state에 대한 정보를 담고 있다. 가장 많이 쓰는 프로퍼티는 errors와 isValid인데, 에러가 발생한다면 이곳에 에러 내용이 담긴다.
isValid는 form이 에러가 없다면 true이고, 있다면 false를 반환한다.

watch

form의 실시간 state를 볼 수 있다. 등록한 key값을 파라미터로 넣으면 그 element의 state만을 보고, 아무것도 넣지 않으면 전체 element의 state를 볼 수 있다.
input의 값이 올바르다면 특정 값을 리턴하는데에 쓸 수 있는듯하다.

{watch("Job") === "professor" ? (
  <Row>
    <Label>phone: </Label>
    <ControlInputText<IForm> control={control} name="phone" />
  </Row>
) : null
}

getValues

watch와 같이 state의 값을 가져오는 것이지만, 차이점이 있다면 input의 변화가 있다해도 리렌더링을 하지 않는다.
즉, 호출하는 시점의 값을 가져온다.
개발자가 몇 시 몇 분에 state의 상태를 가져오고 싶을 때 사용할 수 있는듯하다.

reset

버튼을 클릭하면 input의 값을 초기값이나 설정값으로 되돌릴 때 사용한다. 주로 정보 수정 등을 위해 기존의 데이터를 fetch로 불러올 때 사용한다.

export default function App() {
  const { register, handleSubmit, reset } = useForm();
  const resetAsyncForm = useCallback(async () => {
    const result = await fetch('./api/formValues.json'); // result: { firstName: 'test', lastName: 'test2' }
    reset(result); // asynchronously reset your form values
  }, [reset]);
  
  useEffect(() => {
    resetAsyncForm()
  }, [resetAsyncForm])

  return (
    <form onSubmit={handleSubmit((data) => {})}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      
      <input type="button" onClick={() => {
        reset({
          firstName: "bill"
        }, {
          keepErrors: true, 
          keepDirty: true,
        });
      }} />

handleSubmit, setError

submit 이벤트가 발생하면 register에서 등록한 form의 Validation 검사 시행되고, 통과되면 전체 form data를 불러온다. 이때 handleSubmit의 콜백함수로 우리가 정의한 onSubmit 이벤트를 등록해야한다. 추가 콜백함수로 Validation 검사에서 통과하지 못했을 때의 함수를 등록할 수 있는데, 주로 formState의 errors를 통해 활용하는 경우가 더 많은 듯 하다.

Validation을 통과해도 패스워드 일치 등의 추가 검사가 필요한 경우가 있다. 이때 setError을 사용하여 추가 검사를 통과하지 못하면 error을 발생하도록 할 수 있다.

const onValid = (data: IForm) => {
    if (data.password !== data.password1) {
      return setError(
        "password1", // register에 등록한 key 값
        { message: "Not Equal" },
        {
          // password1으로 focus됨
          shouldFocus: true
        }
      );
    }
  };

Controller

React Hook Form embraces uncontrolled components and native inputs, however it's hard to avoid working with external controlled component such as React-Select, AntD and MUI. This wrapper component will make it easier for you to work with them.

react-hook-form은 html input에 대해서만 지원해서 공통 Input을 위해 따로 작성한 react 컴포넌트나 MUI 같은 외부 컴포넌트에 대해선 지원하지 않는다. 그래서 나온 대안이 바로 Controller이다.

Controller API와 useController hook 모두 같은 기능인데, 활용은 useController가 더 쉽다. 위에서 사용한 방식과 비슷하기 때문이다. 따라서, useController을 토대로 글을 작성하겠다.

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

const {
  field: { value, onChange },
  fieldState: { isDirty, isTouched, error },
  formState,
} = useController(params);

useController의 기본 명세이다.

params

export type UseControllerProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
    name: TName;
    rules?: Omit<RegisterOptions<TFieldValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
    shouldUnregister?: boolean;
    defaultValue?: FieldPathValue<TFieldValues, TName>;
    control?: Control<TFieldValues>;
};
  • name: 해당 필드의 이름
  • rules: Validation 판정을 위한 규칙
  • defaultValue: 해당 필드의 기본값, 만약 defaultValue를 준다면 useForm에도 defaultValue를 주어 undefined가 나지 않도록 해야함
  • control: react-hook-form으로 제어할 수 있도록하는 객체

return values

export type UseControllerReturn<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
    field: ControllerRenderProps<TFieldValues, TName>;
    formState: UseFormStateReturn<TFieldValues>;
    fieldState: ControllerFieldState;
};

리턴 값은 모두 객체이다.

  • field: 해당 필드(요소)를 관리하는 객체로, 현재 컴포넌트의 값을 담는 value, 현재 컴포넌트의 값을 감지하는 onChange, onBlur 등이 있다.
  • formState: 해당 form의 상태를 관리하고, errors 등이 있다.
  • fieldState: 해당 field의 상태를 관리한다.

객체 프로퍼티로 무엇이 있는지는 공식 사이트를 참조하자.

https://react-hook-form.com/api/usecontroller

공통 컴포넌트에 적용하기

// components/InputText.tsx

import {
  Control,
  FieldPath,
  FieldValues,
  RegisterOptions,
} from "react-hook-form";

// useController을 사용하는 컴포넌트를 위한 type 지정
export type TControl<T extends FieldValues> = {
  control: Control<T>;
  name: FieldPath<T>;
  rules?: Omit<
    RegisterOptions<T>,
    "valueAsNumber" | "valueAsDate" | "setValueAs" | "disabled"
  >;
};

function InputText({control, name, rules}: TControl) {
  // field 객체의 프로퍼티인 value, onChange를 꺼내 input 태그와 연결
  const {field: {value, onChange}} = useController({name, rules, control});
  return <input className="input" value={value} onChange={onChange} />
};
  
export default InputText;

Omit<T, K>: 타입을 지정할 때 T를 받는데 거기서 K를 제외한 T만 받도록 한다.

type 지정 부분에 대해 자세히 다루면 다음과 같다.

공식 문서를 참조하면(https://react-hook-form.com/api/usecontroller/controller)
control, name, rules의 타입이 나온다(Control, FieldPath, RegisterOptions)

그런데, FieldValues는 무엇일까?
공식 문서에선 다음과 같이 정의한다.

export type FieldValues = Record<string, any>;

type을 지정할 때 key: value로 지정을 하는데, Record<T, K>라고 하면 T가 key, value가 K가 된다.

코드로 보면 다음과 같다.

type people = {
 firstname: string,
 lastname: string,
}

// 위와 같다.
type people = Record<'firstname' | 'lastname', string>
export type TControl<T extends FieldValues>

이 부분은 결국 T가 Record<string, any>를 받는다는 뜻이고, RegisterOptions는 결국 key값이 string, value는 any임을 알 수 있다.
required: 'true'같은 RegisterOptions의 한 예를 보면 쉽게 이해할 수 있다.

이제 공통 컴포넌트로 작성된 input에 useForm을 적용할 수 있다!

/* ... */
  const { control } = useForm({ defaultValue: { name:"name"}});
  return (
  	<form onSubmit={handleSubmit(() => {}}
      <InputText name="name" control={control} rules={{required:"입력해주세요"}} />
      <button>제출</button
    </form>
  );
}

useForm 이 리턴해주는 객체에서 control 객체를 꺼내서 InputText 의 control props 에 넣어준다.그리고 rules에 들어가는 객체는 기존에 register에 들어가는 option의 validation 객체와 같다.

마치며

useForm api만 사용해오다가, 코드의 재사용성을 위해 useController api에 대해서도 공부해보았다. 공통 컴포넌트에도 사용되지만 더 많이 활용되는 이유는 MUI같은 UI 라이브러리를 react-hook-form과 접목시킥 위해 사용한단다. 아직 MUI에 대해선 무지해서, 나중에 추가로 공부하여 또 정리해야겠다.
또 그 외에 여러 api가 있는데, 나중에 필요하다면 추가로 공부해야겠다.

출처

https://react-hook-form.com/
https://tech.osci.kr/2023/01/05/react-hook-form-with-mui/
https://velog.io/@leitmotif/Hook-Form%EC%9C%BC%EB%A1%9C-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0#%EC%97%AC%EB%9F%AC-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90-form%EC%9D%B4-%EC%9E%88%EC%9C%BC%EB%A9%B4-%EC%96%B4%EB%96%A1%ED%95%B4

0개의 댓글