카카오 쇼핑하기를 구현하면서, 체크박스를 어떻게 관리하는 것이 효과적일까? 하는 생각이 들었다.

체크박스를 사용하는 상황은 다음과 같았다.

- 전체 동의의 체크 상태에 따라서 나머지 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 함수에서 가독성이 아주 살짝 떨어질 수 있다는 단점이 있지만, 항목이 늘어날 경우에도 대응이 매~~우 쉽다는 장점이 있다.

그러나 리랜더링이 매번 이뤄진다.

 

나는 첫 번째 방법으로 완성했다.

결제 과정에서 체크해야할 항목은 딱히 변화하지 않을 것 같다는 것이 이유였다.

 

내 결정이 옳았는지는 모른다.

이번주 코드리뷰에서 멘토님께 여쭤보는 편이 좋을 것 같다.

+ Recent posts