간만에 react-hook-form
으로 여러 체크박스를 한 번에 전체 선택하고, 선택 취소할 수 있는 selectAll
기능을 구현해보았다.
대부분의 예제가 간단하게 한 페이지, 한 컴포넌트 안에서 이루어진다.
하지만 실제로 개발을 하다보면 컴포넌트 구조가 복잡해지기 마련이다.
input이 여기도 있고 저기도 있고.. button은 또 따로 저~ 구석에 있고...
이번에는 input 을 각기 다른 컴포넌트에 분산해두고, button 역시 input들과 다른 곳에 두어서 react-hook-form
의 useFormContext
를 활용해보았다.
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
가 제대로 동작하는지 끊임없이 의심하게 되는 부담을 덜 수 있어서 매우 유용하다.(ㅋㅋㅋ)
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의 값에 접근할 수 있다.
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으로 감싸주지 않았다.
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 객체의 프로퍼티로 조회된다.
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
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