React hook form 사용법

이수빈·2023년 9월 25일
1

React

목록 보기
4/20
post-thumbnail

제어컴포넌트 vs 비제어 컴포넌트

  • 제어컴포넌트 : React에 의해 값이 제어되는 컴포넌트, useState hook을 통해 데이터를 관리함.

  • 비제어 컴포넌트 : React에 의해 값이 제어되지 않는 컴포넌트, useREf를 통해 데이터를 관리함.

  • 리렌더링이 발생하지 않기 때문에 필요에 따라 활용가능함.

const [input1, setInput1] = useState("");
const [input2, setInput2] = useState("");
const [input3, setInput3] = useState("");
...
const [input8, setInput8] = useState("");
const [input9, setInput9] = useState("");
const [input10, setInput10] = useState("");
// 값이 100개라면...???


const [inputs, setInputs] = useState({
 input1:"",
 input2:"",
 ...
 input9:"",
 input10:"",
}
// 음 묶으니까 나은데 계속해서 렌더링이 되야하네...?                                     

React hook form

  • 제어컴포넌트로 form을 관리하면, 여러 state와 값을 변경하는 event Handler, 또 값을 검증하는 validation 이 존재함 => 코드양이 많고 관리하기 번거로움, 불필요한 리렌더링이 발생함.

  • react-hook-form 의 useForm hook을 사용하면 form을 쉽게 사용 할 수 있음, 주로 사용하는 props와 return 값들을 설명함

  • 기본적으로 동작하는 방식은 비제어(uncontrolled) 방식으로 동작함. 비제어방식에서는 register 함수를 통해 react-hook-form이 input에 대한 값들을 추적하도록 도와줌- (불필요한 렌더링을 줄임, 성능이 좋다)

  • Controller 방식을 사용하면 MUI와 같은 라이브러리와 함께 사용가능함. (보통 React 관련 라이브러리들은 Controlled 방식으로 동작하기 때문)

useForm props type & Return type

  • props type
export type UseFormProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = Partial<{
    mode: Mode;
    reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
    defaultValues: DefaultValues<TFieldValues>;
    resolver: Resolver<TFieldValues, TContext>;
    context: TContext;
    shouldFocusError: boolean;
    shouldUnregister: boolean;
    shouldUseNativeValidation: boolean;
    criteriaMode: CriteriaMode;
    delayError: number;
}>;
  • return type
export type UseFormReturn<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
    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>; // controlled 방식으로 동작
    register: UseFormRegister<TFieldValues>;
    setFocus: UseFormSetFocus<TFieldValues>;

};

props type => mode, defaultValues, resolver

mode, defaultValues

  • mode는 동작모드 설정 및 유효성 검사방법을 지정하는 option

  • onBlur(기본값) : 입력 필드의 유효성 검사가 입력 필드가 포커스를 잃을 때만 수행. 사용자가 입력 필드를 편집하고 다른 곳을 클릭하거나 탭할 때 유효성 검사가 실행됨. 이 모드는 사용자 경험을 향상시키고 입력 필드가 자주 변경되는 경우 유효성 검사를 줄이는 데 유용함

  • onChange : 입력 필드의 값이 변경될 때마다 즉시 유효성 검사가 수행됨. 사용자가 텍스트를 입력할 때마다 오류 메시지를 표시하거나 숨기고자 할 때 유용.(유효성 검사가 가장 많이 일어남 + 리렌더링 이슈 발생가능)

  • onSubmit : 폼 제출시에만 유효성 검사

  • defaultValues 는 기본값을 설정하는 역할 => 처음 렌더링될때만 적용, 이후에는 setValue 메서드를 사용해 동적으로 값 변경 가능
 const { register, handleSubmit, setValue } = useForm({
 	mode:"onChange",
    defaultValues: {
      fieldName1: 'Default Value 1',
      fieldName2: 'Default Value 2',
    },
  });

resolver

  • 비동기 유효성 검사를 수행하기 위해 사용함.

  • resolver는 폼 제출 시 실행되는 함수를 정의함. 이 함수는 입력데이터를 인자로 받아서 비동기적으로 유효성 검사를 실행하고 => Promise로 유효성 검사 결과를 반환함.

  • Promise가 resolve 되면 실행이 계속되고 아니면 중단됨.

  • react hook form과 함께 사용하는 resolver는 yup이 있음. 클라이언트단에서 yupResolver를 이용해 input값들을 서버로 보내기전 검증하는 과정을 거침.

  • 스키마를 정의해서 정의한 스키마 기반으로 정보 검증

  • shape 속성을 사용하여 해당 객체에 대한 검사조건을 설정함.

import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import * as yup from 'yup';

// Yup 스키마 정의
const schema = yup.object().shape({
  firstName: yup.string().required('First name is required'),
  lastName: yup.string().required('Last name is required'),
  email: yup.string().email('Invalid email address').required('Email is required'),
});

function MyForm() {
  const { control, handleSubmit, errors } = useForm({
    resolver: yupResolver(schema), // Yup Resolver를 사용하여 유효성 검사를 설정합니다.
  });

  const onSubmit = (data) => {
    // 유효성 검사를 통과한 데이터를 처리합니다.
schema.validate(data) 로 검사
성공시 .then(()=>{})
실패시 .catch((error) => {})

    console.log(data);
  };

return type => setValue, watch, register, handleSubmit, setError, setFocus

register

  • react-hook-form에 입력요소를 등록하는데 사용함. register하는 과정을 통해 해당 input 값을 제어하고, 값을 수집, 유효성 검사를 실행 가능 (함수형태임)

  • 첫번째 parmas는 name으로 해당 필드에 대한 key값

  • 두번째는 optional 객체이고, 유효성 검사를 위한 프로퍼티들을 넣을 수 있음.

  • onChange handler를 바꾸는 것도 가능. optional 객체에 onChange 프로퍼티를 override 해주면 됨.

<input
  type="text"
  {...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: "이메일 형식에 맞지 않습니다.",
    },
  })}
  • 참고) 6버전까지는 register를 ref에 등록하는 방식으로 사용함. 7버전부터는 spread operator 형식을 따라야함
   <input
        type="text"
        name="fieldName"
        ref={register} // 입력 요소를 등록합니다.
      />
          
 //공식문서
          
const { onChange, onBlur, name, ref } = register('firstName'); 
// include type check against field path with the name you have supplied.
        
<input 
  onChange={onChange} // assign onChange event 
  onBlur={onBlur} // assign onBlur event
  name={name} // assign name prop
  ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />

setValue, getValue, watch

  • setValue(name, value)형태로 사용함. key값에 대한 value를 등록함

  • watch : 폼에 입력된 값을 구독하여 실시간으로 체크 할 수 있게 해주는 함수. 매개변수를 주지 않으면 전체값을 관찰가능, 매개변수를 주면 해당 name의 값을 관찰가능함.

  • 해당값에 따라 리렌더링을 발생시킴 => 폼 내에서 값이 변경할때마다 해당 값이 변함

  • getValues : 값을 반환하지만, 리렌더링을 발생하지않고 해당 값을 추적하지 않음.(그 순간의 값만 가져옴)

handleSubmit, setError, setFocus

  • handleSubmit : submit 이벤트가 발생했을때, form태그에 onSubmit 이벤트 프로퍼티에 handleSubmit이라는 함수를 넣어주는 형태로 사용함

  • 파라미터는 미리 정의한 submit에 대한 이벤트핸들러 함수를 넣어주면됨. e.preventDefault() 사용할 필요 없음.

  • setError, setFocus : submit이벤트 핸들러 함수에서 에러가 발생했다면, setError 함수를 사용해 에러를 발생시킬 수 있고, 에러가 발생한 필드에 setFocus를 통해 강조하기 가능

import React from 'react';
import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit, setValue, getValue, watch, setError, setFocus } = useForm();

  // 폼 제출 핸들러
  const onSubmit = (data) => {
    const { username, password } = data;

    // 실제 로그인 로직은 여기에서 수행될 수 있습니다.
    // 예를 들어, 서버로 요청을 보내고 응답을 처리합니다.

    if (username === 'user' && password === 'password') {
      alert('로그인 성공!');
    } else {
      // 유효성 검사 오류를 설정합니다.
      setError('password', {
        type: 'manual',
        message: '잘못된 사용자 이름 또는 비밀번호',
      });

      // 입력 필드에 포커스를 설정합니다.
      setFocus('username');
    }
  };

  // 'password' 필드의 값이 변경될 때마다 호출되는 함수
  const onPasswordChange = () => {
    const password = getValue('password');

    // 비밀번호가 'password'인 경우 오류 메시지를 삭제합니다.
    if (password === 'password') {
      setError('password', {});
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="username">사용자 이름:</label>
        <input
          type="text"
          name="username"
          id="username"
          ref={register}
        />
      </div>

      <div>
        <label htmlFor="password">비밀번호:</label>
        <input
          type="password"
          name="password"
          id="password"
          ref={register}
          onChange={onPasswordChange}
        />
        {watch('password') === 'password' && (
          <p style={{ color: 'red' }}>비밀번호를 변경하세요!</p>
        )}
        {errors.password && (
          <p style={{ color: 'red' }}>{errors.password.message}</p>
        )}
      </div>

      <button type="submit">로그인</button>
    </form>
  );
}

export default LoginForm;

control

  • useForm의 반환한값중 control이라는 속성이 존재

  • Controller라는 컴포넌트에 이 값을 전달하면 제어 컴포넌트로 react-hook-form을 사용가능(UI 라이브러리 들과 함께 사용할때 사용함)

  • useController hook이나 Controller라는 API 두가지를 모두 사용해서 구현 가능 (훅이냐 컴포넌트냐 차이..)

  • useController hook 사용추천 (재사용가능하기때문)

// 공식문서 Controller 예제
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
import { TextField, Button } from '@mui/material';

function MyForm() {
  const { control, handleSubmit, reset } = useForm();
  
  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="name" // react-hook-form에서 사용할 필드 이름
        control={control}
        defaultValue="" // 초기값
        render={({ field }) => (
          <TextField
            label="이름"
            {...field}
          />
        )}
      />

      <Button type="submit">제출</Button>
    </form>
  );
}

export default MyForm;
  • Controller 컴포넌트를 사용하여 react-hook-form의 control을 전달하고, name, defaultValue, 그리고 UI 컴포넌트를 렌더링합니다.

  • UI 컴포넌트 (여기서는 TextField)는 render 함수 내에서 field 속성과 함께 전달됩니다. 이렇게 하면 react-hook-form이 필드를 관리하고 UI 컴포넌트와 연결됩니다.

  • 이제 TextField에서 사용자가 입력한 데이터는 react-hook-form에서 처리되고 제출할 때 onSubmit 함수로 전달됩니다. 이 방법으로 react-hook-form와 UI 라이브러리를 함께 사용할 수 있으며, 폼 필드를 간단하게 관리할 수 있습니다.

import ReactDatePicker from "react-datepicker"
import { TextField } from "@material-ui/core"
import { useForm, Controller } from "react-hook-form"

type FormValues = {
  ReactDatepicker: string
}

function App() {
  const { handleSubmit, control } = useForm<FormValues>()

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        control={control}
        name="ReactDatepicker"
        render={({ field: { onChange, onBlur, value, ref } }) => (
          <ReactDatePicker
            onChange={onChange} // send value to hook form
            onBlur={onBlur} // notify when input is touched/blur
            selected={value}
          />
        )}
      />

      <input type="submit" />
    </form>
  )
}

왜 Control 속성이 필요할까?

<input {...register("name", {option})}/>
<input {...register("name2", {option})}/>
<input {...register("name3", {option})}/>
<input {...register("name4", {option})}/>

function InputText() {
  return <input className="input"/>
};

<InputText {...register("name", {required: "반드시 입력해주세요."})}/> // error 
// prop값을 정의안해줌.. register함수의 prop을 extend하거나 다른 방법이 필요함.

useController 사용법

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>;
};
export type UseControllerReturn<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
    field: ControllerRenderProps<TFieldValues, TName>;
    formState: UseFormStateReturn<TFieldValues>;
    fieldState: ControllerFieldState;
};
  • control, name이라는 속성은 정보를 관리하는 부모컴포넌트로부터 받아옴.

  • name, control, rules 라는 parmas를 받음.

  • rules는 register option들.. 을 나타냄

import { TextField } from "@material-ui/core";
import { useController, useForm } from "react-hook-form";

function Input({ control, name }) {
  const {
    field,
    fieldState: { invalid, isTouched, isDirty },
    formState: { touchedFields, dirtyFields }
  } = useController({
    name,
    control,
    rules: { required: true },
  });

  return (
    <TextField 
      onChange={field.onChange} // send value to hook form 
      onBlur={field.onBlur} // notify when input is touched/blur
      value={field.value} // input value
      name={field.name} // send down the input name
      inputRef={field.ref} // send input ref, so we can focus on input when error appear
    />
  );
}

FormProvider, useFormContext

  • react hook form에서 prop drilling을 방지하기 위해 useFormContext와 FormProvider를 사용 할 수 있다.

  • FormProvider를 통해 react-hook-form 관련 메소드를 사용할 컴포넌트를 감싸고, useFormContext를 호출해서 메소드를 가져온다.

  • FormProvider는 React Context기반으로 만들어졌다. => Context기반이기 때문에 전역상태가 변경되면 리렌더링이 일어나지만, memo를 이용해 isDirty 즉 사용자가 react hook form의 상태를 수정했을때만 컴포넌트가 리렌더링이 발생하도록 최적화 할 수 있다.

import React, { memo } from "react"
import { useForm, FormProvider, useFormContext } from "react-hook-form"

// we can use React.memo to prevent re-render except isDirty state changed
const NestedInput = memo(
  ({ register, formState: { isDirty } }) => (
    <div>
      <input {...register("test")} />
      {isDirty && <p>This field is dirty</p>}
    </div>
  ),
  (prevProps, nextProps) =>
    prevProps.formState.isDirty === nextProps.formState.isDirty
)

export const NestedInputContainer = ({ children }) => {
  const methods = useFormContext()

  return <NestedInput {...methods} />
}

export default function App() {
  const methods = useForm()
  const onSubmit = (data) => console.log(data)
  console.log(methods.formState.isDirty) // make sure formState is read before render to enable the Proxy

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)}>
        <NestedInputContainer />
        <input type="submit" />
      </form>
    </FormProvider>
  )
}

ref) Blog : https://tech.osci.kr/introduce-react-hook-form
https://beomy.github.io/tech/react/react-hook-form/
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
https://tech.osci.kr/react-hook-form-with-mui/
공식문서 : https://react-hook-form.com/get-started#Registerfields
https://velog.io/@boyeon_jeong/React-Hook-Form-Controller-useController

profile
응애 나 애기 개발자

0개의 댓글