평소에 테이블을 만들때 <table>
태그 또는 flex
를 이용해서 만들었는데
이번엔 grid 기능을 이용해서 테이블을 만들어 보았다.
사용해보니 grid-template-columns
을 이용해 column의 width를 맞추기가 굉장히 편리했다.
.grid {
display: grid;
grid-template-columns: 36px 1fr 2fr;
height: 30px;
}
아래는 MDN 에서 Set에 대한 정의한 내용이다.
Set
객체는 자료형에 관계 없이 원시 값과 객체 참조 모두 유일한 값을 저장할 수 있습니다.
Set.prototype.size
: Set
객체의 value
수를 반환.
Set.prototype.add(value)
: Set
에 value
를 추가한다. 값이 추가된 Set
객체를 반환.
Set.prototype.delete(value)
: Set
에서 value
를 제거한다. 성공적으로 제거 되었는지에 대한 boolean을 반환.
Set.prototype.clear(value)
: Set
에서 모든 요소를 제거한다.
Set.prototype.has(value)
: Set
에 value
가 존재하는지 여부를 확인하는 boolean을 반환.
위와 같은 Set의 특징을 이용하여 checked 상태관리를 Array가 아닌 Set으로 해보았다.
const [checkedSet, setCheckedSet] = useState(new Set());
- Add 버튼을 클릭해 테이블의 Row 추가
- 해당 Row 마다 삭제 가능하도록(checkbox)
- 다중 Row 선택(전체 선택/해제)
- 모든 Row 선택 시 자동으로 헤더 체크박스(전체 선택 상태) 체크
- 전체 선택 후 한개 이상의 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기능 이므로 설명 생략)
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);
};