스크롤바 설정 (Custom Scrollbar)
네이티브 브라우저 스크롤바와 OS에 종속되지 않는 커스텀 오버레이 스크롤바의 설정 및 도킹(Dock) 옵션을 학습합니다.
import * as React from 'react';
import { AXDataGrid } from '@axboot/datagrid';
import type { AXDGColumn } from '@axboot/datagrid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
import { Segmented, Switch, Select } from 'antd';
const columns: AXDGColumn<any>[] = [
{ key: 'id', label: 'ID', width: 80, align: 'center' },
{ key: 'title', label: 'Title', width: 300 },
{ key: 'count', label: 'Count', width: 100, align: 'right' },
{ key: 'desc', label: 'Description', width: 600 },
];
const data = Array.from({ length: 150 }).map((_, i) => ({
values: {
id: i + 1,
title: `Scrollbar test item ${i + 1}`,
count: i * 10,
desc: `Description for item ${i + 1}. This is to make the row longer to test horizontal scrolling.`,
}
}));
export default function ScrollbarExample() {
const [variant, setVariant] = React.useState<'native' | 'classic' | 'modern'>('classic');
const [statusVisible, setStatusVisible] = React.useState(true);
const [statusContentMode, setStatusContentMode] = React.useState<'default' | 'custom text' | 'custom render'>('default');
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
const getStatusContent = () => {
if (statusContentMode === 'custom text') return 'Last synced: 10:30 AM';
if (statusContentMode === 'custom render') {
return ({ totalItems, visibleItems }: any) => (
<span style={{ color: 'blue', fontWeight: 600 }}>
{visibleItems} / {totalItems} Custom Render
</span>
);
}
return undefined;
};
return (
<>
<div className='flex flex-wrap items-center gap-4 p-3 bg-slate-50 border border-slate-200 rounded-lg text-sm mb-4'>
<div className='flex items-center gap-2'>
<span className='text-xs text-slate-500 font-medium'>Variant:</span>
<Segmented
value={variant}
onChange={val => setVariant(val as any)}
options={['native', 'classic', 'modern']}
/>
</div>
<div className='flex items-center gap-2'>
<span className='text-xs text-slate-500 font-medium'>Status Visible:</span>
<Switch checked={statusVisible} onChange={setStatusVisible} size="small" />
</div>
<div className='flex items-center gap-2'>
<span className='text-xs text-slate-500 font-medium'>Status Content:</span>
<Select
value={statusContentMode}
onChange={setStatusContentMode}
options={[
{ label: 'Default', value: 'default' },
{ label: 'Custom Text', value: 'custom text' },
{ label: 'Custom Render', value: 'custom render' }
]}
style={{ width: 140 }}
size="small"
/>
</div>
</div>
<DataGridContainer ref={containerRef}>
<AXDataGrid
width={containerWidth}
height={containerHeight}
columns={columns}
data={data}
scrollbar={{
variant,
}}
status={{
visible: statusVisible,
content: getStatusContent(),
}}
frozenColumnIndex={1}
/>
</DataGridContainer>
</>
);
}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. 언제 사용하며 왜 필요한가요?
운영체제와 브라우저에 따라 기본 스크롤바의 모양과 점유 공간이 다를 수 있습니다. scrollbar prop으로 native, classic, modern 변형과 가로·세로 스크롤바의 표시 여부를 설정할 수 있습니다. 커스텀 가로 스크롤바는 항상 Bottom Bar에 표시되며 위치는 변경할 수 없습니다.
modern: 얇고 둥근 트랙과 썸, 미니멀한 이동 버튼을 사용하는 기본 스타일classic: 각진 트랙과 화살표 버튼을 사용하는 Windows 스타일native: 브라우저 네이티브 스크롤바에 기존 AXBOOT 테마만 적용하는 호환 스타일
2. 실무 완성형 예제: 커스텀 스크롤바 활성화
import React, { useState } from 'react';
import { AXDataGrid, type AXDGColumn, type AXDGDataItem } from '@axboot/datagrid';
interface Item {
id: number;
col1: string;
col2: string;
col3: string;
col4: string;
col5: string;
}
export default function CustomScrollbarGrid() {
const [data] = useState<AXDGDataItem<Item>[]>(
Array.from({ length: 50 }).map((_, i) => ({
values: {
id: i + 1,
col1: `데이터_1_${i}`,
col2: `데이터_2_${i}`,
col3: `데이터_3_${i}`,
col4: `데이터_4_${i}`,
col5: `데이터_5_${i}`,
},
}))
);
const columns: AXDGColumn<Item>[] = [
{ key: 'id', label: 'ID', width: 60, align: 'center' },
{ key: 'col1', label: '열 1', width: 180 },
{ key: 'col2', label: '열 2', width: 180 },
{ key: 'col3', label: '열 3', width: 180 },
{ key: 'col4', label: '열 4', width: 180 },
{ key: 'col5', label: '열 5', width: 180 },
];
return (
<div>
<AXDataGrid<Item>
width={600} // 가로 스크롤 유도를 위해 좁게 설정
height={260}
columns={columns}
data={data}
rowKey="id"
// 커스텀 스크롤바 설정
scrollbar={{
variant: 'modern', // 'native' | 'classic' | 'modern'
}}
/>
</div>
);
}