안녕하세요, 오늘은 전역 상태관리 라이브러리로 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을 적절히 사용하는 프로젝트를 만들 수 있지 않을까 생각해봅니다.


댓글 열기