Radix UI 팀의 개발자가 작성한 컴파운드 컴포넌트 시리즈가 있어서 번역해왔습니다.
원문 : https://jjenzz.com/compound-components
table 요소는 자체적으로 많은 작업을 수행하지 않습니다.
<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 객체(전지적, 즉 모든 것을(너무 많은 정보를) 알고 있는 객체라)는
안티 패턴, 코드 냄새의 예시입니다.
<Table
caption="Cats"
columns={columns}
rowData={cats}
rowClassName="table-row"
cellClassName="table-cell"
onRowClick={handleRowClick}
/>
API 소비자를 위한 설정
- 전달된 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}
/>
);
};
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>
);
};
유지보수 부담
이것은 제한적일 수 있습니다. 소비자가 새로운 핸들러를 원하면 어떻게 될까요?
예를 들어 "Kitten Count" 셀에 대한 onCellClick 핸들러가 필요한가요?
소비자가 클릭한 셀을 식별할 수 있도록 하는 새 이벤트 핸들러 prop이 포함된 새 버전을 출시해야 합니다.
그런 다음 소비자는 필요한 작업을 수행하기 전에 원하는 셀인지 확인해야 합니다.
<TableCell onClick={handleKittensCountClick}>{cat.kittens.length}</TableCell>
멘탈 모델 파악의 어려움
컴파운드 컴포넌트 만들기
하나의 컴포넌트를 노출하는 대신 부모 컴포넌트와 자식 컴포넌트를 같이 노출합니다.
까다로운 부분은 컴포넌트가 API 수준에서 서로 통신하거나 수정해야 할 때입니다.
React.Children 메서드를 React.cloneElement와 결합하여 children prop을 수정할 수 있지만,
이를 위해서는 자식 컴포넌트가 직계 자손이어야 하고,
소비자에게 해당 prop을 노출해야 합니다.
<List isSmall>
<ListItem isSmall>Cat</ListItem>
<ListItem isSmall>Dog</ListItem>
</List>
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 };
부모의 prop을 참조함으로서 모든 컴포넌트의 일관성을 유지할 수 있습니다!
<List isSmall>
<ListItem>Cat</ListItem>
<ListItem>Dog</ListItem>
</List>
컴파운드 컴포넌트 설계하기
원문에 안나오는 내용이지만 W3C 디자인 패턴을 보는게 좋습니다.
https://www.w3.org/WAI/ARIA/apg/example-index/tabs/tabs-manual.html
reach ui가 이건 참 잘 만들어 뒀습니다.
'FrontEnd' 카테고리의 다른 글
컴파운드 컴포넌트 잘만들기 3탄 : compound component + uncontrolled component + co-location 삼신기 사용하기 (0) | 2022.10.18 |
---|---|
컴파운드 컴포넌트 잘만들기 2편 : Smarter, Dumb Breadcrumb (0) | 2022.10.17 |
보다 탄력적인 웹을 위한 점진적 향상[Progressively enhance for a more resilient web] (0) | 2022.10.14 |
The Web’s Next Transition(웹 애플리케이션 아키텍처의 미래) (0) | 2022.10.14 |
빠른 디자이너-개발자 의사소통을 위한 색상(color) 시스템 프레임워크 (0) | 2022.10.13 |