Skip to main content

Command Palette

Search for a command to run...

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

Updated
4 min readView as Markdown
[북잡 프로젝트] 모달창 상태 관리 중앙화하기(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을 관리하고 있다니..! 굉장히 비효율적이라고 생각했다.
구체적으로 어떤 부분이 비효율적이라고 느꼈냐고 묻는다면

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

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

More from this blog

[React] 클라이언트에서 이미지를 압축해보자

우리는 서버 비용을 직접 부담하고 있기 때문에 항상 최적화와 비용 절감 방법을 고민하게 된다. 현재 자유게시판이 활성화가 많이 되지는 않았지만 사용자들이 업로드하는 이미지가 평균 5MB 이상일 경우를 대비해서 클라이언트에서 이미지 압축을 진행했다. 웹에서는 크게 두 가지 방법이 있었는데 Canvas API를 직접 사용하는 방법 browser-image-compression 라이브러리를 활용하는 방법 이 있다. 여러 장점을 고려해 brow...

Oct 8, 20252 min read
[React] 클라이언트에서 이미지를 압축해보자

[책 추천] 프론트엔드 개발자라면 반드시 알아야 할 '웹 접근성' 이야기

내가 생각하는 프론트엔드 개발자는 단순히 보이는 화면만 구현하는 것을 넘어서 누구나 접근할 수 있는 웹을 만드는 데 중요한 역할을 해야 한다고 생각한다. 실무에 바로 적용하는 웹 접근성 가이드북 이번에 이 책을 받고 프엔들이 읽기에 너무 좋다고 생각해서 블로그에 추천 글까지 적게 되었다 ! 목차 Chatper1. 쉽게 이해하는 접근성 Chapter2. 웹 접근성의 기초 Chapter3. HTML 태그, 의미 있게 사용하기 Chpater4...

Jul 27, 20255 min read
[책 추천] 프론트엔드 개발자라면 반드시 알아야 할 '웹 접근성' 이야기

[Next.js] 나만의 학습 블로그 만들기#3-다국어 지원 (Feat.next-i18next)

오늘은 다국어 지원 가능한 기능 구현 과정을 적어보려고 한다. 원래는 댓글기능보다 먼저 구현하려고 했는데 ,, 갑자기 댓글기능 알아보다가 재밌어서 먼저 끝내버렸다,,,🙄 시작하기 전에 정말 어려웠다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 어려움이 많아서 기능 적용하는 데 시간이 꽤 걸렸다 ,,, 다국어 지원 기능 나는 영어랑 일본어를 추가해줬다 ! (해외취업도 관심 있기 때문에 나중에 이력서 낼 때 도움이 되지 않을까? ..🤭) 다국어 구현 방법 구현 방...

Jul 15, 20254 min read
[Next.js] 나만의 학습 블로그 만들기#3-다국어 지원 (Feat.next-i18next)

[Next.js] 나만의 학습 블로그 만들기#4 - 댓글 기능(Feat. Giscus)

댓글기능이 있으면 무조건 재밌을 거 같아서 넣어보려고 한다🤭 아무래도 개발에 관련된 학습 블로그라서 개발자들이 많을 것으로 예상했다 ! 그래서 깃허브 이슈 기반 댓글 시스템을 기반으로 기능을 구현해보려고 한다. 👉🏻 도움이 많이 된 블로그 ! 방법 Giscus | (https://giscus.app) utterances | (https://utteranc.es) 알아봤을 때는 이렇게 두 개가 유명하다고 한다. 그럼 각각의 특징과 장...

Jul 14, 20253 min read
[Next.js] 나만의 학습 블로그 만들기#4 - 댓글 기능(Feat. Giscus)

[React] React + localStorage로 하루에 한 번만 보이는 팝업 만들기

이번 프로젝트에 설문조사 배너를 제작했는데… 흠… 뭔가 별로라는 의견이 많았다! (팀원들 의견) 🙄 디자인이 문제일까? 해서 래퍼런스를 많이 찾아봤는데 … ! 일반 웹사이트는 설문조사 배너 자체를 안 만들어요,,,🤭 충격 그 자체 ( 난 아직 멀었음 ) 그리고 알아보니깐.. 설문조사 배너는 사용자 경험을 해치기 쉬워 잘 쓰이지 않는다고 한다! 그래서 나는 모달 팝업 방식으로 전환하기로 결정했고 실무에서 자주 사용하는 패턴으로 구현했다. 방...

Jul 9, 20253 min read
[React] React + localStorage로 하루에 한 번만 보이는 팝업 만들기
S

subin-dev-blog

27 posts

끊임없이 배우기 위해 노력합니다. 🌱