공통 모달인데.. 이제 재활용이 안 되는..
새로운 프로젝트를 진행하며 공통 모달 제작이 필요했습니다.
MVP 단계이기도 했고, 그에 따라 수시로 내용이 변경되어
모달에 사용되는 데이터를 인자로 전달받게만 처리하면
1차적인 공용 컴포넌트를 완성할 수 있을 것으로 전망했지만, 이는 착각이었습니다.
수시로 변경되는 것은 내용뿐이 아닌 디자인도 포함되었으니까요.
1차로 작성한 모달 컴포넌트
만약 공용 모달 컴포넌트를 사용하는 모든 곳에서 디자인과 기능이 동일하다면
prop을 전달받아 관리하는 공용 컴포넌트로 사용해도 문제가 없습니다.
하지만 특정 페이지들에서만 색상을 다르게 한다던지,
모바일 환경에서의 레이아웃 변경 등이 필요하다면
다시 컴포넌트를 만들어야 하므로, 상당히 불편함을 야기합니다.
즉, 추상화가 되어있지 않다고 볼 수 있겠죠.
아래의 코드가 대표적인 예시입니다.
import React from 'react';
import { Modal, Button } from '@mantine/core';
import styles from '@/components/global-modal/modal.module.css';
import { getClassNames } from '@/lib/styles';
interface IGlobalModal {
opened: boolean;
close: () => void;
}
const GlobalModal = ({ opened, close }: IGlobalModal) => {
const cx = getClassNames(styles);
return (
<>
<Modal.Root
opened={opened}
onClose={close}
centered
styles={{
header: {
background: '#111827',
color: '#fff',
display: 'flex',
justifyContent: 'center',
},
body: {
background: '#111827',
color: '#fff',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
content: {
padding: '40px',
background: '#111827',
color: '#fff',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
border: '1px solid #303030',
},
}}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>
<p className={cx('header')}>아직 준비 중인 기능입니다.</p>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className={cx('body')}>
더 나은 서비스를 위해 열심히 작업하고 있습니다.
</p>
<p>조금만 기다려 주세요.</p>
</Modal.Body>
<Button onClick={close}>
<p className={cx('button-text')}>확인</p>
</Button>
</Modal.Content>
</Modal.Root>
</>
);
};
export default GlobalModal;
진짜 공용 컴포넌트로 사용하기 위한 효율적인 코드를 고민 중, 팀원분이 알려주신
Compound 패턴을 적용하며 진-짜 공용 컴포넌트로 거듭날 수 있었습니다.
Compound 패턴을 적용해 보자!
compound parrten에 대해 간략하게 설명하자면
여러 컴포넌트들이 모여 하나의 동작을 할 수 있게 만드는 패턴입니다.
이를 통해 컴포넌트 간의 의존성을 줄이고, 재사용성을 높이며
관심사 분리와 상태 관리도 용이해진다는 장점이 있습니다.
UI 분리 관점에서는 Wrapper Component와 Child Component로 나눌 수 있는데요.
Wrapper는 하위 컴포넌트의 상태와 동작을 관리하며, 각 하위 컴포넌트가 서로 상호작용할 수 있도록 합니다.
패턴을 적용한 후의 코드
먼저 wrapper와 연관 컴포넌트들을 작성합니다.
import React from 'react';
import {
Modal as GlobalModal,
ModalTitleProps,
ModalRootProps,
ModalOverlayProps,
ModalHeaderProps,
ModalContentProps,
ModalBodyProps,
} from '@mantine/core';
const Modal = ({ children, ...props }: ModalRootProps) => {
return <GlobalModal.Root {...props}>{children}</GlobalModal.Root>;
};
const ModalOverLay = ({ ...props }: ModalOverlayProps) => {
return <GlobalModal.Overlay {...props}></GlobalModal.Overlay>;
};
const ModalHeader = ({ children, ...props }: ModalHeaderProps) => {
return <GlobalModal.Header {...props}>{children}</GlobalModal.Header>;
};
const ModalTitle = ({ children, ...props }: ModalTitleProps) => {
return <GlobalModal.Title {...props}>{children}</GlobalModal.Title>;
};
const ModalBody = ({ children, ...props }: ModalBodyProps) => {
return <GlobalModal.Body {...props}>{children}</GlobalModal.Body>;
};
const ModalContent = ({ children, ...props }: ModalContentProps) => {
return <GlobalModal.Content {...props}>{children}</GlobalModal.Content>;
};
Modal.Overlay = ModalOverLay;
Modal.Title = ModalTitle;
Modal.Header = ModalHeader;
Modal.Content = ModalContent;
Modal.Body = ModalBody;
export default Modal;
Modal을 wrapper로 활용, 하위 컴포넌트들이 wrapper에 종속되도록 만들었습니다.
저는 각 도메인별로 사용할 모달 컴포넌트를 생성해 사용하면 되겠네요.
컴포넌트 예시
Compound Pattern을 적용한 컴포넌트를 특정 도메인에 사용해 본 코드입니다.
'use client';
import Modal from '@/components/modal';
import { Button } from '@mantine/core';
import { getClassNames } from '@/lib/styles';
import styles from '@/components/global-modal/modal.module.css';
interface IDetailPageModal {
opened: boolean;
close: () => void;
}
const DetailPageModal = ({ opened, close }: IDetailPageModal) => {
const cx = getClassNames(styles);
return (
<Modal
opened={opened}
onClose={close}
centered
style={{
color: '#fff',
}}
>
<Modal.Overlay />
<Modal.Content
styles={{
content: {
padding: '40px',
background: '#111827',
border: '1px solid #303030',
},
}}
>
<Modal.Header
styles={{
header: {
padding: '0px',
display: 'flex',
justifyContent: 'center',
background: '#111827',
},
}}
>
<Modal.Title>
<p className={cx('header')}>아직 준비 중인 기능입니다.</p>
</Modal.Title>
</Modal.Header>
<Modal.Body
styles={{
body: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
}}
>
<p className={cx('body')}>
더 나은 서비스를 위해 열심히 작업하고 있습니다.
</p>
<p>조금만 기다려 주세요.</p>
</Modal.Body>
<Button onClick={close}>
<p className={cx('button-text')}>확인</p>
</Button>
</Modal.Content>
</Modal>
);
};
export default DetailPageModal;
이제 Detail이라는 도메인에 가져다 붙이면 끝나겠는걸요?
패턴을 적용한 후의 고찰
재사용을 위한 컴포넌트를 생성하기 위해 무지성으로 props를 사용하거나
복잡하게 코드를 작성했었습니다.
어떻게든 코드를 집어넣으며 컴포넌트를 만들었더니 오히려 코드의 가독성은 떨어지고
컴포넌트를 사용할 때도 다른 사람들이 보기에 이해하기 어려운 조건으로 가득 찬
컴포넌트를 만들어왔는데요.
Compound Component가 항상 공통 컴포넌트 생성 측면에서 정답이라는 것은 아니겠지만
이번 기회로 공통 컴포넌트 생성에 고려할 옵션 하나가 추가되었다고 생각합니다.
더욱 효과적으로 컴포넌트를 나눌 수 있겠네요!
Reference
'Design Pattern' 카테고리의 다른 글
한 층 더 파본 singleton pattern (1) | 2024.11.27 |
---|---|
HOC(=고차함수)를 사용해 전역 타이머를 구현해보자 (0) | 2024.08.10 |
완전한 싱글톤 패턴으로 리팩토링을 진행해보자 (0) | 2024.05.25 |
그저 작성했는데 싱글톤 패턴이었던 것에 대하여 (0) | 2024.05.23 |