[WEB] 프론트엔드

[React] react-table을 활용해 기본 테이블 만들기

[FE] Lighthouse 2024. 8. 28. 10:48

Overview

프론트엔드 프로젝트를 하다보면 테이블 컴포넌트를 활용해야 할 경우가 있습니다.

이럴때 다양한 선택지를 놓고 고민할 수 있습니다.

 

1. HTML table 태그, div 태그를 활용해 테이블 기능 직접 구현

2. MUI, antd, chakra-ui 와 같은 UI 라이브러리에서 제공해주는 Table 컴포넌트 사용

3. Headless UI 라이브러리를 활용해 Table 컴포넌트 구현

 

요즘에는 첫 번째 선택지의 table 태그로 직접 테이블을 구현하지 않습니다. 태그의 자유도가 떨어져 div 태그를 활용해 구현하는 편 입니다.

 

2번의 UI 라이브러리를 사용한다면 테이블 컴포넌트 뿐만 아닌 다양한 컴포넌트를 사용할 수 있어서 장점이 뚜렷합니다.
하지만 테이블 컴포넌트만 사용하기엔 라이브러리의 번들 사이즈가 상당히 크고, 디자인을 커스텀하기 어렵다는 단점이 있습니다.
그리고 여러 프로젝트에서 해당 라이브러리들을 사용해본 경험으로는 프로젝트가 지속될수록 라이브러리의 버전 관리를 잘 해줘야 하는데, 메인 라이브러리 (예를 들면 react)와의 결합도가 강하기 때문에 버전관리가 복잡하다는 단점이 있었습니다.

오늘 포스팅에서는 3번의 Headless UI 라이브러리를 사용해 테이블의 기능을 붙이고 div 태그를 사용해 데이터를 표현하는 기초 테이블을 구현해보겠습니다.

 

기술 스택

- React

- TypeScript

- @tanstack/react-table

- CSS

 

스타일링은 간단하게 CSS를 사용합니다. CSS는 웹 표준이며 어느 상황에서도 쉽게 사용할 수 있습니다. CSS를 모듈방식으로 사용해 테이블 컴포넌트를 구현해봅니다.

 

설치

pnpm add @tanstack/react-table

 

구현

props

먼저 테이블 컴포넌트를 설계해봅시다. 컴포넌트는 데이터와 컬럼 리스트를 받아서 화면에 표 형식으로 데이터를 그려줘야 합니다.

그렇기 때문에 props로 data와 column을 받아줍니다.

데이터의 타입은 컴포넌트에서 추론할 수 없습니다. 사용하는 곳 마다의 데이터 타입이 다르기 때문입니다. 그렇기 때문에 타입 매개변수인 제네릭을 활용해 데이터와 컬럼에 타입을 전달해주기로 하겠습니다.

import { ColumnDef } from "@tanstack/react-table";

type Props<T> = {
  data: T[];
  columns: ColumnDef<T>[];
};

 

컴포넌트 구현

컴포넌트는 함수 컴포넌트로 구현할 예정입니다. props로는 위에 설계한 타입의 props를 전달 받을 예정이고 타입 매개변수를 전달 받아야 합니다.

function BasicTable<T>(props:Props<T>) {
  return <div></div>
}

 

react-table 라이브러리에서 useReactTable 훅을 import 합니다. 훅의 매개변수로 설정을 위핸 객체를 넘겨주는데, 해당 객체에 넘겨주는 값들로 react-table의 기능을 구현할 수 있습니다.

 

이번 포스팅에서는 간단하게 데이터와 컬럼, 코어모델만 넘겨주어 기초 테이블을 구현해 볼 예정입니다.

 

props로 전달받은 data, columns를 함께 넘겨줍시다. 

getCoreRowModel 이라는 함수를 라이브러리에서 import해와 함수를 getCoreRowModel의 값으로 함수 호출한 값을 넘겨줍니다.

import {
  ColumnDef,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";

function BasicTable<T>({ data, columns }:Props<T>) {
  const table = useReactTable<T>({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });
  
  return <div></div>
}

 

이제 return 될 테이블 컴포넌트를 마크업 해봅시다! 아래는 설명에 앞서 구현할 코드의 전문입니다.

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

  const { getHeaderGroups, getRowModel } = table;
  return (
    <div className={style.tableContainer}>
      <div className={style.tableHeaderContainer}>
        {getHeaderGroups().map((headerGroup) => (
          <div key={headerGroup.id} className={style.tableHeaderRow}>
            {headerGroup.headers.map((header) => (
              <div
                key={header.id}
                className={style.cell}
                style={{ width: header.getSize() }}
              >
                {header.isPlaceholder
                  ? null
                  : flexRender(
                      header.column.columnDef.header,
                      header.getContext()
                    )}
              </div>
            ))}
          </div>
        ))}
      </div>
      <div className={style.tableBodyContainer}>
        {getRowModel().rows.map((row) => (
          <div key={row.id} className={style.row}>
            {row.getVisibleCells().map((cell) => (
              <div
                key={cell.id}
                className={style.cell}
                style={{ width: cell.column.getSize() }}
              >
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

 

먼저 테이블 header, body를 감싸줄 컨테이너 div를 준비합니다.

 

header

header에도 여러 row가 존재할 수 있으니 header을 감싸줄 컨테이너 div도 필요합니다.

useReactTable 훅으로부터 리턴받은 table 인스턴스에서 getHeaderGroups 메서드를 꺼내와 header을 그려주겠습니다.

headerGroup은 각각의 헤더 로우로써 컬럼들(headers)을 담고 있습니다.

headerRow 인 div를 사용하고 headerGroup.headers을 통해 cell을 만들어줍시다!

이때 주의해야 할 점은 header.getSize를 통해 컬럼의 너비를 지정해줘야 합니다.

div태그를 활용해 flexible하게 테이블 컴포넌트를 마크업 하기 떄문에 반드시 테이블 인스턴스에 저장된 셀의 너비를 사용해줘야 합니다.

셀의 내용은 flexRender이라는 메서드를 라이브러리에서 import 해와서 랜더링 시킵니다. 컬럼에 정의된 내용과 셀을 매핑시켜 flexible 하게 셀을 렌더링합니다.

 

body

body도 마찬가지로 여러 row모델이 존재함으로 컨테이너 div를 준비합니다.

useReactTable 훅으로부터 리턴받은 table 인스턴스에서 getRowModel 메서드를 꺼내와 body를 그려주겠습니다.

위 메서드를 순회하며 각각의 row를 화면에 그려주고 화면에 보여지는 셀만 그리기 위해 getVisibleCells 메서드를 사용해 셀 리스트를 받아옵니다. 그리고 header와 똑같은 방식으로 cell을 순회하고 flexRender 메서드를 사용해 플렉서블하게 셀을 렌더링합니다.

 

스타일링

간단하게 스타일링한 CSS 코드도 첨부하겠습니다.

.tableContainer {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  border: 1px solid black;
}

.tableHeaderContainer {
  width: 100%;
  height: 40px;
  border: 1px solid #f4f6f8;
}

.tableHeaderRow {
  width: 100%;
  height: 100%;
  display: flex;
  font-weight: 700;
}

.tableBodyContainer {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow-y: scroll;
}

.row {
  width: 100%;
  height: 100%;
  display: flex;
  border: 1px solid #f4f6f8;
}

.row:hover {
  background-color: #e9f3ff !important;
}
.row:nth-child(odd) {
  background-color: #f9fafc;
}

.cell {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  font-size: 12px;
  border-right: 1px solid #eff1f3;
  border-color: #f4f6f8;
}

 

구현된 테이블

이렇게 완성한 테이블의 모습입니다. 간단하게 데이터만 display하는 테이블의 형태입니다.

 

마치며

@tanstack/react-table을 활용해 아주 간단하게 기초 테이블 컴포넌트를 구현해봤습니다.

해당 라이브러리를 사용해 테이블을 구현하는 과정은 이전에도 포스팅 했던 적이 있습니다.

이번 포스팅에서는 특정 스타일링 라이브러리를 사용하지 않고 정말 간단한 테이블을 구현하는 과정을 남겨놓고 싶었습니다.

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

 

더 자세하게 라이브러리를 살펴보고 싶으시다면 공식 문서를 살펴봐주세요!

공식문서

 

여러 기능이 구현된 테이블 컴포넌트가 궁금하시다면 아래 레포지토리를 참고해주시면 감사하겠습니다.

radix-storybook 레포지토리

 

읽어봐주셔서 감사합니다.