ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React-Hook-Form] FormData 관리하기
    프로그래밍/React 2024. 5. 29. 23:45
    개요

    1. 왜 사용해야하는가?
    2. 주요 함수 간단 정리
    3. React-Hook-Form 사용법?

     


    ✅ Why?


     

    📦 제어 vs 비제어 컴포넌트

    → 제어 컴포넌트

    React에 의해 값이 제어되는 입력 폼 엘리먼트를 뜻하며, 일반적으로 state를 통해서 input값을 받아올 때의 상태를 말한다.
    • 실시간으로 값이 동기화되므로 값의 변화에 따라서 리랜더링이발생한다.
    • 사용자의 입력 폼이 늘어날수록 랜더링되는 요소가 늘어나 불필요한 연산이 발생한다.
    • 유지보수에 힘들고 유효성 검사까지 진행한다면 코드가 매우 길어져 가독성이 나빠진다.
    const [value, setValue] = useState("");
    
    <input
      type="text"
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />

     

    → 비제어 컴포넌트

    React에 의해 값이 제어되지 않는 엘리먼트를 뜻하며, 실시간으로 값을 동기화시키지 않고, submit 버튼과 같은 특정 이벤트가 발생 시 값을 받아온다.
    • 실시간으로 값이 동기화되지 않아 불필요한 리랜더링이 발생하지 않아 성능 개선에 필수적인 요소이다.
    • 폼이 늘어나도 관리한 state값이 늘어나지 않는다.
    • 실시간으로 값이 동기화되지 않기에 실시간 유효성 검사, 버튼의 조건부 활성화 등의 기능을 사용할 수 없다. 
    const inputRef = useRef()
    const onClick = () => {
      console.log(inputRef.current.value);
    };
    
    <div className="App">
      <input ref={inputRef} />
      <button type="submit" onClick={onClick}>
        전송
      </button>
    </div>

     

    Recat-Hook-Form은 이러한 제어, 비제어 컴포넌트의 장점만을 그대로 살린 라이브러리로 비제어 컴포넌트의 불필요한 리렌더링 억제 및 제어 컴포넌트의 실시간 동기화 등을 가능하게 해주며, 훨씬 적은 코드로 더 나은 성능을 제공해준다.

     


     

    📦 그 외 장점

    → 작은 사이즈, 꾸준한 업데이트

    라이브러리 자체의 사이즈도 작고, 꾸준히 업데이트되고 있다.

    2024.05 기준

     

    → 친절한 공식문서, 활발한 커뮤니티

    다양한 커뮤니티 및 기업에서 선정될 만큼 많이 사용되어 활발한 커뮤니티를 형성하고 있으며, 공식 문서의 설명과 예제가 잘 나와있어 쉽게 배울 수 있다.

     

    → 뛰어난 호환성

    TS를 기본적으로 지원하며, 다양한 입력 타입(텍스트, 체크박스, 셀렉터 등등)에 대해서도 사용을 할 수 있으며, React Hook을 사용하기에 함수 함수 컴포넌트에서 간편하게 사용할 수 있다. 또한 다른 라이브러리와 통합하기 용이하여 유효성 검사 구현 시 유효성 검사 라이브러리(Yup, Joi, Zod 등등)와 통합하여 손쉽게 규칙을 정의할 수 있다.

     

     

     

    ✅ 주요 API


     

    📦 useForm

    form을 쉽게 관리할 수 있는 API로 parameter로 여러 옵션을 넣을 수 있지만 가장 주요하게 mode와 defalutValue가 있다.
    • mode: 유저가 submit을 하기 전에 설정한 모드로 유효성 검사를 진행한다.
    • defalutValue: form에 대한 defalut값으로 동기/비동기 모두 지원한다.
    import { useState } from "react";
    import { useForm, SubmitHandler } from "react-hook-form";
    
    export default function App() {
      const { register, handleSubmit, formState } = useForm<{firstName: string}>({
        mode: "onChange",
        defaultValues: {
          firstName: "",
        },
      });
      const { errors } = formState;
      const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data);
    
      return (
          <form onSubmit={handleSubmit(onSubmit)}>
            <input
              {...register("firstName", {
                required: "필수",
                maxLength: { value: 10, message: "최대 10자" },
                pattern: { value: /^[A-Za-z]+$/i, message: "알파벳" }
              })}
            />
            <input type="submit" />
            <p>errors.firstName.message: {errors.firstName?.message}</p>
            <p>errors.firstName.types: {JSON.stringify(errors.firstName?.types)}</p>
          </form>
      );
    }

     

     

    → register

    입력 필드를 react-hook-form 폼 상태와 연결하는 함수로 입력 필드의 name 속성을 등록하고, 필요한 유효성 검사 규칙을 설정할 수 있다. 반환 값은 보통 구조분해를 사용해서 전달이 된다.
    • 전달되는 값: onChagne, onBlur, name, ref

     

    → handleSubmit

    유효성 검사에 통과된 폼 데이터를 받을 수 있는 콜백 함수를 제공하는 함수로 첫번째 parameter는 resolve상태를 받을 수 있는 콜백 함수, 두번째 parameter는 reject상태에서 실행되는 콜백함수이다.
    const onSubmit = (data: IFormInput) => console.log(data);
    const onError = (error: any) => console.log(error);
    
    <form onSubmit={handleSubmit(onSubmit, onError)}>

     

    → formState

    form 전체 상태 정보를 담고 있는 객체로 폼의 상태를 알고 싶을 때 사용할 수 있는 값이다.
    const {
        ...
        formState: {
          errors,
          isDirty,
          isLoading,
          isSubmitted,
          isSubmitSuccessful,
          isSubmitting,
          isValid,
          submitCount,
          defaultValues,
          dirtyFields,
          touchedFields
        }
      } = useForm<{ test: string }>({
        mode: "onChange",
        defaultValues: () => {
          return new Promise((resolve) => {
            setTimeout(() => resolve({ test: "123" }), 1000);
          });
        }
      });

     

    → watch

    특정요소를 감시하여 그 요소의 값을 반환하게 해준다. 해당 요소의 값을 가져와서 사용해야할 때 유용하게 사용되는 함수이다.
    • watch: (names: string | stsring[] | (data, options) => void, defaultValue: Object | unknown ) => unknown
    import { useEffect } from "react";
    import { useForm, SubmitHandler } from "react-hook-form";
    
    export default function App() {
      const { register, handleSubmit, watch } = useForm<{firstName: string}>();
      const watchFirstName = watch("firstName");
      const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data);
    
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          <input {...register("firstName")} />
          <input type="submit" />
        </form>
      );
    }

     

    → reset

    폼의 입력 값을 초기 상태로 되돌리는 함수로 재설정 혹은 초기화에 사용된다

     

     

     

    ✅ How?


     

    📦 Install

    npm install react-hook-form

     

    📦 Example

    import { useState } from "react";
    import { useForm } from "react-hook-form";
    import Header from "./Header";
    
    export function App() {
      const { register, handleSubmit } = useForm();
      const [data, setData] = useState("");
    
      return (
        <form onSubmit={handleSubmit((data) => setData(JSON.stringify(data)))}>
          <Header />
          <input {...register("firstName")} placeholder="First name" />
          <select {...register("category", { required: true })}>
            <option value="">Select...</option>
            <option value="A">Option A</option>
            <option value="B">Option B</option>
          </select>
          <textarea {...register("aboutYou")} placeholder="About you" />
          <p>{data}</p>
          <input type="submit" />
        </form>
      );
    }

     

    📦 모듈화

    • register의 option을 별도로 관리하고, 전달할 수 있는 값에 대해서 전달하여 코드를 간결하게 구성할 수 있었다.
    // page > signup.tsx
    
    import { useForm } from 'react-hook-form';
    import { ExtendedSignupForm } from '@/types/user';
    import InputArea from '@/components'
    
    const SignupPage = () => {
      const {
        watch,
        register,
        handleSubmit,
        formState: { errors },
      } = useForm<{email: string}>({
        mode: 'onChange',
        defaultValues: {email: ''},
      });
      const onSubmit = (data: {email: string}) => console.log(data);
      
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          {essentiolFormData.map(({label, value, registerOptions})=> (
            <InputArea
              key={value}
              label={label}
    		  register={register}
    		  errors={errors}
              registerOptions={registerOptions}
              value={value}
            />))}
          <button type='submit'>submit</button>
        </form>
      );
    };
    
    export default SignupPage;
    
    // InputArea.tsx
    
    import { FieldErrors, UseFormRegister } from 'react-hook-form';
    import { ExtendedSignupForm } from '@/types/user';
    import { SignupValues } from '@/consts/signup';
    
    import * as S from './styles';
    
    interface InputAreaProps {
      label: string;
      value: SignupValues;
      register: UseFormRegister<ExtendedSignupForm>;
      errors: FieldErrors<ExtendedSignupForm>;
      registerOptions?: object;
    }
    
    const InputArea = ({
      label,
      value,
      register,
      errors,
      registerOptions,
    }: InputAreaProps) => {
      return (
        <S.Container>
          <S.Label htmlFor={value} children={label} />
          <S.Input
            {...register(value, registerOptions)}
            id={value}
          />
          {errors[value] && (
            <S.ErrorMessage color="red" typography="t6">
              {errors[value]?.message}
            </S.ErrorMessage>
          )}
        </S.Container>
      );
    };
    
    export default InputArea;
    
    // registerOptions
    
    export const essentiolFormData = [
      {
        label: '이메일',
        value: 'email',
        registerOptions: {
          required: '이메일을 입력해주세요',
          pattern: {
            value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
            message: '이메일 형식을 지켜주세요',
          },
        },
      },
    ]

     

    직접 사용해보았을 때 결국 실시간 유효성 검사를 하기 위해서는 제어 컴포넌트 형식으로 돌아갈 수 밖에 없다는 것이 아쉬웠다. 비제어 방식으로 사용할 수도 있겠지만 그렇게 하면 submit 이벤트가 발생하기 전에는 유효성 검사가 실시되지 않아 UX에 좋은지는 고려해보아야 할 요소이고 만약 react-hook-form을 사용하지 않고 구현한다면 디바운싱을 이용해서 유효성 검사 시기를 조절한다면 좀 더 성능적으로 나은 form을 구현할 수 있을 것 같다.

     

     

    📌 Reference

     

    댓글

Designed by Tistory.