파편화된 모달 관리를 효율적으로 한 곳에서 함으로써 해결 (중앙화된 모달관리)
1. 기존 프로젝트에서의 모달의 문제
- 모달 사용 부분
// Header.tsx const [modalShow, setModalShow] = useState(false); const [modalContent, setModalContent] = useState(''); ... <Button onClick={() => { setModalShow(true) setModalContent('login')}> 로그인 </Button> ... ... {modalShow && ( <Modal content={modalContent} setModalShow={setModalShow} /> )}
- 모달 컴포넌트
// Modal.tsx const Modal = ({ setModalShow, content }: Props) => { const getComponentByContent = (contentType: string) => { switch (contentType) { case 'login': return <LoginForm setModalShow={setModalShow} />; case 'signup': return <SignUpForm setModalShow={setModalShow} />; ... } }; return ( <S.Wrapper onClick={() => {setModalShow(false)}}> {getComponentByContent(content)} </S.Wrapper> ); }; export default Modal;
[문제점]
- 모달 사용 부분에 모달 상태를 관리하기 위한 부수적인 코드가 많아진다.
(
modalShow, modalContent
에 대한 useState, 매번 modalShow에 따른 <Modal>을 하단에 입력해주어야함) - props로 모달을 열고 닫기 위한 setter 함수를 반드시 넘겨주어야 한다.
- 즉 모달컴포넌트 자체가 사용하는 부모컴포넌트에 결합될 수 밖에 없는 구조
- 모달의 비즈니스 로직을 사용하는 부분에서 알 수 없다.
- 모달의 핵심은 어떤 모달을 열것인지, 취소-확인에 따른 비즈니스 로직은 무엇인지를 사용 부분에서 알 수 있어야 한다.
- 현재 구조에서는 사용 부분이 아닌 Modal.tsx의 switch문 내의 비즈니스 로직이 작성되는 구조로 모달의 역할과 책임이 분산되어 있다.
- 확장성이 부족한 모달 컴포넌트 설계
- 여러개의 모달이 필요한 경우 대응하기 어렵다.
- 현재 모달의 경우, 취소 및 확인 에 대한 비즈니스 로직에 대응하기 어렵다.
[해결방법]
모달을 열고 닫는 상태와 비즈니스 로직 처리를 위한 코드를 분리해낸다.
1.모달을 열고 닫는 상태는 ContextAPI를 통해 전역으로 관리한다.
효과
modal을 열고 닫기 위한 불필요한 props 제거
효과
여러개의 모달을 사용하더라도 중복된 코드가 발생하지 않는다 (상태관리 직접하지 않기)
How
1) 현재 열려져 있는 모달 상태와 이를 dispatch 하는 함수를 Context API를 통한 전역상태로 관리
// ModalsContext.js import { createContext } from 'react'; export const ModalsDispatchContext = createContext({ open: () => {}, close: () => {}, }); export const ModalsStateContext = createContext([]) // {Component, props}[]
참고
: 두 개의 Context로 분리하는 것은 useModal Hook에서 dispatch Context만을 사용하여, hook을 사용하는 컴포넌트에서 리렌더링이 일어나는 이슈를 방지하기 위함ModalState
는 렌더링할 모달 Component와 해당 모달에서 사용할 props 형태로 저장하여 준다.How
2) 모달을 열고 닫기 위한 useModal hook 구현
import { useContext } from "react"; import { ModalDispatchContext } from "./ModalContext"; export default function useModals() { const { open, close } = useContext(ModalDispatchContext); const openModal = (Component, props) => { open(Component, props); }; const closeModal = (Component) => { close(Component); }; return { openModal, closeModal, }; }
- before
const [modalShow, setModalShow] = useState(false); const [modalContent, setModalContent] = useState(''); return ( <Button onClick={() => { setModalShow(true); setModalContent('login') }}> 로그인 </Button> {modalShow && ( <Modal content={modalContent} setModalShow={setModalShow} /> )} )
- after
const { openModal } = useModals(); return ( <button onClick={() => { openModal(LoginModal, { onSubmit: ... , }); }}> 로그인 </button> )
- 모달 상태 관리를 위한 코드가 useModals hook으로 처리 되면서, Modal의 핵심(
열리는 컴포넌트, 비즈니스 처리를 위한 핸들러
)에 대한 가독성이 명확해짐
2. 모든 모달에 공통적으로 적용할 부분과 각 컴포넌트의 비즈니스 로직 처리를 구분한다.
효과
공통 모달 로직과 비즈니스 로직을 분리할 수 있어 유지보수에 유리하다.How
1) 공통 모달 로직은 Modal을 렌더링하는 부분에서 정의하여, 각 모달 컴포넌트의 props로 내려준다.// Modals.js import { useContext } from "react"; import { ModalDispatchContext, ModalsStateContext } from "./ModalContext"; const Modals = () => { const opendModals = useContext(ModalsStateContext); const { close } = useContext(ModalDispatchContext); return opendModals.map((modal, index) => { const { Component, props } = modal; const { onSubmit, ...restProps } = props; const onClose = () => { close(Component); }; const handleSubmit = async () => { onSubmit && (await onSubmit()); onClose(); }; return ( <Component {...restProps} key={index} onClose={onClose} onSubmit={handleSubmit}></Component> ); }); }; export default Modals;
추가
- onSubmit() 함수가 매개변수를 필요로 한다면, Modals에서 공통으로 처리하는 것이 아닌, 각 모달 컴포넌트 단에서 인자를 넘겨줄 수 있도록 한다.
// App <button onClick={() => { openModal(modals.login, { onSubmit: (value) => console.log(value), // '매개변수-value' }); }}> 로그인 모달 열기 </button> //LoginModal const handleClickSubmit = () => { onSubmit('매개변수-value'); onClose(); }; // Modals <Component {...props} onSubmit={onSubmit}></Component>
3. 모달 컴포넌트 중앙에서 관리하기
효과
Lazy로딩을 통해 모달이 실제로 사용되는 순간에 import 되어 초기 로딩 속도 향상을 가져온다.how
사용되는 모든 모달 컴포넌트를 객체(이름:component)로 매핑하고, 사용부분에서는 컴포넌트가 아닌 이름을 통해 불러온다.// Modals.js export const modals = { basic: lazy(() => import("./BasicModal")), login: lazy(() => import("./LoginModal")), }; const Modals = () => { ... } // App.js import { modals } from './Modals' <button onClick={() => { openModal(modals.login, { onSubmit: () => console.log("로그인 제출"), }); }}> 로그인 모달 열기 </button>
export const modals = { basic: lazy(() => import("./BasicModal")), login: lazy(() => import("./LoginModal")), }; const Modals = () => { ... return ( <Suspense> <Component {...restProps} key={index} onClose={onClose} onSubmit={handleSubmit}></Component> </Suspense> ) }
- 최초 로딩 시, Modal 컴포넌트는 로드 되지 않는다.

- 로그인 모달을 열었을 때, LoginModal 모듈이 로드된다.
