thumbnail

React 상태관리 zustand로 Modal 관리하기

생성일2023. 5. 2.
태그
작성자지한솔

안녕하세요, 오늘은 전역 상태관리 라이브러리로 modal을 관리해보도록 하겠습니다.
왜 모달을 전역상태로 관리하나요? 라고 물어볼 수 있지만.. 그렇게 하면 더 편할 거 같아서..? 라는 생각에… 혹시나 가장 최적의 방법을 알고계시다면 언제든지 공유 부탁드립니다.

zustand란?

zustand[공식문서] 를 보면 정말 간단하게 설명되어있습니다. react 상태관리로는 주로 recoil이나, redux를 사용하는데 zustand의 공식문서에는 다음과 같이 설명합니다.

왜 redux 대신 zustand를 사용하나요?

  • 간단하고 unopinionated함
  • 훅을 상태 소비의 주요한 수단으로 사용함
  • 컨텍스트 프로바이더가 필요 없음

왜 context 대신 zustand를 사용하나요?

  • 적은 상용구
  • 값이 바뀔 때만 렌더링함. (기본 deepEqual)
  • 중앙화된 액션 베이스 상태 관리
이걸 보고 zustand는 참 자신감이 있고 당당하구나 라고 느꼈는데, 사용해보니 왜 그렇게 자신있었는지 알 수 있었습니다.
일단 너무 쉽고, 간단하고 추가적인 설정이 따로 필요 없다는게 러닝커브도 그렇고 매우 적을 것 같았습니다.

zustand 로 커스텀 모달 만들어보기

모달 만들기

저는 모달을 만들때 가장 기본이 되는 모달 템플릿을 하나 만들어둡니다. 큰 프로젝트일 경우 모달의 종류가 다양하고, 버튼의 형식, 바디의 내용 등 또한 다를 것입니다. 그렇기 때문에 모달에서 가장 기본이 되는 템플릿을 하나 마련해둔다면 사용할때 훨씬 큰 이득을 볼 수 있을겁니다.
import { useCallback, useEffect, useState } from "react"; interface ModalProps { isOpen: boolean; onClose: () => void; body?: React.ReactElement; } export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, body }) => { const [showModal, setShowModal] = useState(isOpen); useEffect(() => { setShowModal(isOpen); }, [isOpen]); const handleClose = useCallback(() => { setShowModal(false); setTimeout(() => { onClose(); }, 300); }, [onClose]); if (!isOpen) { return null; } return ( <> <S.Container> <S.Dim onClick={handleClose}> <S.Modal showModal={showModal} onClick={(e) => e.stopPropagation()}> <S.Header> <S.CloseBtn onClick={handleClose}>x</S.CloseBtn> </S.Header> {body} </S.Modal> </S.Dim> </S.Container> </> ); };
간단하게 만든 modal입니다. 모달은 Dim 영역도 있고, Dim 영역을 클릭시 modal이 닫히는 구조로 되어있습니다. <S>는 styled-component로 생성한 style로 무시하셔도 되고 대충 구조가 컨테이너 안에 dim과 modal이 있다 정도로 이해해주셔도 되겠습니다.
그럼 이 모달을 어떻게 사용할까요? 사용하기 전에 우리는 custom hook을 사용해서 zustand 전역 모달 상태를 추가하도록 하겠습니다.

zustand 사용하기

npm install zustand # or yarn add zustand
를 통해 먼저 zustand 를 설치해줍니다. 설치한 zustand를 활용한 hook useDetailModal() 을 만들면 다음과 같이 만들 수 있습니다.
import { create } from "zustand"; interface TDetailModalStore { isOpen: boolean; onOpen: (id: string) => void; onClose: () => void; } const useDetailModal = create<TDetailModalStore>((set) => ({ isOpen: false, onOpen: () => set({ isOpen: true }), onClose: () => set({ isOpen: false }), })); export default useDetailModal;
만들어진 useDetailModal을 detailmodal.tsx에서 불러 사용해보도록 하겠습니다.
import { useEffect, useMemo, useState } from "react"; import useDetailModal from "../../hook/useDetailModal"; import { Modal } from "./Modal"; interface CarDetailsProps { carDetail: TCarDetail; } const Detail = () => { return ( <> detail </> ); }; export const CarDetailModal = () => { const detailModal = useDetailModal(); return ( <Modal isOpen={detailModal.isOpen} onClose={detailModal.onClose} body={<Detail carDetail={detail} />} /> ); };
자 이렇게 modal 템플릿을 만들어 둔 것을 불러와 사용한 케이스 입니다. 여기엔 사실 header와 footer, action, secondAction 등의 메소드들도 들어갈 수 있을겁니다.
import useDetailModal from "../hook/useDetailModal"; export const Home = () => { const carDetailModal = useCarDetailModal(); return ( <> <button onClick={() => carDetailModal.onOpen()}>모달 열기</button> </> ); };
이렇게 필요한 곳에서 hook을 불러와 사용할 수 있을겁니다. 어떤가요 상당히 클린하고, 깔끔하고, 편하지 않나요? zustand가 깃허브에서 당당하던 이유가 여기에 있는 것 같습니다.
주로 recoil의 familySelector를 주로 사용했었는데, zustand도 한번 고려해봐야겠습니다.

결론

전역 상태의 모달을 관리하는게 좋을까? 라는 생각을 하였습니다. 하지만 상당히 쉽고, 간단하게 처리할 수 있다는 사실을 알게되었고 next.js와 같은 환경에서 client only 컴포넌트들을 만들어 사용한다면 ssr과 csr을 적절히 사용하는 프로젝트를 만들 수 있지 않을까 생각해봅니다.