본문 바로가기

FrontEnd

컴파운드 컴포넌트 잘만들기 1편 : 컴파운드 컴포넌트 알아보기

반응형

Radix UI 팀의 개발자가 작성한 컴파운드 컴포넌트 시리즈가 있어서 번역해왔습니다.

원문 : https://jjenzz.com/compound-components

 

Compound Components

Compound components provide a declarative API that can allow for some impressive solutions to everyday problems. But what are they?

jjenzz.com

 
컴파운드 컴포넌트는 일상적인 문제에 대한 몇 가지 인상적인 솔루션일 수 있는 선언적 API를 제공합니다.
잠시 React에서 벗어나 HTML table 엘리먼트를 살펴보겠습니다.

 

table 요소는 자체적으로 많은 작업을 수행하지 않습니다.

완벽한 테이블을 렌더링하려면 일부 자식 요소(thead, tbody, tr, td 등)을 추가해야 합니다.
<table>
  <caption>
    Cats
  </caption>
  <thead>
    <tr>
      <th>Name</th>
      <th>Breed</th>
      <th>Age</th>
      <th>Kitten count</th>
    </tr>
  </thead>
  <tbody>
    <tr class="table-row">
      <td class="table-cell">Sharky</td>
      <td class="table-cell">Maine Coon</td>
      <td class="table-cell">4</td>
      <td class="table-cell">2</td>
    </tr>
    <tr class="table-row">
      <td class="table-cell">George</td>
      <td class="table-cell">British Shorthair</td>
      <td class="table-cell">6</td>
      <td class="table-cell">1</td>
    </tr>
  </tbody>
</table>
우리는
  • 추가하는 이러한 자식 요소의 수,
  • 열을 그룹화하는 방법,
  • 이벤트를 바인딩하려는 요소

모든것을 제어할 수 있으며

이 모든 것을 선언적으로 제어할 수 있습니다.

행의 스타일을 지정해야 하나요?

클래스 또는 스타일 속성을 직접 추가하기만 하면 됩니다.

브라우저는 개발자가 제공한 정보를 사용하여
개발자자가 요청한 방식으로 테이블을 구성하고
사양에 따라 테이블이 동작하도록 합니다
이것이 컴파운드 컴포넌트 입니다.

God-like 대안

객체 지향 프로그래밍에서 God 객체(전지적, 즉 모든 것을(너무 많은 정보를) 알고 있는 객체라)는
안티 패턴, 코드 냄새의 예시입니다.
React로 돌아가 God-like 보다,
컴파운드 컴포넌트 패턴이 종종 더 유익한지 봅시다.
아래 Table을 비교 기준으로 사용하겠습니다.
<Table
  caption="Cats"
  columns={columns}
  rowData={cats}
  rowClassName="table-row"
  cellClassName="table-cell"
  onRowClick={handleRowClick}
/>

API 소비자를 위한 설정

God-like 접근법은 테이블의 구현에서 보통 아래와 같은 것들을 요구합니다
 
  • 전달된 rowData를 테이블이 기본적으로 기대하는 포맷으로 변환하거나 (변환)
  • 사전 정의된 포맷을 사용하여  데이터 세트에서 값을 찾을 수 있는 위치를 알려주는 것. (찾기)
const Cats = () => {
  // A query that gets cats data structure from server
  const cats = useGetCats();
  const columns = [
    {
      label: 'Name',
      valueGetter: ({ data }) => data.name,
    },
    {
      label: 'Breed',
      valueGetter: ({ data }) => data.breed,
    },
    {
      label: 'Age',
      valueGetter: ({ data }) => data.age,
    },
    {
      label: 'Kitten count',
      valueGetter: ({ data }) => data.kittens.length,
    },
  ];

  // ...

  return (
    <Table
      caption="Cats"
      columns={columns}
      rowData={cats}
      rowClassName="table-row"
      cellClassName="table-cell"
      onRowClick={handleRowClick}
    />
  );
};​
이것은 여러 번 사용할 때 상당한 양의 추가 코드가 될 수 있으며
일반적으로 개발자가 컴포넌트가 기대하는 설정(option, 또는 config) 또는 데이터 구조를 찾기 위해
매뉴얼를 읽는 데 상당한 시간을 소비한다는 의미입니다.
컴파운드 컴포넌트를 사용하면 아래와 같이 구현 가능합니다.
const Cats = () => {
  const cats = useGetCats();

  // ...

  return (
    <Table>
      <TableCaption>Cats</TableCaption>
      <TableHead>
        <TableRow>
          <TableCell>Name</TableCell>
          <TableCell>Breed</TableCell>
          <TableCell>Age</TableCell>
          <TableCell>Kitten count</TableCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {cats.map(cat => (
          <TableRow key={cat.id} className="table-row" onClick={handleRowClick}>
            <TableCell className="table-cell">{cat.name}</TableCell>
            <TableCell className="table-cell">{cat.breed}</TableCell>
            <TableCell className="table-cell">{cat.age}</TableCell>
            <TableCell className="table-cell">{cat.kittens.length}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
};​
소비자는 스스로 필요한 위치에 무언가를 넣을 수 있는 유연성을 얻었기 때문에,
컴포넌트는 무엇을 찾고 무엇을 넣어야 하는지 알 필요가 없습니다.
컴포넌트 개발자의 코딩 양, 기능 구현 책임이 줄어듭니다.
객체 또는 데이터 변환이 전혀 필요하지 않으며,
컴포넌트는 데이터 구조에 대해 몰라도 되고,
API 컨슈머가 데이터에서 어떻게 필드를 찾는지 알려줄 필요가 없습니다.

유지보수 부담

God-like 대안을 사용하면 Row, Col 또는 Cell 접두사가 붙은 props의 긴 목록을 구현하게 됩니다.
(celValue, onCellClick 등...)
그들은 관련 엘리먼트에 내부적으로 전달되어야 하는 접두사가 붙은 prop이 필요합니다

이것은 제한적일 수 있습니다. 소비자가 새로운 핸들러를 원하면 어떻게 될까요?

예를 들어 "Kitten Count" 셀에 대한 onCellClick 핸들러가 필요한가요?

소비자가 클릭한 셀을 식별할 수 있도록 하는 새 이벤트 핸들러 prop이 포함된 새 버전을 출시해야 합니다.

그런 다음 소비자는 필요한 작업을 수행하기 전에 원하는 셀인지 확인해야 합니다.

컴파운드 컴포넌트 패턴 접근 방식을 사용하면 사용자가 원하는 셀에 직접 바인딩할 수 있기 때문에
어떤 셀인지 확인할 필요가 없습니다.
<TableCell onClick={handleKittensCountClick}>{cat.kittens.length}</TableCell>

멘탈 모델 파악의 어려움

저는 보통 컴포넌트에 의해 렌더링되는 내용과
잠재적으로 화면에 어떻게 보일지 알아야 할 때
함수 컴포넌트의 리턴 또는 클래스 컴포넌트의 렌더링 메서드로 바로 이동합니다.
 
그런 다음 이 그림을 그리기 위해 추상화를 따라야 한다면
기억에 유지해야 하는 추가 처리가 필요하며,
우리 모두는 인간이 그 일에 능숙하지 않다는 것을 알고 있습니다.
개발자의 속도를 늦추고 종종 휴먼 에러로 이어질 수 있습니다.
God-like 접근 방식은 추상화의 단일체입니다.
반환값을 보고 얼마나 많은 열이 렌더링되는지
또는 어떤 데이터가 어떤 열에 렌더링되는지 그림을 그릴 수 없습니다.
반면에 컴파운드 컴포넌트는 모든 것이 명확하게 배치되어 있습니다.

컴파운드 컴포넌트 만들기

하나의 컴포넌트를 노출하는 대신 부모 컴포넌트와 자식 컴포넌트를 같이 노출합니다.
까다로운 부분은 컴포넌트가 API 수준에서 서로 통신하거나 수정해야 할 때입니다.
React.Children 메서드를 React.cloneElement와 결합하여 children prop을 수정할 수 있지만,

이를 위해서는 자식 컴포넌트가 직계 자손이어야 하고,

소비자에게 해당 prop을 노출해야 합니다.

대안은 Context API를 사용하는 것입니다.
이를 통해 부모 컴포넌트는 더 깊은 위치의 children과 통신할 수 있으며,
자식 컴포넌트로 전달되는 prop을 가로챌 필요가 없습니다.
 
isSmall prop이 있고
모든 자식에게도 해당 정보가 필요한 컴포넌트를 상상해 보세요
<List isSmall>
  <ListItem isSmall>Cat</ListItem>
  <ListItem isSmall>Dog</ListItem>
</List>​

 

 
현재는 isSmall이 여러 곳에서 반복되며, 일부 아이템만 작게 렌더링 할 수 있습니다.
모든 컴포넌트의 크기 일관성을 유지하려면 어떻게 해야 할까요?
Context API를 사용해 부모 prop을 자식과 공유하면 DRY함과 일관성을 유지할 수 있습니다.
import React, { useContext } from 'react';

const Context = React.createContext();

const List = ({ isSmall = false, children, ...props }) => (
  <ul {...props} style={{ padding: isSmall ? '5px' : '10px' }}>
    <Context.Provider value={isSmall}>{children}</Context.Provider>
  </ul>
);

const ListItem = ({ children, ...props }) => {
  const isSmall = useContext(Context);

  return (
    <li {...props} style={{ padding: isSmall ? '5px' : '10px' }}>
      {children}
    </li>
  );
};

export { List, ListItem };
ListItem에 대한 isSmall prop이 더 이상 필요 없으며,

부모의 prop을 참조함으로서 모든 컴포넌트의 일관성을 유지할 수 있습니다!

<List isSmall>
  <ListItem>Cat</ListItem>
  <ListItem>Dog</ListItem>
</List>

컴파운드 컴포넌트 설계하기

원문에 안나오는 내용이지만 W3C 디자인 패턴을 보는게 좋습니다.

https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html

 

Example of Tabs with Manual Activation

Accessibility resources free online from the international standards organization: W3C Web Accessibility Initiative (WAI).

www.w3.org

reach ui가 이건 참 잘 만들어 뒀습니다.

https://reach.tech/tabs/

 

Tabs

Accessible tabs component for React

reach.tech


 

반응형