| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- provider 패턴
- frontend
- react
- Web
- 에세이
- 리팩토링
- 접근성
- 자바스크립트
- JavaScript
- TypeScript
- MFA
- 리액트
- 웹워커
- vite
- Webworker
- 프론트엔드
- CustomHook
- Function Region
- CRA
- 이것저것
- MicroFrontEnd
- Aria
- AI
- context.api
- 클린코드
- sharedworker
- 회고
- 아키텍처
- 오블완
- 티스토리챌린지
- Today
- Total
Lighthouse of FE beginner
[React] 합성(Composition) 컴포넌트 본문
Overview
이번 포스팅에서는 합성 컴포넌트에 대해서 살펴보도록 하겠습니다.
합성 컴포넌트는 컴포넌트를 조합하여 UI를 만드는 것으로 컴포넌트의 재사용성을 극대화 시키는 패턴입니다.
합성 컴포넌트 패턴을 활용하면 여러가지 이점을 볼 수 있습니다
- 합성된 컴포넌트들은 독립적으로 유지되지만 서로 상태를 공유하며 긴밀하게 연결된다.
- 비지니스 로직이 섞이지 않으며 비지니스 로직이 컴포넌트 단위로 잘게 쪼개진다.
- 컴포넌트를 잘 쪼개고 상태 관리를 최적화 할 수 있어서 랜더링 최적화가 된다.
예제를 살펴보며 합성 컴포넌트를 자세히 살펴봅시다.
예제
일반 컴포넌트
리스트를 구현해야 하는 상황이 발생 했습니다. 초기에는 단순히 라벨만 노출시키면 되는 단순한 리스트입니다.
type Props = {
items: Item[];
};
const List: React.FC<Props> = ({ items }) => {
return (
<ul>
{items.map(({ id, label }) => {
return (
<li key={id}>
{label}
</li>
);
})}
</ul>
);
};
정말 간단하게 리스트를 구현했습니다.
얼마 지나지 않아 클릭한 라벨을 빨간색으로 표현해달라는 요청이 들어왔습니다. 게다가 각 라벨에 제목도 붙이고 싶다고 합니다.
리스트 컴포넌트에 selectItem 이라는 state를 만들었습니다. 그리고 li 태그 안에 title을 가진 h3 태그도 만들어줍니다.
type Props = {
items: Item[];
};
const List: React.FC<Props> = ({ items }) => {
const [selectItem, setSelectItem] = useState("");
return (
<ul>
{items.map(({ id, label }) => {
const isSelected = selectItem === id;
const color = isSelected ? "red" : "black";
return (
<li key={id} onClick={() => setSelectItem(id)} style={{ color }}>
{title && <h3 style={{ margin: 0, color }}>{title}</h3>}
{label}
</li>
);
})}
</ul>
);
};
또 구분선이 필요하다는 요구사항이 들어왔습니다. List 컴포넌트에 hr 태그를 추가합니다.
완성된 컴포넌트는 아래와 같습니다.
import React, { useState } from "react";
export type Item = {
id: string;
label: string;
title?: string;
split?: boolean;
};
type Props = {
items: Item[];
};
const List: React.FC<Props> = ({ items }) => {
const [selectItem, setSelectItem] = useState("");
return (
<ul>
{items.map(({ id, title, label, split }) => {
const isSelected = selectItem === id;
const color = isSelected ? "red" : "black";
return (
<React.Fragment key={id}>
<li onClick={() => setSelectItem(id)} style={{ color }}>
{title && <h3 style={{ margin: 0, color }}>{title}</h3>}
{label}
</li>
{split && <hr />}
</React.Fragment>
);
})}
</ul>
);
};
export default List;
위 컴포넌트를 사용하는 예시입니다. 추상화 된 객체를 정의하고 List 컴포넌트에 넘겨주고 있습니다.
import List, { type Item as ItemType } from "./list";
export default function App() {
const items: ItemType[] = [
{ id: "id-1", label: "first", title: "title" },
{ id: "id-2", label: "second", split: true },
{ id: "id-3", label: "third" },
];
return (
<div className="App">
<List items={items} />
</div>
);
}
List 컴포넌트에 기능을 구현하기 위해 여러가지 비즈니스 로직이 섞였습니다. 물론 li 컴포넌트를 분리하여 사용자 정의 컴포넌트로 구현하여 List 컴포넌트를 단순하게 할 수 있습니다.
지금의 예제는 단순한 컴포넌트 이지만 여러가지 기능이 섞여있는 복잡한 컴포넌트라면 기능이 추가 될수록 컴포넌트의 로직이 점점 섞이게 되고 유지보수에 어려움을 느끼게 됩니다.
즉 특정 기능을 붙이기 위해서 List 컴포넌트를 계속해서 수정해야 하고 비즈니스 로직이 섞이고 List 컴포넌트는 관리 포인트가 늘어나며 유지보수가 어렵게 됩니다. 기능이 붙으면 붙을수록 재사용성도 떨어지게 됩니다.
그럼 위 컴포넌트를 복합 컴포넌트로 구현해보도록 하겠습니다.
복합 컴포넌트
복합 컴포넌트를 구현하기 위해서 List 컴포넌트에서 li 컴포넌트를 분리할 예정입니다.
그리고 분리된 각 컴포넌트에서 상태를 공유하기 위해 Context API를 사용합니다.
import React, { createContext, PropsWithChildren, useState } from "react";
type ListContextType = {
selectItem: string;
setSelectItem: (id: string) => void;
};
export const ListContext = createContext<ListContextType>({
selectItem: "",
setSelectItem: () => {},
});
const CompositionList: React.FC<PropsWithChildren> = ({ children }) => {
const [selectItem, setSelectItem] = useState("");
return (
<ListContext.Provider
value={{ selectItem, setSelectItem: (id) => setSelectItem(id) }}
>
<ul>{children}</ul>
</ListContext.Provider>
);
};
export default CompositionList;
selectItem 상태는 List 컴포넌트에서 소유하고 있어야 합니다.
그리고 해당 상태는 children에 props로 넘겨주기 보다는 컴포넌트 간에 느슨하게 결합할 수 있도록 Context API를 통해 상태를 공유하도록 합니다.
이제 li 컴포넌트를 살펴보도록 하겠습니다.
import React, { PropsWithChildren, useContext, useMemo } from "react";
import { ListContext } from "./composition-list";
import "./item.css";
type Props = PropsWithChildren<{
id: string;
}>;
const Item: React.FC<Props> = ({ id, children }) => {
const { selectItem, setSelectItem } = useContext(ListContext);
const isSelected = selectItem === id;
const className = useMemo(
() => ["list", ...(isSelected ? ["selected"] : [])].join(" "),
[isSelected]
);
return (
<li onClick={() => setSelectItem(id)} className={className}>
{children}
</li>
);
};
export default React.memo(Item);
Item 컴포넌트에서 Context API를 통해 상태를 공유하고 있습니다.
이 두 개의 컴포넌트를 합성해 UI를 만들어봅시다.
import CompositionList from "./composition-list";
import Item from "./item";
export default function App() {
return (
<div className="App">
<CompositionList>
<Item id="id-1">
<h3>title</h3>
first
</Item>
<Item id="id-2">second</Item>
<hr />
<Item id="id-3">third</Item>
</CompositionList>
</div>
);
}
이전에는 list를 추상화 해서 넘겨줬던 반면에, 합성 컴포넌트를 사용해 더욱 직관적이고 선언적으로 UI를 구현하게 됐습니다.
이전의 title 기능과 split 기능은 실제 UI를 구성하는 App 컴포넌트에서 관리하고 List와 Item은 모두 각각의 역할에 충실하고 있습니다. 합성 컴포넌트 패턴으로 느슨하게 UI를 구현 함으로써 다른 기능을 추가할 때 더욱 직관적이고 간편하게 구현할 수 있게 됩니다.
예를 들어서 체크 기능을 통해 특정 Item을 숨김 처리를 하는 기능이 추가된다고 합시다.
합성 컴포넌트 패턴을 이용한다면 기능 추가를 위해 List, Item 컴포넌트를 직접 수정하지 않고 유연하게 기능을 추가할 수 있습니다.
export default function App() {
const [checked, setChecked] = useState(false);
return (
<div className="App">
<Checkbox
checked={checked}
setChecked={(checked) => setChecked(checked)}
/>
<CompositionList>
<Item id="id-1">
<h3>title</h3>
first
</Item>
{checked && (
<>
<Item id="id-2">second</Item>
<hr />
</>
)}
<Item id="id-3">third</Item>
</CompositionList>
</div>
);
}
만약 List 컴포넌트를 직접 수정하게 된다면 사이드 이펙트 유무를 체크하기 위해 해당 컴포넌트를 사용하는 모든 곳을 테스트 해봐야 합니다. 합성 컴포넌트를 사용한다면 유연하게 대응할 수 있기 때문에 사이드 이펙트의 위험에서 벗어날 수 있습니다.
마치며
예제를 통해 합성 컴포넌트에 대해 살펴 봤습니다.
컴포넌트를 잘게 분할하고 비즈니스 로직을 적합한 컴포넌트에 위치 시킴으로써 변화에 유연하게 대응하고 랜더링 최적화까지 챙길 수 있게 됐습니다.
합성 컴포넌트 패턴을 이용한다면 리액트의 재사용성과 아토믹 디자인 패턴에 어울리는 컴포넌트를 구현할 수 있습니다.
앞으로 진행하는 프로젝트에서 합성 컴포넌트 패턴을 적극적으로 활용해보는 것은 어떨까요?
읽어봐주셔서 감사합니다.
'[WEB] 프론트엔드' 카테고리의 다른 글
| [React] react-query prefetch (1) | 2025.04.27 |
|---|---|
| [MFA] Micro App의 관심사 공유 (1) | 2025.02.13 |
| [트러블슈팅] WebWorker 빌드 시 worker 파일이 포함이 안되는 문제 (2) | 2024.10.19 |
| SharedWorker에 WebSocket 얹어보기 (7) | 2024.10.17 |
| Web Worker 알아보기 (4) | 2024.10.13 |