컴포넌드 컴파운트 패턴에 대해 관심을 가지게 된 계기는 작년 12월에 들었던
원티드 프리온보딩 프론트엔드 12월 챌린지에서 비즈니스 로직과 기능이 섞인다면 "소프트웨어"가 "하드"해지는 나중에는 겉잡을 수 없이 복잡해지는 혹은 재사용이 불가능한 컴포넌트가 된다는 설명에서 디자인 패턴 중 하나인 컴포넌드 컴파운드 패턴에 대해 간략한 설명이 있어 해당 패턴에 관심이 생겨 더 알아보게 되었다.
컴파운드 컴포넌트의 예시
<Card>
<Card.CardContent>
{content.map((item, i) => {
return <CardItem key={i} item={item} />;
})}
</Card.CardContent>
<Card.Expand>
<div>show more</div>
</Card.Expand>
<Card.Collapse>
<div>show less</div>
</Card.Collapse>
</Card>
isCollapsed
상태에 따라서 전역변수로 설정한 LIMIT(=3)
만큼 보여주고, show more
버튼을 클릭하면 모든 컨텐츠가 렌더링되도록 하고 있다.🪄 App.jsx
import Card from "./components/Card/Card";
import CONTENTS from "./constants/CONTENTS.JS";
function App() {
return (
<div>
<Card
contents={CONTENTS}
expandLabel="더보기"
collapseLabel="접기"
/>
</div>
);
}
export default App;
🪄 Card.jsx
import {useState} from "react";
const LIMIT = 3;
const Card = ({expandLabel, collapseLabel, contents}) => {
const [isCollapsed, setIsCollapsed] = useState(true);
const handleToggle = () => {
setIsCollapsed(!isCollapsed);
};
return (
<div>
<ul>
{contents.map((content, i) => {
if (isCollapsed) {
while (LIMIT > i) {
return <p key={i}>{content.displayName}</p>;
}
} else {
return <p key={i}>{content.displayName}</p>;
}
})}
</ul>
<button onClick={handleToggle}>
{isCollapsed ? expandLabel : collapseLabel}
</button>
</div>
);
};
export default Card;
😖 예를 들어서..
- 각
CardItem
에 대한 좋아요 로직 추가하기- 특정 조건에 따른 렌더링 로직 추가하기
isCollapsed
상테에 따른 다양한 스타일 추가하기
이러한 추가적인 비즈니스로직이 생길때마다 Card
컴포넌트 하나가 그러한 요구사항들을 직접관리하려고하면 Card
컴포넌트는 점점 더 복잡해지고 또한 전달해야할 props
도 많아질것이고, 가독성 또한 나빠질것이다.
확장성 부족
Card
컴포넌트는 특정한 형태의 컨텐츠만을 다루는 구조로 이루어져있다.Card
컴포넌트는 수정해야한다.Card
컴포넌트는 더욱 복잡해질 것이다.재사용성 제한
Card
컴포넌트에서 만약 부분적인 기능이나 스타일을 재사용할때, 현재의 구조에선 이를 구현하기가 어렵다.Card
컴포넌트에서 더보기 버튼
이나 접기 버튼
만을 재사용하고 싶을때, Card
컴포넌트 전체를 재사용해야한다.컴파운드 컴포넌트 패턴 구조에선 하위 컴포넌트가 될 컴포넌트들이 상위 컴포넌트의 상태를 공유하거나, 상태 변경함수를 사용해야할 경우가 많다
useContext
를 사용한다면, 컴포넌트 트리 내부에서 전역적으로 데이터를 공유할 수 있게된다.먼저 cardContext
라는 변수에 createContext
반환값으로 할당한다.
CardContext.Provider
를 통해 value
를 하위 컴포넌트에 상태 및 함수들을 전역적으로 공급해줄 수 있게되었다.CardContent
는 UI
를 렌더링하는 "로직"만, Expand
와 Collapse
는 상태를 토글하는 "로직"으로 분리되었다.Card.CardContent = CardContent
이 부분들이 있는데,Card
의 프로퍼티로 CardContent,Expand,Collapse
로 추가한것 뿐이다Card
컴포넌트가 만들어졌다.
// 🪄 리팩토링한 Card.jsx
const CardContext = createContext();
const LIMIT = 3;
const Card = ({children}) => {
const [isCollapsed, setIsCollapsed] = useState(true);
const expand = () => {
setIsCollapsed(!isCollapsed);
};
const collapse = () => {
setIsCollapsed(!isCollapsed);
};
const value = {isCollapsed, expand, collapse};
return <CardContext.Provider value={value}>{children}</CardContext.Provider>;
};
const CardContent = ({children}) => {
const {isCollapsed} = useContext(CardContext);
return children.map((child, index) => {
if (isCollapsed) {
while (LIMIT > index) {
return <div key={index}>{child}</div>;
}
} else {
return <div key={index}>{child}</div>;
}
});
};
const Expand = ({children}) => {
const {expand, isCollapsed} = useContext(CardContext);
return isCollapsed && cloneElement(children, {onClick: expand});
};
const Collapse = ({children}) => {
const {collapse, isCollapsed} = useContext(CardContext);
return !isCollapsed && cloneElement(children, {onClick: collapse});
};
Card.CardContent = CardContent;
Card.Expand = Expand;
Card.Collapse = Collapse;
export default Card;
Card.CardContent
, Card.Expand
, Card.Collapse
안에 어떤 요소든 자식으로 넣을 수 있으며, 해당 요소는 항상 컴포넌트에 주어진 기능을 가지고 있다.import Card from "./components/Card/Card";
import CONTENTS from "./constants/CONTENTS.JS";
function App() {
return (
<div>
<Card>
<Card.CardContent>
{CONTENTS.map((item, index) => (
<p key={index}>{item.displayName}</p>
))}
</Card.CardContent>
<Card.Collapse>
<div>더보기</div>
</Card.Collapse>
<Card.Expand>
<div>접기</div>
</Card.Expand>
</Card>
</div>
);
}
export default App;