Check Box 상태 관리(feat. Set 활용법)

하태현·2022년 6월 27일
1

React

목록 보기
9/11
post-thumbnail

평소에 테이블을 만들때 <table> 태그 또는 flex를 이용해서 만들었는데
이번엔 grid 기능을 이용해서 테이블을 만들어 보았다.
사용해보니 grid-template-columns을 이용해 column의 width를 맞추기가 굉장히 편리했다.

.grid {
  display: grid;
  grid-template-columns: 36px 1fr 2fr;
  height: 30px;
}

Set

아래는 MDN 에서 Set에 대한 정의한 내용이다.

Set 객체는 자료형에 관계 없이 원시 값과 객체 참조 모두 유일한 값을 저장할 수 있습니다.

속성

Set.prototype.size : Set 객체의 value 수를 반환.

메서드

Set.prototype.add(value) : Setvalue를 추가한다. 값이 추가된 Set 객체를 반환.
Set.prototype.delete(value) : Set에서 value를 제거한다. 성공적으로 제거 되었는지에 대한 boolean을 반환.
Set.prototype.clear(value) : Set 에서 모든 요소를 제거한다.
Set.prototype.has(value) : Setvalue가 존재하는지 여부를 확인하는 boolean을 반환.

위와 같은 Set의 특징을 이용하여 checked 상태관리를 Array가 아닌 Set으로 해보았다.
const [checkedSet, setCheckedSet] = useState(new Set());

요구 기능 명세

  1. Add 버튼을 클릭해 테이블의 Row 추가
  2. 해당 Row 마다 삭제 가능하도록(checkbox)
  3. 다중 Row 선택(전체 선택/해제)
  4. 모든 Row 선택 시 자동으로 헤더 체크박스(전체 선택 상태) 체크
  5. 전체 선택 후 한개 이상의 Row 해제 시 헤더 체크박스(전체 선택 상태) 체크 해제
// 기능을 제외한 마크업
const GridTable = () => {
  {...}
  return (
    <>
      <button className="rowControlBtn" onClick={addRow}>
        add row
      </button>
      <button className="rowControlBtn" onClick={deleteRow}>
        delete row
      </button>
      <span>selected rows: {checkedSet.size}</span>
      <div className="container">
        <div className="grid">
          <div className="header">
            <CheckBox id="allCheck" onCheck={allCheck} checked={allChecked} />
          </div>
          <div className="header">id</div>
          <div className="header">name</div>
        </div>
        {rows.map((data) => (
          <Row
            key={data.id}
            rowId={data.id}
            name={data.name}
            checked={checkedSet.has(data.id)}
            onCheckHandler={onCheckHandler}
          />
        ))}
      </div>
    </>
  );
};
const Row = ({ rowId, name, onCheckHandler, checked }) => {
  return (
    <div key={`row${rowId}`} className="grid row">
      <div className="cell">
        <CheckBox id={rowId} onCheck={onCheckHandler} checked={checked} />
      </div>
      <div className="cell">{rowId}</div>
      <div className="cell">{name}</div>
    </div>
  );
};

const CheckBox = ({ id, onCheck, checked }) => {
  const [_checked, setChecked] = useState(false);

  useEffect(() => {
    setChecked(checked);
  }, [checked]);
  const toggleCheck = () => {
    setChecked(!_checked);
    onCheck && onCheck(!_checked, id);
  };

  return (
    <div
      className="checkBox"
      style={{
        backgroundColor: _checked ? "#546A78" : "#ffffff",
        borderWidth: 1,
        border: "1px solid #546A78"
      }}
      onClick={toggleCheck}
    >
      {_checked && <BiCheck size={14} color="#fff" />}
    </div>
  );
};

<Row> 컴포넌트는 부모인 <GridTable>에서 전에 row data를 이용해 생성해주는 용도이다.
<CheckBox>는 row 선택을 위한 컴포넌트(간단한 toggle기능 이므로 설명 생략)

Set 자료구조를 이용해 Check 상태 관리

const GridTable = () => {
  const [rows, setRows] = useState([]); // 전체 rowData
  const uniqueId = useRef(0); // row 삭제/추가 할때 중복되지 않는 rowId 생성을 위함
  const {
    checkedSet,
    addToSet,
    deleteToSet,
    clearSet,
    replaceSet
  } = useCheckGroup(); //checkGroup: new Set() 자료구조를 이용한 custom hook

  const [allChecked, setAllChecked] = useState(false); // 전체 선택/해제를 위한 상태
  useEffect(() => {
    // row data 와 checkedSet를 이용해 전체 선택 체크박스 핸들링
    if (rows.length === 0) {
      setAllChecked(false);
      return;
    }
    if (checkedSet.size === rows.length) setAllChecked(true);
    else setAllChecked(false);
  }, [rows, checkedSet]);
  const allCheck = (checked) => {
    setAllChecked(checked);
    if (checked) {
      const ids = rows.map((row) => row.id);
      replaceSet(ids);
      return;
    }
    clearSet();
  };

  const onCheckHandler = (checked, id) => {
    if (checked) {
      addToSet(id); // 체크되면 Set에 id(Unique ID: 중복 허용X)를 담아준다.
      return;
    }
    deleteToSet(id); // 체크 해제 되면 Set에서 제거
  };

  const addRow = () => {
    const id = uniqueId.current++;
    const newRowData = { id: id, name: "joker" + id };
    setRows([...rows, newRowData]);
  };
  const deleteRow = () => {
    const newRows = rows.filter((row) => !checkedSet.has(row.id)); // 체크되지 않은 Row만 남긴 newRows를 저장
    clearSet(); // row삭제 이후 기존 체크 상태를 해제 해주기 위해 Set을 비워준다.
    setRows(newRows);
  };

  return (
    {...}
  )
}

문제점

기존에 Check 상태를 관리 할때
const [checked, setChecked] = useState(new Array(rows.length).fill(false))와 같이 상태를 만들었다.
전체 Row 개수 만큼 배열을 생성 해야 하고
심지어 Row가 추가되거나 삭제 되면 checked 매번 크기(length)를 변경해줘야 한다.

이런 생각을 하던중 Set 자료구조를 활용 해보기로 했는데

Set 객체는 값 콜렉션으로, 삽입 순서대로 요소를 순회할 수 있습니다. 하나의 Set 내 값은 한 번만 나타날 수 있습니다. 즉, 어떤 값은 그 Set 콜렉션 내에서 유일합니다.
Set - Javascript | MDN

Set은 중복이 없고, 삽입 순서를 보장한다. 이정도면 기존에 배열을 이용하던걸 Set으로 교체 할수 있다고 생각했다.
그리고 배열도 훌륭한 method(filter, concat, push, shift 등)를 보유하고 있지만,
Set은 넘사벽 method들과 속성 및 훌륭한 네이밍을 보유하고 있다. add(), delete(), clear(), has(), size

이 훌륭한 method를 이용해서 useCheckGroup hook을 작성 해보았다.

import { useState } from "react";

const useCheckGroup = () => {
  const [checkedSet, setCheckedSet] = useState(new Set()); // 체크된 rowId들을 담는 상태
  
  // item(rowId)을 checkedSet에 추가 한다.
  const addToSet = (item) =>
    setCheckedSet((prev) => {
      const set = new Set(prev);
      set.add(item);
      return set;
    });
  // checkedSet에서 item(rowId)을 삭제 한다.
  const deleteToSet = (item) =>
    setCheckedSet((prev) => {
      const set = new Set(prev);
      set.delete(item);
      return set;
    });
  // checkSet을 비운다.
  const clearSet = () => {
    setCheckedSet(new Set());
  };
  // checkSet을 items로 교체 한다.
  const replaceSet = (items) => {
    setCheckedSet(new Set(items));
  };

  // { checkedSet, addToSet, deleteToSet, clearSet, replaceSet }` 굉장히 직관적인 함수들을 반환한다.
  return {
    checkedSet,
    addToSet,
    deleteToSet,
    clearSet,
    replaceSet
  };
};
export default useCheckGroup;

장점

checked 상태를 체크된 녀석들만 보관할 수 있다.
추가/삭제 구현이 매우 간단하다.
특히 Row 삭제 하는 부분이 너무 아름답게 구현된다.

  const deleteRow = () => {
    const newRows = rows.filter((row) => !checkedSet.has(row.id));
    clearSet();
    setRows(newRows);
  };

전체 코드

profile
왜?를 생각하며 개발하기, 다양한 프로젝트를 경험하는 것 또한 중요하지만 내가 사용하는 기술이 어떤 배경과 이유에서 만들어진 건지, 코드를 작성할 때에도 이게 최선의 방법인지를 끊임없이 질문하고 고민하자. 이 과정은 앞으로 개발자로 커리어를 쌓아 나갈 때 중요한 발판이 될 것이다.

0개의 댓글