[React] Compound Components 로 재사용성 극대화해보기

babypig·2023년 2월 28일
3

React.js

목록 보기
6/8
post-thumbnail

📝 아토믹 디자인 패턴을 활용한 Table Compound Components

지난 페이지네이션포스트 에서 타입스크립트로 아토믹 디자인 패턴을 활용하여 Compound Components 로 리팩토링 예정 글을 올렸는데 타입스크립트 학습과 오랜 삽질로 우선 아토믹 디자인 패턴을 활용하여 Table을 Compound Components 로 만들었다. 추후에 드롭다운과 페이지네이션도 같이 추가하여 포스트할 예정 💬

1. 😥 삽질

  return (
      <>
          <Table>
            <Table.Thead>
              <Table.Row>
                {dataTitle.map((item, index) => <Table.Cell key={index} header>{item}</Table.Cell>)}
              </Table.Row>
            </Table.Thead>
            <Table.Tbody>
              {data.content.map((item) =>
                  <Table.Row key={item.id}>
                    <Table.Cell>{item.title}</Table.Cell>
                    <Table.Cell>{item.body}</Table.Cell>
                    <Table.Cell>{item.price}</Table.Cell>
                    <Table.Cell>{item.isChecked}</Table.Cell>
                  </Table.Row>
              )}
            </Table.Tbody>
          </Table>
      </>
  )

만들고나서도 이게 뭔가 싶었다, Table 컴포넌트 내에서 다른 하위 컴포넌트들을 사용하는 방식으로 구성되어있고, 이 방식은 컴포넌트의 재사용성이 떨어지기 때문에 컴파운드 컴포넌트의 개념에 부합하지 않았다.

만약 어떤 데이터가 들어올지 모르거나 변동이 심한 경우에는 기존 코드가 더 적절할 수 있을거같다,

왜냐하면 기존 코드는 특정한 데이터 형태에 종속되어 있지 않기 때문이고. 반면에 컴파운드 컴포넌트 패턴을 사용하는 경우에는 데이터의 형태가 일정한 패턴을 가지고 있을 때 가장 효과적이기때문이다,

따라서, 사용자가 다양한 종류의 데이터를 다룰 수 있도록 하려면 컴파운드 컴포넌트보다는 기존 코드를 사용하는 것이 더 좋을 수 있다고 생각하지만, 내가 만들고싶은것은 재사용성이 뛰어난 컴포넌트를 만들고 싶었다.

2. ⚙️ 구조 및 설계

제일 중점으로 생각한 것 3가지가 있다.

  1. 컴포넌트의 props를 어떻게 설계할 것인가?
  2. 어떤 데이터가 들어와도 동작할 수 있도록 어떻게 처리할 것인가?
  3. 어떤 변화가 생겨도 대응할 수 있는 유연한 코드를 작성할 수 있도록 어떤 방식으로 설계할 것인가?

1. 컴포넌트의 props를 어떻게 설계할 것인가?

props는 각 컴포넌트가 필요로 하는 데이터와 함수들을 명시하고 있으며, 인터페이스를 사용하여 타입을 명확히하고 있다.

2. 어떤 데이터가 들어와도 동작할 수 있도록 어떻게 처리할 것인가?

generic type을 사용하여 어떤 데이터 타입이 들어와도 동작할 수 있도록 처리하고 있다.

3. 어떤 변화가 생겨도 대응할 수 있는 유연한 코드를 작성할 수 있도록 어떤 방식으로 설계할 것인가?

각 컴포넌트는 하나의 역할만 담당하고 있으며, Table 컴포넌트에서는 다른 컴포넌트를 조합하여 만들어졌기 때문에 변경이 생겨도 대응할 수 있는 유연한 코드가 될 것이다.

directory

atom 폴더: 재사용 가능한 단순한 UI 컴포넌트를 담고 있는 폴더, TableBody, TableHeader, TableRow, TableCell 컴포넌트가 있다.

  • Atom은 하나의 단순한 컴포넌트이며, TableBody는 테이블의 몸체 역할을 하는 간단한 컴포넌트이다, 이 컴포넌트는 또 다른 Atom 컴포넌트(TableCell, TableRow)를 사용하여 테이블을 렌더링하는 역할이며, TableHeader 컴포넌트는 TableCell과 TableRow를 import하여 사용하고, 자체적으로 UI를 구성하는 역할을 수행한다.

molecule 폴더: 2개 이상의 atom 컴포넌트를 조합하여 만들어진 비교적 복잡한 UI 컴포넌트를 담고 있는 폴더, Table 컴포넌트가 있다.

  • Molecule은 어플리케이션에서 특정한 기능을 수행하고 그 자체로도 독립적인 기능을 가진 중소규모의 컴포넌트 집합으로 볼 수 있다, Table은 TableHeader와 TableBody를 조합한 Molecule이다.
src/
└── components/
    ├── atom/
    │   ├── TableBody/
    │   │   └── index.tsx
    │   ├── TableHeader/
    │   │   └── index.tsx
    │   ├── TableRow/
    │   │   └── index.tsx
    │   ├── TableCell/
    │   │   └── index.tsx
    │   └── index.tsx
    └── molecule/
        └── Table/
            └── index.tsx

TableCell


export interface ITableCell{
  children?:ReactNode,
  header?:boolean;
}
export const TableCell = ({children, header}:ITableCell) => {
  const TableCell = header ? 'th' : 'td';
  return (
    <TableCell>
      {children}
    </TableCell>
  )
}

TableRow


export interface ITableRow {
  children?:ReactNode;
  className?: string;
}

export const TableRow = ({children}:ITableRow) => {
  return (
      <tr>
        {children}
      </tr>
  )
}

TableHeader

TableHeader 컴포넌트는 columns 배열을 순회하여 TableCell 컴포넌트를 생성하고 TableRow 컴포넌트 내에 렌더링한다, TableCell 컴포넌트는 header props와 함께 생성되어 th 요소로 렌더링되고, 결과적으로 TableHeader 컴포넌트는 columns 배열의 길이만큼 TableCell 컴포넌트를 생성하고, 이를 TableRow 컴포넌트 내에 렌더링하여 Table 헤더를 구성한다.


import {TableRow} from "@/components/atom/TableRow";
import {TableCell} from "@/components/atom/TableCell";

export interface ITableHeader {
  className?: string;
  columns:string[];
}

export const TableHeader = ({className, columns}:ITableHeader) => {
  return (
      <thead className={className}>
        <TableRow>
          {columns.map((column, index) => (
              <TableCell header key={index}>{column}</TableCell>
          ))}
        </TableRow>
      </thead>
  )
}

TableBody

TableBody 컴포넌트에서는 데이터를 가공하는 함수를 인자로 받을 수 있도록 만들고 이를 통해, 데이터 형식이 달라져도 함수를 변경하지 않고 재사용이 가능하게 만든다.
코드에서 renderRow는 각각의 데이터 항목과 해당 항목의 인덱스를 인자로 받아 ReactNode를 반환하는 함수이다. 이 함수를 인자로 받아 TableBody는 데이터를 가공하여 렌더링한다.


import {ReactNode} from "react";

export interface ITableBody<T> {
  className?: string;
  data:T[];
  renderRow:(item:T, index:number) => ReactNode;
}


export const TableBody = <T extends unknown>({className, data, renderRow}:ITableBody<T>) => {
  return (
      <tbody className={className}>
      {data.map(renderRow)}
      </tbody>
  )
}

Table

data는 테이블에 표시될 데이터 배열을 나타내며, columns는 열 제목 배열이다, renderRow는 각각의 데이터 항목을 렌더링하기 위한 콜백 함수이다.
Table 컴포넌트는 TableHeader, TableBody, TableRow, TableCell 컴포넌트를 조합하여 테이블을 렌더링하는 역할이다.


import {TableHeader} from "@/components/atom/TableHeader";
import {TableBody} from "@/components/atom/TableBody";
import {TableRow} from "@/components/atom/TableRow";
import {TableCell} from "@/components/atom/TableCell";
import {ReactNode} from "react";

export interface ITable<T> {
  className?: string;
  data:T[];
  columns:string[];
  renderRow: (item: T, index: number) => ReactNode;
}

export const Table = <T extends unknown>({className, columns, data, renderRow}:ITable<T>) => {
  return(
      <table className={`table${className ? `${className}` : ''}`}>
        <Table.Thead columns={columns}/>
        <Table.Tbody data={data} renderRow={renderRow}/>
      </table>
  )
}

Table.Thead = TableHeader;
Table.Tbody = TableBody;
Table.Row = TableRow;
Table.Cell = TableCell;

ex) organism 에서의 사용 예시

dataTitle이라는 변수에 테이블의 헤더에 들어갈 데이터를 배열 형태로 저장해 놓고, IData라는 인터페이스를 선언하여 데이터가 어떤 형태로 들어올지 정의한다.
renderRow 함수를 선언하여, 테이블의 각 행을 렌더링하는 역할을 하면 재사용성이 뛰어난 컴포넌트 완성


import React from 'react';
import {Table} from "@/components/molecule/Table";
import {TableRow} from "@/components/atom/TableRow";
import {TableCell} from "@/components/atom/TableCell";

const dataTitle = ['No','이메일','이름','권한','정지']

interface IData {
  no: number;
  email: string;
  name: string;
  admin: string;
  isChecked: boolean;
}

const data: IData[] = [
  {
    no: 1,
    email: "test@naver.com",
    name: "김테스트",
    admin: '최고관리자',
    isChecked: false,
  },
  {
    no: 2,
    email: "test2@naver.com",
    name: "박테스트",
    admin: '회원',
    isChecked: true,
  },
  {
    no: 3,
    email: "test3@naver.com",
    name: "이테스트",
    admin: '최고관리자',
    isChecked: false,
  },
];


function TableMember() {
  const renderRow = (item: IData, index: number) => {
    return (
        <TableRow key={index}>
          <TableCell>{item.no}</TableCell>
          <TableCell>{item.email}</TableCell>
          <TableCell>{item.name}</TableCell>
          <TableCell>{item.admin}</TableCell>
          <TableCell>{item.isChecked ? 'Y' : 'N'}</TableCell>
        </TableRow>
    );
  };

  return (
      <>
        <Table data={data} columns={dataTitle} renderRow={renderRow}/>
      </>
  );
}

export default TableMember;

💬 마무리

처음으로 추상화를 해보니 설계 단계에서 충분한 고민이 필요했다, 어떤 기능을 추상화하려면 그 기능의 핵심적인 부분을 파악해야 하고, 그 부분을 추상화할 수 있는 방법을 찾아내는 게 많이 부족한 것 같다.

profile
babypig

0개의 댓글