lil.dev
Published on

๐ŸŽ‰ ์ปฌ๋Ÿฌ๋ฆฌ์ŠคํŠธ ํ”„๋กœ์ ํŠธ #11 Pallete table - resizable columns

๊ธ€์“ด์ด

    ๐Ÿ“Œ ๋ชฉ์ฐจ

    Welcome

    โœจ ์ปฌ๋Ÿฌ๋ฆฌ์ŠคํŠธ ์‚ฌ์ดํŠธ(์•„์ง ์—†์Œ

    ๐Ÿ’๐Ÿป

    1. ์„ ํƒ์นธ ํ…Œ์ด๋ธ” width ์กฐ์ ˆํ•˜๊ฒŒ ํ•ด๋ณด๊ธฐ 1-1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๋ฐฉ๋ฒ• 1-2. react table - column resizing 1-3. react table ์„ค์น˜ 1-4. ๋ฐ๋ชจ ์ฝ”๋“œ ์ ์šฉ 1-5. pallete์— ๋งž๊ฒŒ ์ ์šฉ

    ์„ ํƒ์นธ ํ…Œ์ด๋ธ” width ์กฐ์ ˆํ•˜๊ฒŒ ํ•ด๋ณด๊ธฐ

    ์„ ํƒ์นธ ํญ ๋ณ€๊ฒฝํ•˜๊ธฐ

    ์‹œ๋‚˜๋ฆฌ์˜ค

    Scenario: ์„ ํƒ์นธ์˜ ๊ฐ€๋กœ ๊ธธ์ด๋ฅผ ๋งˆ์Œ๋Œ€๋กœ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค
      Given ์„ ํƒ์นธ์„ ๋ Œ๋”ํ•˜๊ณ 
      When ์›ํ•˜๋Š” ์นธ์„ ์„ ํƒํ•ด์„œ ์žก์•„๋‹น๊ธฐ๋ฉด
      Then ์„ ํƒ์นธ์„ ์ค„์ด๊ฑฐ๋‚˜ ๋Š˜์ผ ์ˆ˜ ์žˆ๋‹ค
    

    1-1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๋ฐฉ๋ฒ•

    ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ๋‹ค.

    1. ์ปดํฌ๋„ŒํŠธ๋กœ ์“ฐ๋Š” ๊ฒƒ ์˜ˆ) Radix

    2. ํ›…์ฒ˜๋Ÿผ Props๋“ค์„ ๊ฐ€์ง€๊ณ  ์“ฐ๋Š” ๊ฒƒ ์˜ˆ) React table

      ; pagination๋„ ์‰ฝ๊ฒŒ ์ง€์›ํ•  ์ˆ˜ ์žˆ๊ณ  ์ปค์Šคํ…€ ๋””์ž์ธ์— ๋งž์ถฐ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‹ค.

    ์˜ค๋Š˜์€ React table์„ ์‚ฌ์šฉํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.


    1-2. react table - column resizing

    ๋ฆฌ์•กํŠธ ํ…Œ์ด๋ธ” - ์ปฌ๋Ÿผ ๋ฆฌ์‚ฌ์ด์ง• ์‚ฌ์ดํŠธ

    ์‚ฌ์ดํŠธ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด ํ—ค๋”๋ฅผ ์›€์ง์—ฌ์„œ ์ปฌ๋Ÿผ์˜ ๊ฐ€๋กœ ํญ์„ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.


    1-3. react table ์„ค์น˜

    ํ„ฐ๋ฏธ๋„์— ์•„๋ž˜์ฒ˜๋Ÿผ React-table์„ ์„ค์น˜ํ•˜๊ณ ,
    bun add react-table

    ๋งž๋Š” ํƒ€์ž…๋„ ์„ค์น˜ํ•ด์ค€๋‹ค.
    bun add -d @types/react-table


    1-4. ๋ฐ๋ชจ ์ฝ”๋“œ ์ ์šฉ

    ์ผ๋‹จ ์‚ฌ์ดํŠธ์— ์žˆ๋Š” ์ฝ”๋“œ๋“ค์„ ๊ฐ€์ ธ์™€์„œ ์ ์šฉํ•ด๋ณธ๋‹ค.

    //๋จผ์ € react-table์—์„œ ์“ธ ๊ฒƒ๋“ค์„ importํ•ด์ค€๋‹ค.
    import { useTable, useBlockLayout, useResizeColumns } from 'react-table'
    
    function Table({ columns, data }) {
      //๊ธฐ๋ณธ column์˜ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•ด์ฃผ๋Š” ๊ณณ. ๋‚˜์ค‘์— ์ด๊ฑธ ๋ฐ”๊พธ๊ณ  ์‹ถ์—ˆ์ง€๋งŒ ์•„์ง ์–ด๋–ป๊ฒŒ ๋ฐ”๊ฟ€์ง€ ๋ชจ๋ฅด๊ฒ ๋‹ค.
      const defaultColumn = React.useMemo(
        () => ({
          minWidth: 30,
          width: 150,
          maxWidth: 400,
        }),
        []
      )
    
    //์šฐ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ๋ถ€๋ถ„์€ getTableProps,headerGroups,rows,prepareRow์ด๋‹ค.
    //์ฒ˜์Œ์—” getTableBodyProps๋ฅผ ๋‘๊ณ  headerGroups๋ฅผ ์—†์•ด๋Š”๋ฐ, column์˜ ๊ฐ€๋กœ ํญ์„ ์กฐ์ ˆํ•˜๋Š” ๊ฒƒ์ด
    //header ๋ถ€๋ถ„์—์„œ ๊ฐ€๋Šฅํ•œ ๊ฑธ ์•Œ๊ณ  headerGroups๋ฅผ ๋†”๋‘๊ธฐ๋กœ ํ–ˆ๋‹ค.
    const {
      getTableProps,
      getTableBodyProps,
      headerGroups,
      rows,
      prepareRow,
      state,
      resetResizing,
    } = useTable(
      {
        columns,
        // ์—ฌ๊ธฐ์˜ column๋“ค์ด ์šฐ๋ฆฌ๊ฐ€ ๋ฐ”๊ฟ€ ๊ฐ€๋กœํญ๋“ค์„ ์ง€์นญํ•œ๋‹ค.
        data,
        // ์—ฌ๊ธฐ์˜ data๊ฐ€ ๊ฐ column์— ๋“ค์–ด๊ฐ€๋Š” ์ปฌ๋Ÿฌ ์ฝ”๋“œ ๊ฐ’๋“ค์„ ์˜๋ฏธํ•œ๋‹ค.
        defaultColumn,
      },
      useBlockLayout,
      useResizeColumns
    )
    

    ์ด ์•„๋ž˜ ๋ถ€๋ถ„์€ ๋ฐ๋ชจ ์ฝ”๋“œ์™€ Pallete.tsx์™€ ์ข€ ๋งŽ์ด ๋‹ค๋ฅด๋‹ค.

    return (
      <>
        <button onClick={resetResizing}>Reset Resizing</button>
        <div>
          <div {...getTableProps()} className="table">
            <div>
              {headerGroups.map((headerGroup) => (
                <div {...headerGroup.getHeaderGroupProps()} className="tr">
                  {headerGroup.headers.map((column) => (
                    <div {...column.getHeaderProps()} className="th">
                      {column.render('Header')}
                      {/* Use column.getResizerProps to hook up the events correctly */}
                      <div
                        {...column.getResizerProps()}
                        className={`resizer ${column.isResizing ? 'isResizing' : ''}`}
                      />
                    </div>
                  ))}
                </div>
              ))}
            </div>
    
            <div {...getTableBodyProps()}>
              {rows.map((row, i) => {
                prepareRow(row)
                return (
                  <div {...row.getRowProps()} className="tr">
                    {row.cells.map((cell) => {
                      return (
                        <div {...cell.getCellProps()} className="td">
                          {cell.render('Cell')}
                        </div>
                      )
                    })}
                  </div>
                )
              })}
            </div>
          </div>
        </div>
        <pre>
          <code>{JSON.stringify(state, null, 2)}</code>
        </pre>
      </>
    )
    }
    

    1-5. pallete์— ๋งž๊ฒŒ ์ ์šฉ

    ๋‚˜๋Š” ์ด๋ฏธ ํŒ”๋ ˆํŠธ์˜ ํ…Œ์ด๋ธ”์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์—,
    ๊ทธ ๊ฐ’์„ ์ด์šฉํ•ด์„œ ๋ฐฐ์—ด์„ ๊ฐ์ฒด๋กœ ๋งŒ๋“ค๊ณ , ๋‹ค์‹œ ๊ทธ ๊ฐ์ฒด๋ฅผ ๋ฐฐ์—ด์— ์ง‘์–ด๋„ฃ์—ˆ๋‹ค.

    function App() {
      const columns = React.useMemo(
        () => [
          {
            Header: 'Name',
            columns: [
              {
                Header: 'First Name',
                accessor: 'firstName',
              },
              {
                Header: 'Last Name',
                accessor: 'lastName',
              },
            ],
          },
          {
            Header: 'Info',
            columns: [
              {
                Header: 'Age',
                accessor: 'age',
                width: 50,
              },
              {
                Header: 'Visits',
                accessor: 'visits',
                width: 60,
              },
              {
                Header: 'Status',
                accessor: 'status',
              },
              {
                Header: 'Profile Progress',
                accessor: 'progress',
              },
            ],
          },
        ],
        []
      )
    
      const data = React.useMemo(() => makeData(10), [])
    
      return (
        <Styles>
          <Table columns={columns} data={data} />
        </Styles>
      )
    }
    
    export default App
    
    boxcount ๋ฐฐ์—ด
    const columns = Array.from({ length: BOX_COUNT }, (_, i) => {
      return {
        Header: `Column ${i}`,
        accessor: i.toString(),
      }
    })
    console.log(columns)
    

    ์ด๋Ÿฐ์‹์œผ๋กœ ๋ฐ•์Šค ์นด์šดํŠธ์˜ ๊ธธ์ด๋ฅผ ๋ฐฐ์—ด๋กœ ๊ฐ€์ ธ์™€์„œ ์ธ๋ฑ์Šค ๊ฐ’์„ ๊ฐ€์ ธ์™€, ํ—ค๋”์™€ ์•ก์„ธ์„œ์— ๋„ฃ์–ด์ค€๋‹ค.

    ๊ทธ๋ฆฌ๊ณ  data๊ฐ™์€ ๊ฒฝ์šฐ

    entries,row
    // ์ด ๋ชจ์–‘์„ ์–ด๋–ป๊ฒŒ data์— ๋งž๊ฒŒ ๋ณ€ํ˜•ํ• ๊นŒ? map๊ณผ fromEntries๋ฅผ ํ™œ์šฉํ–ˆ๋‹ค
    // input: colors ["#ffffff","#ffffff","#ffffff","#ffffff", ...,"#ffffff"]
    
    const entries = colors.map((v, i) => [i.toString(), v])
    console.log('entries', entries)
    
    const row = Object.fromEntries(entries)
    console.log('row', row)
    
    row array

    ์ƒ‰๋“ค์„ mapํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด ์ธ๋ฑ์Šค์™€ ์ปฌ๋Ÿฌ ์ฝ”๋“œ๋ฅผ ์ง๋งž์ถ”์–ด ๋ฐฐ์—ด๋กœ ๋งŒ๋“ค์–ด์ฃผ๊ณ ,
    row๋ผ๋Š” ๋ณ€์ˆ˜์— fromEntriesํ•จ์ˆ˜๋ฅผ ์จ์„œ ๋‹ค์‹œ ๊ฐ์ฒด๋กœ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๋‹ค.

    const {
      getTableProps,
      headerGroups,
      rows,
      prepareRow,
      // getTableBodyProps,
      // state,
      // resetResizing,
    } = useTable(
      {
        columns,
        data: [row],
        defaultColumn,
      },
      useBlockLayout,
      useResizeColumns
    )
    

    ์—ฌ๊ธฐ์— ๋‹ค์‹œ ๋ฐฐ์—ด์•ˆ์— row๋ฅผ ๋„ฃ์–ด ๊ฐ์ฒด๋ฅผ ๋ฐฐ์—ด๋กœ ๋งŒ๋“ค์–ด์„œ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

    return (
        <Tooltip.Provider delayDuration={800} skipDelayDuration={500}>
          <input type="text" name="colors" value={JSON.stringify(colors)} hidden />
          <table
            className="selectedContainer flex flex-col max-w-full"
            {...getTableProps()}
          >
            {/* 2๋ฒˆ - map์„ ์จ์„œ ๋ฐฐ์—ด์˜ ๊ฐ’์„ ํ™”๋ฉด์— ๋ฟŒ๋ ค์ค€๋‹ค. */}
            {/* https://beta.reactjs.org/learn/rendering-lists */}
            <thead>
              {headerGroups.map((headerGroup) => (
                <tr {...headerGroup.getHeaderGroupProps()}>
                  {headerGroup.headers.map((column) => (
                    <th {...column.getHeaderProps()}>
                      {column.id}
                      <div
                        {...(column as any).getResizerProps()}
                        className={`resizer ${
                          (column as any).isResizing ? "isResizing" : ""
                        }`}
                      />
                    </th>
                  ))}
                </tr>
              ))}
            </thead>
            <tbody className="flex flex-row">
              {rows.map((row) => {
                prepareRow(row);
                return (
                  <tr {...row.getRowProps()}>
                    {row.cells.map((cell) => {
                      return (
                        <td
                          {...cell.getCellProps()}
                          onClick={() => deleteSelected(parseInt(cell.column.id))}
                          style={{
                            backgroundColor: cell.value,
                            width: cell.column.width,
                          }}
                        >
                          {cell.value}
                        </td>
                      );
                    })}
                  </tr>
                );
              })}
            </tbody>
          </table>
    

    ์œ„์˜ ์ฝ”๋“œ๋Š” ๋‚˜์—๊ฒŒ ๋งž๊ฒŒ ๋ฐ”๋€ ๋ถ€๋ถ„์ธ๋ฐ

    ์ผ๋‹จ

    <table
            className="selectedContainer flex flex-col max-w-full"
            {...getTableProps()}
          >
    

    ์—ฌ๊ธฐ์„œ role๋Œ€์‹  {...getTableProps()}๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.
    ๊ทธ ์ด์œ ๋Š” ์ง€๊ธˆ์€ props๊ฐ€ ํ•˜๋‚˜์ด์ง€๋งŒ ๋‚˜์ค‘์—๋Š” ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

    ๊ทธ๋ฆฌ๊ณ  thead์•ˆ์— headerGroups๋ฅผ ๋„ฃ์–ด์„œ ์ปฌ๋Ÿผ์˜ ํญ์„ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ์กฐ์ •๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ์„ ๋‚˜์—ดํ•œ๋‹ค.

    <thead>
      //headerGroup์€ headerGroup์„ map์„ ํ•˜๋Š”๋ฐ
      {headerGroups.map((headerGroup) => (
        //tr์— getHeaderGroupProps๋ฅผ ๋‚˜์—ดํ•œ๋‹ค.
        <tr {...headerGroup.getHeaderGroupProps()}>
          //headerGroup.headers์€ column์„ map์œผ๋กœ ๋‚˜์—ดํ•˜๋Š”๋ฐ
          {headerGroup.headers.map((column) => (
            //th์— column์˜ getHeaderProps๋ฅผ ๋‚˜์—ดํ•ด ๋„ฃ์–ด์ค€๋‹ค.
            <th {...column.getHeaderProps()}>
              //column.id๋Š” ์ปฌ๋Ÿผ์˜ ์ธ๋ฑ์Šค๊ฐ’์œผ๋กœ ์ด ๊ฐ’์„ ๊ฐ์ฒด๋กœ ๋„ฃ์–ด์ฃผ์–ด ๋‚˜์—ดํ•œ๋‹ค.
              {column.id}
              //์ด div์€ getResizerPropsํ•จ์ˆ˜๋ฅผ ํ†ตํ•œ ๊ฐ’์„ ๋‚˜์—ดํ•˜๋Š”๋ฐ, ์•„๋งˆ๋„ ๋ฆฌ์‚ฌ์ด์ง•ํ•œ ๊ฐ’์„ ๋‚˜์—ดํ•ด์ฃผ๋Š”
              ๊ฒƒ ๊ฐ™๋‹ค.
              <div
                {...(column as any).getResizerProps()}
                className={`resizer ${(column as any).isResizing ? 'isResizing' : ''}`}
              />
            </th>
          ))}
        </tr>
      ))}
    </thead>
    

    tbody์—๋Š”

    <tbody className="flex flex-row">
      //rows๋ฅผ map์œผ๋กœ ๋‚˜์—ดํ•ด์ฃผ๋Š”๋ฐ
      {rows.map((row) => {
        //์ผ๋‹จ ์ดˆ๊ธฐ์ƒํƒœ์˜ row๋ฅผ prepareRow๋กœ returnํ•ด์ฃผ๊ณ 
        prepareRow(row)
        return (
          //์ƒ‰๊น”์„ ์„ ํƒํ•ด์„œ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด getRowProps์˜ ๊ฐ’์„ ๋‚˜์—ดํ•ด์ฃผ๋Š” ๊ฑฐ ๊ฐ™๋‹ค.
          <tr {...row.getRowProps()}>
            {row.cells.map((cell) => {
              return (
                <td
                  {...cell.getCellProps()}
                  //๋งŒ์•ฝ ์ด๋ฏธ ์„ ํƒ๋œ ์ƒ‰์„ ์ง€์šฐ๋ ค๊ณ  ํ•˜๋ฉด ํ•ด๋‹น ์นธ์˜ id์ธ index๋ฅผ string์—์„œ int๋กœ ๋ฐ”๊พธ์–ด ์‚ญ์ œํ•ด์ค€๋‹ค.
                  onClick={() => deleteSelected(parseInt(cell.column.id))}
                  style={{
                    //์Šคํƒ€์ผ์— ๋ฐฐ๊ฒฝ์—๋Š” ์„ ํƒํ•œ cell์˜ ์ƒ‰์ธ value๋ฅผ ๋„ฃ์–ด์ฃผ๊ณ 
                    //width์—๋Š” header์—์„œ ๋‚ด๋ ค์˜จ column์˜ width๊ฐ€ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋˜๋„๋ก ํ•œ๋‹ค.
                    backgroundColor: cell.value,
                    width: cell.column.width,
                  }}
                >
                  //์„ ํƒ๋œ ์ƒ‰๊น” ์นธ์—๋Š” ํ•ด๋‹น ์ƒ‰ ์ฝ”๋“œ๋„ string์œผ๋กœ ๋‚˜ํƒ€๋‚ด์–ด์ค€๋‹ค.
                  {cell.value}
                </td>
              )
            })}
          </tr>
        )
      })}
    </tbody>
    

    ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์‚ฌ์šฉํ•œ Props๋“ค์„
    ๋‚˜์˜ ๊ฒฝ์šฐ์— ๋งž์ถ”์–ด ๋ณ€ํ˜•ํ•˜๊ณ ,
    ๋ณ€ํ˜•ํ•œ ๊ฐ์ฒด๋“ค์„ ์•Œ๋งž๊ฒŒ ๋„ฃ์–ด์ฃผ๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ๋‹ค.

    ์•„์ง get~~~Props์˜ ์—ญํ• ์„ ์ •ํ™•ํžˆ ์ดํ•ดํ•˜์ง€๋Š” ๋ชปํ•œ ๊ฒƒ ๊ฐ™์•„์„œ
    ๋ช‡๋ฒˆ ๋” ์ฝ๊ณ  ๋ถ„์„ํ•ด๋ณด๋ฉด์„œ ์ดํ•ดํ•ด์•ผ๊ฒ ๋‹ค.

    ๊ทธ๋ฆฌ๊ณ  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ ์ด๋ฏธ ์ •ํ•ด๋‘” ํญ ์ด์™ธ์— ์ •๋ ฌ์ด๋‚˜ ์ตœ๋Œ€ ํญ ํฌ๊ธฐ๋ฅผ ๋งˆ์Œ๋Œ€๋กœ
    ์ •ํ•˜๋Š” ๋ฐ ์–ด๋ ค์šด ๋ถ€๋ถ„์ด ์žˆ์–ด์„œ, ์ด๋ถ€๋ถ„์€ ํ›„์— ํŒ”๋ ˆํŠธ๋ฅผ pure css๋กœ ๊ตฌํ˜„ํ•ด์„œ ๋‹ค์‹œ react-table์„ ์ž…ํ˜€๋ณด๋ฉด์„œ
    ์ข€ ๋” ์ •ํ™•ํžˆ ์ฝ”๋“œ๋ฅผ ์งœ๋ด์•ผํ•  ๊ฒƒ ๊ฐ™๋‹ค.