[React] react-hook-form의 checkbox selectAll

sjoleee·2022년 11월 28일
4
post-thumbnail

react-hook-form

간만에 react-hook-form으로 여러 체크박스를 한 번에 전체 선택하고, 선택 취소할 수 있는 selectAll 기능을 구현해보았다.

대부분의 예제가 간단하게 한 페이지, 한 컴포넌트 안에서 이루어진다.
하지만 실제로 개발을 하다보면 컴포넌트 구조가 복잡해지기 마련이다.
input이 여기도 있고 저기도 있고.. button은 또 따로 저~ 구석에 있고...

이번에는 input 을 각기 다른 컴포넌트에 분산해두고, button 역시 input들과 다른 곳에 두어서 react-hook-formuseFormContext를 활용해보았다.

App 구조

SubmitButton을 클릭하면 Table 내부의 각 체크박스들의 checked 값과 Header 내부의 input 2개의 값을 가져오고 싶다. + SelectAll 체크박스를 통한 전체 선택 및 선택 해제

이를 위해 컴포넌트를 작성했다. (css는 작성하지 않았다)

📦src
 ┣ 📂components
 ┃ ┣ 📜Header.tsx
 ┃ ┣ 📜SubmitButton.tsx
 ┃ ┗ 📜Table.tsx
 ┣ 📜App.tsx
 ┗ 📜index.tsx

비제어 컴포넌트

react-hook-form비제어 컴포넌트를 활용하고 있다.
ref 기반의 비제어 컴포넌트는 렌더링 최적화 측면에서 상당한 이점을 가질 수 있고, 제어 컴포넌트에서 setState가 제대로 동작하는지 끊임없이 의심하게 되는 부담을 덜 수 있어서 매우 유용하다.(ㅋㅋㅋ)

App.jsx

import React from "react";
import { FormProvider, useForm } from "react-hook-form";
import Header from "./components/Header";
import Table from "./components/Table";

function App() {
  const methods = useForm();
  return (
    <FormProvider {...methods}>
      <Header />
      <Table />
    </FormProvider>
  );
}

export default App;

FormProvider로 컴포넌트를 감싸주면 어디서든 react-hook-form에 등록된 input의 값에 접근할 수 있다.

Header.jsx

import { useFormContext } from "react-hook-form";
import SubmitButton from "./SubmitButton";

const Header = () => {
  const { register } = useFormContext();
  return (
    <>
      <input {...register("input1")} placeholder="input1" />
      <input {...register("input2")} placeholder="input2" />
      <button
        type="button"
        onClick={() => {
          console.log("다른행동");
        }}
      >
        다른버튼
      </button>
      <SubmitButton />
    </>
  );
};

export default Header;

input 2개와 button 2개를 작성해주었다.

다른 행동을 하는 버튼이 존재하는 이유는, App 전체를 form으로 감싸서 react-hook-form의 handleSubmit을 사용하는 시나리오를 생각했기 때문이다.
form 내부의 button은 클릭 시 기본적으로 submit을 실행한다.
다만, type="button" 으로 그것을 방지할 수 있다.

그런데, form 내부의 input에 focus를 두고 enter를 입력하면 submit이 실행된다.
이것도 막으려면... input에 submit을 방지하는 코드를 추가해주어야 한다.

onKeyDown={(e) => {
  if (e.code === "Enter") e.preventDefault();
}}

다만, submit을 사용하지 않고 getValues로 가져온 값을 가공하는 경우가 더 많을 것 같아서 form으로 감싸주지 않았다.

Table.jsx

import React from "react";
import { useFormContext } from "react-hook-form";

const data = ["S00001", "S00002", "S00003", "S00004", "S00005"];

const Table = () => {
  const { register, setValue } = useFormContext();

// 모든 체크박스를 체크, 체크해제하는 로직
  const handleSelectAll = (e: React.FormEvent<HTMLInputElement>) => {
    if (e.currentTarget.checked) { // selectAll 체크박스가 체크되면
      data.forEach((item) => {
        setValue(`id.${item}`, true); // 모든 체크박스의 value를 true로
      });
    } else { // selectAll 체크박스가 체크해제되면
      data.forEach((item) => {
        setValue(`id.${item}`, false); // 모든 체크박스의 value를 false로
      });
    }
  };

  return (
    <>
      <div>
        <input
          type="checkbox"
          {...register("selectAll")} // 전체 체크, 체크해제를 담당하는 체크박스
          onChange={handleSelectAll}
        />
      </div>
      {data.map((item) => ( // data를 체크박스로
        <input {...register(`id.${item}`)} key={`${item}`} type="checkbox" />
      ))}
    </>
  );
};

export default Table;

register를 통해 등록해놓은 input은 react-hook-form이 제공하는 각종 메서드를 통해 접근 및 수정할 수 있다.

id.{item}으로 등록할 경우, data에서 id 객체의 프로퍼티로 조회된다.

SubmitButton.jsx

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

const SubmitButton = () => {
  const { getValues } = useFormContext();
  return (
    <button
      onClick={() => {
        console.log(getValues());
      }}
    >
      제출하기
    </button>
  );
};

export default SubmitButton;

SubmitButton에서 getValues를 이용하면 FormProvider 내의 모든 input의 값을 조회할 수 있다.
이것으로 복잡한 구조의 input이라도 손쉽게 다룰 수 있다.

리렌더링

비제어 컴포넌트의 장점은 state를 변화시키지 않기 때문에 렌더링에서 이득을 본다는 점이다.
작동하는 화면에서 사실 React Developer Tools에서 제공하는 렌더링 체크 기능을 실행해둔 상태다.
즉, 위 작업을 하는 동안 한 번도 리렌더링이 발생하지 않았다.
꽤나 만족스러운 결과물이다.

다만, 한 가지 문제가 있다.
아래 이미지처럼 모든 체크박스를 체크할 경우 자동으로 selectAll 체크박스가 체크되고, 하나라도 체크 해제될 경우 selectAll 체크박스가 체크 해제되는 기능을 구현하는 것이다.

잘 작동하는데 뭐가 문제냐면,
실제로는 체크박스를 조작할 때마다 리렌더링이 발생하고 있다.

이렇게 리렌더링이 발생하는 방식으로 구현한 이유는...
해당 기능을 구현하기 위해서는 모든 체크박스의 값이 true가 될 때를 찾아야 하기 때문이다.
즉, react-hook-form이 제공하는 watch를 사용해서 체크박스를 조작할 때마다 값을 가져와야 하는데, watch는 context를 구독한 컴포넌트의 렌더링을 트리거한다.

getValues는 실행된 시점의 값을 가져오는 것이라서 적합하지 않다.

  useEffect(
    () => {
      if (data.every((item) => watch(`id.${item}`) === true)) {
        setValue("selectAll", true);
      } else {
        setValue("selectAll", false);
      }
    },
    data.map((item) => watch(`id.${item}`))
  );

위 기능을 구현하게 되면 체크박스를 조작할 때마다 리렌더링이 발생하는 비효율적인 상황이 벌어진다.

useWatch를 사용하면 리렌더링의 범위를 해당 컴포넌트로 한정할 수 있으니 필요하다면 사용해볼 수 있겠다.

혹시 위 기능을 렌더링 관점에서 효율적으로 구현할 수 있는 방법을 알고 있다면.. 알려주세요...

느낀점

react-hook-form을 공부하면서 비제어 컴포넌트의 유용함을 느꼈다.
개발중인 아이돌만들기 서비스에도 여러개의 아이템을 선택해서 다음 페이지로 넘겨줘야하는 기능이 있는데, 비제어 방식으로 동작하게 만든다면 렌더링 관점에서 훨씬 효율적일 것 같다.

다만... 아직까지 내공이 부족한 탓인지 렌더링을 최적화 하면서 사용하기가 꽤나 까다롭다 ㅠㅠ

작성된 내용은 아래 레포지토리에서 확인할 수 있습니다.
https://github.com/sjoleee/reacthookform-practice

profile
상조의 개발일지

1개의 댓글

comment-user-thumbnail
2024년 10월 8일

The issue with the aforementioned way is that the A-1 component will be re-rendered in accordance with the render, isChecked, and the issue is re-reading it if the parent component (A-0) of the component (A-1) has this checkbox likewise chosen Issues using the Rebound State. drift boss

답글 달기