라이브러리/React Hook Form

[React Hook Form] Controller로 직접 만든 컴포넌트 폼 관리하기

눙엉 2024. 2. 14. 23:48

폼을 만들 때, 다양한 입력 방식을 다루다 보면 input 뿐만 아니라 select(dropdown), calendar 등의 컴포넌트를 사용하거나, 외부 라이브러리(React-Select, AntD, MUI 등)를 통해 폼을 구성하는 경우가 있습니다. 이때 useFormregitser를 연결하는데 어려움을 겪는 경우가 생깁니다.

 

이런 상황에서 Controller를 사용하면 useForm과 해당 컴포넌트를 효과적으로 연결하여 폼 상태를 관리할 수 있습니다. 아래는 직접 만든 Dropdown 컴포넌트를 예시로 설명한 코드입니다.

 

import { useState } from "React"

interface DropdownProps {
  dropdowns: {key: string; label: string}[];
  selectedDropdown: {key: string; label:string} | null;
  handleSelect: (selectKey: string) => void;
}

const Dropdown = ({ dropdowns, 
  selectedDropdown, handleSelect }: DropdownProps) => {
  
  const [isOpen, setIsOpen] = useState(false);
  
  const handleToggleDropdown = ():void => {
    setIsOpen(!isOpen);
  };
  
  const handleDropdown = (key:string):void => {
    handleToggleDropdown();
    handleSelect(key)
  };
  
    return {
      <>
        <button type="button" onClick={handleToggleDropdown}>
          {selectedDropdown !== null ? selectedDropdown.label : "선택"}
        </button>
        <ul>
        {dropdowns.map(({ key, label }, i) => ({
          <li key={i}>
            <button type="button" onClick={() => handleDropdown(key)}>
              {label}
            </button>
          </li>
        }))}
        </ul>
      </>
  }
}

 

 

 

직접 제작한 Dropdown의 컴포넌트의 경우 useFormregister를 사용해서 폼 상태관리를 할 수 없어 useFormsetValue를 사용해서 폼 상태관리를 할 수 있습니다. 페이지 컴포넌트에서 사용하는 예시를 아래의 코드를 통해 확인해보겠습니다.

 

import { useForm } from "react-hook-form";

interface Form {
  fruit: {key: string; label: string} | null;
}

const dropdowns = [
  {key: "apple", label: "사과"},
  {key: "banana", label: "바나나"}
];

const Page = () => {
  const { watch, setValue, clearErrors } = useForm<Form>({
    defaultValues: { fruit: null }
  });
  
  const handleSelect = (selectKey: string) => {
    const selectDropdownIndex = 
      dropdowns.findIndex(({key}) => key === selectKey);
    
    setValue("fruit", dropdowns[selectDropdownIndex]);
    clearErrors("fruit");
  }
  
  return {
    <Dropdown 
      dropdowns={dropdowns}
      selectedDropdown={watch("fruit")}
      handleSelect={handleSelect}
    />
  }
}

 

 

useFormregister를 사용하지 않고 setValue를 통해서 폼 상태를 관리하게 된다면 handleSelect 내부에서 clearErrors와 같이 직접 해당 상태에 대한 에러 관리를 해주어야 합니다. 물론 필수 값 체크도 따로 작성을 해주어야 되고 만약 저런 코드가 많아진다면 모두 관리하게 어렵게 되겠죠.

 

Controller를 사용하게 되면 register를 사용하는 것과 동일하게 폼 상태와 에러까지 한 번에 관리할 수 있습니다.

페이지 컴포넌트에서 DropdownController를 사용해서 폼 상태 관리하는 코드를 아래의 예시를 통해 확인해 보겠습니다.

 

import { useForm, Controller } from "react-hook-form";

interface Form {
  fruit: {key: string; label: string} | null;
}

const dropdowns = [
  {key: "apple", label: "사과"},
  {key: "banana", label: "바나나"}
];

const Page = () => {
  const { control } = useForm<Form>({
    defaultValues: { fruit: null }
  });
  
  return {
    <Controller
      control={control}
      name="fruit"
      rules={{ required: true }}
      render={({filed: { value, onChange }}) => {
        const handleSelect = (selectKey: string) => {
          const selectDropdownIndex = 
            dropdowns.findIndex(({key}) => key === selectKey);
            
          onChange(dropdowns[selectDropdownIndex]);
        }
      
        return (
          <Dropdown 
            dropdowns={dropdowns}
            selectedDropdown={value}
            handleSelect={handleSelect} 
          />
        );
      }}
    />
  }
}

 

Controller를 사용해 useForm과 커스텀 컴포넌트를 연결해 보았습니다. setValue를 사용해 상태관리를 하는 것보다 Controller를 사용하게 되면 나중에 폼이 커지더라도 유지보수성이 훨씬 높아지는 것을 알 수 있습니다.

 


기존에는 Controller를 사용하지 않고 직접 폼 상태를 관리하는 커스텀 훅이 복잡해지며 코드 양이 700줄까지 늘어난 것을 확인할 수 있었습니다. 입력 폼을 양이 많다 보니 각각의 Dropdown에서 커스텀 핸들러를 제작하고 에러 체크를 따로 처리하는 등의 방식은 유지보수를 어렵게 만들었습니다.

 

Controller를 사용하면 코드 양을 획기적으로 줄일 수 있었지만, 위의 코드에서 보듯이 컴포넌트를 렌더링 하는 부분의 코드가 길어져서 해당 컴포넌트가 어떻게 동작하는지 파악하기 어려워진 측면이 있습니다.

 

이러한 상황에서 코드 간결성과 가독성 사이에서 고민이 필요합니다. 코드를 간결하게 유지하면서도 가독성을 높일 수 있는 방법을 찾아봐야 될 시기가 된 것 같습니다.

 

새로운 의견 있거나 잘못된 부분에 있으면 댓글로 알려주시면 감사하겠습니다 :)