React

[React] 전역 모달 구현하기 :: 마이구미

mygumi 2021. 11. 3. 22:00
반응형
이 글은 React 에서 전역 모달을 구현하는 방법을 다룬다.
코드 예제는 React + Recoil + Typescript 로 구성되어있다.
Recoil 은 단순 모달 on/off 상태 관리 용도로 학습을 거의 요구하지 않는다. (코드는 3줄이다)
참고 글 - https://opensource.com/article/21/5/global-modals-react
코드 예제 - https://codesandbox.io/s/bitter-brook-rep1f?file=/src/recoil/modal.ts:530-621

 

이 글에서 "전역 모달" 을 의미하는 것은 전역에서 관리되는 상태를 가지는 모달이다.

반대로 지역으로 관리되는 상태를 가지는 모달을 확인해보자.

모달을 import 하고 모달의 on/off 를 관리하는 state 와  필요 시 props 을 요구하게 된다.

 

const Parent = () => {
  const [isModal, setIsModal] = useState();

  // ...
  
  return (
    <>
      <Modal isModal={isModal} />
      <Child items={items} setIsModal={setIsModal} />
    </>
  )
}

 

 

이게 뭐가 문제가 되는 것인가?

만약 모달이 window.alert() 이나 window.confirm() 과 같은 형태를 커스텀한 모달이라면 어떨까?

정말 많은 곳에서 사용하게 될 것이다.

흔히 다음과 같은 사례가 있다.

 

  • POST, PUT, DELETE 와 같은 API 요청한 후 성공이나 실패에 따른 얼럿창
  • 예기치 못한 에러 발생 시 얼럿창
  • Form 작성 도중 페이지 이동 시 컨펌창
  • 저장 버튼 클릭 시 컨펌창

 

 

 

window.alert(), window.confirm() 으로 한정적으로 보더라도 굉장히 많은 경우가 존재한다.

다른 곳에서도 모달이 필요하면 각 컴포넌트에서는 위처럼 매번 작성해야한다.

하지만 전역으로 관리되는 모달을 있다면, 매번 각 컴포넌트에서 위와 같은 코드를 작성할 필요가 없어진다.

 

코드 작성에 앞서, 전역 모달이 완성되면 달라지는 그림을 한번 미리 확인해보자.

모달 import 측면에서는 다음과 같다.

 

<Parent1>
  <ConfirmModal />
</Parent1>

<Parent2>
  <AlertModal />
</Parent2>

<Parent3>
  <ConfirmModal />
</Parent3>

 

전역 모달이 아니라면 각 컴포넌트에서는 필요한 모달을 각각 import 해야했다.

전역 모달이라면 다음과 같은 코드로 작성된다.

 

<>
  <GlobalModal />
</>

<Parent1>
  ...
</Parent1>

<Parent2>
  ...
</Parent2>

 

최상단 레벨에 미리 GlobalModal 을 import 하기 때문에 다른 곳에서는 할 필요가 없게 된다.

매번 선언하는 것이 아닌 최상단에 한번 선언해놓으면 된다.

추가로 GlobalModal 에서는 다양한 모달(ConfirmModal, AlertModal) 을 담고 있게 된다.

 

모달 on/off 제어 측면은 다음과 같다.

 

const Parent = () => {
  const [isModal, setIsModal] = useState();
  
  return (
    <>
      <Modal isModal={isModal} />
      <Child items={items} setIsModal={setIsModal} />
    </>
  )
}

 

 

위에서 언급한대로, 모달을 import 하고 모달의 on/off 를 관리하는 state 와  필요 시 props 을 요구하게 된다.

전역 모달이라면 다음과 같은 코드로 작성된다.

 

 

const Child = () => {
  const { showModal } = useModal();

  const handleClick = () => {
    showModal({ modalType: 'AlertModal' });
    // showModal({ modalType: 'ConfirmModal' });
  }
  
  return (
    <>
      <Button onClick={handleClick} />
    </>
  )
}

 

모달 import, state 없이 단순 커스텀 훅으로 된 showModal 함수를 통해 모달을 제어하게 된다.

 

크게 2가지 측면에서 달라지는 모습을 간략하게 살펴보았다.

이제는 전역 모달에 필요한 각각의 코드들을 하나씩 확인해보자.

 


 

(하단 결과물 존재 - CodeSandbox)

파일 기준으로는 다음과 같이 작성될 것이다.

 

  • GlolbalModal.tsx - 전역 모달 컴포넌트
  • AlertModal.tsx - 커스텀 얼럿창 컴포넌트
  • ConfirmModal.tsx - 커스텀 컨펌창 컴포넌트
  • useModal.ts - 커스텀 훅
  • modal.ts -  전역 상태 관리

 

GlobalModal.tsx 

 

import React from "react";
import { useRecoilState } from "recoil";
import ConfirmModal from "./ConfirmModal";
import AlertModal from "./AlertModal";
import { modalState } from "../recoil/modal";

export const MODAL_TYPES = {
  ConfirmModal: "ConfirmModal",
  AlertModal: "AlertModal"
} as const;

const MODAL_COMPONENTS: any = {
  [MODAL_TYPES.ConfirmModal]: ConfirmModal,
  [MODAL_TYPES.AlertModal]: AlertModal
};

const GlobalModal = () => {
  const { modalType, modalProps } = useRecoilState(modalState)[0] || {};

  const renderComponent = () => {
    if (!modalType) {
      return null;
    }
    const ModalComponent = MODAL_COMPONENTS[modalType];

    return <ModalComponent {...modalProps} />;
  };

  return <>{renderComponent()}</>;
};

export default GlobalModal;

 

  • MODAL_TYPES 는 다양한 모달을 의미하는 Key 용도이다.
  • MODAL_COMPONENTS 는 실제 다양한 모달 컴포넌트가 객체의 Key 에 따라 import 되어있다. 
  • useRecoilState 는 단순히 store 에 있는 모달의 state 라고 보면 된다.
  • 모달의 state 는 modalType, modalProps 으로 2가지로 구성되어있는 모습을 볼 수 있다.
  • modalType 은 MODAL_TYPES 의 타입으로 정의되어 "ConfrimModal" 또는 "AlertModal" 을 의미하고, modalProps 는 optional 로 모달 컴포넌트에 props 로 전달할 요소들이다.

 

renderComponent 함수에서 모달의 state 인 modalType 을 통해 특정 모달 컴포넌트를 선택하고 modalProps 를 컴포넌트에 넘겨주는 모습을 볼 수 있다.

즉, store 에 저장된 modalType 이 "AlertModal" 이라면 MODAL_COMPONENTS["AlertModal"] 을 의미하여 AlertModal 컴포넌트가 된다.

 

modal.tsx

 

import { atom } from "recoil";
import { MODAL_TYPES } from "../components/GlobalModal";
import { ConfirmModalProps } from "../components/ConfirmModal";
import { AlertModalProps } from "../components/AlertModal";

const { ConfirmModal, AlertModal } = MODAL_TYPES;

export interface ConfirmModalType {
  modalType: typeof ConfirmModal;
  modalProps: ConfirmModalProps;
}

export interface AlertModalType {
  modalType: typeof AlertModal;
  modalProps: AlertModalProps;
}

export type ModalType = ConfirmModalType | AlertModalType;

export const modalState = atom<ModalType | null>({
  key: "modalState",
  default: null
});

 

모달 state 를 관리하는 store 를 의미한다.

modalState 는 modalType, modalProps 2가지 값을 가지게 된다.

AlertModal 과 Confirm 모달이 가지는 modalProps 는 서로 다르다.

예를 들어 alertModal 은 확인용 버튼이 하나만 존재하면 되고, confirmModal 의 경우에는 확인, 취소 2가지 버튼을 제공해줘야한다.

그래서 ConfirmModalType, AlertModalType 각각 정의하여, 타입 추론을 더 명확하게 해줄 수 있다.

 

 

useModal.ts

 

import { useRecoilState } from "recoil";
import { ModalType, modalState } from "../recoil/modal";

export default function useModal() {
  const [modal, setModal] = useRecoilState(modalState);

  const showModal = ({ modalType, modalProps }: ModalType) => {
    setModal({ modalType, modalProps });
  };

  const hideModal = () => {
    setModal(null);
  };

  return {
    modal,
    setModal,
    showModal,
    hideModal
  };
}

 

유용하게 재사용에 도움을 주는 방법으로 커스텀 훅으로 store 에 있는 모달 state 를 가져와서 맵핑 하는 모습을 볼 수 있다.

커스텀 훅의 반환값을 보다시피 실제 컴포넌트에서는 showModal, hideModal 을 사용하게 된다.

 

App.tsx

 

import useModal from "./hooks/useModal";
import GlobalModal from "./components/GlobalModal";

export default function App() {
  const { showModal } = useModal();

  const handleClickAlertModal = () => {
    showModal({
      modalType: "AlertModal",
      modalProps: {
        message: "Success!"
      }
    });
  };

  const handleClickConfirmModal = () => {
    showModal({
      modalType: "ConfirmModal",
      modalProps: {
        message: "Yes or No",
        confirmText: "Yes",
        cancelText: "No",
        handleConfirm: () => {
          console.log("Yes!");
        },
        handleClose: () => {
          console.log("No!");
        }
      }
    });
  };

  return (
    <div className="App">
      ...
    </div>
  );
}

 

전역으로 모달을 관리하는 것이 상황에 따라 복잡함 없이 더 좋은 코드로 작성할 수 있게 된다.

전체 예제 코드를 확인해보면 더 도움이 될 것이다.

 

 

 

 

 

 

반응형