페이지네이션 (Pagination)
DataGrid 하단 푸터 바에 페이지 번호 네비게이션을 배치하고, 서버 측 페이징 API와 연동하는 방법을 학습합니다.
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;import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* AXDataGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .axdg-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}1. 언제 사용하며 왜 필요한가요?
가상 스크롤(Virtual Scroll)이 전체 데이터를 한 번에 조회하여 무한 스크롤처럼 탐색할 때 유용하다면, **페이지네이션(Pagination)**은 다음과 같은 경우에 최적의 선택입니다:
- 대용량 DB 부하 절감: 백엔드에서 10~50건씩만 분할 조회(
LIMIT / OFFSET)하여 네트워크 비용 절감 - 명확한 페이지 위치 파악: “3페이지의 5번째 항목”처럼 특정 페이지로 바로 점프할 수 있는 사용자 경험
- 인쇄 및 리포트 출력: 특정 페이지 단위로 문서를 출력하거나 검토해야 하는 업무 환경
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를 표시하려면currentPage와totalPages를 함께 제공해야 합니다.