페이지네이션 (Pagination)

DataGrid 하단 푸터 바에 페이지 번호 네비게이션을 배치하고, 서버 측 페이징 API와 연동하는 방법을 학습합니다.

#pagination#page#server-side-paging#pageSize#totalElements#bottomBarHeight
검토일: 2026-08-17
GitHub
import * as React from 'react';
import { AXDataGrid, AXDGColumn } from '@axboot/datagrid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';

interface Props {}

interface IListItem {
  id: string;
  title: string;
  writer: string;
  createAt: string;
}

const list = Array.from(Array(1000)).map((v, i) => ({
  values: {
    id: `ID_${i}`,
    title: `title_${i}`,
    writer: `writer_${i}`,
    createAt: `2022-09-08`,
  },
}));

function PagingExample(props: Props) {
  const [currentPage, setCurrentPage] = React.useState(1);
  const [columns, setColumns] = React.useState<AXDGColumn<IListItem>[]>([
    {
      key: 'id',
      label: '아이디 IS LONG !',
      width: 100,
      sortDisable: true,
    },
    {
      key: 'title',
      label: '제목',
      width: 300,
    },
    {
      key: 'writer',
      label: '작성자',
      width: 100,
    },
    {
      key: 'createAt',
      label: '작성일',
      width: 100,
    },
  ]);
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);

  return (
    <DataGridContainer ref={containerRef}>
      <AXDataGrid<IListItem>
        width={containerWidth}
        height={containerHeight}
        headerHeight={35}
        data={list}
        columns={columns}
        page={{
          currentPage: currentPage,
          pageSize: 50,
          totalPages: 10,
          totalElements: 498,
          loading: false,
          onChange: (pageNo, pageSize) => {
            console.log(pageNo, pageSize);
            setCurrentPage(pageNo);
          },
          displayPaginationLength: 5,
        }}
        onClick={({ item, index, columnIndex, column }) => {
          console.log('click tr', item, index, columnIndex, column);
        }}
      />
    </DataGridContainer>
  );
}

export default PagingExample;

1. 언제 사용하며 왜 필요한가요?

가상 스크롤(Virtual Scroll)이 전체 데이터를 한 번에 조회하여 무한 스크롤처럼 탐색할 때 유용하다면, **페이지네이션(Pagination)**은 다음과 같은 경우에 최적의 선택입니다:

  1. 대용량 DB 부하 절감: 백엔드에서 10~50건씩만 분할 조회(LIMIT / OFFSET)하여 네트워크 비용 절감
  2. 명확한 페이지 위치 파악: “3페이지의 5번째 항목”처럼 특정 페이지로 바로 점프할 수 있는 사용자 경험
  3. 인쇄 및 리포트 출력: 특정 페이지 단위로 문서를 출력하거나 검토해야 하는 업무 환경

2. 실무 완성형 예제: 서버 연동 페이지네이션

아래 코드는 페이지 변경 시 비동기 API 요청을 시뮬레이션하여 데이터를 갱신하는 실무 패턴입니다:

import React, { useState, useEffect } from 'react';
import { AXDataGrid, type AXDGColumn, type AXDGDataItem } from '@axboot/datagrid';

interface MemberItem {
  id: number;
  email: string;
  name: string;
  joinDate: string;
  status: 'ACTIVE' | 'DORMANT' | 'BLOCKED';
}

export default function MemberPaginationGrid() {
  const [currentPage, setCurrentPage] = useState(1); // 1-based page number
  const pageSize = 10;
  const totalElements = 145; // 전체 145건

  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<AXDGDataItem<MemberItem>[]>([]);

  // 페이지 변경 시 데이터 로딩 시뮬레이션
  useEffect(() => {
    setLoading(true);
    const timer = setTimeout(() => {
      const startIdx = (currentPage - 1) * pageSize;
      const mockItems: AXDGDataItem<MemberItem>[] = Array.from({ length: pageSize }).map((_, i) => {
        const itemIndex = startIdx + i + 1;
        return {
          values: {
            id: itemIndex,
            email: `user_${itemIndex}@example.com`,
            name: `사용자_${itemIndex}`,
            joinDate: '2026-08-01',
            status: itemIndex % 5 === 0 ? 'DORMANT' : 'ACTIVE',
          },
        };
      });
      setData(mockItems);
      setLoading(false);
    }, 200);

    return () => clearTimeout(timer);
  }, [currentPage]);

  const columns: AXDGColumn<MemberItem>[] = [
    { key: 'id', label: '회원번호', width: 90, align: 'center' },
    { key: 'name', label: '회원명', width: 140, align: 'center' },
    { key: 'email', label: '이메일 주소', width: 250 },
    { key: 'joinDate', label: '가입일시', width: 130, align: 'center' },
    {
      key: 'status',
      label: '상태',
      width: 100,
      align: 'center',
      itemRender: ({ values }) => (
        <span style={{ color: values.status === 'ACTIVE' ? '#16a34a' : '#d97706', fontWeight: 600 }}>
          {values.status === 'ACTIVE' ? '정상' : '휴면'}
        </span>
      ),
    },
  ];

  return (
    <div>
      <AXDataGrid<MemberItem>
        width={750}
        height={360}
        columns={columns}
        data={data}
        rowKey="id"
        loading={loading}
        // 페이지네이션 구성
        page={{
          currentPage,
          pageSize,
          totalElements,
          totalPages: Math.ceil(totalElements / pageSize),
          loading,
          onChange: (newPage: number) => {
            console.log(`페이지 이동: ${newPage} 페이지`);
            setCurrentPage(newPage);
          },
        }}
        bottomBarHeight={36} // 하단 페이지네이션 바 높이
        headerHeight={34}
        itemHeight={28}
      />
    </div>
  );
}

3. page 속성 명세

interface AXDGPage {
  // 현재 페이지 번호 (1부터 시작)
  currentPage?: number;

  // 페이지당 행 개수
  pageSize?: number;

  // 전체 페이지 수. 페이지 번호 UI를 렌더링할 때 필요
  totalPages?: number;

  // 서버 전체 데이터 총 레코드 건수
  totalElements?: number;

  // 페이지 데이터 로딩 상태
  loading?: boolean;

  // 사용자가 페이지 번호나 이전/다음 버튼을 클릭했을 때 콜백
  onChange?: (newPage: number, pageSize?: number) => void;
}

4. 실무 팁 & 주의사항 (Gotchas)

[!IMPORTANT] 1-based 페이지 규칙: 내장 페이지 번호 UI는 첫 페이지를 1로 사용합니다. 백엔드 API가 0부터 시작하는 페이지 인덱스를 요구한다면 요청 시 currentPage - 1, 응답을 화면 상태로 옮길 때 pageIndex + 1로 변환하세요. 페이지 번호 UI를 표시하려면 currentPagetotalPages를 함께 제공해야 합니다.