๐Ÿง‘โ€๐Ÿ’ป ์งง์€ํ˜ธํก/React

[React] ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ๋„ค๋ฆญ ํ•จ์ˆ˜๋กœ ์“ฐ๊ธฐ - ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ์ถ”์ƒํ™”

ํ•œ๊ทœ์ง„ 2023. 5. 25. 19:47

๊ท€์ฐฎ์€ ์ผ์€ ๊ฝค๋‚˜ ์ž์ฃผ ์ผ์–ด๋‚œ๋‹ค. ์™ธ์ฃผ ์ž‘์—… ์ค‘ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผํ•˜๋Š” ์ผ์ด ์žˆ์—ˆ๊ณ , ๊ธฐ์กด ์ฝ”๋“œ๋Š” Core UI๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ 4.0๋ฒ„์ „์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ ๋˜๋ฉด์„œ PRO๋ฒ„์ „์ด ์ƒˆ๋กœ ์ƒ๊ฒผ๊ณ , ๊ทธ๋ฅผ ์œ„ํ•œ ๊ธ‰๋‚˜๋ˆ„๊ธฐ์— ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ฑธ๋ ค๋ฒ„๋ ธ๋‹ค. ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ onRowClick, scopedSlots๋“ฑ์˜ ๊ธฐ๋Šฅ๋“ค์ด ์‚ฌ๋ผ์กŒ๊ณ  ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ–ˆ๋‹ค.

 

<Table
  paginationState={[current, setCurrent]}
  column={column}
  data={data}
  onRowClick={data => {
    navigate(`/notices/${data.id}`);
  }}
  customCellItem={{
    info: data => <button onClick={() => alert(data.index)}>์ปค์Šคํ…€cell</button>,
    date: data => <div style={{color: 'red'}}>{data.date}</div>,
  }}
/>;

์ถ”์ƒํ™”๋œ Table์€ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์œ„์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ปฌ๋Ÿผ๊ณผ data ๋ฐฐ์—ด์„ ๋„˜๊ฒจ์ฃผ๋ฉด ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์ ์œผ๋กœ ์ด๋Ÿฌ์ฟต์ €๋Ÿฌ์ฟตํ•ด์„œ ํ…Œ์ด๋ธ”์„ ๋ฉ‹์žˆ๊ฒŒ ๊ทธ๋ ค์ฃผ๋Š” ๋™์ž‘์ด ์ถ”์ƒํ™”๋˜์–ด ์žˆ๋‹ค.

 

์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๋Š” ๊ฐ„๋‹จํžˆ ์ด๋ ‡๊ฒŒ ๋˜์–ด ์žˆ๋‹ค. CoreUI์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์—๋Š” C๊ฐ€ ๋ถ™์–ด์žˆ์Œ.

const {content, size, total} = data;
const itemList = customCellItem
  ? getCustomRow(content, customCellItem)
  : content;

return (
  <>
    <CTable hover striped responsive>
      <CTableHead>
        <CTableRow>
          {column.map(c => (
            <CTableHeaderCell scope='col' key={c.key} style={c._style}>
              {c.label}
            </CTableHeaderCell>
          ))}
        </CTableRow>
      </CTableHead>
      <CTableBody>
        {itemList.map((item, idx) => (
          <CTableRow
            key={`${item.id}-${idx}`}
            onDoubleClick={() => onRowClick?.(item)}
          >
            {Object.keys(filterRowData(item, column)).map(cell => (
              <CTableDataCell key={cell}>{item[cell]}</CTableDataCell>
            ))}
          </CTableRow>
        ))}
      </CTableBody>
    </CTable>
    <Pagination
      current={paginationState[0]}
      setCurrentPage={paginationState[1]}
      totalPage={Math.ceil(total / size)}
    />
  </>
);

props๋กœ ๋ฐ›์€ column๊ณผ item์„ ํ…Œ์ด๋ธ” row๋กœ ๋ Œ๋”๋ง์‹œํ‚จ๋‹ค. getCustomRow ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๊ฐ cell๋งˆ๋‹ค ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์ง€์ •ํ•ด์ฃผ๊ณ , filterRowData ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ response๋กœ ๋ฐ›์€ data ๊ฐ์ฒด๋ฅผ column ๊ฐ์ฒด์™€ ๋Œ€์กฐํ•ด ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์€ ํ‚ค-๊ฐ’๋งŒ ํ•„ํ„ฐ๋งํ•œ๋‹ค.

 


 

tsx์—์„œ ํ•จ์ˆ˜์— ์ œ๋„ค๋ฆญ ์‚ฌ์šฉํ•˜๊ธฐ

ํ…Œ์ด๋ธ” ๊ฐ ํ–‰์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ์‹คํ–‰๋˜๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜์—, ์ธ์ž๋กœ ๊ฐ ํ–‰์˜ data๊ฐ€ ์ „๋‹ฌ๋˜๋„๋ก ํ–ˆ๋‹ค.

๋ฌธ์ œ๋Š” ์—ฌ๊ธฐ์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ data๊ฐ€ ์–ด๋–ค ํƒ€์ž…์ธ์ง€ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์—, ๋ฉ์ฒญํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ถฉ๋ถ„ํžˆ ์‹ค์ˆ˜ํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด ๋งŒ๋“ค์–ด์ง„๋‹ค. ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ํ†ตํ•ด Table ์ปดํฌ๋„ŒํŠธ๋กœ ๋„˜๊ธด ํƒ€์ž…์˜ ์ฝœ๋ฐฑ์—์„œ ์ถ”๋ก ๋˜์–ด ์ ์ ˆํ•œ ํƒ€์ž…์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

 

const GenericReturnFunc = <T>(arg: T): T => {
  return arg;

๊ทธ๋ƒฅ ts ํŒŒ์ผ์—์„œ๋Š” ์œ„์™€ ๊ฐ™์ด ์ œ๋„ค๋ฆญ ํ•จ์ˆ˜๋ฅผ ์“ธ ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ tsx์—์„œ ์œ„์™€ ๊ฐ™์ด ์ž‘์„ฑํ•˜๋ฉด ๋นจ๊ฐ„์ค„์ด ๋œจ๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

const GenericReturnFunc = <T extends {}>(arg: T): T => {
  return arg;
}

์ด๋ ‡๊ฒŒ extend ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜,

const GenericReturnFunc = <T,>(arg: T) : T => {
  return arg;
}

๋‘๊ฐœ ์ด์ƒ์˜ ํƒ€์ž… ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ œ๋„ค๋ฆญ์„ ์ธ์‹ํ•œ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํƒ€์ž… ํŒŒ์ผ์—์„œ ์œ„์™€ ๊ฐ™์ด ์‰ผํ‘œ๋ฅผ ์“ด ์ œ๋„ค๋ฆญ ํƒ€์ž…๋“ค์„ ์ž์ฃผ ๋ด์™”๋Š”๋ฐ, ๋“œ๋””์–ด ์™œ ๊ทธ๋ ‡๊ฒŒ ์“ฐ๋Š”์ง€ ์•Œ์•˜๋‹ค!

 


์ปดํฌ๋„ŒํŠธ์— ์ œ๋„ค๋ฆญ ์‚ฌ์šฉํ•˜๊ธฐ

const Table = <T extends Item>({
  column,
  data,
  customCellItem,
  paginationState,
  onRowClick,
}: TableProps<T>) => {
  const {content, size, total} = data;
  const itemList = customCellItem
    ? getCustomRow(content, customCellItem)
    : content;

  return (
    <>
    // ...

์œ„์™€ ๊ฐ™์ด ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜์— ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.
, ๊ฐ€ ์•„๋‹Œ extends๋ฅผ ํ†ตํ•ด Item ์ด๋ผ๋Š” ํƒ€์ž…์„ ํ™•์žฅํ•˜๊ณ  ์žˆ๋‹ค.

 

export type Item = {
    [key: string]: number | string | any;
    _props?: CTableRowProps;
};

Item์€ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋‚ด์žฅ๋œ ํƒ€์ž…์ด๋‹ค.
string์„ key๋กœ ๊ฐ–๋Š” (๊ต‰์žฅํžˆ ๋Š์Šจํ•œ) ๊ฐ์ฒด์˜ ํƒ€์ž…์ด๋‹ค.

 

{
  itemList.map((item, idx) => (
    <CTableRow
      key={`${item.id}-${idx}`}
      onDoubleClick={() => onRowClick?.(item)}
    >
      {Object.keys(filterRowData(item, column)).map(cell => (
        <CTableDataCell key={cell}>{item[cell]}</CTableDataCell>
      ))}
    </CTableRow>
  ));
}

์ด๋Ÿฐ ์‹์œผ๋กœ ํ‚ค[๊ฐ’]์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, Item ํƒ€์ž…์„ ํ™•์žฅํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ. ๊ทธ๋ ‡์ง€ ์•Š๋‹ค๋ฉด item์—์„œ id ์†์„ฑ์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ณผ์ •์—์„œ 'T' ํ˜•์‹์— 'id' ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.ts(2339) ์˜ค๋ฅ˜๋ฅผ ๋งˆ์ฃผ์น˜๊ฒŒ ๋œ๋‹ค.

 

 

๊ธ€์„ ์“ฐ๋‹ค ๋ฐœ๊ฒฌํ–ˆ๋Š”๋ฐ, ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์–ด์ฉŒ๋ฉด ๊ฝค๋‚˜ ์นœ์ ˆํ• ์ˆ˜๋„.

 

 

export interface CustomCellItem<T> {
  [key: string]: (data: T) => ReactNode;
}

export interface TableProps<T> {
  column: Column[];
  data: TableResponse<T>;
  customCellItem?: CustomCellItem<T>;
  paginationState: [number, Dispatch<SetStateAction<number>>];
  onRowClick?: (data: T) => void;
}

์ด์ œ ์ปดํฌ๋„ŒํŠธ์˜ ์ธํ„ฐํŽ˜์ด์Šค์— ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์žˆ๋‹ค. ๋‹จ์ˆœํžˆ ํ‚ค-๊ฐ’ ํ˜•ํƒœ์˜ ๊ฐ์ฒด๋ผ๋Š” ์˜๋ฏธ๋ฅผ ๋‹ด์€ Item ํƒ€์ž…์ด ์•„๋‹Œ ์ •ํ™•ํ•œ data์˜ ํƒ€์ž…์„ ๋„˜๊ฒจ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

 


 

๋ฉ‹์ง„ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

 

/**
 * ํŒจ์นญ ๋ฐ์ดํ„ฐ ์ค‘ table column์— ์žˆ๋Š” ๊ฐ’๋งŒ ๋ฐ˜ํ™˜
 * @param data ์›๋ณธ ๋ฐ์ดํ„ฐ (ํŒจ์นญํ•œ ๋ฐ์ดํ„ฐ)
 * @param column table์—์„œ ๋ณด์—ฌ์ค„ column key
 */
const filterRowData = <T extends Item>(
  data: T,
  column: Column[],
): Partial<T> => {
  const map = new Map();
  column.forEach(c => {
    const key = c.key;
    map.set(key, data[key]);
  });
  return Object.fromEntries(map);
};

data์˜ ์—ฌ๋Ÿฌ ์†์„ฑ ์ค‘ column์— ์žˆ๋Š” ์†์„ฑ๋งŒ ๋‚จ๊ธด๋‹ค. Partial<T> ์œ ํ‹ธ๋ฆฌํ‹ฐ ํƒ€์ž…์„ ํ†ตํ•ด ๋ฐ˜ํ™˜๊ฐ’์„ ๋ช…์‹œํ•ด์ฃผ์—ˆ๋‹ค. ๋ช…์‹œํ•˜์ง€ ์•Š์œผ๋ฉด ํƒ€์ž…์ด ์ถ”๋ก ๋˜์ง€ ์•Š์•„ any๋กœ ๋‚˜ํƒ€๋‚œ๋‹ค.

 

 

export interface CustomCellItem<T> {
  [key: string]: (data: T) => ReactNode;
}

/**
 * ๋ฒ„ํŠผ, input ๋“ฑ์˜ ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ item์œผ๋กœ ๋ฐ˜ํ™˜
 * @param data ๋ Œ๋”๋งํ•  data
 * @param customCellItem cell์— ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ
 */
const getCustomRow = <T,>(data: T[], customCellItem: CustomCellItem<T>) => {
  let customRow = data;
  Object.keys(customCellItem).forEach(v => {
    customRow = customRow.map(row => {
      return {...row, [v]: customCellItem[v](row)};
    });
  });
  return customRow;
};

customCellItem์„ ์œ„ํ•œ getCustomRow ํ•จ์ˆ˜์ด๋‹ค. ์ธ์ž๋กœ ๋ฐ›์€ ๊ฐ์ฒด์— ์žˆ๋Š” ์†์„ฑ๊ณผ ๊ฐ™์€ ํ‚ค๋ฅผ ๊ฐ€์ง„ ์ปฌ๋Ÿผ์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋กœ ๋Œ€์ฒด๋œ๋‹ค.

 

์•„์ฃผ ๋ฉ‹์ง€๊ฒŒ ํƒ€์ž…์ด ์ถ”๋ก ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.