Web/React

React Hook Form 가이드

Jun_N 2022. 8. 2. 00:50

기존에 React Hook Form을 쓰면서도 잘 모르고 썻던 기능들을 정리하고 가이드 하고자 합니다.


공식문서

시작하기

 

시작하기

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

API

API 설명서

 

API 설명서

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com


React Hook Form이란?

React에서 form의 validation을 빠르고 쉽게 도와주는 라이브러리이다.

전체 폼이 리랜더링 되지 않으면서도 각각의 입력값 변화를 관찰할 수 있기에 성능도 빠르고 의존성 없이 쉽게 사용 가능하다.

  • 라이브러리 설치
npm install react-hook-form

필드 등록

const { register, handleSubmit } = useForm();

 

register는 해당 비제어 컴포넌트(uncontrolled component)의 값을 트래킹하고 Hook과 연결하여 validation 검사를 하기 위해 사용된다. ref에 register를 넘겨주면 된다.

 

*각각의 필드는 고유한 key로 사용되는 name 속성을 반드시 정의해줘야 한다. name은 배열도 허용된다. (name="name.firstName[0]”)

 

<form onSubmit={handleSubmit(onSubmit)}>
	<input type="text" **ref**={register} **name**="firstName" />
	<select name="gender" ref={register}>
	  <option value="female">female</option>
	  <option value="male">male</option>
	  <option value="other">other</option>
	</select>
</form>

 

*커스텀 등록

커스텀 컴포넌트와 Ref에 엑세스 할 수 없으면 수동으로 등록할 수 있다. 하지만 Custome register를 사용하면, 입력은 더이상 ref로 등록되지 않아서 setValue를 통해서 업데이트 해줘야 한다.

 

register('firstName', { required: true, min: 8 }) // 수동 등록
register({ name: 'firstName', **type: 'custom'** }, { required: true, min: 8 }) // 값을 업데이트 하는 동안 다시 렌더링 하도록 설정

 

handleSubmit

handleSubmit 메서드는 form 제출을 핸들링하는 메서드이다.

validation이 success이면 호출 하는 콜백과 fail일 때 에러와 함께 호출되는 콜백이 있다.

 

const onFormSubmit = data => console.log(data);
const onErrors = errors => console.error(errors);
 
<form onSubmit={handleSubmit(onFormSubmit, onErrors)}>
    {/* ... */}
</form>

 

 

Validation Check 및 Error Text 처리

  • required
  • minLength, maxLength (문자열 길이)
  • min, max (숫자)
  • type (input 필드의 타입. email, number, text 등)
  • pattern (regex 정규식)

 

<input name="firstName" ref={register({ required: true, maxLength: 20 })} />
<input name="lastName" ref={register({ pattern: /^[A-Za-z]+$/i })} />
<input name="age" type="number" ref={register({ min: 18, max: 99 })} />

// error 처리
<Input name="firstName" ref={register({ required: true })} />
{errors.firstName && "First name is required"}

자세한 규칙은 아래 링크를 확인해보자.

 

API 설명서

 

API 설명서

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

 

하나의 인풋에서 발생할 수 있는 모든 에러를 리턴하려면 errors.types 사용

 

const { register, formState: { errors }, handleSubmit } = useForm({
  // by setting criteriaMode to 'all', 
  // all validation errors for single field will display at once
  criteriaMode: "all"
});

<input
  type="password"
  {...register("password", { required: true, minLength: 10, pattern: /\\d+/ })}
/>
{/* without enter data for the password input will result both messages to appear */}
{errors?.password?.types?.required && <p>password required</p>}
{errors?.password?.types?.minLength && <p>password minLength 10</p>}
{errors?.password?.types?.pattern && <p>password number only</p>}

 

Schema 유효성 검사

YupSuperstructJoi을 활용하여 스키마 기반의 폼 유효성 검사를 제공한다.validationSchema  를 useForm에 넘겨주어 추가 설정을 할 수 있다.

 

// yup 설치
npm install @hookform/resolvers yup
// schema 정의 
const schema = yup.object().shape({
  firstName: yup.string().required(),
  age: yup.number().positive().integer().required(),
}).required();
// resolver에 schema yupRosolver 등록
const { register, handleSubmit, errors } = **useForm**({
  **resolver**: **yupResolver**(**schema**)
});

 

UI 라이브러리에 적용하기 (Controller)

React-SelectAntD그리고 MUI 같이 외부 라이브러리에서 제공하는 제어 컴포넌트와 함께 사용해야 할 때가 있다. 이 경우에는 register 메서드 대신 control이라는 객체를 사용한다.

 

{/* Option 1: pass a component to the Controller. */}
<Controller as={TextField} name="TextField" control={control} defaultValue="" />

{/* Option 2: use render props to assign events and value */}
<Controller
  name="MyCheckbox"
  control={control}
  defaultValue={false}
  rules={{ required: true }}
  render={props =>
    **<Checkbox
      onChange={e => props.onChange(e.target.checked)}
      checked={props.value}
    />**
  } // props contains: onChange, onBlur and value
/>

 

TypeScript에서 FormData 설정

TS Support

 

TS Support

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

 

Type을 정의해서 해당하는 Type만 value로 제한할 수 있다.

 

interface FormDataRq = {
  firstName: string;
  lastName: string;
};

const { register, setValue, handleSubmit, errors } = useForm<**FormData**>();

...
setValue("lastName", "luo"); // ✅
setValue("firstName", true); // ❌: true is not string

 

*추가 TIP

💡 Partial 유틸리티를 사용하면 ‘부분 함수’ 처리가 들어가서 꼭 들어가지 않아도 되는 type들에 대해 interface를 새로 정의하지 않아도 사용 가능하다. FormData 정의할 때 유용하게 사용 가능하다.

 

interface Address {
  email: string;
  address: string;
}

type MyEmail = Partial<Address>;
const me: MyEmail = {}; // 가능
const you: MyEmail = { email: "noh5524@gmail.com" }; // 가능
const all: MyEmail = { email: "noh5524@gmail.com", address: "secho" }; // 가능

---
const useFormed = useForm<Partial<IMatchProfileCareersRq>>({
  mode: 'onSubmit',
  resolver: yupResolver(careerFormSchema),
  defaultValues: {
    careerAttachFiles: [],
    profileCareers: [
      {
        companySn: undefined,
        companyName: undefined,
        employmentStatus: undefined,
        employmentType: undefined,
        endDate: undefined,
        jobGroupCode: undefined,
        jobTitleCode: undefined,
        performance: undefined,
        startDate: undefined,
      },
    ],
  },
});

API

API 설명서

 

API 설명서

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

 

useForm

 

const { register } = useForm({
  mode: 'onSubmit',
  reValidateMode: 'onChange',
  defaultValues: {},
  resolver: undefined,
  context: undefined,
  criteriaMode: "firstError",
  shouldFocusError: true,
  shouldUnregister: true,
})

 

mode: onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'

잡다에서는 주로 all을 사용한다.

  • all : 유효성 검사를 blur, change 이벤트에서 트리거한다. (성능이 안좋다.)
  • onSubmit : 제출할 때 유효성 검사를 체크한다. 유효하지 않은 입력은 onChange 이벤트 리스너로 연결하여 다시 확인한다. (default)

 

reValidateMode: onChange | onBlur | onSubmit = 'onChange'

입력의 재유효성 검사 설정한다. 기본적으로 입력이 변경될 때 유효성 검사가 트리거 된다.

 

defaultValues: Record<string, any> = {}

  • defaultValues는 커스텀 훅 안에서 캐싱되므로, defaultValues값을 초기화하고 싶으시면 reset API 를 사용

  • defaultValues에서 정의된 값은 watch의 defaultValue값으로 주입

  • defaultValue는 수동으로 등록된 인풋(예: register('test'))에는 자동으로 생성되지 않는다. 왜냐하면 커스텀 register를 사용하여 등록한 필드는 React Hook Form 에 ref를 전달하지 않기 때문이다.

 

const { register } = useForm({
  defaultValues: {
    firstName: "bill",
    lastName: "luo",
    email: "bluebill1049@hotmail.com",
    isDeveloper: true
  }
})

<input name="firstName" ref={register} /> // ✅ working version
<input name="lastName" ref={() => register({ name: 'lastName' })} />
// ❌ above example does not work with "defaultValues" due to its "ref" not being provided

⇒ register로 수동으로 등록했다면 defaultValue가 적용되지 않는다.!! 주의!

 

resolver

외부 유효성 검사 지원한다. Yup와 같은 리졸버 말고 custom resolver를 사용하고 싶다면 여러 제약조건이 있으니 확인해보길 바란다.

 

shouldUnregister

언마운트 되면 unregister한다. 언마운트 되도 입력 상태가 손실되지 않도록 하려면 false로 설정.

 

shouldFocusError

기본적으로 사용자가 폼을 제출하고 에러가 있는 경우, 에러가 있는 첫번째 필드에 포커스

  • ref로 등록된 필드만 작동합니다. 수동으로 등록하면 동작하지 않습니다. register('test') // 동작하지 않음

  • 에러가 발생한 입력값에 포커스되는 순서는 register된 순서


YUP 고도화 예시

 

const ValidationSchema = yup.object({
    birthDate: yup.string().required('생년월일은 필수값입니다.').matches(BIRTHDATE_PATTERN, 'YYYY-MM-DD 형식으로 입력해주세요'),
    military: yup.object().when('militaryCheckBox', {
      is: true,
      then: (schema) => yup.lazy((military) => {
        if (!military) return schema;
        if (military.status === null)
          return schema.shape({ status: yup.string().required('내용을 입력해 주세요.') });

        // 제대, 복무중인 경우에만 내부 필드 필수값
        if ([MilitaryStatus.IN_SERVICE, MilitaryStatus.DISCHARGE].includes(military.status))
          return schema.shape({
            status: yup.string().required('내용을 입력해 주세요.'),
            type: yup.string().required('군별을 입력해 주세요.').nullable(),
            militaryClass: yup.string().required('계급을 입력해 주세요.').nullable(),
            dischargeType: yup.lazy(() => (
              yup.string().when('military.status', (status: string, schema: any) => {
                if (military.status === MilitaryStatus.IN_SERVICE) {
                  return schema.nullable();
                }
                return yup.string().required('제대 구분을 입력해 주세요.').nullable();
              }))),
            startDate: yup.lazy((startDate) => (startDate ? yup.date() : yup.string().required('입대 일자를 입력해 주세요.').nullable())),
            endDate: yup.lazy((endDate) => (endDate
              ? yup.date().when('startDate', (startDate: string, schema: any) => (startDate ? schema.min(startDate, '제대 일자가 입대 일자보다 빠릅니다.') : schema))
              : yup.string().when('military.status', (status: string, schema: any) => {
                // 복무 중인 경우 종료일자 없음.
                if (military.status === MilitaryStatus.IN_SERVICE) {
                  return schema.nullable();
                }
                return schema.required('제대 일자를 입력해 주세요.').nullable();
              }))),
          });

        return schema.shape({ status: yup.string().required('내용을 입력해 주세요.') });
      }),
      otherwise: (schema) => schema,
    }),
    militaryCheckBox: yup.boolean(),
    disability: yup.object().when('profileDisabilityCheckBox', {
      is: true,
      then: (schema) => schema.shape({
        grade: yup.string().required('장애 등급을 입력해 주세요.'),
        type: yup.string().required('내용을 입력해 주세요.'),
      }),
      otherwise: (schema) => schema,
    }),
  });