프론트 공부

리액트에서 주스탄드로 모달 전역적 관리하기

홍구리당당 2024. 1. 16. 22:40

0. 문제 상황

지금 만들고 있는 프로젝트에선 모달 종류가 굉장히 많다.

간단히 말하자면, 웹사이트 링크를 모아서 북마크하고 모아두는 웹 사이트인데, (그냥 즐겨찾기의 시각화.. 정도임.)

폴더를 생성, 삭제, 수정하는 모달, link를 각 폴더에 추가, 삭제, 수정하는 모달 등, 큼직한 모달창이 6개씩이나 있다.

이걸 각 컴포넌트에서 각자 관리하려니 코드가 너무 복잡해지고, 관리하기 힘들어지는 느낌이었다.

그래서 이번에는 전역적으로 모달을 관리하는 코드를 짜보려 한다.

미리 만든 모달 디자인. ㅎㅎ 이쁘다.

1. 과정

전역적으로 모달을 관리하기 위해서는,

  1. useContext 를 활용해 전역 상태 관리.
  2. client side 상태 관리 라이브러리를 활용하기. (예를 들면 zustand)

간단한 코드라 useContext를 써도 좋지만, 공부용으로 zustand를 써보기로 했다. (근데 너무 간단해서 공부라 하기도 민망..)

일단 전역적으로 블린형 상태 isModalOpenshowModal, hideModal 이 세 개를 관리하기로 했다.

그리고 각 컴포넌트에서 모달 컴포넌트를 호출하되, 모달을 조건부렌더링할 isModalOpen 상태와 모달을 켜고 닫을 showModal, hideModal 값들을 주스탄드의 store에서 불러오기로 했다.

처음엔 아예 showModal의 인자로 모달 이름을 넣어주고, 그에 해당하는 모달을 띄우도록 하려 했다. 예를 들어 FolderPage 컴포넌트에서 showModal(FolderAddModal) 코드를 쓰면 FolderAddModal이 호출되고, SettingPage에서 showModal(ProfileEditModal) 코드를 쓰면 ProfileEditModal이 호출되게끔 말이다.

근데 모달마다 필요한 인자값들이 달라서 오히려 코드가 복잡해졌다.

그래서 그냥 모달을 보여주는 데에만 관여할 isModalOpenshowModal, hideModal만 쓰기로 결정했다.

그럴거면 차라리 훅을 쓰면 되지 않겠냐 할 수 있지만,

보통은 모달창을 띄우면 그 뒤엔 반투명한 검은 배경이 덧씌워지고 보통은 뒷배경을 누르면 모달이 꺼지는 기능이 있다. 그런데 이 기능을 훅으로 구현하려니 hideModal 함수를 인자로 쭉쭉 내려줘야 해서, props drilling 문제가 발생했다!

2. 내 환경

next js 14, typescript 바탕으로 프로젝트 작성.
zustand 라이브러리 활용.

3. 코드

먼저 주스탄드 store은 이렇게 짰다.

  • 바보같이 isModalOpen만 있으면 될줄 알았는데,... 한 페이지 안에서 여러 모달을 띄워야 할 경우, 어떤 모달을 띄울지 체크해야 한다.

그래서 modalName을 넣어서, 어떤 모달을 띄울지 체크하게 했다. ㅋㅋ ㅠㅠ

import { create } from "zustand";

type ModalStoreState = {
  isModalOpen: boolean;
  modalName: string;
  showModal: (s:string) => void;
  hideModal: () => void;
};

export const useModalStore = create<ModalStoreState>()((set) => ({
  isModalOpen: false,
  modalName: "",
  showModal: (s:string) => {
    set((state) => ({
      modalName: s,
      isModalOpen: !state.isModalOpen,
    }));
  },
  hideModal: () => {
    set(() => ({
      isModalOpen: false,
    }));
  },
}));

모달 뒷배경 코드는 이렇다.

import { PropsWithChildren } from "react";
import styles from "./ModalBackground.module.scss";
import { useModalStore } from "@/store/useModalStore";

function ModalBackground() {
  const hideModal = useModalStore((state) => state.hideModal);

  return (
    <div onClick={() => hideModal()} className={styles["background"]}></div>
  );
}

function ModalContainer({ children }: PropsWithChildren) {
  return <div className={styles["container"]}>{children}</div>;
}

export { ModalBackground, ModalContainer };

모달창 컴포넌트는 이렇게 만든다.

import { PropsWithChildren } from "react";
import ReactDom from "react-dom";

import { ModalBackground, ModalContainer } from "./ModalBackground";

const ModalCreator = ({ children }: PropsWithChildren) => {
  const modalDiv = document.getElementById("modal-root") as HTMLDivElement;
  if (!modalDiv) return;

  // children 이 바로 모달창 내용물이 들어갈 자리.
  return ReactDom.createPortal(
    <>
      <ModalContainer>{children}</ModalContainer>
      <ModalBackground />
    </>,
    modalDiv,
  );
};

export default ModalCreator;

마지막으로 컴포넌트에서 호출할 땐 이렇게 쓴다.

/*FolderMaker 컴포넌트:
  AddFolderModal 모달을 띄우는 버튼 컴포넌트.
*/
import FolderAddModal from "@/modals/FolderAddModal/FolderAddModal";
import { useModalStore } from "@/store/useModalStore";

import styles from "./FolderMaker.module.scss";

function FolderMaker() {
  const isModalOpen = useModalStore((state) => state.isModalOpen);
  const modalName = useModalStore((state) => state.modalName);
  const showModal = useModalStore((state) => state.showModal);

  return (
    <div className={styles["container"]}>
      {isModalOpen && (modalName === "FolderAddModal") && <FolderAddModal />}
      <button
        id="folderCreateButton"
        onClick={() => showModal("FolderAddModal")}
        className={styles["folder-maker-button"]}
      >
        폴더 추가 +
      </button>
    </div>
  );
}

export default FolderMaker;

그리고 ModalBackground 컴포넌트에다 div onClick={()=>hideModal()} 코드를 적어주면 잘 작동한다!!!

 

3. 느낀 점

주스탄드 진심 레전드 편리.