
잘못된 추상화 : 시간이 지날수록 잘못된 방향으로 진화...
/* 컴포넌트 탄생! 깔끔하다 ✨ */
<Dialog
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
button={{
label: '확인',
onClick: doSomething,
}}
/>
/**************************/
/********* 1주일 뒤 *********/
/**************************/
/**
* "다이얼로그 버튼이 하단에 있던데, 상단에 있는 경우도 필요합니다!"
*
* -> props 추가 (buttonPosition)
*/
<Dialog
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
button={{
label: '확인',
onClick: doSomething
}}
buttonPosition="top"
/>
/**************************/
/********* 2주일 뒤 *********/
/**************************/
/**
* "두 개의 버튼이 있는 다이얼로그가 필요해요! 둘 중 하나는 Primary, 하나는 Secondary 타입으로요"
*
* -> props 변경 (button -> buttons, variant 추가)
*/
<Dialog
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
buttonPosition="top"
buttons={[
{
label: '확인',
onClick: doSomething,
variant: 'primary',
}, {
label: '취소',
onClick: doSomethingElse,
variant: 'secondary',
},
]}
/>
/**************************/
/********* 1개월 뒤 *********/
/**************************/
/**
* "버튼이 세로로 나열되어 있는 다이얼로그도 추가해주세요!"
* "title 위에 아이콘도 하나 넣어주세요!"
*
* -> props 추가 (buttonAlign, iconAboveTitle)
*/
<Dialog
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
buttonPosition="top"
buttons={[
{
label: '확인',
onClick: doSomething,
variant: 'primary',
}, {
label: '취소',
onClick: doSomethingElse,
variant: 'secondary',
},
]}
buttonAlign="vertical"
iconAboveTitle="fancy-icon"
/>
위의 예시에서 Dialog 컴포넌트는 재사용성은 갖추었지만 유연성은 갖추지 못하였다.
해당 컴포넌트가 유연하지 않은 이유는 무엇일까
비즈니스 로직이 컴포넌트 안에 들어있다
버튼 개수나 위치, 버튼의 배열, 일러스트의 위치, 이 모든 것들은 비즈니스 로직입니다.
이러한 규칙은 시간이 지나며 변경될 여지가 많습니다. 우리는 비즈니스 로직을 밖으로 꺼내여야 합니다.
Composition vs Inheritance
리액트는 조합에 특화된 설계를 갖고 있습니다.
공식 홈페이지에도 상속보다는 조합이라는 내용의 글이 있습니다.
위의 Dialog컴포넌트는 조합보다는 상속에 더 가까운 것 같네요...
그러면 저 자이언트 컴포넌트를 쪼개오 조합을 사용해 바꾸어 보겠습니다.
Dialog.Content = ({ title, description }) => (
<React.Fragment>
<Dialog.Title>
{title}
</Dialog.Title>
<Dialog.Description>
{description}
</Dialog.Description>
</React.Fragment>
)
function Page() {
return (
<Dialog>
<Dialog.Content
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
/>
<Dialog.ButtonContainer align="vertical">
<Dialog.Button type="secondary" onClick={doSomethingElse}>
취소
</Dialog.Button>
<Dialog.Button type="primary" onClick={doSomething}>
취소
</Dialog.Button>
</Dialog.ButtonContainer>
<Dialog>
)
}
비교해볼까요
// 자이언트 컴포넌트
<Dialog
iconAboveTitle="fancy-icon"
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
buttonPosition="bottom"
buttonAlign="vertical"
buttons={[{
label: '확인',
onClick: doSomething,
type: 'cta',
}, {
label: '취소',
onClick: doSomethingElse,
type: 'secondary',
},]}
/>
// 조합기반 컴포넌트
<Dialog>
<Dialog.Icon type="fancy" />
<Dialog.Content
title="안내"
description="이것은 멋진 내용을 담고 있는 안내입니다."
/>
<Dialog.ButtonContainer align="vertical">
<Dialog.Button type="secondary" onClick={doSomethingElse}>
취소
</Dialog.Button>
<Dialog.Button type="primary" onClick={doSomething}>
확인
</Dialog.Button>
</Dialog.ButtonContainer>
</Dialog>
조합을 이용한 Dialog는 onClick,align,title등 명확한 props만 남게 되기 때문에
1. 개발자가 props가 어떤 역할을 하는지 파악하기 수월하고
2. props가 명확하기 때문에 별도의 문서화를 할 필요가 없으며
3. 모호한 props가 없기 때문에 작명 고민을 할 필요도 없습니다.
4. 컴포넌트 명세를 변경해야할 때 어디를 고쳐야할지도 명확합니다.
제어역전 (Inversion of Control)
Dialog컴포넌트의 역할이 조합을 사용하기 전과 후에 어떻게 달라졌을까요?
Before : 자이언트 컴포넌트 일 때 담당하던 역할
1. 전달받은 props 값에 따라 내부 UI컴포넌트 배치
2. Title, Description, Button의 style결정
After : 조합 버전에서 담당하는 역할
1. Title, Description, Button의 style결정
어떻게 배치할까에 대한 역할을 dialog가 더이상 담당하고 있지 않습니다. 해당 역할은 dialog를 사용하는 개발자에게 넘어갔습니다. 조합 기반의 컴포넌트를 사용하면 페이지를 개발할 때 해야하는 일이 하나 더 늘어나게 되지만 그로 인해 유연성을 갖는다는 장점을 얻게 됩니다.
프로그래밍에서 API를 사용하는 쪽으로 특정 역할을 넘기는 패턴을 제어역전 IoC라고 부릅니다.
compound coponents를 hooks나 다른 패턴과 함께 사용하면 state를 숨김으로써 더 깔끔한 추상화를 제공할 수 있다는 장점이 있습니다.
아래는 자이언트 컴포넌트 입니다
function Page() {
const [tab, setTab] = React.useState(initialTab)
return (
<Tabs
items={tabItems}
onSelectTab={setTab}
selectedTab={tab}
/>
)
}
쪼개볼까요
function Page() {
const [selectedTab, setTab] = React.useState(initialTab)
return (
<Tabs>
{tabItems.map(tabItem => (
<Tabs.Item
value={tabItem}
isSelected={selectedTab === tabItem}
onSelect={setTab}
>
{tabItem}
</Tabs.Item>
))}
</Tabs>
)
}
매우 유연해졌습니다. alignment를 바꾸거나 탭 컴포넌트 안에 다른 컴포넌트를 삽입하는 요청도 무리 없이 수행할 수 있을 것 같습니다.
Compound Components는 state를 숨길 수 있다는 장점도 갖는다고 했는데요
state를 숨기면 다음과 같은 모습일 것 같습니다.
function Page() {
return (
<Tabs>
{tabItems.map(tabItem => (
<Tabs.Item value={tabItem}>
{tabItem}
</Tabs.Item>
))}
</Tabs>
)
}
링크
단단한 컴포넌트 부수기