Introduction
토이 프로젝트 중 위와 같은 다양한 형태의 모달을 제작해야 할 상황이 생겼고, 예전부터 사용해보고 싶었던 합성 컴포넌트 패턴으로 모달을 제작해보기로 했습니다.
합성 컴포넌트 패턴(Compound Component Pattern) 이란?
합성 컴포넌트 패턴은 여러 작은 컴포넌트들을 조합하여 하나의 컴포넌트를 만드는 React 디자인 패턴입니다.
이 패턴을 사용하여 컴포넌트를 구현한다면 높은 재사용성을 갖습니다.
또한 한 컴포넌트에 무지막지한 양의 props가 주입되는 대신 상위 컴포넌트에서 각 컴포넌트에 필요한 props만 주입할 수 있기 때문에 관심사 / 책임 분리, props drilling 방지 등의 장점을 갖습니다.
예시로는 아코디언 컴포넌트가 있습니다. 아코디언 컴포넌트는 제목, 내용, 이들을 합친 섹션의 반복으로 이루어집니다.
이를 합성 컴포넌트를 이용하여 컴포넌트들의 조합으로 나타낼 수 있는데, 적용 전과 후의 코드를 비교해보면
Before
<div>
{sections.map((section, index) => (
<div key={index}>
<button onClick={() => handleToggle(index)}>{section.title}</button>
{openIndex === index && <div>{section.content}</div>}
</div>
))}
</div>
After
<Accordion>
<Accordion.Section index={0}>
<Accordion.Title>Section 1</Accordion.Title>
<Accordion.Content>Content 1</Accordion.Content>
</Accordion.Section>
<Accordion.Section index={1}>
<Accordion.Title>Section 2</Accordion.Title>
<Accordion.Content>Content 2</Accordion.Content>
</Accordion.Section>
</Accordion>
이와 같이 각 부분을 별도의 컴포넌트로 분리하여 논리적으로 구조화할 수 있습니다.
구현하기
합성 컴포넌트 패턴이 무엇인지 알아봤으니 이를 이용하여 모달을 구현했던 과정을 살펴보겠습니다.
1. 구조화할 컴포넌트 정하기
구조화할 컴포넌트를 정할 때 고민이 많았습니다.
작은 부분까지 다 구조화를 해놓자니 모든 경우를 다 따지면 컴포넌트가 너무 많아질 것 같고.. 그렇다고 큰 틀만 구조화해놓자니 자유도가 너무 높아져 이게 과연 합성하는 의미가 있나? 하는 생각이 들었습니다.
하여 모달들의 공통적인 부분들만 구조화 해놓자는 결론을 냈습니다.
이 정도의 컴포넌트만 합성하기로 하고, 모달마다 거의 다른 Content 같은 경우는 각각 사용할 때 구현하고자 하였습니다.
2. ContextAPI를 사용하여 모달 여닫는 커스텀 훅 제작
공통적으로 모달을 여닫는 함수와 이에 대한 상태를 공유할 수 있어야 해서 ContextAPI 를 이용하였습니다.
interface ModalContextProps {
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
}
// Modal Context: isOpen, openModal, closeModal
const ModalContext = createContext<ModalContextProps | undefined>(undefined);
// context를 관리할 커스텀 훅
const useModalContext = (): ModalContextProps => {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModalContext must be used within a ModalProvider');
}
return context;
};
interface ModalProviderProps {
children: ReactNode
}
// context provider
const ModalProvider = ({ children }: ModalProviderProps) => {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => setIsOpen(true);
const closeModal = () => setIsOpen(false);
return (
<ModalContext.Provider value={{ isOpen, openModal, closeModal }}>
{children}
{isOpen && <Modal.Dimmed />}
</ModalContext.Provider>
)
}
export { ModalProvider, useModalContext };
가장 상위 폴더에 ModalProvider로 감싸 모달의 상태를 저장할 수 있도록 합니다.
3. Modal 컴포넌트와 각 하위 컴포넌트 구현
export const Modal = ({ children, isOpen }: ModalProps) => {
if(!isOpen){
return null;
}
const modalHeader = getModalHeader(children);
const modalContents = getModalContents(children);
const modalFooter = getModalFooter(children);
return createPortal(
<div className={styles.modal}>
{
modalHeader && (
<div className={styles.modal__header}>{modalHeader}</div>
)
}
{
modalContents && (
<div className={styles.modal__content}>{modalContents}</div>
)
}
{
modalFooter && (
<div className={styles.modal__footer}>{modalFooter}</div>
)
}
</div>
, document.body);
}
Modal.Dimmed = ModalDimmed;
Modal.Header = ModalHeader;
Modal.Content = ModalContents;
Modal.Footer = ModalFooter;
Modal.Title = ModalTitle;
Modal.Subtitle = ModalSubtitle;
Modal.Button = ModalButtons;
가장 상위 컴포넌트인 Modal 컴포넌트의 children을 각각 Header, Contents, Footer로 구분했습니다.
모달에는 React Portal를 사용했습니다.
하위 컴포넌트 구현부는 따로 기술하지 않겠습니다.
4. 사용하기
기본 틀은 다음과 같은 형태입니다.
<Modal isOpen={isOpen}>
<Modal.Header>
<Modal.Title></Modal.Title>
<Modal.Subtitle></Modal.Subtitle>
</Modal.Header>
<Modal.Content>
~
</Modal.Content>
<Modal.Footer>
<Modal.Button>
~
</Modal.Button>
</Modal.Footer>
</Modal>
모달마다 다른 부분은 각 컴포넌트의 children으로 구현합니다.
마무리
합성 컴포넌트 패턴을 활용하여 재사용성 높은 모달을 제작해봤습니다.
다른 방식도 있겠지만 저는 Context API 를 이용하여 공통적인 변수, 함수를 관리하고, React Portal을 활용하였으며, 공통적인 여러 컴포넌트를 합성하는 방식으로 진행했습니다.
원래 모달을 제작했을 때는 useState를 사용하여 값을 관리하고 공통적인 부분이 있더라도 매번 새로운 모달 컴포넌트를 제작했지만, 이 방식을 사용하니 재사용성이 훨씬 늘어난게 느껴집니다.
Reference
https://fe-developers.kakaoent.com/2022/220731-composition-component/
'Frontend > ReactJS' 카테고리의 다른 글
[React] useCallback vs useMemo (1) | 2023.09.21 |
---|---|
[React] React-calendar (0) | 2023.09.17 |
[React] Styled-components에서 스크롤 애니메이션 구현 (0) | 2023.08.31 |
[React] React 반응형 웹 (0) | 2023.02.23 |
[React] 로그인 Refresh Token 처리 (0) | 2023.02.22 |