Logo

React Table로 테이블 UI 구현하기

웹 페이지 상에서 많은 양의 데이터를 일목요연하게 보여주기 위해서 테이블(table) UI가 많이 사용됩니다. 정렬이나 검색을 지원하는 완성도 있는 테이블 UI를 직접 개발하려면 생각했던 것보다 구현이 복잡해지는 경우가 많은데요. React Table은 React로 테이블 UI를 간단하게 구현할 수 있도록 도와주는 라이브러리 입니다.

이 번 포스팅에서는 React Table을 이용하여 정렬과 검색을 지원하는 테이블 UI를 구현해보도록 하겠습니다.

테이블 데이터 생성

본인의 React 프로젝트에 npm으로 랜덤 데이터를 생성해주는 faker 라이브러리를 설치합니다.

$ npm i faker

faker를 이용하여 여러 사용자의 데이터를 담고 있는 배열을 만들어 다음 섹션에서 작성할 <Table> 컴포넌트에 prop으로 넘겨줍니다.

// App.jsx

import "./styles.css";
import faker from "faker/locale/ko";
import Table from "./Table";

faker.seed(100);

function App() {
  const columns = ["Name", "Email", "Phone"];

  const data = Array(53)
    .fill()
    .map(() => ({
      name: faker.name.lastName() + faker.name.firstName(),
      email: faker.internet.email(),
      phone: faker.phone.phoneNumber(),
    }));

  return <Table columns={columns} data={data} />;
}

export default App;

기본 테이블 구현

일반적으로 테이블 UI는 <table>, <thead>, <tbody>, <tfoot>, <tr>, <th>, <td>와 같은 다양한 HTML 엘리먼트로 구성이 됩니다. 이전 섹션에서 작성한 <App/> 컴포넌트에서 prop으로 넘긴 columnsdata 배열을 받아 테이블에 랜더링해주는 간단한 React 컴포넌트를 작성해보겠습니다.

// Table.jsx

import React from "react";

function Table({ columns, data }) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column) => (
            <th key={column}>{column}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map(({ name, email, phone }) => (
          <tr key={name + email + phone}>
            <td>{name}</td>
            <td>{email}</td>
            <td>{phone}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default Table;

이런 식으로 별도 라이브러리 없이도 React로 간단한 UI를 구현하는데는 큰 문제가 없습니다. 하지만 실제로 우리가 웹 상에서 사용하는 많은 테이블들은 여기에 검색이나 정렬, 페이징, 필터와 같은 기능을 부가적으로 제공합니다. 테이블 UI에 이러한 추가적인 기능을 직접 구현하다보면 코드는 점점 복잡해지고 버그가 생긴 확률도 커질 것입니다.

React Table 적용

위에서 구현한 테이블 UI를 이번에는 React Table 라이브러리를 이용해서 다시 작성해보겠습니다.

먼저 <App/> 컴포넌트에 약간의 변경이 필요한데요. columns, data 객체에 React의 useMemo()를 훅(hook) 함수를 적용해줍니다. 이렇게 해주면 메모이제이션(memoization)을 통해 불필요한 데이터 생성을 피할 수 있기 때문에 React Table 라이브러리에서는 useMemo()를 사용하는 것을 성능 측면에서 권장하고 있습니다.

columns 객체는 기존에는 단순히 문자열 배열이 었는데, React Table 라이브러리가 이해할 수 있는 객체 배열의 형태로 바꿔줘야 합니다. accessor 속성에는 해당 열을 data 객체의 어느 속성을 읽어야하는지를 명시하고, Header 속성에는 테이블 헤더에 보여줄 텍스트를 명시합니다.

// App.jsx

import { useMemo } from "react";
import "./styles.css";
import faker from "faker/locale/ko";
import Table from "./Table";

faker.seed(100);

function App() {
  const columns = useMemo(
    () => [
      {
        accessor: "name",
        Header: "Name",
      },
      {
        accessor: "email",
        Header: "Email",
      },
      {
        accessor: "phone",
        Header: "Phone",
      },
    ],
    []
  );

  const data = useMemo(
    () =>
      Array(53)
        .fill()
        .map(() => ({
          name: faker.name.lastName() + faker.name.firstName(),
          email: faker.internet.email(),
          phone: faker.phone.phoneNumber(),
        })),
    []
  );

  return <Table columns={columns} data={data} />;
}

export default App;

이제 본격적으로 React Table를 사용하기 위해서 npm으로 react-table 패키지를 설치하겠습니다.

$ npm i react-table

그 다음, <Table/> 컴포넌트를 열고 react-table 패키지로 부터 useTable() 훅(hook) 함수를 불러옵니다. useTable() 함수에는 prop으로 넘어온 columnsdata 객체를 매개변수로 넘기면 테이블을 마크업할 때 사용할 수 있는 다양한 함수와 배열을 반환해줍니다.

예를 들어, getTableProps()getTableBodyProps() 함수는 각각 <table><tbody> 엘리먼트에 적용해줘야 할 prop을 제공해주고, headerGroups 배열은 <thead> 부분에서 랜더링해야하는 데이터를 담고, rows 배열은 <tbody> 부분에서 랜더링해야하는 데이터를 담고 있습니다. 마지막으로 prepareRow()라는 함수는 랜더링할 데이터를 준비해주는 유틸리티 함수인데, 어디서 호출하고 있는지는 아래 코드를 참고바랍니다.

// Table.jsx

import React from "react";
import { useTable } from "react-table";

function Table({ columns, data }) {
  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
    useTable({ columns, data });

  return (
    <table {...getTableProps()}>
      <thead>
        {headerGroups.map((headerGroup) => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map((column) => (
              <th {...column.getHeaderProps()}>{column.render("Header")}</th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map((row) => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.cells.map((cell) => (
                <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
              ))}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

export default Table;

코드가 좀 복잡해보일 수도 있지만 useTable() 훅 함수가 반환하는 함수나 배열을 어떤 HTML 엘리먼트에 적용시켜줘야 하는지 파악하기만 하면 기본적인 감이 잡히실 겁니다.

검색 기능 구현

테이블 UI에서 검색 기능을 구현하면 사용자가 많은 양의 데이터 세트에서 원하는 데이터를 찾기가 훨씬 수월해질 것입니다.

React Table은 필드 별 검색을 위한 useFilters() 훅 함수와, 전체 데이터 검색을 위한 useGlobalFilter() 훅 함수를 제공하고 있는데요. 본 포스팅에서는 useGlobalFilter() 훅 함수를 사용하는 방법에 대해서만 살펴보겟습니다.

먼저, 테이블 UI에 검색창을 추가하기 위해서 <Search/> 컴포넌트를 작성하겠습니다.

// Search.jsx

import React from "react";

function Search({ onSubmit }) {
  const handleSubmit = (event) => {
    event.preventDefault();
    onSubmit(event.target.elements.filter.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="filter" />
      <button>Search</button>
    </form>
  );
}

export default Search;

그 다음, <Table/> 컴포넌트를 열고, react-table 패키지로 부터 useGlobalFilter() 훅 함수를 불러와서 useTable() 훅 함수의 인자로 넘겨줍니다. 이렇게 해주면 useTable() 훅 함수는 setGlobalFilter()라는 함수를 반환해주는데, 이 함수를 <Search/> 컴포넌트에 prop으로 넘겨줍니다.

// Table.jsx

import React from "react";
import { useTable, useGlobalFilter } from "react-table";
import Search from "./Search";

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    setGlobalFilter,
  } = useTable({ columns, data }, useGlobalFilter);

  return (
    <>
      <Search onSubmit={setGlobalFilter} />
      <table {...getTableProps()}>
        <thead>
          {headerGroups.map((headerGroup) => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column) => (
                <th {...column.getHeaderProps()}>{column.render("Header")}</th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>
          {rows.map((row) => {
            prepareRow(row);
            return (
              <tr {...row.getRowProps()}>
                {row.cells.map((cell) => (
                  <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
                ))}
              </tr>
            );
          })}
        </tbody>
      </table>
    </>
  );
}

export default Table;

<Search/> 컴포넌트 내에서 검색어를 입력 후에 양식을 제출하면 이 setGlobalFilter()라는 함수가 호출되고 rows 배열에 필터링된 검색 결과가 반영이 될 것입니다.

정렬 기능 구현

React Table 라이브러리를 사용하면 코드 몇 줄 만으로 테이블 UI에 정렬 기능도 추가할 수 있습니다.

react-table 패키지로 부터 useSortBy() 훅 함수를 불러와서, useTable() 훅 함수를 호출할 때 추가로 인자로 넘깁니다. 그 다음, <th/> 엘리먼트에 넘기는 prop을 다음과 같이 수정 해주기만 하면 됩니다.

// <th {...column.getHeaderProps()}>
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
import React from "react";
import { useTable, useGlobalFilter, useSortBy } from "react-table";
import Search from "./Search";

function Table({ columns, data }) {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    setGlobalFilter,
  } = useTable({ columns, data }, useGlobalFilter, useSortBy);

  return (
    <>
      <Search onSubmit={setGlobalFilter} />
      <table {...getTableProps()}>
        <thead>
          {headerGroups.map((headerGroup) => (
            <tr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column) => (
                <th {...column.getHeaderProps(column.getSortByToggleProps())}>
                  {column.render("Header")}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody {...getTableBodyProps()}>
          {rows.map((row) => {
            prepareRow(row);
            return (
              <tr {...row.getRowProps()}>
                {row.cells.map((cell) => (
                  <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
                ))}
              </tr>
            );
          })}
        </tbody>
      </table>
    </>
  );
}

export default Table;

이제 각 테이블 헤더를 한 번 클릭해보면 데이터가 오름차순으로 정렬되고 또 다시 클릭해보면 내림차순으로 정렬되는 것을 확인할 수 있을 것입니다.

전체 코드

본 포스팅에서 제가 작성한 전체 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

마치면서

React Table이 다른 테이블 라이브러리와 가장 차별화되는 부분은 바로 테이블이 어떤 모습으로 스타일이 될 지에 대해서는 전혀 관여하지 않는 다는 것입니다. 이러한 라이브러리를 흔히 헤드리스(headless)하다고 하는데요. 유연성과 확장성이 뛰어나고 유지보수가 편한다는 이점이 있습니다. 왜냐하면 개발자들이 본인 입맛에 맞게 원하는 CSS 라이브러리를 선택하여 React Table과 함께 사용할 수 있기 때문입니다.