(번역) 데이터 구조를 개선하여 코드 43% 줄이기를 보고, 나도 Set
자료 구조를 사용해서 데이터 테이블을 직접 구현해볼까? 하는 생각이 들어서 후다닥 만들어봤습니다.
React로 데이터 테이블을 구현하는 과정을 기록했으며, 잘못된 정보가 있거나 설명이 부족한 부분이 있다면 댓글로 남겨주시면 감사하겠습니다! 🤗
동작하는 코드는 stackblitz에서 바로 확인해볼 수 있습니다.
가장 먼저 할 일은 컴포넌트를 설계하는 것입니다.
컴포넌트 설계 단계에서는 머리속으로 생각하고 있는 추상적인 기능들을 구체화합니다. 컴포넌트 설계가 미흡하면 추후 컴포넌트를 수정하는 일이 발생하고, 코드가 복잡해질 가능성이 생기기 때문에 매우 중요한 단계입니다.
저는 컴포넌트를 설계할 때 커스텀 가능성
을 중점적으로 고려하는 편이며, 커스텀 가능성에 따라 장단점이 나뉘지기 때문에 컴포넌트의 목적에 따라 설정해야 합니다.
커스텀 가능성 | 장점 | 단점 |
---|---|---|
높다 | 다양한 옵션으로 사용 가능 | 러닝커브가 높아짐, 컴포넌트가 무거워질 수 있음 |
낮다 | 러닝커브가 낮음 | 기능이 적고, 유연하지 못한 컴포넌트가 될 수 있음 |
러닝커브가 높고 낮다는 의미는 내가 만든 컴포넌트를 사용하는 다른 개발자의 입장에서 적은 것 입니다. 커스텀 가능성이 높을수록 컴포넌트에 대한 깊은 이해가 있어야 의도에 맞게 컴포넌트를 사용할 수 있기 때문이죠.
checkbox
가 표시되며, 사용하지 않으면 표시되지 않습니다.disabled: true
요소를 추가하면 행을 비활성화 시킬 수 있습니다.이해를 돕기 위해 사진을 첨부했습니다!
props | 데이터 타입 | 설명 | 필수여부 |
---|---|---|---|
headers | array | 테이블 헤더 행을 정의 | √ |
items | array | 테이블 바디에 보여지는 데이터 목록 | |
itemKey | string | 선택 행의 키값 | |
selectable | boolean | 행 선택 기능 사용여부 | |
updateSelection | function | 선택된 행이 변경되면 실행할 함수 |
데이터 타입이 배열인 header와 items는 객체를 담는 배열로 아래에 자세한 설명을 작성했습니다.
[
{
text: '컬럼명',
value: '데이터명'
}
]
각 컬럼을 객체로 정의하며, 컬럼의 수 만큼 배열에 넣어줘야 합니다.
위 사진을 보면 테이블 헤더에 컬럼이 3개가 있습니다.
const headers = [
{
text: 'Name',
value: 'name'
},
{
text: 'Version',
value: 'version'
},
{
text: 'Launch Date',
value: 'launch'
}
];
테이블 헤더는 이렇게 정의해줄 수 있습니다.
headers에서 지정한 컬럼의 수 만큼 데이터를 입력합니다. 객체 하나가 열 하나에 해당합니다.
const items = [
{
name: 'React',
version: '18.2.0',
launch: '2013-05-29'
},
{
name: 'Vue',
version: '3.2.45',
launch: '2014-02'
},
{
name: 'jQuery',
version: '3.3',
disabled: true,
launch: '2006-08-26'
},
{
name: 'Svelte',
version: '3.53.1',
launch: '2016-11-26'
}
];
headers에서 name, version, launch 세 개의 컬럼을 지정했기 때문에, 데이터 객체에서 지정한 데이터명에 데이터를 넣어줍니다.
export default function DataTable({}) {
return (
<table>
<thead>
{/* TODO 테이블 헤드 바인딩 */}
</thead>
<tbody>
{/* TODO 테이블 데이터 바인딩 */}
</tbody>
</table>
)
}
컴포넌트 테이블의 기본 구성 요소들을 배치합니다.
Props를 보면 headers
는 필수 props입니다. 따라서 headers가 없다면 에러를 발생시키고, headers가 있을 경우에만 map
함수로 순환하며 테이블에 추가해줍니다.
export default function DataTable({ headers }) {
// headers가 있는지 체크하고, 없다면 에러를 던짐
if (!headers || !headers.length) {
throw new Error('<DataTable /> headers is required.')
}
return (
<table>
<thead>
<tr>
{
headers.map((header) =>
<th key={header.text}>
{header.text} {/* 컬럼명 바인딩 */}
</th>
)
}
</tr>
</thead>
<tbody>
{/* TODO 테이블 데이터 바인딩 */}
</tbody>
</table>
)
}
이제 App.js
에서 컴포넌트를 사용해볼까요? props로 넘겨주는 headers 배열은 headers에 있습니다.
import DataTable from "./components/dataTable";
function App() {
return (
<div className="App">
<DataTable
headers={headers} {/* headers props 보내기 */}
/>
</div>
);
}
테이블의 헤더 부분이 잘 나오네요! 😄
데이터를 표시할 때 신경써야 하는 부분은 데이터가 헤더에 설정한 순서대로 정의되어서 오지 않을 수 있다는 점입니다.
따라서 데이터 객체에 들어있는 순서에 상관 없이 헤더에 맞는 데이터를 보여줘야 합니다.
저는 header의 value들이 담겨있는 headerKey
배열을 추가로 만들었고, 행을 표시할 때 headerKey
배열을 순회하면서 행의 요소들에 접근해서 데이터를 바인딩해줬습니다.
export default function DataTable(
{
headers,
items = [], // items props 받기, default parameter 빈 배열로 설정
}) {
if (!headers || !headers.length) {
throw new Error('<DataTable /> headers is required.')
}
// value 순서에 맞게 테이블 데이터를 출력하기 위한 배열
const headerKey = headers.map((header) => header.value);
return (
<table>
<thead>
{/* ... */}
</thead>
<tbody>
{
items.map((item, index) => (
<tr key={index}>
{/* headerKey를 순회하면서 key를 가져옴 */}
{
headerKey.map((key) =>
<td key={key + index}>
{item[key]} {/* key로 객체의 값을 출력 */}
</td>
)
}
</tr>
))
}
</tbody>
</table>
)
}
데이터가 잘 출력되는지 App.js
에서 데이터를 넘겨볼까요? props로 넘겨주는 items 배열은 items에 있습니다.
function App() {
return (
<div className="App">
<DataTable
headers={headers}
items={items} {/* items props 보내기 */}
/>
</div>
);
}
아직 비활성 행에 대한 코드를 작성하지 않아서 모두 활성화된 행으로 보여주고 있지만, 잘 출력되고 있네요! 👍
이제 행 선택 기능 사용여부에 따라서 체크박스를 보여줘야 합니다.
selectable
props를 받아서 true
일 경우에만 체크박스를 보여주면 되겠죠?
export default function DataTable(
{
headers,
items = [],
selectable = false, // selectable props 받기
}) {
if (!headers || !headers.length) {
throw new Error('<DataTable /> headers is required.')
}
const headerKey = headers.map((header) => header.value);
return (
<table>
<thead>
<tr>
{/* 선택 기능을 사용할 때만 바인딩 */}
{
selectable && <th><input type="checkbox" /></th>
}
{
headers.map(
(header) => <th key={header.text}>{ header.text }</th>
)
}
</tr>
</thead>
<tbody>
{
items.map((item, index) => (
<tr key={index}>
{/* 선택 기능을 사용할 때만 바인딩 */}
{
selectable && <td><input type="checkbox" /></td>
}
{
headerKey.map((key) =>
<td key={key + index}>{item[key]}</td>
)
}
</tr>
))
}
</tbody>
</table>
)
}
App.js
에서 props로 selectable={true}
를 보내서 체크박스가 잘 출력되는지 확인해볼까요?
function App() {
return (
<div className="App">
<DataTable
headers={headers}
items={items}
selectable={true} {/* selectable props 보내기 */}
/>
</div>
);
}
체크박스가 잘 보이네요! 이제 데이터 테이블 컴포넌트를 사용하는 개발자가 원하는 경우에 따라 행 선택 여부를 제어할 수 있게 되었습니다.
드디어 Set
을 사용해서 행 선택 기능을 구현할 차례입니다.
코드가 길어지는 것을 방지하기 위해 DataTable의 기존 코드들은 생략했습니다.
export default function DataTable(
{
headers,
items = [],
selectable = false,
itemKey // itemKey props 받기
}
) {
// itemKey가 없다면 headers의 첫번째 요소를 선택
if (!itemKey) {
itemKey = headerKey[0];
}
// 선택한 row의 itemKey를 담은 배열
const [selection, setSelection] = useState(new Set());
}
먼저 선택한 행의 데이터를 담을 배열을 선언해줍니다.
행 전체의 데이터를 담지 않고, itemKey
에 해당하는 데이터만 배열에 담으려고 합니다.
itemKey
는 props가 없을 경우 headerKey
의 첫번째 요소를 선택하는데, 위의 데이터 테이블의 itemKey는 name
이므로 행을 선택하면 ['React', 'Vue', 'jQuery', 'Svelte']
와 같은 데이터가 담기게 됩니다.
단일 행을 선택했을 경우 데이터 객체에서 itemKey로 값을 받아와 넣어주게 됩니다.
export default function DataTable({ /* ... */ }) {
const [selection, setSelection] = useState(new Set());
const onChangeSelect = (value) => {
// 기존의 selection으로 새로운 Set 생성
const newSelection = new Set(selection);
if (newSelection.has(value)) {
// value가 있으면 삭제 (checked가 false이기 때문)
newSelection.delete(value);
} else {
// value가 없으면 추가 (checked가 true이기 때문)
newSelection.add(value);
}
// 새로운 Set으로 state 변경
setSelection(newSelection);
};
}
특정 행 체크박스 클릭시 selection
에 value
가 없다면 value를 추가해줍니다. 이 상태에서 또 같은 행 체크박스를 클릭하면 selection
에 이미 value
가 있기 때문에 기존에 있던 value를 제거하게 되는거죠.
HTML 코드
return (
<table>
<thead>{/* ... */}</thead>
<tbody>
{
items.map((item, index) => (
<tr
key={index}
className={
`
${selection.has(item[itemKey]) ? 'select_row': ''}
${item.disabled ? 'disabled_row' : ''}
`
}>
{/* 속성 넣어주기 */}
{
selectable &&
<td>
<input
type="checkbox"
disabled={item.disabled}
checked={selection.has(item[itemKey])}
onChange={() => onChangeSelect(item[itemKey])}
/>
</td>
}
{
headerKey.map((key) =>
<td key={key + index}>
{item[key]} {/* key로 객체의 값을 출력 */}
</td>
)
}
</tr>
))
}
</tbody>
</table>
)
onChange
이벤트와 checked
, disabled
등의 속성 값도 넣어줍니다. 참고로 tr
의 className
은 css를 위해 클래스를 바인딩하는 것입니다.
단일 행 선택 기능이 완성되었습니다!
전체 선택의 경우 이벤트 객체로 target
의 checked
상태로 전체 선택 상태를 파악합니다.
export default function DataTable({ /* ... */}) {
// disabled가 true인 item만 반환하는 함수
const getAbledItems = (items) => {
return items.filter(({ disabled }) => !disabled );
};
const onChangeSelectAll = (e) => {
if (e.target.checked) {
// checked가 true인 경우 전체 선택
const allCheckedSelection = new Set(
// 활성화된 행의 배열을 순회하며 itemKey로 요소에 접근해 데이터를 저장
getAbledItems(items).map((item) => item[itemKey])
);
setSelection(allCheckedSelection);
} else {
// checked가 false인 경우 전체 선택 해제
setSelection(new Set());
}
};
// 전체 선택 상태 여부
const isSelectedAll = () => {
return selection.size === getAbledItems(items).length;
};
}
selection
의 size
와 활성화 행들의 length
가 같다면 전체 선택이 되어있다는 것을 알 수 있습니다. 전체 선택 체크박스의 checked
속성을 사용하기 위해 함수로 만들었습니다.
HTML 코드
return (
<table>
<thead>
<tr>
{/* 속성 넣어주기 */}
{
selectable &&
<th>
<input
type="checkbox"
checked={isSelectedAll()}
onChange={onChangeSelectAll}
/>
</th>
}
{
headers.map((header) =>
<th key={header.text}>
{header.text} {/* 컬럼명 바인딩 */}
</th>
)
}
</tr>
</thead>
<tbody>{/* ... */}</tbody>
</table>
)
onChange
이벤트와 checked
등의 속성 값도 넣어줍니다.
이제 모든 기능을 완성했습니다. 🎉🎉
하지만 아직까지는 외부에서 선택된 행의 value
값을 담은 selection
을 받을 수 없습니다.
updateSelection
함수를 props로 받아서 selection
을 함수에 인자로 넘겨줘서 외부에서 확인할 수 있도록 해볼까요?
const onChangeSelect = (value) => {
const newSelection = new Set(selection);
if (newSelection.has(value)) {
newSelection.delete(value);
} else {
newSelection.add(value);
}
setSelection(newSelection);
// updateSelection 함수 호출
updateSelection([...newSelection]);
};
const onChangeSelectAll = (e) => {
if (e.target.checked) {
const allCheckedSelection = new Set(
getAbledItems(items).map((item) => item[itemKey])
);
setSelection(allCheckedSelection);
// updateSelection 함수 호출
updateSelection([...allCheckedSelection]);
} else {
setSelection(new Set());
// updateSelection 함수 호출
updateSelection([]);
}
};
onChangeSelect
함수와 onChangeSelectAll
함수에서 호출해서 상태를 업데이트 해줍니다.
마지막으로 App.js
에서 selection을 담을 상태를 선언하고, useEffect
로 selection이 변경하는 것을 감지해서 콘솔에 출력하도록 해보겠습니다.
function App() {
const [selection, setSelection] = useState([]);
useEffect(() => {
console.log(selection);
}, [selection]);
return (
<div className="App">
<DataTable
headers={headers}
items={items}
selectable={true}
updateSelection={setSelection}
/>
</div>
);
}
단일 행 선택과 전체 선택을 해보면 콘솔에 name
값이 담긴 배열이 출력되는 것을 볼 수 있습니다.
이제 정말 완성입니다! 👏👏👏
React Data Table에 소스코드가 공개되어 있습니다.
잘보고갑니다! 이미지, 코드를 같이 보여주셔서 이해하는 데 도움이 많이됐습니다!! 공유해주셔서 감사합니다 : )