• [React] 전역 모달 구현하기 :: 마이구미
    React 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>
      );
    }

     

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

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

     

     

     

     

     

     

    반응형

    댓글

Designed by Tistory.