카카오 쇼핑하기를 구현하면서, 체크박스를 어떻게 관리하는 것이 효과적일까? 하는 생각이 들었다.
체크박스를 사용하는 상황은 다음과 같았다.
- 전체 동의의 체크 상태에 따라서 나머지 2개도 체크가 적용 / 해제
- 나머지 2개의 체크 상태에 따라서 전체 동의도 체크 적용 / 해지
- 전체 동의가 되어있지 않다면 결제하기 버튼 클릭 불가
대충 이 정도의 구현이 필요한 상황이었다.
구현만 놓고 보면 매우 간단하지만, 어떻게 구현하는 게 더 효과적일까 하는 궁금증이 있었다.
첫 번째로 떠올린 것은 다음과 같았다.
const checkAllRef = useRef();
const checkPaymentRef = useRef();
const checkPrivacyRef = useRef();
const handleCheckAllChange = () => {
const checkAllChecked = checkAllRef.current.checked;
checkPaymentRef.current.checked = checkAllChecked;
checkPrivacyRef.current.checked = checkAllChecked;
}
const handleCheckChange = (e) => {
checkAllRef.current.checked = checkPaymentRef.current.checked && checkPrivacyRef.current.checked;
}
/* 결제 버튼 */
const handleOnClick = async () => {
if(checkAllRef.current.checked === false) {
alert("약관에 동의해주세요.");
return;
}
try {
const res = await orderProducts();
} catch (e) {
alert('장바구니가 비어있는지 확인해주세요');
}
}
/* ... */
<div className="border">
<div className="font-bold text-xl border-b py-5 px-3">
<input type="checkbox" ref={checkAllRef} id="checkAll" onChange={handleCheckAllChange} />
<label htmlFor="checkAll">전체동의</label>
</div>
<div className="p-3">
<input type="checkbox" ref={checkPaymentRef} id="checkPayment" onChange={handleCheckChange} />
<label htmlFor="checkPayment">구매조건 확인 및 결제 진행 동의</label>
</div>
<div className="p-3">
<input type="checkbox" ref={checkPrivacyRef} id="checkPrivacy" onChange={handleCheckChange} />
<label htmlFor="checkPrivacy">개인정보 제3자 제공 동의</label>
</div>
</div>
useRef로 요소를 하나하나 찍어와서 관리하는 방법이다.
체크박스 변경과정에서 리랜더링은 이뤄지지 않는다.
당연히 잘 작동했으나, 체크박스 하나가 더 늘어난다면 추가해야 할 코드가 꽤 늘어난다는 문제가 있다. (handleCheckChange, handleCheckAllChange가 더러워질 수 있다.)
그래서 확장성에 좋은 다른 방법을 떠올려봤다.
const [checkboxes, setCheckboxes] = useState([
{ id: "checkAll", label: "전체동의", checked: false },
{ id: "checkPayment", label: "구매조건 확인 및 결제 진행 동의", checked: false },
{ id: "checkPrivacy", label: "개인정보 제3자 제공 동의", checked: false },
// 필요에 따라 체크박스 추가 가능
]);
const handleCheckAllChange = (event) => {
const checkAllChecked = event.target.checked;
setCheckboxes((prevCheckboxes) =>
prevCheckboxes.map((checkbox) => ({ ...checkbox, checked: checkAllChecked }))
);
};
const handleCheckChange = (event) => {
const { id, checked } = event.target;
setCheckboxes((prevCheckboxes) =>
prevCheckboxes.map((checkbox) => (checkbox.id === id ? { ...checkbox, checked } : checkbox))
);
};
const handleOnClick = async () => {
if (!checkboxes[0].checked) {
alert("약관에 동의해주세요.");
return;
}
try {
const res = await orderProducts();
} catch (e) {
alert("장바구니가 비어있는지 확인해주세요");
}
};
/* ... */
<div className="border">
<div className="font-bold text-xl border-b py-5 px-3">
<input
type="checkbox"
id={checkboxes[0].id}
checked={checkboxes[0].checked}
onChange={handleCheckAllChange}
/>
<label htmlFor={checkboxes[0].id}>{checkboxes[0].label}</label>
</div>
{checkboxes.slice(1).map((checkbox) => (
<div key={checkbox.id} className="p-3">
<input
type="checkbox"
id={checkbox.id}
checked={checkbox.checked}
onChange={handleCheckChange}
/>
<label htmlFor={checkbox.id}>{checkbox.label}</label>
</div>
))}
</div>
첫 번째 방법과 작동 방식은 거의 유사하다.
체크박스 자체를 상태로 관리한 점이 좋았다.
체크박스를 컴포넌트로 바꿔서 상태의 초기값을 props로 넘겨줘서 관리하기에 쉬워지는 장점이 있었다.
반복되는 map 함수에서 가독성이 아주 살짝 떨어질 수 있다는 단점이 있지만, 항목이 늘어날 경우에도 대응이 매~~우 쉽다는 장점이 있다.
그러나 리랜더링이 매번 이뤄진다.
나는 첫 번째 방법으로 완성했다.
결제 과정에서 체크해야할 항목은 딱히 변화하지 않을 것 같다는 것이 이유였다.
내 결정이 옳았는지는 모른다.
이번주 코드리뷰에서 멘토님께 여쭤보는 편이 좋을 것 같다.