image

react-hook-form (useForm)

태그
React
상세설명useForm 을 활용해 효과적으로 폼 관리하기
작성일자2024.02.29

로그인 같은 폼을 만들 때 자주 사용한 react-hook-form에서 제공한 useForm에 대해 정리 해보았다.

사용하는 이유

  • 유효성 검사를 쉽게 할 수 있는, 성능이 우수하고 유연하며 확장 가능한 form을 제공하는 라이브러리이다
  • 비제어 컴포넌트 방식으로 구현되어있기에 렌더링 이슈를 해결하면서도, form의 데이터와 상태를 Provider 아래라면 어느 곳에서든지 props drilling 없이 사용할 수 있다
  • 코드 단순화 및 유지보수 비용을 낮춘다
  • *제어 컴포넌트와 비제어 컴포넌트*

  • 제어 컴포넌트
  • HTML에서<input>,<textarea>,<select> 와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.

    폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다. 즉 state가 렌더링을 제어하는 것을 제어 컴포넌트라고 하는데 onChange 방식이 제어 컴포넌트라고 할 수 있다.

    관련 공식 링크 : https://ko.legacy.reactjs.org/docs/forms.html#controlled-components

  • 비제어 컴포넌트
  • 제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어진다. 대안인 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어다.

    모든 state 업데이트에 대한 이벤트 핸들러를 작성하는 대신 비제어 컴포넌트를 만들려면 ref를 사용하여 DOM에서 폼 값을 가져올 수 있다.

    ref는 값을 업데이트 하여도 리랜더링 되지 않는 특성으로, 입력이 모두 되고난 후 ref를 통해 값을 한번에 가져와서 활용한다. state로 값을 관리하지 않기 때문에 값이 바뀔 때마다 리랜더링을 하지 않고 값을 한번에 가져올 수 있는 성능 상 이점이 있으나, 데이터를 완벽하게 가져올 수 없는 단점이 있다.

    즉 만약 a와 b라는 컴포넌트가 있을 때, a에 대한 변화를 즉각적으로 b가 영향을 받아야 할때 비제어 컴포넌트는 이런 방식에 대한 대응을 할 수 없다.

    관련 공식 링크 : https://ko.legacy.reactjs.org/docs/uncontrolled-components.html

    react-hook-form 사용하기

    공식 홈

    https://react-hook-form.com/docs/useform

    설치

    npm install react-hook-form

    사용법

    주요 속성

    const { register, handleSubmit, watch, formState: { errors } } = useForm();

    defaultValue

    useForm에 defaultValues을 넘겨주면 해당 값으로 초기화된 값을 받아온다.

    useForm({
      defaultValues: {
        firstName: '',
        lastName: ''
      }
    })

    register 

    상태를 변경하는데 사용하는 함수로 onChange, onBlur, ref, name을 가지고 있다.

    <input {...register("email")} />

    두 번째 매개변수에 조건을 줄 수 있다.

    <input {...register("email", {
    	 /* option */
    })} />
  • required : 필수 값 지정, true 일 경우 값이 없다면 error를 받는다.
  • maxLength & minLength : 글자 수 제한 적용
  • <input
       type="password"
       placeholder="Password"
       {...register("password", {
           required: "비밀번호는 필수 입력입니다.",
           minLength: {
           value: 4,
           message: "4자리 이상 비밀번호를 입력하세요.",
         },
       })}
    />
  • min & max : 숫자 값 적용
  • <input
      type="number"
      {...register("item", {
        required: "itme 갯수는 필수 값입니다.",
        valueAsNumber: true,
        min: {
          value: 1,
          message: "1개 이상이어야 합니다."
        },
        max: {
          value: 10,
          message: "10개 이어야 합니다."
        }
      })}
    />;
  • pattern : 정규식 적용
  • <input
      {...register("email", {
        required: "이메일은 필수 입력입니다.",
        pattern: {
          value: /\S+@\S+\.\S+/,
          message: "이메일 형식에 맞지 않습니다."
        }
      })}
      type="text"
      placeholder="Email"
    />;
  • validate : 커스텀 validation
  • <input
    	type="email"
      placeholder="email"
      {...register("email", {
        required: "email은 필수 값 입니다.",
        validate: {
          domainCheck: email =>
            email.split("@")[1] === "gmail.com" || "gmail만 가능합니다."
        }
      })}
    />;

    handleSubmit 

    submit 이벤트를 할당 해주는 함수로 기본적으로 e.preventDefault()를 가지고 있기 때문에 작성하지 않아도 된다. handleSubmit() 의 첫 번째 인자는 성공했을 때 실행시키는 함수를 받고, 두 번째 인자는 실패했을 때 실행시키는 함수를 받는다.

    const onValid = async (data: loginFormType) => {
        const result = await signIn("credentials", {
          redirect: false,
          email: data.email,
          password: data.password,
        });
        if (result?.error) {
         setError("loginError", {
            message: "이메일 주소와 비밀번호를 다시 확인해주세요.",
          });
        }
    };
    
    <form onSubmit={handleSubmit(onValid)}>
    	//...
    </form>

    watch

    form에서 입력된 값을 구독하여 실시간으로 체크할 수 있게 해주는 함수이다.

    const id = watch("id");

    formState

    에러에 대한 정보는 formState 객체의 errors에 담겨있다.

    {errors.email && (
         <span className="errorTxt">{errors.email.message}</span>
    )}

    formState에는 errors 외에도 다양한 속성이 있다.

  • submitCount : submit 한 횟수
  • dirtyFields : 기본 값이 수정된 필드들이 담김 - defaultValues가 있어야만 사용 가능
  • touchedFields : 사용자에 의해 수정된 필드들이 담겨져 있음
  • isValid : 에러가 있는지 알 수 있음
  • isSubmitting : 양식이 현재 제출 중인 상태인지 아닌 지를 알아낼 수 있음
  • <button type="submit" disabled={isSubmitting}>

    disabled 속성에 이 isSubmitting 값을 설정해주면 로그인 버튼이 양식의 제출 처리가 끝날 때까지 비활성화가 된다.

    useForm을 적용한 로그인 페이지

    import { useForm } from "react-hook-form";
    
    interface loginFormType {
      email: string;
      password: string;
      loginError?: string;
    }
    
    export default function Login() {
    	  const {
        register,
        handleSubmit,
        setError,
        formState: { errors, isSubmitting },
    	  } = useForm<loginFormType>();
    
      const onValid = async (data: loginFormType) => {
        const result = await signIn("credentials", {
          redirect: false,
          email: data.email,
          password: data.password,
        });
        if (result?.error) {
         setError("loginError", {
            message: "이메일 주소와 비밀번호를 다시 확인해주세요.",
          });
        }
      };
    
      return (
        <div css={loginPage}>
          <div>
            <h1>
              PoledCS
              <br /> 관리자 로그인
            </h1>
            <form onSubmit={handleSubmit(onValid)} css={loginForm}>
              <div className="input_area">
                <div>
                  <input
                    type="text"
                    placeholder="Email"
                    {...register("email", {
                      required: "이메일은 필수 입력입니다.",
                      pattern: {
                        value: /\S+@\S+\.\S+/,
                        message: "이메일 형식에 맞지 않습니다.",
                      },
                    })}
                    onChange={onChange}
                  />
                  {errors.email && (
                    <span className="errorTxt">{errors.email.message}</span>
                  )}
                </div>
                <div>
                  <input
                    type="password"
                    placeholder="Password"
                    {...register("password", {
                      required: "비밀번호는 필수 입력입니다.",
                      minLength: {
                        value: 4,
                        message: "4자리 이상 비밀번호를 입력하세요.",
                      },
                    })}
                  />
                  {errors.password && (
                    <span className="errorTxt">{errors.password.message}</span>
                  )}
                </div>
                <p {...register("loginError")}>
                  {errors.loginError && (
                    <span className="errorLogin">{errors.loginError.message}</span>
                  )}
                </p>
              </div>
              <button type="submit" disabled={isSubmitting}>
                로그인
              </button>
            </form>
          </div>
        </div>
      );
    }

    참고