이번 글에서는 Compound Pattern에 대해 알아보겠습니다. 그러기 위해선 먼저 Compound(합성)에 대해 알아볼 필요가 있습니다. 합성에 대해 익숙하지 않은 분은 아래의 포스팅을 보시고 돌아와서 다시 글을 읽는 것을 권장드립니다.
[React] 컴포넌트 합성이란?
Compund Pattern이란?
Compund(합성) 컴포넌트 패턴은 컴포넌트 합성의 장점을 활용하기 위해서 구안된 패턴입니다.
즉, “하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 분리된 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 방식” 이라고 볼 수 있습니다.
저희가 즐겨 쓰는 select 컴포넌트가 바로 합성 컴포넌트 패턴의 형태를 잘 띈 대표적인 예시라고 볼 수 있습니다.
select 컴포넌트는 변경에 대한 관심사를 담고 있고, option은 value에 대한 관심사를 갖고 있습니다. 그리고 각각의 컴포넌트들은 독립적으로 사용될 수 없고 사용하는 쪽에서 조합을 했을 경우 사용이 될 수 있는 형태입니다.
<select>
<option value="value1">value1</option>
<option value="value2">value2</option>
<option value="value3">value3</option>
</select>
Compound Pattern 적용
아래의 코드를 Compund Pattern을 적용하여 변경해보도록 하겠습니다.
const ReviewFilterModal = () => {
const [isOpen, openModal, closeModal] = useModal();
return (
<>
<AwesomeButton onClick={openModal}>필터</AwesomeButton>
{isOpen && (
<Portal>
<Backdrop onClick={closeModal) />
<AwesomeModalBox>
<Header>
<div>리뷰 검색 필터</div>
<Close>X</Close>
</Header>
{/* Modal Content */}
</AwesomeModalBox>
</Portal>
)}
</>
)
};
텍스트로 하드코딩되어있는 “필터” 부분과 “리뷰 검색 필터”와 같은 부분들은 딱 봐도 변경에 유연하지 못하다는 부분들은 눈에 띕니다. 이 부분들을 한 번 변경해보도록 하겠습니다.
const Modal = ({ trigger, title }) => {
const [isOpen, openModal, closeModal] = useModal();
return (
<>
{trigger} // 바뀐 부분
{isOpen && (
<Portal>
<Backdrop onClick={closeModal) />
<AwesomeModalBox>
<Header>
{title} // 바뀐 부분
<Close>X</Close>
</Header>
{/* Modal Content */}
</AwesomeModalBox>
</Portal>
)}
</>
)
};
변경이 예상되는 부분들을 props로(trigger, title) 변경해주었습니다. props로 변경을 하고 나니, 컴포넌트 내에서 도메인이 제거되어 컴포넌트도 좀 더 범용적인 ReviewFilterModal에서 Modal 컴포넌트로 변경할 수 있을 것 같습니다.
변경된 컴포넌트로 기존의 컴포넌트를 재구성하면 아래와 같은 형태가 됩니다. 이런식으로 코드를 작성하면 변경에 더 유연해질 수 있습니다. Modal 컴포넌트가 trigger와 title과는 서로 무관한 컴포넌트이기 때문입니다.
const ReviewFilterModal = () = (
<Modal
trigger={<AwesomeButton>필터</AwesomeButton>}
title={<Title>리뷰 검색 필터</Title>}>
{/* Modal Content */}
</Modal>
)
하지만, 아래와 같은 요구사항들이 계속 온다면 어떻게 될까요?
- 닫기 버튼을 오른쪽으로 옮겨주세요~
- 여기는 백드랍을 제거해 주세요!
- 닫기 아이콘을 별로 변경해 주세요!
물론 위와 같은 방식으로 props를 받으면 크게 어렵지 않을 수 있습니다. 하지만 아래 코드를 보시면 아시겠지만, 작성한 코드가 뭔가 불안하고 찝찝하다느 느낌을 받으실 수 있습니다. 왜냐하면 props가 늘어날수록 구현부가 복잡하기 때문입니다.
const ReviewFilterModal = () = (
<Modal
trigger={<AwesomeButton>필터</AwesomeButton>}
title={<Title>리뷰 검색 필터</Title>}
backdrop={null}
closePosition='right'>
{/* Modal Content */}
</Modal>
)
따라서, Modal이 가지고 있는 기본적인 기능을 기준으로 집합체들을 한 번 분리해 보겠습니다.
기능들로 구분하여 Trigger, Backdrop, Content, Close로 나타내봤습니다.
// 메인 컴포넌트
const Modal = ({ children }) => children;
// 서브 컴포넌트들
const ModalTrigger = ({ children, style }) => (
<button style={style || defaultTriggerStyle}>{children}</button>
);
const ModalBackdrop = ({ style }) => (
<div aria-hidden style={style || defaultBackdropStyle} />
);
const ModalContent = ({ style }) => (
<div style={style || defaultContentStyle} />
)
const ModalClose = ({ children, style }) => (
<button style={style || defaultCloseStyle}>{children}</button>
);
위의 컴포넌트들을 사용하여 내부 구조를 한 번 예상해보면 아래와 같은 형태가 될 것입니다.
<Modal>
<ModalTrigger />
<ModalBackdrop />
<ModalContent>
<ModalClose />
</ModalContent>
</Modal>
위의 코드들을 ReviewFilterModal에 적용해보면 아래와 같은 코드가 됩니다.
const ReviewFilterModal = () => {
<Modal>
<ModalTrigger>필터</ModalTrigger>
<Portal>
<ModalBackdrop />
<ModalContent>
<Flex>
<Title>리뷰 검색 필터</Title>
<ModalClose>X</ModalClose>
</Flex>
<FilterSection>custom하게 작성....</FilterSection>
</ModalContent>
</Portal>
</Modal>
}
그런데 위와 같이 코드를 작성하면 상태를 조작하거나 영향을 받는 컴포넌트들은 상태에 어떻게 접근할 수 있을까요? 예를 들어, ModalTrigger, ModalClose의 경우 Modal을 열거나 닫아야하기 때문에 상태 조작이 필요합니다. 이 외에도 위에서사용되는 다른 컴포넌트들도 상태에 영향을 받을 수 있습니다.
위에서 정의한 서브 컴포넌트들은 다 독립적으로 개발되어있기 때문에 상태 공유가 쉽지 않다는 문제가 발생합니다. 따라서, 합성 컴포넌트 패턴은 이러한 문제를 Context API를 통해서 해결합니다.
합성 컴포넌트에서의 상태 관리 - Context API
메인 컴포넌트에서 상태를 정의하고 context API로 서브 컴포넌트들에게 상태를 전달하는 방식입니다.
const Modal = ({ children }) => {
const [isOpen, openModal, closeModal] = useModal();
return (
<ModalProvider value={{ isOpen, openModal, closeModal }}>
{children}
</ModalProvider>
)
}
그러면 서브 컴포넌트에서 아래와 같이 전달받아 사용할 수 있습니다.
const ModalTrigger = ({ children, style }) => {
const { openModal } = useModalContext();
return (
<button style={style || defaultTriggerStyle} onClick={openModal}>
{children}
</button>
)
}
아래의 코드로만 해도 잘 작동은 하겠지만, ModalTrigger, FilterSection 컴포넌트들을 사용한 부분에 있어서 가독성에 아쉬움이 있긴 합니다. modal이라는 prefix로 구분을 해놓았지만 한 눈에 들어오지는 않습니다.
const ReviewFilterModal = () => {
<Modal>
<ModalTrigger>필터</ModalTrigger>
<Portal>
<ModalBackdrop />
<ModalContent>
<Flex>
<Title>리뷰 검색 필터</Title>
<ModalClose>X</ModalClose>
</Flex>
<FilterSection>custom하게 작성....</FilterSection>
</ModalContent>
</Portal>
</Modal>
}
그래서 합성 컴포넌트 패턴은 일반적으로 서브 컴포넌트를 메인 컴포넌트에 속성으로 추가함으로써 이 문제를 해결하곤 합니다.
const Modal = ({ children }) => {
const [isOpen, openModal, closeModal] = useModal();
return (
<ModalProvider value={{ isOpen, openModal, closeModal }}>
{children}
</ModalProvider>
)
}
Modal.Trigger = ModalTrigger;
Modal.Backdrop = ModalBackdrop;
Modal.Content = ModalContent;
Modal.Close = ModalClose;
이렇게 추가된 서브 컴포넌트들은 Modal. 이라는 prefix의 형태가 생기면서 같은 관심사인지 한 눈에 확인이 가능합니다.
const ReviewFilterModal = () => {
<Modal>
<Modal.Trigger>필터</Modal.Trigger>
<Portal>
<Modal.Backdrop />
<Modal.Content>
<Flex>
<Title>리뷰 검색 필터</Title>
<Modal.Close>X</Modal.Close>
</Flex>
<FilterSection>custom하게 작성....</FilterSection>
</Modal.Content>
</Portal>
</Modal>
}
Compound Pattern의 장점
- 변경에 유연한 UI
- 더 적은 prop driling
- 가독성 (컴포넌트 계층 구조 직관적으로 확인 가능)
Compound Pattern의 단점
- 가독성 (계층구조가 드러나있기 때문에 관심사를 찾는데 가독성이 떨어질 수 있음)
- 코드의 길이(prettier나 lint 같은 포메터가 느려질 수 있음)
- 구현 복잡도(제어권, 분기 처리 필요)
가독성부분만 봐도 장점이 될 수 있고, 단점이 될 수 있습니다. 아무래도 패턴이 패턴이니만큼 장점과 단점이 모두 공존하는 것 같습니다. 필요에 따라, 프로젝트에 따라 필요하면 해당 패턴을 사용하여 유연한 UI를 적절하게 만드는 것을 권장드립니다:)
References
https://www.youtube.com/watch?v=jEiUyh_lnFI
https://www.patterns.dev/react/compound-pattern/
https://ko.legacy.reactjs.org/docs/composition-vs-inheritance.html
'1. 웹개발 > 1_1_5 React JS' 카테고리의 다른 글
[React] 컴포넌트 합성이란? (0) | 2024.07.06 |
---|---|
[React] Flux 패턴과 Elm 아키텍처 (0) | 2024.05.18 |
[React] 성능을 측정하는 지표 (LCP, FID, CLS, FCP, TTFB 등) (0) | 2024.05.11 |
[React] forwardRef와 useImperativeHandle 훅이란? (0) | 2024.05.01 |
[React] 커스텀훅 vs 고차 컴포넌트 (0) | 2024.04.27 |