react-table 활용해 테이블 만들기 - 1

Thomas·2023년 10월 15일
3

react-table

목록 보기
1/1
post-thumbnail
post-custom-banner

👐 Hello Table!

얼마전 시작한 사이드 프로젝트의 백오피스에서 테이블 컴포넌트를 구현해야 했습니다.
테이블 컴포넌트는 단순히 데이터 디스플레이 용도로 구현할수도 있지만
백오피스에서 사용하는 테이블 컴포넌트는 정렬, 페이지네이션, 선택 등 다양한 기능이 필요했습니다.
모든 기능을 하나하나 구현하기엔 공수가 부족한 상황에서 회사에서 사용했던 react-table 을 활용해 구현하기로 결정했습니다.
이전 회사에서 사용한 react-table 의 버전은 너무 오래된 버전이였고 코드의 복잡도가 너무 심해서 새롭게 직접 구현하기로 결정했고, 구현했던 과정을 남기는 글 입니다.

react-table 이란?

react-table 은 headless UI for building powerful tables and datagrids 라고 소개되어 있습니다.
headless UI 는 제공되는 UI 없이 UI를 위한 기능만을 제공하는 UI 를 말합니다. react-table 공식문서를 살펴보면 selection, sorting, expander, pagination 등 정말 다양한 기능들을 제공해주고 있는것을 확인할 수 있답니다.

react-table 설치

먼저 react-table 라이브러리를 설치해줍니다.

yarn install @tanstack/react-table

UI 선택

headless UI 이기 때문에 직접 디자인을 붙일지, UI 프레임워크를 사용할지 결정해야 합니다.
저는 처음엔 MUI 에 붙여서 사용했다가 MUI 가 <table> 태그로 구성되어 있어서 컬럼의 사이즈를 설정하는데 불편함을 느껴 <div> 컴포넌트로 직접 구현을 했습니다.
MUI 에 붙이는 것과 직접 구현하는 것에 있어서 난이도 차이는 존재하지 않습니다. 공통 프레임워크를 활용해 더욱 편하게 구현하고 싶으신 분들은 MUI 를 활용하는 것이 좋을것 같습니다.

구현

가장 기본적인 형태의 테이블을 구현해보도록 하겠습니다.
먼저 테이블 컴포넌트에 useReactTable 훅을 import 하고 컴포넌트에 작성해줍니다.
useReactTable 훅의 매개변수로는 옵션 객체가 들어가는 data, columns 는 필수로 넣어줘야 합니다.
저는 테이블 컴포넌트를 페이지마다 선언해서 사용하는 것이 아닌 재사용되는 공통 컴포넌트로 활용할 예정입니다.
때문에 테이블에 들어갈 데이터의 타입을 알 수 없기 때문에 제네릭을 활용해 타입을 알려줬습니다.

기본 모양이 완성되었다면 옵션에 하나의 함수를 넣어줘야 합니다. getCoreRowModel 함수를 라이브러리에서 import 해와서 사용합니다. 해당 함수는 라이브러리에서 제공해주는 테이블 코어 팩토리 함수로써 자동으로 행을 계산하고 반환해주는 기능을 하고 있습니다.

const table = useReactTable<T>({
  data,
  columns,
  getCoreRowModel: getCoreRowModel()
});

훅을 사용해 리턴받은 table 인스턴스를 사용해 실제 UI 를 만들어봅시다!

const { getHeaderGroups, getRowModel } = table;

table 인스턴스에서 여러 함수를 리턴받을 수 있습니다.
먼저 테이블 헤더를 리턴받는 함수와 테이블의 행을 리턴받는 함수를 사용해봅시다.

<TableContainer>
  {getHeaderGroups().map((headerGroup) => (
    <TableHeader className="row">
      {headerGroup.headers.map((header) =>
        header.isPlaceholder ? null : (
          <TableCell key={header.id} width={header.column.getSize()}>
            {flexRender(header.column.columnDef.header, header.getContext())}
          </TableCell>
        ),
      )}
    </TableHeader>
  ))}
  <TableBody useMinHeight={useMinHeight}>
    {isNoData ? (
      <NoDataComponent useMinHeight={useMinHeight}>{noDataMessage}</NoDataComponent>
    ) : (
      getRowModel().rows.map((row) => (
        <TableRow className="row">
          {row.getVisibleCells().map((cell) => (
            <TableCell key={cell.id} width={cell.column.getSize()}>
              {flexRender(cell.column.columnDef.cell, cell.getContext())}
            </TableCell>
          ))}
        </TableRow>
      ))
    )}
  </TableBody>
</TableContainer>

getHeaderGroups 함수를 통해서 각각의 headerGroup 을 map 함수를 활용해 만들어줍니다.
headerGroup.headers 에는 정의한 컬럼들이 들어있습니다. header 가 placeholder 이면 null 을 리턴해주고, 그렇지 않다면 flexRender 이라는 함수를 라이브러리에서 import 해와 해당 매개변수들을 넘겨줍니다.
TableBody 에는 getRowModel 이라는 함수를 활용해 행들을 뿌려줄 수 있습니다. getRowModel 을 실행해 리턴받은 rows 를 map 함수를 활용해 각각의 row 를 만들어주고, row 의 getVisibleCells 함수를 통해 각각의 cell 들을 map 을 통해 만들어줍니다.

테이블 컴포넌트를 완성했다면 이제 해당 테이블을 사용할 페이지 혹은 다른 컴포넌트에서 컬럼과 데이터를 넘겨줍니다.

아래는 샘플 컬럼과 데이터 입니다. react-table 에서는 컬럼을 useMemo 로 감싸 메모이제이션 시키는 것을 권고하고 있습니다. accessorFn 에는 각 컬럼에 넣어줄 데이터를 넣어줄 수 있습니다. 데이터에는 샘플 데이터를 넣어줍니다.

const column: ColumnDef<Sample>[] = [
  { id: 'id', header: '아이디', accessorFn: (row: Sample) => row.id },
  { id: 'name', header: '이름', accessorFn: (row: Sample) => row.name },
  { id: 'age', header: '나이', accessorFn: (row: Sample) => row.age },
  { id: 'phone', header: '전화번호', accessorFn: (row: Sample) => row.phone },
  { id: 'email', header: '이메일', accessorFn: (row: Sample) => row.email },
  {
    id: 'addr',
    header: '주소',
    accessorFn: (row: Sample) => row.addr,
  },
];

const data: Sample[] = [
  {
    id: random(1, 1000),
    name: 'dh',
    age: 23,
    phone: '010-1234-1234',
    email: 'dhkang@naver.com',
    addr: 'seoul, korea',
  },
  {
    id: random(1, 1000),
    name: 'mike',
    age: 23,
    phone: '010-1234-1234',
    email: 'mike@naver.com',
    addr: 'seoul, korea',
  },
];

만들어본 컴포넌트를 Storybook 을 통해서 확인해봅니다.

react-table-story

간단하게 react-table 을 활용해 데이터만 담고있는 테이블을 구현해봤습니다.

💻 전체 코드

import { ColumnDef, getCoreRowModel, useReactTable, flexRender, Row } from '@tanstack/react-table';
import styled from '@emotion/styled';
import { Fragment } from 'react';

export type TableProps<T> = {
  name: string;
  data: T[];
  columns: ColumnDef<T>[];
  noDataMessage?: string;
  useMinHeight?: boolean;
};
export type TableRenderSubRowComponent<T> = (props: { row: Row<T> }) => React.ReactElement;

function Table<T>(props: TableProps<T>) {
  const { useMinHeight = true, data, columns, noDataMessage } = props;
  const table = useReactTable<T>({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  const { getHeaderGroups, getRowModel } = table;

  const isNoData = getRowModel().rows.length === 0;

  return (
    <TableContainer>
      {getHeaderGroups().map((headerGroup) => (
        <TableHeader className="row">
          {headerGroup.headers.map((header) =>
            header.isPlaceholder ? null : (
              <TableCell key={header.id} width={header.column.getSize()}>
                {flexRender(header.column.columnDef.header, header.getContext())}
              </TableCell>
            ),
          )}
        </TableHeader>
      ))}
      <TableBody useMinHeight={useMinHeight}>
        {isNoData ? (
          <NoDataComponent useMinHeight={useMinHeight}>{noDataMessage}</NoDataComponent>
        ) : (
          getRowModel().rows.map((row) => (
            <Fragment key={row.id}>
              <TableRow className="row">
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id} width={cell.column.getSize()}>
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </TableCell>
                ))}
              </TableRow>
            </Fragment>
          ))
        )}
      </TableBody>
    </TableContainer>
  );
}

export default Table;

const TableContainer = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  font-size: 14px;

  .row {
    width: 100%;
    display: flex;
    border-bottom: 1px solid rgba(224, 224, 224, 1);
  }
`;

const TableCell = styled.div<{ width: number }>`
  width: ${({ width }) => width}px;
  padding: 16px;
  color: rgba(0, 0, 0, 0.87);
  display: flex;
  align-items: center;
  word-break: break-all;
`;

const TableRow = styled.div`
  &:hover {
    background-color: rgba(0, 0, 0, 0.04);
  }
`;

const TableSubRow = styled.div`
  width: 100%;
  padding: 24px;
`;

const TableHeader = styled.div`
  font-weight: 500;
`;

const TableBody = styled.div<{ useMinHeight: boolean }>`
  min-height: ${({ useMinHeight }) => (useMinHeight ? '560px' : 'auto')};
  display: flex;
  flex-direction: column;
`;

const NoDataComponent = styled.div<{ useMinHeight: boolean }>`
  width: 100%;
  height: ${({ useMinHeight }) => (useMinHeight ? '560px' : 'auto')};
  display: flex;
  justify-content: center;
  align-items: center;
`;

🚪< Quit/ >

다음 포스팅에서는 해당 테이블에 선택하는 기능을 붙이고 페이지네이션을 붙여보도록 하겠습니다.
자세한 내용은 react-table 공식 문서에 잘 기재가 되어있으니 참고하시면 좋을 것 같습니다.
공식문서에는 다양한 예제들을 함께 확인할 수 있어서 구현하는데 엄청난 도움이 됩니다.

https://tanstack.com/table/v8


읽어봐주셔서 감사합니다.
profile
안녕하세요! 주니어 웹 개발자입니다 😆
post-custom-banner

0개의 댓글