컬럼 순서 재배치 (Column Reorder)
마우스 드래그 앤 드롭으로 컬럼 헤더의 순서를 자유롭게 변경하고 변경된 순서를 저장하는 방법을 학습합니다.
LIVE DEMO컬럼 순서 재배치 (Column Reorder) 인터랙티브 예제
인터랙티브 데모를 불러오는 중입니다…
LIVE DEMO컬럼 순서 재배치 (Column Reorder) 인터랙티브 예제
인터랙티브 데모를 불러오는 중입니다…
import { t } from './i18n';
import * as React from 'react';
import {
BGrid,
BGridColumn,
BGridColumnGroup,
BGridDataItem,
BGridItemRenderProps,
BGridSortParam,
toMoney,
} from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
import { Progress } from 'antd';
import { useState } from 'react';
interface Props {}
interface IListItem {
nation: string;
awpc: number;
wpc: number;
man: number;
woman: number;
ratio: number;
ratioMan: number;
ratioWoman: number;
}
const rawData = [
[t('대한민국(15+ LFS)', 'South Korea (15+ LFS)'), 44504, 28186, 16090, 12097, 63, 73, 53],
[t('아르메니아(15~75 LFS)', 'Armenia (15~75 LFS)'), 2204, 1563, 780, 783, 70, 76, 66],
[t('아제르바이잔(15+ LFS)', 'Azerbaijan (15+ LFS)'), 0, 5190, 2664, 2526, 66, 69, 63],
[t('부탄(15+ LFS)', 'Bhutan (15+ LFS)'), 482, 320, 169, 151, 66, 71, 61],
[t('브루나이(15+ LFS)', 'Brunei (15+ LFS)'), 370, 238, 144, 94, 64, 72, 54],
[t('캄보디아(15+ LFS)', 'Cambodia (15+ LFS)'), 11515, 8756, 4481, 4274, 76, 82, 69],
[t('키프로스(15+ LFS)', 'Cyprus (15+ LFS)'), 712, 448, 236, 212, 63, 68, 57],
[t('조지아(15+ LFS)', 'Georgia (15+ LFS)'), 3037, 1911, 1025, 886, 62, 72, 54],
[t('홍콩(15+ LFS)', 'Hong Kong (15+ LFS)'), 6573, 3988, 1990, 1998, 60, 67, 55],
[t('인도네시아(15+ LFS)', 'Indonesia (15+ LFS)'), 200485, 136808, 82760, 54048, 68, 82, 53],
[t('이란(15+ LFS)', 'Iran (15+ LFS)'), 61658, 26940, 21707, 5233, 43, 70, 17],
[t('이스라엘(15+ LFS)', 'Israel (15+ LFS)'), 6494, 4124, 2149, 1974, 63, 67, 59],
[t('일본(15+ LFS)', 'Japan (15+ LFS)'), 110271, 68377, 37997, 30380, 62, 71, 53],
[t('카자흐스탄(15+ LFS)', 'Kazakhstan (15+ LFS)'), 13131, 9203, 0, 0, 70, 0, 0],
[t('키르기스스탄(15+ LFS)', 'Kyrgyzstan (15+ LFS)'), 4288, 2755, 1620, 1136, 64, 77, 51],
[t('레바논(15+ LFS)', 'Lebanon (15+ LFS)'), 3677, 1798, 1230, 567, 48, 70, 29],
[t('마카오(16+ LFS)', 'Macau (16+ LFS)'), 0, 395, 193, 202, 0, 74, 66],
[t('말레이시아(15~64 LFS)', 'Malaysia (15~64 LFS)'), 22685, 15582, 9503, 6078, 68, 80, 55],
[t('몰디브(15+ HIES)', 'Maldives (15+ HIES)'), 317, 202, 116, 86, 63, 78, 50],
[t('몽골(15+ LFS)', 'Mongolia (15+ LFS)'), 2106, 1326, 706, 620, 63, 70, 55],
[t('파키스탄(15+ LFS)', 'Pakistan (15+ LFS)'), 120220, 62030, 47845, 14185, 51, 79, 23],
[t('필리핀(15+ LFS )', 'Philippines (15+ LFS)'), 73008, 43399, 26527, 16872, 59, 72, 46],
[t('카타르(15+ LFS)', 'Qatar (15+ LFS)'), 2393, 2108, 1823, 285, 88, 95, 57],
[t('사우디아라비아(15+ LFS)', 'Saudi Arabia (15+ LFS)'), 0, 0, 0, 0, 57, 80, 24],
[t('싱가포르(15+ LFS)', 'Singapore (15+ LFS)'), 3422, 2329, 1251, 1077, 68, 75, 61],
[t('스리랑카(15+ LFS)', 'Sri Lanka (15+ LFS)'), 16424, 8581, 5550, 3032, 52, 72, 34],
[t('대만(15+ LFS)', 'Taiwan (15+ LFS)'), 20188, 11946, 6631, 5315, 59, 67, 51],
[t('태국(15+ LFS)', 'Thailand (15+ LFS)'), 56575, 37885, 20611, 17274, 67, 75, 59],
[t('튀르키예(15+ LFS)', 'Turkey (15+ LFS)'), 61468, 32524, 21855, 10669, 52, 72, 34],
[t('아랍에미리트(15+ LFS)', 'United Arab Emirates (15+ LFS)'), 9432, 7565, 5693, 1871, 80, 92, 57],
[t('베트남(15+ LFS)', 'Vietnam (15+ LFS)'), 73394, 55507, 29068, 26440, 75, 81, 70],
];
const list = rawData.map((data, index) => {
return {
values: {
nation: data[0],
awpc: data[1],
wpc: data[2],
man: data[3],
woman: data[4],
ratio: data[5],
ratioMan: data[5],
ratioWoman: data[5],
},
};
});
const numRender = (item: BGridItemRenderProps<IListItem>) => <>{toMoney(item.value)}</>;
function SortExample(props: Props) {
const [sortParams, setSortParams] = useState<BGridSortParam[]>([]);
const [columns, setColumns] = useState<BGridColumn<IListItem>[]>([
{
key: 'nation',
label: 'Nation',
width: 150,
},
{
key: 'awpc',
label: 'active population',
width: 150,
align: 'right',
itemRender: numRender,
},
{ key: 'wpc', label: 'population', width: 100, align: 'right', itemRender: numRender },
{ key: 'man', label: 'Man', width: 100, align: 'right', itemRender: numRender },
{ key: 'woman', label: 'Woman', width: 100, align: 'right', itemRender: numRender },
{
key: 'ratio',
label: 'Ratio',
width: 150,
align: 'right',
itemRender: item => {
return <Progress size={'small'} percent={item.values.ratio} style={{ margin: 0 }} />;
},
},
{ key: 'ratioMan', label: 'Man', width: 100, align: 'right' },
{ key: 'ratioWoman', label: 'Woman', width: 100, align: 'right' },
]);
const [columnsGroup, setColumnsGroup] = useState<BGridColumnGroup[] | undefined>([
{ label: 'Group', groupStartIndex: 2, groupEndIndex: 4, align: 'center' },
]);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
const sortedList = React.useMemo(() => {
let i = 0,
l = sortParams.length;
return list
.sort((a, b) => {
for (i = 0; i < l; i++) {
const sortInfo = sortParams[i];
if (sortInfo.key === undefined) {
continue;
}
let valueA = a.values[sortInfo.key as keyof IListItem],
valueB = b.values[sortInfo.key as keyof IListItem];
if (typeof valueA !== typeof valueB) {
valueA = '' + valueA;
valueB = '' + valueB;
}
if (valueA < valueB) {
return sortInfo.orderBy === 'asc' ? -1 : 1;
} else if (valueA > valueB) {
return sortInfo.orderBy === 'asc' ? 1 : -1;
}
}
return 0;
})
.slice() as BGridDataItem<IListItem>[];
}, [sortParams]);
return (
<DataGridContainer ref={containerRef}>
<BGrid<IListItem>
width={containerWidth}
height={containerHeight}
headerHeight={48}
data={sortedList}
columns={columns}
columnsGroup={columnsGroup}
columnSortable
onChangeColumns={(columnIndex, { columns, columnsGroup }) => {
console.log('onChangeColumnWidths', columnIndex, columns, columnsGroup);
setColumns(columns);
setColumnsGroup(columnsGroup);
}}
rowChecked={{
checkedIndexes: [],
onChange: (ids, selectedAll) => {
console.log('onChange rowSelection', ids, selectedAll);
},
}}
sort={{
sortParams,
onChange: sortParams => {
console.log('onChange: sortParams', sortParams);
setSortParams(sortParams);
},
}}
showLineNumber
/>
</DataGridContainer>
);
}
export default SortExample;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.
*
* BGrid'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 > .bgrid-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. 언제 사용하며 왜 필요한가요?
사용자마다 업무 시 자주 확인하는 컬럼이 다릅니다. 어떤 담당자는 “연락처”를 먼저 보고 싶고, 어떤 담당자는 “주문금액”을 먼저 보고 싶어 합니다.
columnSortable={true}를 설정하면 사용자가 헤더를 마우스로 드래그하여 자신이 원하는 순서로 컬럼 위치를 즉시 바꿀 수 있습니다.
2. 실무 완성형 예제: 드래그 컬럼 순서 변경
import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
interface TaskItem {
id: number;
title: string;
assignee: string;
priority: string;
dueDate: string;
}
export default function ColumnReorderGrid() {
const [columns, setColumns] = useState<BGridColumn<TaskItem>[]>([
{ key: 'id', label: 'ID', width: 70, align: 'center' },
{ key: 'title', label: '업무명', width: 220 },
{ key: 'assignee', label: '담당자', width: 120, align: 'center' },
{ key: 'priority', label: '우선순위', width: 100, align: 'center' },
{ key: 'dueDate', label: '마감일', width: 120, align: 'center' },
]);
const [data] = useState<BGridDataItem<TaskItem>[]>([
{ values: { id: 1, title: '결제 모듈 v2 마이그레이션', assignee: '김민수', priority: '높음', dueDate: '2026-08-25' } },
{ values: { id: 2, title: '모바일 뷰 반응형 UI 개선', assignee: '이수진', priority: '보통', dueDate: '2026-08-28' } },
{ values: { id: 3, title: '분기별 보안 감사 보고서 작성', assignee: '박도현', priority: '긴급', dueDate: '2026-08-20' } },
]);
return (
<div>
<div style={{ marginBottom: 10, fontSize: 13, color: '#475569' }}>
💡 컬럼 헤더 라벨을 마우스로 잡고 좌우로 드래그하여 순서를 바꿔보세요.
</div>
<BGrid<TaskItem>
width={700}
height={240}
columns={columns}
data={data}
rowKey="id"
columnSortable={true} // 헤더 드래그 순서 변경 활성화
onChangeColumns={(_, { columns: nextColumns }) => {
console.log('변경된 컬럼 순서:', nextColumns.map(c => c.key));
setColumns(nextColumns);
}}
/>
</div>
);
}