프론트 공부

react hook form으로 폼 다루기 (에러 해결! Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? )

홍구리당당 2023. 12. 31. 19:24

0. 오늘의 배울 것

리액트로 프론트 개발을 하다보면 input 을 다룰 일이 정말 많은데, 최근에 이 인풋을 짱 편하게 다룰 수 있게 하는 api를 알아냈다. 바로 react hook form!!!!! 라는 폼 관리 라이브러리다.

각 input 마다 useState로 값을 맞춰야 하고, 정규식을 각각 둬서 validation도 체크해야 하고, 값이 있는지 없는지도 체크해야 하는 등 여러 경우의 수가 많은데, 이걸 단번에 체크해주는 게 바로 react hook form이었다.

이번 포스팅에서는 로그인, 회원가입 폼을 만들면서 react hook form을 공부했던 내용에 대해 적어보고자 한다.

기능에 집중하기 위해 css 적인 부분은 빼고, js 코드만 보겠다.

1. 왜 react hook form을 쓸까?

리액트에서 폼을 관리하는 라이브러리는 여러 가지가 있다. 대표적인 걸론 react hook form, formik, rc filed form 정도가 있겠다.

  • 1, 다운로드 횟수) react hook form과 formik가 압도적으로 많다. 이 둘의 github star 개수는 각각 35k, 32k 정도이다.
    1. 라이브러리 크기) react hook form은 26.6kb 정도인데 반해 다른 라이브러리는 두 배 이상 크기가 크다.
    1. 성능) 리렌더링 횟수도 react hook form이 적고, 개발자가 적어야 하는 코드 양도 적다.
  • 4) 타입스크립트를 지원하는가?) 보통 ts로 플젝을 하고 있어서, 이것도 중요한 판단 기준 중 하나였다. react hook form은 다행이 ts를 지원한다.

리액트 훅 폼은 관리가 꾸준히 되고 있는 라이브러리라 써도 좋을 것이란 판단!!

2. 사용하기

  1. 설치
    npm install react-hook-form
    혹은
    yarn add react-hook-form
  1. import 해오기
import { useForm } from "react-hook-form";

export default function SignForm () {
    return (
        <form>
              <label>이메일</label>
              <input type="text"/>

            <label>비밀번호</label>
               <input type="password"/>

             <button>제출</button>
          </form>
    );
}
  1. 필요한 메소드와 속성 찾아보기.

참고로 공식문서에서 보여주는 기능은 이정도로 많다.

useForm, useController, useFormContext, useWatch, useFormState, useFieldArray 이렇게 hook이 있고, 각 hook에서 제공하는 속성과 메소드들도 여러 개가 있다.
단순히 useForm에서 register, handleSubmit 이 두 속성만 가져와도 바로 회원가입 폼 기능은 구현할 수 있지만 이 포스팅에선 공부를 위해 최대한 많은 기능을 활용해보도록 하겠다.

useForm에게 줄 수 있는 인자는 다음과 같다,

useForm({
            mode: form submit 이전 인풋에서 이벤트가 일어날 때 onBlur일 때 값을 validation check할지, onChange일 때 체크할지 선택하게 해줌. 만약 onChange를 선택했다면, input에서 onChange 이벤트가 일어날 때마다(즉 키를 입력할 때마다) 인풋 값을 체크한다.
            defaultValues: 객체 형태로 적어야 함. input 태그가 email, password, passwordRepeat 이렇게 있다면 defaultValues에 들어갈 값은 {email:"원하는 값", password:"원하는 값", passwordRepeat:"원하는 값",} 이다. react hook form을 사용하는데 defaultValues 인자를 주지 않으면 input 초기값은 undefined로 관리되므로, 웬만하면 써주자.
            delayError, resolver, context, resetOptions, errors, values, reValidateMode, criteriaMode, shouldeFocusError, shouldUseNativeValidation, shouldUnregister ... : 다양한 옵션이 있지만 여기선 안 쓸 거니까 패스.   
})

useForm 훅을 통해 리턴받는 값들은 다음과 같다.

const {
    register: register은 name, options(required, maxLength, minLength, max, min, pattern, validate, valueAsNumber) 등을 받아 onChange, onBlur, ref, name 값을 리턴한다. 자세한 건 아래 예시에서 보자.
      formState: 인풋의 validation을 검사해서 error를 감지하면, formState 객체의 errors 속성에 담긴다. 또한 몇 번 submit을 시도했는지 알려주는 submitCount, 현재 인풋 값이 유효한지 확인하는 isValid 등의 속성이 있다.
      watch: 폼에 입력된 값을 실시간으로 체크해준다. getValues는 메소드가 실행된 시점에서의 input 값을 읽지, 실시간으로 렌더링되진 않는다. (react hook form은 비제어 컴포넌트임) 그래서 실시간 체크가 필요하다면 watch를 쓰자.
  reset: 인풋 안의 값을 리셋시킬 때 쓰는 메소드.
  handleSubmit: 폼을 제출할 때 등록할 메소드.
  setValues: 입력폼 바깥에서 값을 변경하려면 setValues 메소드를 쓴다.
  getValues: 폼의 값을 읽을 때 사용하며, watch와 다르게 리렌더링을 일으키지 않는다. 
  setError: 에러 관련 설정에서 사용되는 함수들. 
  setFocus: 인풋에 focus될 때 사용되는 함수들. 

  unregister,clearErrors, getFieldState, trigger, control, Form
} = useForm<SignFormType>({ ... })
  1. 회원가입 폼 코드 짜기.
/* signun 페이지에 사용할 Form 컴포넌트*/

import { useForm } from 'react-hook-form'
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
// ...
import PasswordInput from '@/components/signInput/PasswordInput'
import TextInput from '@/components/signInput/TextInput'
import {
  emailValidationRules,
  nicknameValidationRules,
  passwordValidationRules,
} from '@/utils/formInputValidationRules'

export default function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    getValues,
    // onChange 나 all mode는 성능 저하가 일어날 수 있어 onBlur로 설정.
  } = useForm<SignUpInputsType>({ mode: 'onBlur' })

  const { mutate } = useMutation({
    mutationKey: ['create-user-key'],
    mutationFn: (data: SignUpDataType) => createUser({ data: data }),
    onSuccess: () => {
      alert('가입이 완료되었습니다!')},
  })

  const onSubmit = () => {
    // getValues("id") 로 input의 값을 가져와 request를 보내는 로직.
    mutate({
      email: getValues('email'),
      nickname: getValues('nickname'),
      password: getValues('password'),
    })
  }

  const passwordRepeatChecker = (passwordRepeatValue: string) => {
    if (getValues('password') !== passwordRepeatValue) {
      return '비밀번호가 일치하지 않습니다.'
    }
  }

  return (
    // form의 onSubmit에 handleSubmit(내가 커스텀한 submit 함수)를 등록한다.
    <form onSubmit={handleSubmit(onSubmit)}>
        <TextInput
          {...register('email', emailValidationRules)}
          hasError={errors}
        />
        {errors.email && (
          <div className={styles['error-message']} role="alert">
            {errors.email.message}
          </div>
        )}

        <TextInput
          placeholder="닉네임을 입력해 주세요."
          labelName="닉네임"
          {...register('nickname', nicknameValidationRules)}
          hasError={errors}
        />
        {errors.nickname && (
          <div className={styles['error-message']} role="alert">
            {errors.nickname.message}
          </div>
        )}

        <PasswordInput
          placeholder="비밀번호를 입력해 주세요."
          labelName="비밀번호"
          {...register('password', passwordValidationRules)}
          hasError={errors}
        />
        {errors.password && (
          <div className={styles['error-message']} role="alert">
            {errors.password.message}
          </div>
        )}

        <PasswordInput
          placeholder="비밀번호를 다시 입력해 주세요."
          labelName="비밀번호 확인"
          {...register('passwordRepeat', {
            required: '비밀번호를 확인해 주세요.',
            validate: {
              check: passwordRepeatChecker,
            },
          })}
          hasError={errors}
        />
        {errors.passwordRepeat && (
          <div className={styles['error-message']} role="alert">
            {errors.passwordRepeat.message}
          </div>
        )}

      <button disabled={!isValid}>
        회원가입
      </button>
    </form>
  )
}

주의!!! Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? 에러 해결법.

주의 2!!! forwardRef 사용할 때 나타나는 Component definition is missing display name 에러 해결법.

만약!!!! input 태그 자체에 register 인자를 내려주는 게 아니라 내가 위에서 썼던 것처럼 사용자 정의 컴포넌트에 인자를 내려주려면 다음과 같이 ref를 받아야 한다. 이걸 몰라서 한참 헤맸다.

근데 forwardRef를 쓰면 가끔 eslint에 걸려서 Component definition is missing display name 에러가 나기도 한다. eslint를 끄면 해결되긴 하지만 그것보단 displayName을 추가하는 게 더 좋은 방법이다.

/* 이메일 값을 받는 인풋

- signpage에 사용하는 이메일 인풋.
- id, placeholder, labelName 을 인자로 받음.
- hasError 은 상위 컴포넌트로부터 errors 객체를 받아옴.
- 나머지 인자인 onChange, onBlur, name은 상위 컴포넌트로부터 register 객체를 받아옴.
*/

import { forwardRef } from 'react'
// ...

// ref를 자식에게 내려주려면 forwardRef 메소드를 써야 한다!!!! 주의!!!! 
// 만약 ts를 쓰고 있다면... forwardRef는 제네릭 타입을 명시해야 한다.
const TextInput = forwardRef<HTMLInputElement, SignInputProps>(
  ({ onChange, onBlur, name , hasError }, ref) => {
    return (
      <div>
        <label htmlFor={name}>
          {labelName}
        </label>
        <input
          id={name}
          ref={ref}
          name={name}
          type="text"
          onChange={onChange}
          onBlur={onBlur}
        />
      </div>
    )
  },
)

// 이 문장 적는 게 좋음!!
TextInput.displayName = 'TextInput'

export default TextInput

##

  1. 완성.
    css까지 적용했을 때의 모습.

3. 배운 걸 정리하자면...

react hook form은 리액트에서 폼을 관리해주는 훌륭한 라이브러리.

ref를 하위 컴포넌트에게 전달할 땐 forwardRef 메소드 쓰기.

forwardRef쓸 때엔 displayName 명시해주기.