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

송수빈송수빈
4 min read

이번 프로젝트에서 모달을 별도의 컴포넌트로 분리해서 사용하고 있었다.
페이지나 상황에 따라 모달을 띄우기 위해, 컴포넌트에서 직접 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을 관리하고 있다니..! 굉장히 비효율적이라고 생각했다.
구체적으로 어떤 부분이 비효율적이라고 느꼈냐고 묻는다면

  1. 중복 코드가 많아진다
    → 여러 컴포넌트에서 모달 상태를 따로 관리하니까 관리가 분산되고 귀찮아짐 ‼️

  2. 상태 공유가 어렵다
    → 예를 들어 어떤 이벤트에서 모달 열고 닫는 상태를 다른 컴포넌트가 알아야 할 때 어려움이 있을수 밖에 없다.

  3. 복잡도가 늘어난다
    → 프로젝트가 커지면 상태가 여기저기 흩어져서 유지보수성 저하되는 이슈 발생

  4. 사용 불편

    → 매번 useStateonClose, 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 훅 하나로 모달 상태 열기/닫기 기능을 손쉽게 호출 가능해서 편했다.

  • 컴포넌트마다 모달 상태 코드를 작성하지 않아도 되어서 코드가 엄청 깔끔해짐 !

0
Subscribe to my newsletter

Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

송수빈
송수빈