TanStack Table๋กœ ์„ธ๋กœ ๋ณ‘ํ•ฉ ํ…Œ์ด๋ธ” ๋งŒ๋“ค๊ธฐ

์ด๋ฒˆ์— ์–ด๋“œ๋ฏผ ํˆด ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ํ…Œ์ด๋ธ” ๊ตฌํ˜„์ด ํ•„์š”ํ•ด TanStack Table์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.
๋””์ž์ธ ์‹œ์Šคํ…œ์šฉ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์ •๋ ฌ, ๋ฆฌ์‚ฌ์ด์ง•์ฒ˜๋Ÿผ TanStack Table์ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์„ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ๋‹ค ์“ฐ๋ฉด ๋˜๋Š” ๋ถ€๋ถ„์€ ํฌ๊ฒŒ ์–ด๋ ต์ง€ ์•Š์•˜๋‹ค. ๋‹ค๋งŒ ๋ฌธ์ œ๋Š” ์„ธ๋กœ ๋ณ‘ํ•ฉ์„ on/off ํ•  ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์ด์—ˆ๋‹ค. ํ—ค๋” ์ชฝ์—๋Š” ์ด๋ฏธ rowSpan API๊ฐ€ ์กด์žฌํ•ด์„œ ํ—ค๋” ๊ทธ๋ฃนํ•‘์€ ๋น„๊ต์  ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๋Š”๋ฐ, tbody ์˜์—ญ์—๋Š” ๊ฐ™์€ ๊ธฐ๋Šฅ์ด ์—†์–ด์„œ ์ง์ ‘ ๊ตฌํ˜„์ด ํ•„์š”ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์–ด๋–ค ์ปฌ๋Ÿผ์ด๋“  ํ•„์š”ํ•  ๋•Œ๋งŒ rowSpan์„ ์ผฐ๋‹ค๊ฐ€ ๋Œ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„์„ ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋ณด์•˜๋‹ค.

1. ๊ธฐ๋ณธ ์•„์ด๋””์–ด

์ฒ˜์Œ์— ๋‚ด๊ฐ€ ์ƒ๊ฐํ•œ ๊ตฌ์กฐ๋Š” ์ปฌ๋Ÿผ ๋ฉ”ํƒ€์— enableRowSpan ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ด์„œ ์—ด ๋‹จ์œ„๋กœ ๋ณ‘ํ•ฉ ์—ฌ๋ถ€๋ฅผ ์ œ์–ดํ•˜๊ณ , ๋ณ‘ํ•ฉ์ด ์ผœ์ง„ ์—ด์€ ํ–‰ ๋ฐ์ดํ„ฐ๋ฅผ ํ›‘์–ด๋ณด๋ฉด์„œ ๊ฐ™์€ ๊ฐ’์ด ์—ฐ์†๋˜๋Š” ๊ตฌ๊ฐ„์„ ์ฐพ์€ ๋‹ค์Œ ๊ตฌ๊ฐ„์˜ ์ฒซ ํ–‰์—๋Š” ๋ณ‘ํ•ฉ๋  ํ–‰ ์ˆ˜๋ฅผ ๋„ฃ๊ณ , ๋‚˜๋จธ์ง€ ํ–‰์€ 0์œผ๋กœ ํ‘œ์‹œํ•ด ๋ Œ๋”๋ง์—์„œ ์ œ์™ธํ•˜๋Š” ๋ฐฉ์‹์„ ์ผ๋‹จ ์ƒ๊ฐํ•˜์˜€๋‹ค!

2. rowSpan ๊ณ„์‚ฐ ๋กœ์ง

rowSpan ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ ๊ฑด ํ•œ ์—ด์—์„œ ์—ฐ์†๋œ ๊ฐ’์ด ๋ช‡ ํ–‰ ๋™์•ˆ ์ด์–ด์ง€๋Š”์ง€ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด category ์—ด์ด ์•„๋ž˜์ฒ˜๋Ÿผ ๋˜์–ด ์žˆ๋‹ค๊ณ  ํ•ด๋ณด์ž.

0: ๊ณผ์ผ
1: ๊ณผ์ผ
2: ๊ณผ์ผ
3: ๊ณผ์ผ
4: ์ฑ„์†Œ
5: ์ฑ„์†Œ

์—ฌ๊ธฐ์„œ ‘๊ณผ์ผ’์€ ์—ฐ์†ํ•ด์„œ 4๋ฒˆ ‘์ฑ„์†Œ’๋Š” ์—ฐ์†ํ•ด์„œ 2๋ฒˆ ๋‚˜์˜จ๋‹ค.

<td rowspan="4">๊ณผ์ผ</td>  <!-- 0~3ํ–‰ -->
<td rowspan="2">์ฑ„์†Œ</td>  <!-- 4~5ํ–‰ -->

์ฆ‰, ์—ฐ์†๋œ ๊ตฌ๊ฐ„์˜ ๊ธธ์ด๋ฅผ ๊ตฌ๊ฐ„์˜ ์‹œ์ž‘ ์…€ ์ •๋ณด๊ฐ€ ํ•„์š”ํ–ˆ๊ณ , ๋‚˜๋Š” ๊ฐ ํ–‰์— ๋Œ€ํ•ด rowSpan ๊ฐ’์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋“ค๊ณ  ์žˆ๋„๋ก ํ–ˆ๋‹ค.

[4, 0, 0, 0, 2, 0]
  • ์ธ๋ฑ์Šค 0: 4 → 0๋ฒˆ์งธ ํ–‰์—์„œ ์•„๋ž˜๋กœ 4ํ–‰์„ ๋ณ‘ํ•ฉํ•œ๋‹ค๋Š” ์˜๋ฏธ
  • ์ธ๋ฑ์Šค 1~3: 0 → ์ด๋ฏธ ์œ„์˜ ์…€์— ๋ณ‘ํ•ฉ๋˜์–ด ์žˆ์œผ๋‹ˆ๋ฅผ ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ
  • ์ธ๋ฑ์Šค 4: 2 → 4๋ฒˆ์งธ ํ–‰์—์„œ ์•„๋ž˜๋กœ 2ํ–‰ ๋ณ‘ํ•ฉ
  • ์ธ๋ฑ์Šค 5: 0 → ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ˆจ๊น€
  1. currentValue : ์ง€๊ธˆ ๋ณด๊ณ  ์žˆ๋Š” ๊ฐ’์ด ๋ฌด์—‡์ธ์ง€ (ex: '๊ณผ์ผ', '์ฑ„์†Œ'..)
  2. spanStartIndex: ํ˜„์žฌ ๊ฐ’์ด ์‹œ์ž‘๋œ ํ–‰ ์ธ๋ฑ์Šค (ex: '๊ณผ์ผ'์ด 0๋ฒˆ์งธ์—์„œ ์‹œ์ž‘ํ•˜๋ฉด 0, '์ฑ„์†Œ'๊ฐ€ 4๋ฒˆ์งธ์—์„œ ์‹œ์ž‘ํ•˜๋ฉด 4)
  3. rowIndex : ํ˜„์žฌ ๋ช‡ ๋ฒˆ์งธ ํ–‰์„ ์ˆœํšŒ ์ค‘์ธ์ง€!

๋ Œ๋”๋ง ์‹œ rowSpan > 0์ธ ์…€๋งŒ๋ฅผ ๋ Œ๋”๋ง ํ•˜๊ณ  0์ด๋ฉด์ž์ฒด๋ฅผ ๋ Œ๋”๋ง ํ•˜์ง€ ์•Š๋Š”๋‹ค.

1) ์—ด ๋‹จ์œ„ ๋ฐ˜๋ณต

rowSpan์€ enableRowSpan์ด ์ผœ์ง„ ์—ด๋งŒ ๊ณ„์‚ฐํ•˜๋ฉด ๋˜๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ์กฐ๊ฑด๋ฌธ์„ ๊ฑธ์—ˆ๋‹ค.

for column in columns:
  if enableRowSpan์ด ์•„๋‹ˆ๋ฉด ์Šคํ‚ต
  spans = []
  currentValue = null
  spanStartIndex = 0
  ํ–‰ ๋ฐ˜๋ณต ์‹œ์ž‘

2) ์ฒซ ๋ฒˆ์งธ ํ–‰์€ ๋ฌด์กฐ๊ฑด ์ƒˆ๋กœ์šด ๊ตฌ๊ฐ„์˜ ์‹œ์ž‘

rowIndex = 0
currentValue = rows[0][columnId]   // ex. '๊ณผ์ผ'
spanStartIndex = 0

3) ๋‹ค์Œ ํ–‰๋ถ€ํ„ฐ๋Š” ๊ฐ’์ด ๋ฐ”๋€Œ๋Š”์ง€ ํ™•์ธ

rowIndex 1: ๊ณผ์ผ → ๊ณผ์ผ (๋ณ€ํ™” ์—†์Œ)
rowIndex 2: ๊ณผ์ผ → ๊ณผ์ผ (๋ณ€ํ™” ์—†์Œ)
rowIndex 3: ๊ณผ์ผ → ๊ณผ์ผ (๋ณ€ํ™” ์—†์Œ)
rowIndex 4: ๊ณผ์ผ → ์ฑ„์†Œ (๊ฐ’ ๋ณ€๊ฒฝ!)

๊ฐ’์ด ๋ฐ”๋€Œ๋Š” ์ˆœ๊ฐ„์ด ๋ฐ”๋กœ ์ด์ „ ๊ตฌ๊ฐ„์„ ์ข…๋ฃŒํ•ด์•ผ ํ•˜๋Š” ์‹œ์ ์ด๋‹ค.

4) ์ด์ „ ๊ตฌ๊ฐ„ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ

์ด์ „ ๊ตฌ๊ฐ„์€ spanStartIndex๋ถ€ํ„ฐ rowIndex - 1๊นŒ์ง€๋‹ค.

spanStartIndex = 0
rowIndex = 4
๊ตฌ๊ฐ„ = 0,1,2,3 → ๊ธธ์ด = 4
spans[0] = 4
spans[1] = 0
spans[2] = 0
spans[3] = 0

๊ทธ๋ฆฌ๊ณ  ์ƒˆ๋กœ์šด ๊ตฌ๊ฐ„์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•œ๋‹ค.

currentValue = '์ฑ„์†Œ'
spanStartIndex = 4

5) ๊ฒฐ๊ณผ

์œ„ ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•˜๋ฉด ํ•˜๋‚˜์˜ ์—ด์— ๋Œ€ํ•œ rowSpan ๋ฐฐ์—ด์ด ์™„์„ฑ๋œ๋‹ค.

[4, 0, 0, 0, 2, 0]

์ด ๋ฐฐ์—ด์„ ๋ Œ๋”๋ง์—์„œ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

  • ๊ฐ’์ด 0 → ๋ Œ๋”๋ง ์ƒ๋žต
  • ๊ฐ’์ด 1 ์ด์ƒ →๋ Œ๋”๋ง

์•„๋ž˜ ๋ชฉ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋ณ‘ํ•ฉ ํ…Œ์ด๋ธ”์ด ๊ทธ๋ ค์ง„๋‹ค!

const data: Product[] = [
  { category: "๊ณผ์ผ", subcategory: "์‚ฌ๊ณผ", price: 3000 },
  { category: "๊ณผ์ผ", subcategory: "์‚ฌ๊ณผ", price: 2500 },
  { category: "๊ณผ์ผ", subcategory: "๋ฐฐ", price: 4000 },
  { category: "๊ณผ์ผ", subcategory: "๋ฐฐ", price: 3500 },
  { category: "์ฑ„์†Œ", subcategory: "๋ฐฐ์ถ”", price: 5000 },
  { category: "์ฑ„์†Œ", subcategory: "๋ฌด", price: 2000 },
];

const columns: ColumnDef<Product, any>[] = [
  {
    accessorKey: "category",
    header: "์นดํ…Œ๊ณ ๋ฆฌ",
    meta: {
      enableRowSpan: true,
    },
  },
  {
    accessorKey: "subcategory",
    header: "์„œ๋ธŒ์นดํ…Œ๊ณ ๋ฆฌ",
    meta: {
      enableRowSpan: true,
    },
  },
  {
    accessorKey: "price",
    header: "๊ฐ€๊ฒฉ",
    cell: (info) => `${info.getValue().toLocaleString()}์›`,
  },
];