로딩 및 빈 상태 (Loading & Empty State)
데이터 비동기 로딩 시 스피너 오버레이 표시 및 검색 결과가 없을 때의 안내 메시지 커스터마이징을 학습합니다.
import * as React from 'react';
import { AXDataGrid, AXDGColumn } from '@axboot/datagrid';
import { Button, Space } from 'antd';
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 LoadingExample(props: Props) {
const [loading, setLoading] = React.useState(false);
const [spinning, setSpinning] = React.useState(false);
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 (
<div>
<div className={'flex items-center gap-[5px] py-2.5'}>
<Button onClick={() => setLoading(true)}>Loading : true</Button>
<Button onClick={() => setLoading(false)}>Loading : false</Button>
<Button onClick={() => setSpinning(true)}>Spinning : true</Button>
<Button onClick={() => setSpinning(false)}>Spinning : false</Button>
</div>
<DataGridContainer ref={containerRef}>
<AXDataGrid<IListItem>
width={containerWidth}
height={containerHeight}
headerHeight={35}
data={list}
columns={columns}
onChangeColumns={(columnIndex, { width, columns }) => {
console.log('onChangeColumnWidths', columnIndex, width, columns);
setColumns(columns);
}}
rowChecked={{
checkedIndexes: [],
onChange: (ids, selectedAll) => {
console.log('onChange rowSelection', ids, selectedAll);
},
}}
loading={loading}
spinning={spinning}
/>
</DataGridContainer>
</div>
);
}
export default LoadingExample;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. 언제 사용하며 왜 필요한가요?
API 호출 중 사용자가 빈 화면을 보고 시스템이 멈춘 것으로 오해하지 않도록 반투명 로딩 오버레이와 스피너를 띄우고, 검색 결과가 0건일 때 **“조회된 데이터가 없습니다”**라는 친절한 안내 문구를 보여주는 것은 완성도 높은 UX의 기본입니다.
AXBOOT DataGrid는 loading, spinning, msg.emptyList 속성으로 로딩 표시와 빈 데이터 메시지를 제어합니다.
2. 실무 완성형 예제: 로딩 및 빈 데이터 상태 시뮬레이션
import React, { useState } from 'react';
import { AXDataGrid, type AXDGColumn, type AXDGDataItem } from '@axboot/datagrid';
interface Product {
id: number;
name: string;
price: number;
}
export default function LoadingDemoGrid() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<AXDGDataItem<Product>[]>([]);
const columns: AXDGColumn<Product>[] = [
{ key: 'id', label: 'ID', width: 80, align: 'center' },
{ key: 'name', label: '상품명', width: 220 },
{ key: 'price', label: '가격', width: 140, align: 'right', itemRender: ({ values }) => `${values.price.toLocaleString()}원` },
];
const handleFetch = () => {
setLoading(true);
setTimeout(() => {
setData([
{ values: { id: 1, name: '에르고노믹 마우스', price: 65000 } },
{ values: { id: 2, name: '텐키리스 키보드', price: 129000 } },
]);
setLoading(false);
}, 1000);
};
const handleClear = () => {
setData([]);
};
return (
<div>
<div style={{ marginBottom: 12, display: 'flex', gap: 8 }}>
<button onClick={handleFetch} style={{ padding: '6px 12px', background: '#2563eb', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
데이터 불러오기 (로딩 1초)
</button>
<button onClick={handleClear} style={{ padding: '6px 12px', border: '1px solid #cbd5e1', background: '#fff', borderRadius: 4, cursor: 'pointer' }}>
데이터 비우기 (Empty State)
</button>
</div>
<AXDataGrid<Product>
width={500}
height={240}
columns={columns}
data={data}
rowKey="id"
loading={loading} // 로딩 상태 활성화 시 오버레이 스피너 자동 렌더링
msg={{
emptyList: '조회 조건에 일치하는 상품 데이터가 없습니다.',
}}
/>
</div>
);
}