셀 병합 (Cell Merge)

인접한 행의 동일한 데이터를 시각적으로 하나로 합쳐서 그룹화된 가독성 높은 보고서 테이블을 만드는 방법을 학습합니다.

#cellMergeOptions#mergeBy#rowspan#reporting#grouping
검토일: 2026-08-23
GitHub
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem } from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';

interface InventoryItem {
  mainCategory: string;
  subCategory: string;
  itemCode: string;
  itemName: string;
  unitPrice: number;
  stockQty: number;
  warehouse: string;
}

const inventoryRows: InventoryItem[] = [
  { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-001', itemName: '프리미엄 무선 키보드', unitPrice: 129000, stockQty: 84, warehouse: 'A-01' },
  { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-002', itemName: '인체공학 마우스', unitPrice: 69000, stockQty: 46, warehouse: 'A-01' },
  { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-003', itemName: 'USB-C 멀티 허브', unitPrice: 89000, stockQty: 31, warehouse: 'A-02' },
  { mainCategory: '가전/디지털', subCategory: '모니터/디스플레이', itemCode: 'IT-004', itemName: '27인치 QHD 모니터', unitPrice: 389000, stockQty: 18, warehouse: 'B-01' },
  { mainCategory: '가전/디지털', subCategory: '모니터/디스플레이', itemCode: 'IT-005', itemName: '32인치 4K 모니터', unitPrice: 629000, stockQty: 9, warehouse: 'B-01' },
  { mainCategory: '가구/인테리어', subCategory: '사무용 가구', itemCode: 'FN-001', itemName: '모션 데스크 1400', unitPrice: 459000, stockQty: 22, warehouse: 'C-01' },
  { mainCategory: '가구/인테리어', subCategory: '사무용 가구', itemCode: 'FN-002', itemName: '인체공학 메시 의자', unitPrice: 329000, stockQty: 37, warehouse: 'C-01' },
  { mainCategory: '가구/인테리어', subCategory: '수납 가구', itemCode: 'FN-003', itemName: '이동식 서랍장', unitPrice: 119000, stockQty: 41, warehouse: 'C-02' },
  { mainCategory: '가구/인테리어', subCategory: '수납 가구', itemCode: 'FN-004', itemName: '5단 철제 선반', unitPrice: 149000, stockQty: 26, warehouse: 'C-02' },
  { mainCategory: '생활/주방', subCategory: '홈카페', itemCode: 'KT-001', itemName: '전자동 커피머신', unitPrice: 749000, stockQty: 12, warehouse: 'D-01' },
  { mainCategory: '생활/주방', subCategory: '홈카페', itemCode: 'KT-002', itemName: '온도조절 전기포트', unitPrice: 99000, stockQty: 53, warehouse: 'D-01' },
  { mainCategory: '생활/주방', subCategory: '조리도구', itemCode: 'KT-003', itemName: '스테인리스 팬 세트', unitPrice: 189000, stockQty: 29, warehouse: 'D-02' },
];

const data: BGridDataItem<InventoryItem>[] = inventoryRows.map(values => ({ values }));

function CellMergeExample() {
  const [columns, setColumns] = React.useState<BGridColumn<InventoryItem>[]>([
    { key: 'mainCategory', label: '대분류', width: 130, align: 'center' },
    { key: 'subCategory', label: '중분류', width: 150, align: 'center' },
    { key: 'itemCode', label: '품목코드', width: 100, align: 'center' },
    { key: 'itemName', label: '품목명', width: 220 },
    {
      key: 'unitPrice',
      label: '단가',
      width: 120,
      align: 'right',
      itemRender: ({ values }) => <>{values.unitPrice.toLocaleString()}원</>,
    },
    { key: 'stockQty', label: '재고', width: 80, align: 'right', itemRender: ({ values }) => <>{values.stockQty}개</> },
    { key: 'warehouse', label: '창고', width: 90, align: 'center' },
  ]);
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { width, height } = useContainerSize(containerRef);

  return (
    <DataGridContainer ref={containerRef}>
      <BGrid<InventoryItem>
        showLineNumber
        frozenColumnIndex={2}
        width={width}
        height={height}
        data={data}
        columns={columns}
        rowKey='itemCode'
        onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
        cellMergeOptions={{
          columnsMap: {
            0: { mergeBy: 'mainCategory' },
            1: { mergeBy: 'subCategory' },
          },
        }}
        variant='vertical-bordered'
      />
    </DataGridContainer>
  );
}

export default CellMergeExample;

1. 언제 사용하며 왜 필요한가요?

경영 정보 대시보드나 정산 리포트, 재고 집계표를 작성할 때 “대분류”, “중분류”, **“담당자”**처럼 동일한 값이 여러 행에 걸쳐 반복 출력되면 테이블이 산만해 보입니다.

BeautifulGrid의 셀 병합(Cell Merge) 기능을 사용하면:

  • 동일한 연속된 값을 가지는 인접 행의 셀을 자동으로 감지하여 rowspan 효과로 시각적 병합을 수행합니다.
  • 일반 컬럼과 Frozen 컬럼에서 같은 columnsMap 병합 기준을 사용합니다.
  • 데이터의 정렬 순서에 맞추어 유연하게 병합 기준(mergeBy)을 설정할 수 있습니다.

2. 실무 완성형 예제: 카테고리별 판매 품목 보고서

아래 코드는 대분류중분류 컬럼을 기준으로 셀을 병합하여 렌더링하는 예제입니다:

import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';

interface CategoryItem {
  mainCategory: string;
  subCategory: string;
  itemCode: string;
  itemName: string;
  unitPrice: number;
  stockQty: number;
}

export default function CategoryMergeGrid() {
  const [data] = useState<BGridDataItem<CategoryItem>[]>([
    { values: { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-01', itemName: '무선 키보드', unitPrice: 45000, stockQty: 120 } },
    { values: { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-02', itemName: '게이밍 마우스', unitPrice: 38000, stockQty: 85 } },
    { values: { mainCategory: '가전/디지털', subCategory: '모니터/디스플레이', itemCode: 'IT-03', itemName: '27인치 4K 모니터', unitPrice: 420000, stockQty: 30 } },
    { values: { mainCategory: '가전/디지털', subCategory: '모니터/디스플레이', itemCode: 'IT-04', itemName: '32인치 커브드 모니터', unitPrice: 580000, stockQty: 15 } },
    { values: { mainCategory: '가구/인테리어', subCategory: '사무용 가구', itemCode: 'FN-01', itemName: '모션 데스크 1400', unitPrice: 350000, stockQty: 25 } },
    { values: { mainCategory: '가구/인테리어', subCategory: '사무용 가구', itemCode: 'FN-02', itemName: '인체공학 메시 의자', unitPrice: 280000, stockQty: 40 } },
  ]);

  const columns: BGridColumn<CategoryItem>[] = [
    { key: 'mainCategory', label: '대분류', width: 140, align: 'center' },
    { key: 'subCategory', label: '중분류', width: 160, align: 'center' },
    { key: 'itemCode', label: '품목코드', width: 100, align: 'center' },
    { key: 'itemName', label: '품목명', width: 200 },
    {
      key: 'unitPrice',
      label: '단가',
      width: 120,
      align: 'right',
      itemRender: ({ values }) => `${values.unitPrice.toLocaleString()}원`,
    },
    {
      key: 'stockQty',
      label: '재고',
      width: 90,
      align: 'right',
      itemRender: ({ values }) => `${values.stockQty}개`,
    },
  ];

  return (
    <div>
      <BGrid<CategoryItem>
        width={810}
        height={320}
        columns={columns}
        data={data}
        rowKey="itemCode"
        frozenColumnIndex={2} // 대분류, 중분류를 좌측 고정
        // 셀 병합 옵션 구성
        cellMergeOptions={{
          columnsMap: {
            0: { mergeBy: 'mainCategory' }, // 0번째 컬럼(대분류)은 mainCategory 값이 같을 때 병합
            1: { mergeBy: 'subCategory' },  // 1번째 컬럼(중분류)은 subCategory 값이 같을 때 병합
          },
        }}
        headerHeight={34}
        itemHeight={30}
      />
    </div>
  );
}

3. cellMergeOptions 설정 명세

type CellMergeOptions = {
  columnsMap: {
    [columnIndex: number]: BGridCellMergeColumn;
  };
};

interface BGridCellMergeColumn {
      wordWrap?: boolean;
      mergeBy: string | string[]; // 동일성 판단 기준 데이터 키
}
  • columnIndex: 병합을 적용할 컬럼의 0-based 인덱스 번호입니다.
  • mergeBy: 인접 행끼리 값이 같은지 비교할 데이터 필드명입니다.

4. 실무 팁 & 주의사항 (Gotchas)

[!IMPORTANT] 데이터 정렬 선행 필수: 셀 병합은 연속된 인접 행의 값이 같을 때만 병합됩니다. 만약 대분류: '가전'인 행 사이에 대분류: '가구' 행이 끼어있으면 병합이 끊어지므로, 데이터를 그리드에 넘기기 전에 병합 기준 컬럼으로 미리 sort()를 수행해두어야 깔끔하게 병합됩니다.