TLDR : Compound Component 패턴은 컴포넌트에 (암시적으로 상태를 공유하는) 선언적 서브 컴포넌트 API를 제공하는 방법이다.
컴파운드 컴포넌트 패턴이란?
컴파운드 컴포넌트 패턴은 하나의 완벽한 컴포넌트를 구성하는 암시적 상태 공유 컴포넌트 API 집합을 제공하는 방법이다.
우리는 html의 select구성요소에서 이미 유사한 API를 본 적이 있다.
<select>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
아래 jsx와 차이가 뭘까?
<CustomSelect
options={[
{value: '1', display: 'Option 1'},
{value: '2', display: 'Option 2'},
]}
/>
가장 큰 문제는 스타일을 커스터마이징 하기 어렵다는 것일 것이다.
또한 내부에 다른 종류의 엘리먼트를 지원해야 하는 api들을 제공할 경우, 일은 더 복잡해진다.
위 케이스의 경우 각각의 option은 굉장히 유연한 api다.
반대로 아래 케이스의 경우 내부 컴포넌트의 reference를 넘기기부터 어렵다.
이는 select가 아니라 레이아웃 같은 요소면 더욱 두드러진다.
추가로 select된 옵션의 상태를 어디엔가 갖고 있어야 한다면,
해당 상태를 굳이 사용자가 외부에서 관리할 필요가 있을까?
컴포넌트 내에서 알아서 관리해주면 좋을 것이다.
(보통 초기화 변수와 onChange 콜백을 통해서만 접근 가능하도록한다.)
Compound Component 패턴을 몸소 체험하고 싶다면 아래 라이브러리를 참조하자.
2. 구현
꺼졌을 때와 켜졌을 때 메세지를 커스터마이징 가능하며, 내부 컴포넌트들의 style을 커스커마이징 가능한 API를 개발해 보자.
1. 먼저 내부 상태를 암시적으로 공유할 컨텍스트와, 컨텍스트 프로바이더 컴포넌트틀 만든다.
const ToggleContext = React.createContext()
ToggleContext.displayName = 'ToggleContext'
function Toggle({children}) {
const [on, setOn] = React.useState(false)
const toggle = () => setOn(!on)
return (
<ToggleContext.Provider value={{on, toggle}}>
{children}
</ToggleContext.Provider>
)
}
2. useContext훅을 만든다. 해당 컨텍스트 내에서만 사용할 수 있도록 한다.
function useToggle() {
const context = React.useContext(ToggleContext)
if (context === undefined) {
throw new Error('useToggle must be used within a <Toggle />')
}
return context
}
3. 제공할 서브 컴포넌트 들의 API 들을 만든다.
function ToggleOn({children}) {
const {on} = useToggle()
return on ? children : null
}
function ToggleOff({children}) {
const {on} = useToggle()
return on ? null : children
}
function ToggleButton({...props}) {
const {on, toggle} = useToggle()
return <Switch on={on} onClick={toggle} {...props} />
}
4. 알맞게 사용한다.
사용자는 컨텍스트 내부의 상태에 대해 전혀 몰라도 된다.
api 명세만 알면 된다.
function App() {
return (
<div>
<Toggle>
<ToggleOn>The button is on</ToggleOn>
<ToggleOff>The button is off</ToggleOff>
<div>
<ToggleButton />
</div>
</Toggle>
</div>
)
}
참고 : Antd는 Toggle 함수의 property(함수도 객체임)로 서브 컴포넌트를 노출한다.
Antd는 오픈소스이므로 관심이 있는 사람들은 직접 들어가서 소스를 읽어보자.
Toggle.ToggleOn=function ToggleOn({children}) {
const {on} = useToggle()
return on ? children : null
}
Toggle.ToggleOff=function ToggleOff({children}) {
const {on} = useToggle()
return on ? null : children
}
Toggle.ToggleButton=function ToggleButton({...props}) {
const {on, toggle} = useToggle()
return <Switch on={on} onClick={toggle} {...props} />
}
export default Toggle;
// other files...
import ToggleV2 from 'Toggle'
// ...
<ToggleV2>
<ToggleV2.ToggleOn>The button is on</ToggleV2.ToggleOn>
<ToggleV2.ToggleOff>The button is off</ToggleV2.ToggleOff>
<div>
<ToggleV2.ToggleButton />
</div>
</ToggleV2>
2022.07.17 추가
이 글이 어디서 링크가 돌았는지 조회수가 꽤 나오고 있습니다.
그리고 이 글을 주제로
컴파운드 컴포넌트 패턴이 실무에서 사용되는가?
실제로 효과가 있는가?에
대해서 오픈채팅방에서 의견을 주고받는 걸 보았는데요
제 대답은 definitely! 입니다.
오히려 모든 리액트 컴포넌트 디자인 패턴중에 가장 중요하다 주장하고 싶네요.
왜일까요? 리액트의 합성(composition)모델을 가장 효과적으로 활용하는 패턴이기 때문입니다!
Vercel과 CodeSandbox에서 사용하는 해드리스 UI 프레임워크 Radix UI 내부는 해당 패턴으로 구현되어 있습니다.
추가로 한국 자료에도 해당 패턴을 잘 활용하는 방법에 대하여 잘 정리해둔 게시물이 많습니다.
아래 두 글 다 stateless한 컴파운드 컴포넌트 패턴을 활용하여 컴포넌트를 만들고 있습니다.
암시적으로 상태를 공유하는 케이스의 예제는 Radix UI Primitive의 내부 구현을 보시면 많은 것을 배우실 수 있습니다.
(Radix UI는 내부적으로 stitches라는 CSS-IN-JS를 통해 개발되어 있습니다.)
https://fe-developers.kakaoent.com/2022/220609-storybookwise-component-refactoring/
https://fe-developers.kakaoent.com/2022/220505-how-page-part-use-atomic-design-system/
또 다른 구현을 보고 싶으시다면 아래로...
https://itchallenger.tistory.com/710
해당 패턴의 끝을 보고 싶으면
https://itchallenger.tistory.com/758
'FrontEnd' 카테고리의 다른 글
리액트 디자인패턴 : State Reducer Pattern (스테이트 리듀서 패턴) (0) | 2022.06.01 |
---|---|
리액트 디자인패턴 : Prop Collections and Getters (프롭 컬렉션 엔 게터 패턴) (0) | 2022.06.01 |
React children with typescript. 리액트 children 컴포넌트 타이핑 (0) | 2022.06.01 |
리액트 디자인패턴 : Context Module Function (컨텍스트 모듈 함수 패턴) (0) | 2022.06.01 |
타입스크립트의 타입 추론과 힌들리 밀러 타입 시스템 (2) | 2022.05.26 |