Compound 패턴
하나의 작업을 위해 여러 컴포넌트를 만들어 역할을 분담하게 한다.
📜 원문: patterns.dev - compound pattern
📜 번역: https://patterns-dev-kr.github.io/design-patterns/compound-pattern/
앱을 개발하다 보면 종종 서로를 참조하는 컴포넌트를 만들기도 합니다. 컴포넌트들은 서로 상태를 공유하기도 하고 특정
로직을 함께 사용하기도 하죠. 아마 이런 코드는 select, 드롭다운 컴포넌트 또는 메뉴 컴포넌트에서 보았을 것입니다.
컴파운드 컴포넌트 패턴은 여러 컴포넌트들이 모여 하나의 동작을 할 수 있게 해 줍니다.
Context API
아래 예제에서는 다람쥐 사진 목록을 보여주고 있습니다. 여기에 버튼을 추가하여 각각의 사진을 수정하거나 삭제하도록 하려고
합니다. Flyout 컴포넌트를 구현하여 사용자가 메뉴를 누르면 토글할 수 있도록 할 수 있습니다.
Flyout 컴포넌트 내에서는 세가지 구현이 필요합니다.
- 토글 버튼과 메뉴 리스트를 포함한 Flyout 래퍼.
- 메뉴를 토글할 수 있는 Toggle 버튼.
- 메뉴를 포함한 List 컴포넌트.
React의 Context API 를 활용해 컴파운드 패턴을 활용하여 예제를 구현해 보겠습니다.
먼저 FlyOut 컴포넌트를 구현해보죠. 이 컴포넌트는 상태를 포함하고 또 자식 컴포넌트들이 받게 될
값을 FlyOutProvider 컴포넌트를 반환하고 있습니다.
const FlyOutContext = createContext()
function FlyOut(props) {
const [open, toggle] = useState(false)
const providerValue = { open, toggle }
return (
<FlyOutContext.Provider value={providerValue}>
{props.children}
</FlyOutContext.Provider>
)
}
위의 코드에서 메뉴가 열렸는지 여부를 나타내는 open그리고 토글 가능한 toggle함수를 상태로 포함한 FlyOut 컴포넌트를
만들었습니다.
이제 Toggle 컴포넌트를 만들어 볼까요? 이 컴포넌트는 사용자가 토글 버튼을 눌렀을 때 나타날 메뉴를 렌더링하고 있습니
다.
function Toggle() {
const { open, toggle } = useContext(FlyOutContext)
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
)
}
Toggle 컴포넌트가 FlyOutContext 프로바이더에 접근할 수 있도록 해당 컴포넌트는 FlyOut 의 자식 컴포넌트로 렌더링
해야 합니다. 따라서 단순히 자식 컴포넌트로 렌더링 하면 되지만 여기서는 FlyOut 컴포넌트의 Static property로 만들고 있
습니다.
const FlyOutContext = createContext()
function FlyOut(props) {
const [open, toggle] = useState(false)
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
)
}
function Toggle() {
const { open, toggle } = useContext(FlyOutContext)
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
)
}
FlyOut.Toggle = Toggle
이렇게 하면 FlyOut 컴포넌트를 사용하는 쪽에서 토글 버튼이 필요한 경우라도 그냥 FlyOut컴포넌트만 import하면 됩니다.
import React from 'react'
import { FlyOut } from './FlyOut'
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
</FlyOut>
)
}
토글 뿐만 아니라 메뉴가 화면에 나와야 한다. Toggle 과 같이 컨텍스트를 통해 상태를 가져와 처리할 수 있습니다.
function List({ children }) {
const { open } = React.useContext(FlyOutContext)
return open && <ul>{children}</ul>
}
function Item({ children }) {
return <li>{children}</li>
}
const FlyOutContext = createContext()
function FlyOut(props) {
const [open, toggle] = useState(false)
return (
<FlyOutContext.Provider value={{ open, toggle }}>
{props.children}
</FlyOutContext.Provider>
)
}
function Toggle() {
const { open, toggle } = useContext(FlyOutContext)
return (
<div onClick={() => toggle(!open)}>
<Icon />
</div>
)
}
function List({ children }) {
const { open } = useContext(FlyOutContext)
return open && <ul>{children}</ul>
}
function Item({ children }) {
return <li>{children}</li>
}
FlyOut.Toggle = Toggle
FlyOut.List = List
FlyOut.Item = Item
지금까지 구현한 것들 모두 FlyOut 컴포넌트만 가지고 사용할 수 있습니다. 예제에서는 “수정” 메뉴와 “삭제” 메뉴를 제공해
야 하므로. FlyOut.Item 을 두개 가진 FlyOut.List 컴포넌트를 사용하면 됩니다.
import React from 'react'
import { FlyOut } from './FlyOut'
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</FlyOut>
)
}
React.Children.map
자식 컴포넌트들을 순회 처리 하는데에도 컴파운드 패턴을 사용할 수 있습니다.
React.cloneElement를 사용하여 자식 컴포넌트를 복제하여 각각에게 open과 toggle 메서드를 넘길 수 있습니다.
export function FlyOut(props) {
const [open, toggle] = React.useState(false)
return (
<div>
{React.Children.map(props.children, child =>
React.cloneElement(child, { open, toggle })
)}
</div>
)
}
모든 메뉴 컴포넌트들은 복제되며 open, toggle메서드를 전달받았습니다. 위에서 해당 값을 받기 위해 Context API를 사용
했던 것에 비교하여 그냥 prop에서 두 값을 사용하면 됩니다.
장점
컴파운드 패턴은 동작 구현에 필요한 상태를 내부적으로 가지고 있는데 이 것을 사용하는 쪽에서는 드러나지 않아 걱정 없
이 사용할 수 있습니다.
또 이 패턴을 사용하면 아래와 같이 자식 컴포넌트들을 일일히 import할 필요 없이 기능을 이용할 수 있죠.
import { FlyOut } from './FlyOut'
export default function FlyoutMenu() {
return (
<FlyOut>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</FlyOut>
)
}
단점
내부에서 React.Children.map을 사용하고 있기 때문에 쓰는 쪽에서 자식 컴포넌트를 약속된 형태로 넘겨야 하는 제약이 생깁니다.
export default function FlyoutMenu() {
return (
<FlyOut>
{/* This breaks */}
<div>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</div>
</FlyOut>
)
}
엘리먼트를 복제하는 경우 복제 대상 컴포넌트가 기존에 갖고 있는 prop과 이름이 충돌될 수 있습니다.
이 경우 React.cloneElement를 사용할 때 넘어간 값으로 해당 prop은 덮어써질 것입니다.
참조
'프로그래밍(Basic) > 디자인 패턴(JS)' 카테고리의 다른 글
[바미] 스트래티지 패턴(Strategy Pattern) (0) | 2024.03.30 |
---|---|
[바미] Command 패턴 (0) | 2022.10.17 |
[바미] - Hooks 패턴 (2) | 2022.10.04 |
[바미] Render Props 패턴 (0) | 2022.09.25 |
[바미] HOC 패턴 (0) | 2022.09.24 |