셀 병합 (Cell Merge)

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

#cellMergeOptions#mergeBy#rowspan#reporting#grouping
검토일: 2026-08-23
LIVE DEMO셀 병합 (Cell Merge) 인터랙티브 예제
인터랙티브 데모를 불러오는 중입니다…
GitHub
import { t } from './i18n';
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: t('가전/디지털', 'Home Appliances/Digital'), subCategory: t('컴퓨터 주변기기', 'Computer Peripherals'), itemCode: 'IT-001', itemName: t('프리미엄 무선 키보드', 'Premium Wireless Keyboard'), unitPrice: 129000, stockQty: 84, warehouse: 'A-01' },
  { mainCategory: t('가전/디지털', 'Home Appliances/Digital'), subCategory: t('컴퓨터 주변기기', 'Computer Peripherals'), itemCode: 'IT-002', itemName: t('인체공학 마우스', 'Ergonomic Mouse'), unitPrice: 69000, stockQty: 46, warehouse: 'A-01' },
  { mainCategory: t('가전/디지털', 'Home Appliances/Digital'), subCategory: t('컴퓨터 주변기기', 'Computer Peripherals'), itemCode: 'IT-003', itemName: t('USB-C 멀티 허브', 'USB-C Multi Hub'), unitPrice: 89000, stockQty: 31, warehouse: 'A-02' },
  { mainCategory: t('가전/디지털', 'Home Appliances/Digital'), subCategory: t('모니터/디스플레이', 'Monitor/Display'), itemCode: 'IT-004', itemName: t('27인치 QHD 모니터', '27-inch QHD Monitor'), unitPrice: 389000, stockQty: 18, warehouse: 'B-01' },
  { mainCategory: t('가전/디지털', 'Home Appliances/Digital'), subCategory: t('모니터/디스플레이', 'Monitor/Display'), itemCode: 'IT-005', itemName: t('32인치 4K 모니터', '32-inch 4K Monitor'), unitPrice: 629000, stockQty: 9, warehouse: 'B-01' },
  { mainCategory: t('가구/인테리어', 'Furniture/Interior'), subCategory: t('사무용 가구', 'Office Furniture'), itemCode: 'FN-001', itemName: t('모션 데스크 1400', 'Motion Desk 1400'), unitPrice: 459000, stockQty: 22, warehouse: 'C-01' },
  { mainCategory: t('가구/인테리어', 'Furniture/Interior'), subCategory: t('사무용 가구', 'Office Furniture'), itemCode: 'FN-002', itemName: t('인체공학 메시 의자', 'Ergonomic Mesh Chair'), unitPrice: 329000, stockQty: 37, warehouse: 'C-01' },
  { mainCategory: t('가구/인테리어', 'Furniture/Interior'), subCategory: t('수납 가구', 'Storage Furniture'), itemCode: 'FN-003', itemName: t('이동식 서랍장', 'Mobile Drawer'), unitPrice: 119000, stockQty: 41, warehouse: 'C-02' },
  { mainCategory: t('가구/인테리어', 'Furniture/Interior'), subCategory: t('수납 가구', 'Storage Furniture'), itemCode: 'FN-004', itemName: t('5단 철제 선반', '5-tier steel shelf'), unitPrice: 149000, stockQty: 26, warehouse: 'C-02' },
  { mainCategory: t('생활/주방', 'Living/Kitchen'), subCategory: t('홈카페', 'Home Cafe'), itemCode: 'KT-001', itemName: t('전자동 커피머신', 'Fully Automatic Coffee Machine'), unitPrice: 749000, stockQty: 12, warehouse: 'D-01' },
  { mainCategory: t('생활/주방', 'Living/Kitchen'), subCategory: t('홈카페', 'Home Cafe'), itemCode: 'KT-002', itemName: t('온도조절 전기포트', 'Temperature Control Electric Kettle'), unitPrice: 99000, stockQty: 53, warehouse: 'D-01' },
  { mainCategory: t('생활/주방', 'Living/Kitchen'), subCategory: t('조리도구', 'Cooking Utensils'), itemCode: 'KT-003', itemName: t('스테인리스 팬 세트', 'Stainless Steel Pan Set'), 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: t('대분류', 'Large Category'), width: 130, align: 'center' },
    { key: 'subCategory', label: t('중분류', 'Medium Category'), width: 150, align: 'center' },
    { key: 'itemCode', label: t('품목코드', 'Item Code'), width: 100, align: 'center' },
    { key: 'itemName', label: t('품목명', 'Item Name'), width: 220 },
    {
      key: 'unitPrice',
      label: t('단가', 'Unit Price'),
      width: 120,
      align: 'right',
      itemRender: ({ values }) => <>{values.unitPrice.toLocaleString()}{t('원', 'KRW')}</>,
    },
    { key: 'stockQty', label: t('재고', 'Stock'), width: 80, align: 'right', itemRender: ({ values }) => <>{values.stockQty}{t('개', 'ea')}</> },
    { key: 'warehouse', label: t('창고', 'Warehouse'), 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()를 수행해두어야 깔끔하게 병합됩니다.

[!TIP] 모바일 환경에서의 병합 열 틀고정: 모바일 화면에서 병합 컬럼을 여러 개 고정(frozenColumnIndex)하면 화면 폭이 좁아 가로 스크롤 영역이 가려질 수 있습니다. 모바일 뷰포트에서는 고정 열을 1개로 줄이거나 해제하는 반응형 레이아웃을 권장합니다.