- Published on
๐ ์ปฌ๋ฌ๋ฆฌ์คํธ ํ๋ก์ ํธ #11 Pallete table - resizable columns
- ๊ธ์ด์ด
๐ ๋ชฉ์ฐจ
๐๐ป
- ์ ํ์นธ ํ ์ด๋ธ 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. ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ๋ ๋ฐฉ๋ฒ
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ ๊ฐ์ง๊ฐ ์๋ค.
์ปดํฌ๋ํธ๋ก ์ฐ๋ ๊ฒ ์) Radix
ํ ์ฒ๋ผ 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
const columns = Array.from({ length: BOX_COUNT }, (_, i) => {
return {
Header: `Column ${i}`,
accessor: i.toString(),
}
})
console.log(columns)
์ด๋ฐ์์ผ๋ก ๋ฐ์ค ์นด์ดํธ์ ๊ธธ์ด๋ฅผ ๋ฐฐ์ด๋ก ๊ฐ์ ธ์์ ์ธ๋ฑ์ค ๊ฐ์ ๊ฐ์ ธ์, ํค๋์ ์ก์ธ์์ ๋ฃ์ด์ค๋ค.
๊ทธ๋ฆฌ๊ณ data๊ฐ์ ๊ฒฝ์ฐ
// ์ด ๋ชจ์์ ์ด๋ป๊ฒ 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)
์๋ค์ 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
์ ์
ํ๋ณด๋ฉด์
์ข ๋ ์ ํํ ์ฝ๋๋ฅผ ์ง๋ด์ผํ ๊ฒ ๊ฐ๋ค.