Context API를 사용해 React-modal 효율적으로 관리하기

dev_hyun·2023년 11월 7일
6

개요

모달은 프론트 개발에 빼놓을 수 없는 UI 컴포넌트이다.
디자인이 나오면 모달로 다양한 요소들이 배치되어서 나오게 된다.
모달을 구현하는 것 보다는 모달을 어떻게 하면 효율적으로 관리할 수 있는지에 대한 방법을 적어보려고 한다.

모달은 react-modal 라이브러리를 사용했고, react 내장 함수인 context API를 사용했다.

참고한 블로그 : https://nakta.dev/how-to-manage-modals-1

참고한 블로그에서 내 프로젝트에 맞게 좀더 개편했다.(typescript 적용, 모달 애니메이션 효과 및 기본 라이브러리 기능 사용)

1. 모달 상태 관리하기

import { ComponentType, HTMLAttributes, createContext } from "react";
export type ModalComponent = ComponentType<any>;
export type ModalProps = Record<string, any> | HTMLAttributes<HTMLDivElement>;

export type ModalsState = Array<{ Component: ModalComponent; props?: ModalProps; isOpen: boolean }>;
export const ModalsStateContext = createContext<ModalsState>([]);

type ModalsDispatch = {
  open: (Component: ModalComponent, props: ModalProps) => void;
  close: (Component: ModalComponent) => void;
};
export const ModalsDispatchContext = createContext<ModalsDispatch>({
  open: () => {},
  close: () => {},
});

모달의 상태를 관리하는 컨택스트인ModalsStateContext 와 모달의 기능부분을 담당하는 컨택스트인 ModalsDispatchContext 를 만들어 준다.

이제 context를 주입시켜줄 provider를 만들어줘야하는데 다음과 같다.

const disableScroll = () => {
  document.body.style.cssText = `
  position: fixed; 
  top: -${window.scrollY}px;
  overflow-y: scroll;
  width: 100%;`;
};

const ableScroll = () => {
  const scrollY = document.body.style.top;
  document.body.style.cssText = "";
  window.scrollTo(0, parseInt(scrollY || "0", 10) * -1);
};

const ModalsProvider = ({ children }: PropsWithChildren) => {
  const [openedModals, setOpenedModals] = useState<ModalsState>([]);

  const open = (Component: ModalComponent, props: ModalProps) => {
    disableScroll();
    setOpenedModals((modals) => {
      const modalIndex = modals.findIndex((modal) => modal.Component === Component);
      if (modalIndex !== -1) {
        // 모달이 이미 배열에 있는 경우, 해당 모달의 isOpen 값을 true로 변경
        modals[modalIndex].isOpen = true;
        modals[modalIndex].props = props;
        return [...modals];
      }
      return [...modals, { Component, props, isOpen: true }];
    });
  };

  const close = (Component: ModalComponent) => {
    ableScroll();
    setOpenedModals((modals) =>
      modals.map((modal) => (modal.Component === Component ? { ...modal, isOpen: false } : modal)),
    );
  };

  const dispatch = useMemo(() => ({ open, close }), []);

  return (
    <ModalsStateContext.Provider value={openedModals}>
      <ModalsDispatchContext.Provider value={dispatch}>
        {children}
        <Modals />
      </ModalsDispatchContext.Provider>
    </ModalsStateContext.Provider>
  );
};
export default ModalsProvider;

openedModals은 모달들을 관리하는 상태이다. 즉, 모달 컴포넌트, 모달의 props, 열려있는지의 여부(isOpen) 3가지를 배열로 가지는 상태이다. 모달을 열고 닫는 함수의 구현은 그렇게 어렵지 않다. isOpen을 각 모달별로 관리해주어야 react-modal 라이브러리의 기능을 사용할 수 있다.

  const open = (Component: ModalComponent, props: ModalProps) => {
    disableScroll();
    setOpenedModals((modals) => {
      const modalIndex = modals.findIndex((modal) => modal.Component === Component);
      if (modalIndex !== -1) {
        // 모달이 이미 배열에 있는 경우, 해당 모달의 isOpen 값을 true로 변경
        modals[modalIndex].isOpen = true;
        modals[modalIndex].props = props;
        return [...modals];
      }
      return [...modals, { Component, props, isOpen: true }];
    });
  };

open 함수는 모달이 열렸을 때 scroll을 막는 disableScroll()과 이미 있는 모달 컴포넌트가 아닐때만 open을 true로 만들어주는 로직을 담고 있다.

const close = (Component: ModalComponent) => {
    ableScroll();
    setOpenedModals((modals) =>
      modals.map((modal) => (modal.Component === Component ? { ...modal, isOpen: false } : modal)),
    );
  };

close 함수는 더 간단하다. disable한 스크롤을 다시 able시키는 ableScroll()과 해당 모달 컴포넌트의 isOpen 속성을 false로 해서 다시 모달을 닫아주면 된다.

Modals 컴포넌트

<Modals/> 컴포넌트가 직접적으로 랜더링하는 부분인데 이부분은 다음과 같이 구현되어 있다.

export const modals = {
  // 사용할 모달들 
};

const Modals = () => {
  const openedModals = useContext(ModalsStateContext);
  const { close } = useContext(ModalsDispatchContext);

  return openedModals.map((modal, index) => {
    const { Component, props, isOpen } = modal;
    if (!props) return null;

    const { onSubmit, ...rest } = props;

    const onClose = () => {
      close(Component);
    };

    const handleSubmit = async () => {
      if (typeof onSubmit === "function") {
        await onSubmit();
      }
      onClose();
    };

    return <Component key={index} isOpen={isOpen} onClose={onClose} onSubmit={handleSubmit} {...rest} />;
  });
};
export default Modals;

provider에서 세팅한 열려있는 모달들을 꺼내와서 반복 랜더링을 시켜주는 로직이다.
openedModals를 돌리면서 해당 모달의 컴포넌트를 return해주고 props로 isOpen, onClose, onSubmit을 넘겨주고 그 외의 props는 모두 넘겨주면 된다.

이때 onSubmit은 확인 버튼을 눌렀을때 사용할 함수로 개발 프로젝트에 맞춰서 변경이 필요하면 변경을 하면 된다.

2. 모달 명령적으로 사용하기

위에서 만든 context 와 provider을 이제 적용하고 모달을 사용해야 할 때이다.

ReactModal.setAppElement("#root");

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
      <ModalsProvider>
          <App />
      </ModalsProvider>
  </React.StrictMode>,
);

react-modal라이브러에서 어디에 모달을 이동시킬지(#root)를 정한뒤 provider로 감싸주면 된다.

이제 모달을 사용하기 쉽게 hook을 만들 차례이다.

export function useModals() {
  const { open, close } = useContext(ModalsDispatchContext);

  const openModal = (Component: ModalComponent, props: ModalProps) => {
    open(Component, props);
  };

  const closeModal = (Component: ModalComponent) => {
    close(Component);
  };

  return {
    openModal,
    closeModal,
  };
}

ModalsDispatchContext 에서 만들어놓은 open, close함수를 가져와서 각각 모달을 열고 닫는 함수를 가진 hook을 만들어 주면 된다.

다음으로 모달을 어떻게 사용하면 되는지에 대한 예시이다.

export const Modal = ({ children, isOpen, onClose }: Props) => {
  return (
    <ReactModal
      isOpen={isOpen}
      contentLabel="modal"
      closeTimeoutMS={150}
      onRequestClose={onClose}
    >
     {children}
    </ReactModal>
  );
};

참고한 블로그에서는 모달이 열고 닫혔을 때 애니메이션을 넣을 수 없어서 변경한 사항 중 하나가 여기 있다.
react-modal을 사용할 때 isOpen을 항상 true로 하면 안되고 다음과 같이 isOpen을 받아야 하고
onRequestClose에 close 되는 함수를 props로 넣어주어야 한다. 마지막으로 closeTimeoutMS에도 시간을 넣어줘야 동작을 한다.

모달 open 하기

// 특정 컴포넌트에서 modal을 오픈하고 싶을 떄
const { openModal } = useModals();

  const clickHandler = (props: propsType) => {
    openModal(modals.someModal, {
      onSubmit: () => {}
      props,
      // .. 등등
    });
  };

이제 useModals 훅을 사용해서 오픈할 모달 컴포넌트와 원하는 props를 넘겨서 원하는 모달을 쉽고 간편하게 관리할 수 있게 된다.

modals.someModal<Modals/> 컴포넌트에서 설정한 모달 객체로 한번에 관리하기 위한 방법중 하나이다.

내가 구현한 방법은 상태관리 라이브러리를 사용하고 싶지 않아서 Context API를 도입했지만, recoil, zotai,zustand 와 같은 다른 전역 상태관리 라이브러리를 사용하더라도 로직은 같기에 구현에 크게 어려움은 있을 것 같지는 않다.

보다 자세한 구현 방법이나 이런 관리방법을 알게된 경로는 아까 위에 써놓은 참고 블로그를 먼저 읽고 보면 좋을 것 같다.

더 좋은 의견이 있다면 남겨주세요 :)

끗 🥳🥳🥳

profile
하다보면 안되는 것이 없다고 생각하는 3년차 프론트엔드 개발자입니다.

0개의 댓글