여러 서비스에 modal 창을 기본적으로 사용하게 되는데요, 지금 진행하고 있는 Escapers 프로젝트 규모가 커지면서 모달 창을 사용하는 페이지들이 많아졌습니다. 이 때 모달 창을 유지 관리하기 위해 많은 코드가 중복되고 비슷한 로직들이 반복되었는데요, 이러한 불편한 점을 Redux toolkit
을 사용하여 중복되는 로직을 개선하였습니다.
프로젝트에 사용한 코드를 재구성하였습니다
styled components를 사용하여 기본적인 모달 창을 만들어주었습니다.
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
z-index: 1;
`;
const Overlay = styled.div`
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.3);
`;
const Content = styled.section`
width: 20rem;
height: 5rem;
background: #fff;
padding: 2rem;
z-index: 1;
`;
function Modal() {
return (
<Container>
<Overlay/>
<Content>모달 창입니다</Content>
</Container>
);
}
기본 모달 창이 준비되었고 이를 일반적으로 사용하는 방법에 대해서 살펴보겠습니다.
앞서 만들어 둔 모달 컴포넌트를 App 컴포넌트에서 사용해보겠습니다. App 컴포넌트에서 버튼을 클릭하여 모달 창을 띄울 수 있도록 만들었습니다.
function App() {
const [visible, setVisible] = useState(false);
const handleModalOpen = () => {
setVisible(true);
};
const handleModalClose = () => {
setVisible(false);
};
return (
<div>
{visible && (
<Modal onClose={handleModalClose}/>
)}
<ButtonList>
<Button onClick={handleModalOpen}>1번 모달 열기</Button>
</ButtonList>
</div>
);
}
App 컴포넌트에서 모달 창을 열고 닫기 위해서, 기본 state 를 false로 지정하고, 버튼을 누르면 visible을 true로 바꿉니다. 또한 유저 편의성을 위해서 모달창 overlay 부분을 누르면 모달 창을 닫을 수 있도록 Modal 컴포넌트에 handlemodalClose 를 전달해주었습니다.
<Container>
<Overlay onClick={onClose} />
<Content>모달 창입니다</Content>
</Container>
이렇게 기본적인 모달 창과 모달 창을 열고 닫는 기능을 만들어주었습니다! 하지만 실제 서비스에서 한 컴포넌트에서 여러 모달창을 필요로 하는 경우가 많습니다. Escapers 서비스에서는 기본 모달 창 외에도, 사용자들에게 로그인을 요청하는 모달이 있었습니다. 로그인 요청 모달창에는 아래와 같이 모달 창을 닫는 취소 버튼, 로그인을 진행하는 로그인 버튼이 있다고 생각해봅시다.
코드는 아래와 같습니다.
function App() {
const [visible, setVisible] = useState(false);
const [loginModalVisible, setLoginModalVisible] = useState(false);
const handleModalOpen = () => {
setVisible(true);
};
const handleModalClose = () => {
setVisible(false);
};
const handleLoginModalOpen = () => {
setLoginModalVisible(true);
};
const handleLoginModalClose = () => {
setLoginModalVisible(false);
};
const handleLoginModalSubmit = () => {
/**
* login modal 비즈니스 로직...
*/
setLoginModalVisible(false);
};
return (
<div>
{visible && <Modal onClose={handleModalClose} />}
{loginModalVisible && (
<LoginModal
onClose={handleLoginModalClose}
onSubmit={handleLoginModalSubmit}
/>
)}
<ButtonList>
<Button onClick={handleModalOpen}>1번 모달 열기</Button>
<Button onClick={handleLoginModalOpen}>2번 모달 열기</Button>
</ButtonList>
</div>
);
}
모달 창을 하나 더 추가하였을 뿐인데 중복되는 상태관리 코드가 상당히 많아지고 각 모달창마다 필요로 하는 비즈니스 로직들이 각각 작성해주어야합니다.
//하나의 컴포넌트에서 여러 개의 모달을 필요로 해서 컴포넌트가 비대해지는 현상이 발생합니다.
const [visible1 setVisible1]= useState(false);
const [visible2 setVisible2]= useState(false);
const [visible3 setVisible3]= useState(false);
const [visible4 setVisible4]= useState(false);
const [visible5 setVisible5]= useState(false);
{visible1 && <Modal1 onClose={handleModalClose1} />}
{visible2 && <Modal2 onClose={handleModalClose2} />}
{visible3 && <Modal3 onClose={handleModalClose3} />}
{visible4 && <Modal4 onClose={handleModalClose4} />}
{visible5 && <Modal5 onClose={handleModalClose5} />}
위의 코드를 보면 이제 문제가 심각해졌다고 느끼게 되는데요, 문제는 여기에서 그치지 않고, 하위 컴포넌트에서 컴포넌트를 열어야 하는 상황이 생기면 props의 복잡도 또한 올라가는 문제가 생깁니다. 지금까지 살펴본 문제들을 다음과 같습니다.
1. 컴포넌트에서 모든 상태관리를 하고 있습니다.
2. 모달을 열고 닫는 로직이 중복되어 있습니다.
3. 하위 컴포넌트로 로직을 전달하여야 할 경우, props의 복잡도가 높아집니다.
이를 어떻게 해결할 수 있을까요?
제가 선택한 방식은 프로젝트에서 사용하고 있던 redux toolkit을 사용하여 modal의 상태를 전역적으로 관리하여 복잡도를 개선하였습니다.
모달의 state는 모달 type을 string으로, modal 오픈 여부를 나타내는 isOpen으로 가지고 있습니다.
const initialState = {
modalType: "",
isOpen: false,
};
export const modalSlice = createSlice({
name: "modal",
initialState,
reducers: {
openModal: (state, actions) => {
const { modalType } = actions.payload;
state.modalType = modalType;
state.isOpen = true;
},
closeModal: (state) => {
state.isOpen = false;
},
},
});
export const { openModal, closeModal } = modalSlice.actions;
export const selectModal = (state) => state.modal;
export default modalSlice.reducer;
이어서, 모든 Modal을 관리하는 Global Modal을 만들어 줍니다. Global Modal에서는 string의 modalType
을 받아 MODAL_COMPONENTS을 통하여 Component
형태로 변환합니다. redux toolkit에서 Component로 상태관리를 하면 이러한 생략할 수 있다고 의문을 가질 수 있는데요, Redux 공식문서에서 이를 Redux dev tools 오류를 발생시킬 수 있어 안티 패턴 으로 정의하고 있습니다.
const MODAL_TYPES = {
LoginModal: "LoginModal",
BasicModal: "BasicModal",
};
const MODAL_COMPONENTS = [
{
type: MODAL_TYPES.LoginModal,
component: <LoginModal />,
},
{
type: MODAL_TYPES.BasicModal,
component: <BasicModal />,
},
];
function GlobalModal() {
// modal type을 string 형태로 받습니다.
const { modalType, isOpen } = useSelector(selectModal);
const dispatch = useDispatch();
if (!isOpen) return;
const findModal = MODAL_COMPONENTS.find((modal) => {
return modal.type === modalType;
});
const renderModal = () => {
return findModal.component;
};
return (
<Container>
<Overlay onClick={() => dispatch(closeModal())} />
{renderModal()}
</Container>
);
}
위와 같이 global modal을 만들어 줌으로써, App 컴포넌트에서 필요한 모든 modal 창을 rendering 해주어야 한다는 문제를 해결하였습니다. 또한 여러 모달 창에 공통적으로 필요한 Overlay의 중복을 제거할 수 있었습니다.
먼저 필요한 컴포넌트에서 직접 접근할 수 있도록, App 컴포넌트를 Provider로 감싸줍니다. <GlobalModal/>
도 렌더링 시켜주면 모든 컴포넌트에서 modal을 렌더링하지 않아도 되겠죠.
root.render(
<React.StrictMode>
<Provider store={store}>
<GlobalModal />
<App />
</Provider>
</React.StrictMode>
);
다음으로, 컴포넌트에서 모달 창을 여는 방법을 알아보겠습니다. App.jsx 에서 dispatch를 통하여 열고자 하는 modal Type을 전달하기만 한다면 원하는 모달 창을 열 수 있습니다. 이로써, 모든 상태관리를 컴포넌트에서 해주어야 한다는 문제도 해결하였고, 하위 컴포넌트에서 모달 창을 열고 닫을 때에 발생하는 props 복잡도가 높아진다는 문제도 해결하였습니다.
function App() {
const dispatch = useDispatch();
const handleOpenLoginModal = () => {
dispatch(
openModal({
modalType: "LoginModal",
isOpen: true,
})
);
};
const handleOpenBasicModal = () => {
dispatch(
openModal({
modalType: "BasicModal",
isOpen: true,
})
);
};
return (
<div>
<ButtonList>
<Button onClick={handleOpenLoginModal}>로그인 열기</Button>
<Button onClick={handleOpenBasicModal}>기본 모달 열기</Button>
</ButtonList>
</div>
);
}
마지막으로, modal 창에서 로그인, 취소 등 비즈니스 로직은 어떻게 처리하였는지 알아보겠습니다.
function LoginModal() {
const dispatch = useDispatch();
const handleModalClose = () => {
dispatch(closeModal());
};
const handleLogin = () => {
// 로그인 로직...
handleModalClose();
};
return (
<Content>
<h2>로그인 모달 창입니다</h2>
<ButtonWrapper>
<Button onClick={handleModalClose}>취소</Button>
<Button onClick={handleLogin}>로그인</Button>
</ButtonWrapper>
</Content>
);
}
Login Modal 창에서 직접 비즈니스 로직을 작성할 수 있게 되었습니다. 앞서 App.jsx에서 비즈니스 로직을 처리하였던 것에 비해, Login Modal에서 비즈니스 로직을 처리하고, Redux를 통해서 모달 창을 닫습니다.
이번 포스팅을 통하여 프로젝트의 규모가 커지고 여러 모달창을 사용하면서 생기는 문제를 Redux toolkit
을 통하여 효과적으로 modal 창을 관리하는 방법을 알아보았습니다. 중복되는 로직 개선, props 복잡도 개선, 반복적인 렌더링 문제를 해결할 수 있었습니다.