[북잡 프로젝트] 모달창 상태 관리 중앙화하기(feat. Zustand)


이번 프로젝트에서 모달을 별도의 컴포넌트로 분리해서 사용하고 있었다.
페이지나 상황에 따라 모달을 띄우기 위해, 컴포넌트에서 직접 useState
를 선언해 모달의 열림/닫힘을 관리했다.
🌱 기존 사용 방법
Modal.jsx
import { useNavigate } from 'react-router-dom'
import cancelIcon from '../../assets/icons/common/common_cancel.svg'
const Modal = ({ isOpen, onClose, title, description, buttonLabel, onButtonClick }) => {
const navigate = useNavigate()
if (!isOpen) return null
const handleButtonClick = () => {
if (onButtonClick) {
onButtonClick(navigate)
}
onClose()
}
return (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm'>
<div className='relative w-[90%] max-w-md bg-white rounded-2xl p-6 shadow-xl animate-fadeIn'>
<button
onClick={() => navigate('/')}
className='absolute top-4 right-4 text-gray-400 hover:text-gray-600'
>
<img src={cancelIcon} alt='닫는 아이콘' />
</button>
<h2 className='mb-3 text-xl font-semibold text-center text-gray-900'>{title}</h2>
<p className='mb-6 text-sm text-center text-gray-600 whitespace-pre-line'>{description}</p>
<button
onClick={handleButtonClick}
className='w-full py-3 rounded-xl bg-pink-500 text-white font-semibold text-sm shadow-md hover:bg-pink-600 active:scale-95 transition-all duration-200'
>
{buttonLabel}
</button>
</div>
</div>
)
}
export default Modal
const [showModal, setShowModal] = useState(false)
{showModal && <ChooseWriteForm onSelect={handleSelect} onClose={() => setShowModal(false)} />}
이런 식으로 useState로 모달 상태를 각각 관리하며 사용하고 있다.
👉🏻 현재 모달을 사용하고 있는 파일들
꽤 많다.. 이렇게 많은 파일들이 각각 Modal을 관리하고 있다니..! 굉장히 비효율적이라고 생각했다.
구체적으로 어떤 부분이 비효율적이라고 느꼈냐고 묻는다면
중복 코드가 많아진다
→ 여러 컴포넌트에서 모달 상태를 따로 관리하니까 관리가 분산되고 귀찮아짐 ‼️상태 공유가 어렵다
→ 예를 들어 어떤 이벤트에서 모달 열고 닫는 상태를 다른 컴포넌트가 알아야 할 때 어려움이 있을수 밖에 없다.복잡도가 늘어난다
→ 프로젝트가 커지면 상태가 여기저기 흩어져서 유지보수성 저하되는 이슈 발생사용 불편
→ 매번
useState
와onClose
,isOpen
같은 props를 직접 다뤄야 해서 번거로움(굉장해 엄청나!!!)
이런 비효율적인 부분을 개선하고자 Zustand
를 사용해서 중앙화하기로 결정했다.
useModalStore.js
import { create } from 'zustand'
const useModalStore = create((set) => ({
isOpen: false,
title: '',
description: '',
buttonLabel: '',
onButtonClick: null,
openModal: ({ title, description, buttonLabel, onButtonClick }) => {
set({
isOpen: true,
title,
description,
buttonLabel,
onButtonClick,
})
},
closeModal: () => set({ isOpen: false }),
}))
export default useModalStore
기존 모달창 사용 파일
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import useAuthStore from '../store/login/useAuthStore'
import ROUTER_PATHS from './RouterPath'
import Modal from '../components/web/Modal'
const ProtectedRoute = ({ children }) => {
const { isAuthenticated } = useAuthStore()
const navigate = useNavigate()
const [showAlert, setShowAlert] = useState(false)
const [checked, setChecked] = useState(false)
useEffect(() => {
if (!isAuthenticated) {
setShowAlert(true)
} else {
setChecked(true)
}
}, [isAuthenticated])
const handleAlertClose = () => {
setShowAlert(false)
setChecked(true)
}
const handleAlertAction = () => {
navigate(ROUTER_PATHS.LOGIN_MAIN, { replace: true })
}
if (!checked && !showAlert) return null
return (
<>
{isAuthenticated ? children : null}
<Modal
isOpen={showAlert}
onClose={handleAlertClose}
title='로그인이 필요합니다'
description={`로그인이 필요한 페이지입니다.\n로그인 페이지로 이동하시겠습니까?`}
buttonLabel='로그인하기'
onButtonClick={handleAlertAction}
/>
</>
)
}
export default ProtectedRoute
전역상태 추가 후 👇🏻
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import useAuthStore from '../store/login/useAuthStore'
import ROUTER_PATHS from './RouterPath'
import Modal from '../components/web/Modal'
import useModalStore from '../store/modal/useModalStore'
const ProtectedRoute = ({ children }) => {
const { isAuthenticated } = useAuthStore()
const navigate = useNavigate()
const openModal = useModalStore((state) => state.openModal)
const closeModal = useModalStore((state) => state.closeModal)
const isOpen = useModalStore((state) => state.isOpen)
useEffect(() => {
if (!isAuthenticated) {
openModal({
title: '로그인이 필요합니다',
description: '로그인이 필요한 페이지입니다.\n로그인 페이지로 이동하시겠습니까?',
buttonLabel: '로그인하기',
onButtonClick: () => {
navigate(ROUTER_PATHS.LOGIN_MAIN, { replace: true })
},
})
} else {
closeModal()
}
}, [isAuthenticated, navigate, openModal, closeModal])
if (!isAuthenticated && !isOpen) return null
return (
<>
{isAuthenticated ? children : null}
<Modal />
</>
)
}
export default ProtectedRoute
이런식으로 useState()
를 사용하지 않아도 된다✨
Modal을 사용하고 있는 모든 파일들을 수정해줬다,,, 🤦🏻♀️ !
App.jsx
return (
<BrowserRouter>
<ToastContainer position='top-center' autoClose={2000} />
<PageScrollToTop />
<AppRoutes />
<Modal />
</BrowserRouter>
)
}
export default App
그리고 꼭 App.jsx에 import 해줘야한다는 점 잊지말기!!
기존 코드를 수정하는 데 확실히 비효율적이게 사용하고 있었다는 점을 다시한번 느꼈다..Modal
을 사용하고 있는 모든 파일에서 useState
와 함께
<Modal
isOpen={alertState.isOpen}
onClose={closeAlert}
title={alertState.title}
description={alertState.description}
buttonLabel={alertState.buttonLabel}
onButtonClick={alertState.onButtonClick}
/>
Modal
를 사용하고 있었다..! …..🙂↔️(현실부정)
그리고 디자인도 수정해줬다..(TMI)
기존 디자인
수정된 디자인
👍🏻 Zustand를 사용하고 느낀 장점
useModalStore
훅 하나로 모달 상태 열기/닫기 기능을 손쉽게 호출 가능해서 편했다.컴포넌트마다 모달 상태 코드를 작성하지 않아도 되어서 코드가 엄청 깔끔해짐 !
Subscribe to my newsletter
Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
