테이블은 어떤 웹 프로젝트이든 필수로 적용하는 컴포넌트에 해당한다. 프로젝트마다 검색(필터)나 엑셀 출력 등 필요로 하는 요구사항이 비슷비슷한데, React에서는 어떤 상태관리 라이브러리를 사용하는지에 따라 이런 부가기능들을 구현하는 과정이 다 달랐다.
➜ 예를 들어 recoil을 사용하는 경우는 필터나 페이지네이션 기능은 selector를 활용했고, react-query의 경우는 hook 형태로 만들어 재활용
이번에 react-query를 최신 버전으로 업데이트하면서 성능을 고려한 기능들이 함께 업데이트 된것을 적용해본 후 API 호출에는 react-query를 적용하기로 결정했다.
(Suspense와 함께 활용이 가능한 useSuspenseQuery, 여러 쿼리를 병렬로 사용가능한 useSuspenseQueries 등이 업데이트 됨)
위의 기술 조사 과정에서 tanstack에서 Table 라이브러리도 제공하는 것을 접했고,
antd나 shadcn/ui등 UI framework에서 제공하는 라이브러리들은 이런 테이블 연계 기능 서포트에 한계가 있어 아쉬움이 있었기 때문에 본 라이브러리를 적용해보는 시간을 가지게 되었다.
헤드리스 UI 패턴의 라이브러리로
디자인까지 완성된 테이블 컴포넌트를 제공하는 것보다는, 필터나 페이지네이션부터 출력, 정렬에 대한 부가 기능들과 관련해 상태 관리를 하기 쉽도록 도와준다.
(테이블 등) 어떤 기능에 대한 부가 기능이나 상태 처리를 위한 로직만을 제공함으로써, 이외의 스타일이나 마크업 등은 제공하지 않는 라이브러리를 말한다.
$ npm install @tanstack/react-table
$ yarn add @tanstack/react-table
react-query를 통해 API를 호출한 응답값으로 준비한 예제 코드
const { data: users } = useGetUsersQuery();
Accessor Columns
➜ sort, filter, group화 가능
Display Columns
➜ sort, filter는 안되지만 버튼이나 체크박스 등을 표시할 때 사용
Grouping Columns
➜ sort, filter 그 외의 컬럼의 그룹화를 위해 사용 헤더나 풋터를 정의하는 것에 일반적으로 사용한다
필터 없이 기본적인 표현을 위해서는 columnHelper.accessor 사용
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
/*
첫번째 인자: 표시하려고 하는 값을 raw data 혹은 약간의 가공을 통해 정의한다
두번째 인자: 컬럼 설정 객체
id: 컬럼의 고유 식별자
header: 테이블 헤더를 표시할 부분을 렌더링 하는 함수
cell: 각 셀을 렌더링하는 함수로 info 객체를 받아서 값을 가져온다
info.getValue(id) 혹은
info.raw.original을 통해 테이블로 넘겨진 원래의 응답값 데이터를 참조하는 것도 가능
*/
columnHelper.accessor(
(row) => {
const role = ROLES.find((role) => role.key === row.role.name);
return role.name;
},
{
id: 'role',
header: () => <div className="text-center">그룹</div>,
cell: (info) => <div className="text-center">{info.getValue('role')}</div>,
}
),
2~3에서 정의한 테이블 컬럼과 데이터들을 테이블로 넘겨주기 위해 useReactTable훅을 사용하여 정의한다.
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
const table = useReactTable({
data: users,
columns,
// 기본 행 모델 생성 함수
getCoreRowModel: getCoreRowModel(),
});
<Table className="bg-white rounded-sm">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>