타입스크립트 Generic을 활용하여 재사용할 수 있는 테이블 컴포넌트 만들기

모성종·2023년 3월 4일
0

일반적인 테이블 컴포넌트

import React from "react";

export default function App() {
  return (
    <>
      <h4>데이터 테이블</h4>
      <table>
        <thead>
          <tr>
            <th>name</th>
            <th>job</th>
            <th>age</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>mo<td>
            <td>dev<td>
            <td>22<td>
          </tr>
          <tr>
            <td>lee<td>
            <td>dentist<td>
            <td>22<td>
          </tr>
        </tbody>
      </table>
    </>
  );
}

정적인 데이터를 활용해 단순히 렌더링만 해주기 때문에
테이블의 모양이 바뀌거나 데이터가 다른 경우 재사용성이 낮아진다

이 쯤에서 보통 "테이블 렌더링을 데이터를 받는 컴포넌트로 분리해보자" 라고 생각하게 된다.


Table.tsx

interface TableProps {
  columns: Array<"name" | "job" | "age">;
  data: { name: string; job: string; age: number; }[];
}

export default function Table({ columns, data }: TableProps) {
  return (
    <table>
      <thead>
    	<tr>
          {columns.map((column, i) => (<th key={i}>{column}</th>)}
    	</tr>
      </thead>
      <tbody>
    	{data.map((row, i) => {
          return (
            <tr key={i}>
              {column.map((column, j) => {
                return (<td key={j}>{row[column]}</td>)
			  })}
            </tr>
          )}
        )}
      </tbody>
    </table>
  );
}

App.tsx

import Table from "./Table";

const data = [
  { name: "mo", job: "dev", age 22 },
  { name: "lee", job: "dentist", age: 22 }
];

export default function App() {
  return (
    <>
      <h4>데이터 테이블</h4>
      <Table column={["name", "job", "age"]} data={data} />
    </>
  )
}

data를 전달해 그려주는 컴포넌트로 만들었다!
column도 선택적으로 전달해서 괜찮은 컴포넌트 처럼 보인다.

하지만 넘겨주는 데이터의 타입이 { name: string; job: string; age: number }로 고정되어 있기 때문에 타입이 달라지면 이 Table 컴포넌트를 사용할 수 없다.

재사용할 수 있는 컴포넌트

컴포넌트를 재사용하기 위해서는 타입을 지정하지 않고 "주입" 받아야 한다.
다른 언어에도 종종 보이는 제네릭 타입을 사용하면 해결할 수 있다.

Generic type으로 테이블 컴포넌트 수정

interface TableProps<T> {
  columns: Array<keyof T>;
  data: T[];
}

제네릭 타입으로 테이블 컴포넌트의 prop을 수정하여 data의 모양을 type prop T로 적용한다.
column는 해당 data의 키만 사용가능해야 하므로 Array<keyof T>를 사용한다.

이렇게 제한해야 <Table /> 컴포넌트를 사용할 때 column 속성으로 넘겨주는 키값을 타입스크립트가 추론해서 자동완성을 사용할 수 있고

사용자가 데이터에 존재하지 않는 컬럼명을 입력하는 타입에러를 사전에 방지할 수 있다.

테이블의 각 요소도 사용자에게 넘기기

지금까지 테이블 컴포넌트는 데이터를 받아서 렌더링해주고, column 필드의 타입을 추론하여 사용자의 실수를 방지할 수 있다.
하지만 테이블을 단순히 텍스트를 그려주기만 할 뿐이라 각 컬럼의 요소를 선택적으로 다르게 렌더링한다거나 클릭 등 사용자 액션은 할 수 없다.

여기서 테이블 컴포넌트의 자식 요소를 사용자에게 넘기고 컴포넌트를 사용하는 쪽에서 제어할 수 있도록하면 더 자유로운 컴포넌트가 된다.

그러면 어떻게 해야할까?

컴포넌트의 property를 사용하는 게 아닌 children을 사용하면 된다.
컴포넌트에서는 사용자에로부터 받은 children comp를 실행하고, 사용자는 children comp를 테이블 컴포넌트 하위로 작성하여 자유롭게 반환 컴포넌트를 만들 수 있다.

interface TableProps<T> {
  columns: Array<keyof T>;
  data: T[];
  children: () => JSX.Element;
}

// Table Comp의 tbody
<tbody>
  {data.map((row, i) => {
    return (
      <tr key={i}>
        {column.map((column, j) => {
          /* 전달받은 children으로 수정된 코드 */
          return (<td key={j}>
            {children()}
          </td>)
        })}
      </tr>
    )}
  )}
</tbody>


children을 실행할 때 row, column의 값을 넘겨서 사용자가 자유롭게 이용할 수 있도록 해주자.
interface TableProps<T> {
  columns: Array<keyof T>;
  data: T[];
  children: (payload: { rowData: T, property: keyof T, index?: number }) => JSX.Element;
}

// Table Comp의 tbody
<tbody>
  {data.map((row, i) => {
    return (
      <tr key={i}>
        {column.map((column, j) => {
          /* 전달받은 children으로 수정된 코드 */
          return (<td key={j}>
            {children({ rowData: row, property: column, index: i})}
          </td>)
        })}
      </tr>
    )}
  )}
</tbody>

이렇게하면 테이블 컴포넌트를 실행될 때 children을 rowData, property, index를 컴포넌트를 사용하는 사용자에게 넘겨준다.


App.tsx

export default function App() {
  return (
    <>
      <h4>데이터 테이블</h4>
      <Table column={["name", "job", "age"]} data={data}>
        {({ rowData, property, index }) => {
          if (property === "job") {
            return (<span>{rowData[property} 👍</span>);
          }
          return (
            <React.Fragment key={index}>
              {rowData[property}
            </React.Fragment>
          );
        }}
      </Table>
    </>
  )
}

Table 컴포넌트를 사용할 때 children 함수에서 반환하는 값에 따라 다르게 렌더링할 수 있게 됐다!! 🙌


마무리

컴포넌트를 만들 때 사용자에게 일방향으로 전달받아 렌더링하는 컴포넌트를 생각하는 게 일반적인데,
이런식으로 활용하면 컴포넌트를 사용하는 사용자에게 제어를 넘겨줄 수 있어서 더 사용성 높은 컴포넌트를 만들 수 있다.



최종 코드

import React from "react";
import "./styles.css";

const data = [
  { name: "mo", job: "dev", age 22 },
  { name: "lee", job: "dentist", age: 22 }
];
const data2 = [
  { brand: "samsung", industry: "반도체", employees: 1_000_222 },
  { brand: "apple", industry: "농장", employees: 50_000_222 }
];

interface TableProps<T> {
  columns: Array<keyof T>;
  data: T[];
  children: (payload: { rowData: T; property: keyof T; index?: number; }) => JSX.Element;
}

function Table<T>({ columns, data, children }: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((column, i) => (
            <th key={i}>{column as React.ReactNode}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, i) => (
          <tr key={i}>
            {columns.map((column, j) => (
              <td key={j}>
                {children({ rowData: row, property: column, index: j })}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

export default function App() {
  return (
    <div className="App">
      <h4>데이터 테이블</h4>

      <Table columns={["name", "job", "age"]} data={data}>
        {({ rowData, property, index }) => {
          if (property === "job") {
            return (<span>{rowData[property]} 👍</span>)
          }
          return (
            <React.Fragment key={index}>{rowData[property]}</React.Fragment>
          );
        }}
      </Table>
    </div>
  );
}
profile
FE Developer

0개의 댓글