React-Hook-Form에 대해 알아보고 적용해보기 1일차

김형빈·2024년 9월 23일

TIL

목록 보기
1/13

React-Hook-Form이란?

React-Hook-Form에 대해서 알아보기 전에 이를 이해하기 위한 비제어 컴포넌트제어 컴포넌트에 대해서 먼저 알아보자

제어 컴포넌트

공식문서의 설명
제어 컴포넌트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.
(...)
이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.

  • React에 의해 값이 제어되므로 제어 컴포넌트라고 부른다.
  • 사용자의 입력을 받는 컴포넌트에서state로 값을 저장하는 방식

코드 예시

import React, { useState } from 'react';

function MyInput() {
  const [inputValue, setInputValue] = useState(null);

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  return <input onChange={(e) => handleChange(e)} value={inputValue} />;
}
  • 제어 컴포넌트는 모든 Input 값이 변경될때 마다 리렌더링 된다.
  • 사용자가 입력한 값과 저장되는 값이 실시간으로 동기화 된다.

비제어 컴포넌트

  • 제어 컴포넌트와는 반대로 React에 의해 제어되지 않는 컴포넌트
  • 사용자의 입력을 받는 컴포넌트에서ref로 값을 저장하는 방식

코드 예시

import React, { useRef } from 'react';

function MyInput() {
  const inputNode = useRef(null);

  return <input ref={inputNode} />;
}

export default MyInput;
  • 비제어 컴포넌트는 모든 Input 값이 변경되더라도 input을 제외한 필드가 리렌더링 되지 않는다.
  • 사용자가 입력한 값과 저장되는 값이 이벤트를 발생시키기 전까지 값을 동기화 하지 않는다.

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

기능제어 컴포넌트비제어 컴포넌트
일회성 정보 검색 (예: 제출)OO
제출 시 값 검증OO
실시간으로 필드값의 유효성 검사OX
조건부로 제출 버튼 비활성화 (disabled)OX
실시간으로 입력 형식 적용하기 (숫자만 가능하게 등)OX
동적 입력OX
  • 실시간으로 값에 대한 피드백이 필요하다
    • 제어 컴포넌트 사용
  • 실시간 피드백이 불필요하고 제출시에만 값이 필요하다, 불필요한 렌더링과 값 동기화가 싫다.
    • 비제어 컴포넌트 사용

그렇다면 React-Hook-Form은 무엇일까??

  • React-Hook-Form은 React 애플리케이션에서 폼을 효율적으로 관리하고 검증하기 위한 비제어 컴포넌트 기반의 라이브러리이다.

React-Hook-Form의 특징

  • 빠른 성능: 비제어 컴포넌트를 사용하여 리렌더링을 최소화한다.
  • 간편한 사용: 최소한의 코드로 폼을 관리할 수 있다.
  • 유연성: 다양한 검증 라이브러리와 통합이 가능하다.

React-Hook-Form 설치 방법

npm install react-rook-form

기본 폼 구성 및 사용법

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

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

  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: true })} placeholder="First Name" />
      {errors.firstName && <p>email name is required</p>}

      <input {...register("password", { required: true })} placeholder="Last Name" />
      {errors.lastName && <p>password is required</p>}

      <button type="submit">Submit</button>
    </form>
  );
}

export default MyForm;

내가 React-Hook-Form을 사용하기로 결심한 이유

  • work connect 프로젝트의 프로필 수정 페이지와 일정 입력 페이지의 복잡한 코드를 줄이고, 제어 컴포넌트를 사용하여 발생하는 불필요한 리렌더링 문제를 해결해보기 위해서

일단은 프로필 수정 페이지를 우선적으로 진행해보자!

  • 프로필 수정 페이지의 기존 코드
'use client';

const ProfileEditPage = () => {
  const { editInputs, name, state, email, phone, setName, setState, setEmail, setPhone } = useInput();
  const { openSnackBar } = useSnackBar();
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const userId = useUserStore((state) => state.userId);
  const workspaceId = useWorkspaceId();
  const router = useRouter();
  const [image, setImage] = useState<File | null>();
  const [imageURL, setImageURL] = useState<string | ArrayBuffer | null>();
  const params = useParams();
  const workspaceUserId = params.targetWorkspaceUserId as string;
  const { workspaceUser, isPending, isError, updateWorkspaceUser } = useWorkspaceUser(workspaceUserId);
  const profileImage = workspaceUser && workspaceUser.profile_image;
  const workspaceName = workspaceUser && workspaceUser.name;
  const workspaceEmail = workspaceUser && workspaceUser.email;
  const workspacePhone = workspaceUser && workspaceUser.phone;
  const workspaceState = workspaceUser && workspaceUser.state;
  const workspaceIsOpen = workspaceUser && workspaceUser.is_open;

  const setEmptyStr = (category: string | null | undefined): string => {
    if (!category) return '';
    else return category;
  };

  useEffect(() => {
    setImageURL(profileImage);
    setName(setEmptyStr(workspaceName));
    setState(setEmptyStr(workspaceState));
    setEmail(setEmptyStr(workspaceEmail));
    setPhone(setEmptyStr(workspacePhone));
    setIsOpen(workspaceIsOpen ? workspaceIsOpen : false);
  }, [profileImage, workspaceName, workspaceEmail, workspacePhone, workspaceState, workspaceIsOpen]);

  const handleIsOpenClick = () => {
    setIsOpen((prev) => !prev);
  };

  const handleProfileImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return;
    const file = e.target.files[0];
    if (!file) {
      openSnackBar({ message: '파일이 존재하지 않습니다' });
      return;
    }
    if (file.size > 3000000) {
      openSnackBar({ message: '이미지는 3MB를 넘길 수 없어요' });
      return;
    }
    setImage(file);
    const reader = new FileReader();
    reader.readAsDataURL(file);

    reader.onload = () => {
      setImageURL(reader.result);
    };
  };

  const handleEdit = async () => {
    if (!(userId && workspaceId)) return;
    if (!name) {
      openSnackBar({ message: '이름이 존재하지 않아요' });
      return;
    }
    if (image) {
      const filename = crypto.randomUUID();
      await api.storageProfile.postStorageProfile(image, filename);
      const profile_image = await api.storageProfile.getStorageProfile(filename);
      const workspaceUser = {
        id: workspaceUserId,
        user_id: userId,
        workspace_id: workspaceId,
        name,
        email,
        phone,
        state,
        is_open: isOpen,
        profile_image
      };
      await updateWorkspaceUser(workspaceUser);
      router.back();
      return;
    } else {
      const workspaceUser = {
        id: workspaceUserId,
        user_id: userId,
        workspace_id: workspaceId,
        name,
        email,
        phone,
        is_open: isOpen,
        state
      };
      await updateWorkspaceUser(workspaceUser);
      router.back();
      return;
    }
  };

  if (isPending) return <Loading />;
  if (isError) return <NotFoundError />;

  return (
    <div>
      <Header title="내 프로필 편집" type="edit" />
      <main>
        <div className="flex flex-col w-full items-center px-5 pb-[36px]">
          <ProfileImgButton imageURL={imageURL} handleProfileImageChange={handleProfileImageChange} />
          <div className="flex flex-col w-full gap-[16px] mb-[30px]">
            {editInputs.map((editInput) => {
              if (editInput.label === '활동상태')
                return (
                  <div key={editInput.label}>
                    <BottomSheetModal isUp={true}>
                      <InputBottomSheet editInput={editInput} />
                    </BottomSheetModal>
                    <TextFieldButton
                      LabelColor="grey400"
                      label={editInput.label}
                      value={editInput.value}
                      onClick={() => editInput.handleFn(editInput.value)}
                    />
                    <div className="lg:border-grey50 lg:border-b-[1px]" />
                  </div>
                );
              else
                return (
                  <div key={editInput.label}>
                    <EditTextfield
                      label={editInput.label}
                      labelColor="grey400"
                      value={editInput.value}
                      onChange={editInput.handleFn}
                      isRequired={editInput.isRequeired}
                    />
                    <div className="lg:border-grey50 lg:border-b-[1px]" />
                  </div>
                );
            })}
            <IsOpenInput isOpen={isOpen} handleIsOpenClick={handleIsOpenClick} />
          </div>
          <Button theme="primary" isFullWidth onClick={handleEdit}>
            수정하기
          </Button>
        </div>
      </main>
    </div>
  );
};

export default ProfileEditPage;
  • 프로필 수정 페이지에서 여러 input 값을 관리하기 위한 custom hook 코드
import useBottomsheetModalBackDropStore from '@/store/bottomsheetModalBackDropStore';
import { useState } from 'react';

const useInput = () => {
  const [name, setName] = useState<string | undefined>('');
  const [state, setState] = useState<string | undefined>('');
  const [email, setEmail] = useState<string | undefined>('');
  const [phone, setPhone] = useState<string | undefined>('');
  const handleOpen = useBottomsheetModalBackDropStore((state) => state.handleOpen);

  const handleNameChange = (value: string | undefined) => {
    setName(value);
  };

  const handleStateChange = (value: string | undefined) => {
    setState(value);
    handleOpen();
  };

  const handleEmailChange = (value: string | undefined) => {
    setEmail(value);
  };
  const handlePhoneChange = (value: string | undefined) => {
    setPhone(value);
  };

  const editInputs = [
    {
      label: '이름',
      value: name,
      isRequeired: true,
      handleFn: handleNameChange
    },
    {
      label: '활동상태',
      value: state,
      isRequeired: false,
      handleFn: handleStateChange
    },
    {
      label: '이메일',
      value: email,
      isRequeired: false,
      handleFn: handleEmailChange
    },
    {
      label: '전화번호',
      value: phone,
      isRequeired: false,
      handleFn: handlePhoneChange
    }
  ];

  return {
    editInputs,
    name,
    state,
    email,
    phone,
    setName,
    setState,
    setEmail,
    setPhone
  };
};

export default useInput;
  • 이제 이 코드들을 React-Hook-Form을 사용하면 코드가 얼마나 줄어드는지, 불필요한 리렌더링은 정말 사라지는지 확인해보자
profile
The secret of getting ahead is getting started.

0개의 댓글