조건부 행 스타일링 (Row Styling)
특정 조건(예: 결제 취소, 재고 부족, VIP 회원 등)을 만족하는 행에 커스텀 CSS 클래스를 동적으로 부여하는 방법을 학습합니다.
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem } from 'beautiful-grid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
interface InventoryRisk {
sku: string;
productName: string;
warehouse: string;
stock: number;
safetyStock: number;
inboundDate: string;
}
const productNames = ['고속 충전 어댑터 65W', '무선 블루투스 이어폰', '강화유리 필름', '태블릿 마그네틱 거치대', 'USB-C 케이블'];
const data: BGridDataItem<InventoryRisk>[] = Array.from({ length: 120 }, (_, index) => {
const safetyStock = 10 + (index % 4) * 5;
const stock = index % 13 === 0 ? 0 : (index * 7) % 65;
return {
values: {
sku: `INV-${String(index + 1).padStart(5, '0')}`,
productName: productNames[index % productNames.length],
warehouse: `센터 ${String.fromCharCode(65 + (index % 4))}`,
stock,
safetyStock,
inboundDate: stock < safetyStock ? `2026-08-${String((index % 7) + 24).padStart(2, '0')}` : '-',
},
};
});
export default function GetRowClassName() {
const [columns, setColumns] = React.useState<BGridColumn<InventoryRisk>[]>([
{ key: 'sku', label: '품목코드', width: 110, align: 'center' },
{ key: 'productName', label: '품목명', width: 240 },
{ key: 'warehouse', label: '보관센터', width: 100, align: 'center' },
{ key: 'stock', label: '현재 재고', width: 100, align: 'right', itemRender: ({ values }) => <strong>{values.stock}개</strong> },
{ key: 'safetyStock', label: '안전 재고', width: 100, align: 'right', itemRender: ({ values }) => <>{values.safetyStock}개</> },
{
key: 'inboundDate',
label: '입고 예정일',
width: 120,
align: 'center',
itemRender: ({ values }) => <>{values.stock === 0 ? '품절 · 긴급 발주' : values.inboundDate}</>,
},
]);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
return (
<DataGridContainer ref={containerRef} className='get-row-class-example'>
<BGrid<InventoryRisk>
showLineNumber
width={width}
height={height}
headerHeight={35}
data={data}
columns={columns}
rowKey='sku'
onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
getRowClassName={(_rowIndex, item) => {
if (item.values.stock === 0) return 'row-out-of-stock';
if (item.values.stock < item.values.safetyStock) return 'row-low-stock';
return undefined;
}}
/>
</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.
*
* 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. 언제 사용하며 왜 필요한가요?
경고 상태(에러 발생 로그, 위험 재고 알림)나 중요 상태(VIP 고객, 완료된 작업)의 행 전체에 빨간색/노란색/초록색 배경 하이라이트를 적용하여 사용자의 시선을 집중시켜야 할 때 getRowClassName을 사용합니다.
2. 실무 완성형 예제: 재고 부족 품목 경고 하이라이트
import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
interface InventoryItem {
id: string;
name: string;
stock: number;
threshold: number;
}
export default function RowStylingGrid() {
const [data] = useState<BGridDataItem<InventoryItem>[]>([
{ values: { id: 'P1', name: '고속 충전 어댑터 65W', stock: 4, threshold: 10 } }, // 재고 위험
{ values: { id: 'P2', name: '무선 블루투스 이어폰', stock: 28, threshold: 10 } }, // 정상
{ values: { id: 'P3', name: '스마트폰 강화유리 필름', stock: 0, threshold: 5 } }, // 품절
{ values: { id: 'P4', name: '태블릿 마그네틱 거치대', stock: 15, threshold: 5 } }, // 정상
]);
const columns: BGridColumn<InventoryItem>[] = [
{ key: 'id', label: '코드', width: 80, align: 'center' },
{ key: 'name', label: '품목명', width: 240 },
{
key: 'stock',
label: '현재 재고',
width: 100,
align: 'right',
itemRender: ({ values }) => <strong>{values.stock}개</strong>,
},
{ key: 'threshold', label: '안전 재고', width: 100, align: 'right', itemRender: ({ values }) => `${values.threshold}개` },
];
return (
<div>
<style>{`
.row-out-of-stock {
background-color: #fee2e2 !important; /* 연한 빨강 */
color: #991b1b;
}
.row-low-stock {
background-color: #fef3c7 !important; /* 연한 노랑 */
color: #92400e;
}
`}</style>
<BGrid<InventoryItem>
width={560}
height={220}
columns={columns}
data={data}
rowKey="id"
// 조건부 행 클래스 반환
getRowClassName={(_, item) => {
if (item.values.stock === 0) return 'row-out-of-stock';
if (item.values.stock < item.values.threshold) return 'row-low-stock';
return '';
}}
/>
</div>
);
}