개발공부

[React] Context Api와 createPortal로 모달 핸들러 만들기

떡볶이가 최고야 2024. 3. 20. 16:04

Context Api와 createPortal을 사용하여 효율적으로 모달을 관리하는 방법을 정리해 보겠습니다.

 

Context로 모달을 제어하면 어떤 점이 좋을까?

  • 여러개의 모달을 사용할 경우 각각 열고 닫는 상태를 만들어주지 않아도 됩니다.
  • 부모의 상태 개입없이 독립적으로 모달을 열고 닫을 수 있습니다.
  • 한 곳에서 모달을 관리하여 유지보수성이 높아집니다.

 

구현 방법 정리

  1. createContext - DispatchContext(모달 열고 닫는 기능), ModalStateContext(모달) 생성
  2. ModalProvider - openedModals 상태 관리, open, close 함수 작성
  3. Modals - createPortal을 이용해 오픈된 모달을 렌더링하는 로직 
  4. CustomModal - 모달 컴포넌트 작성
  5. App - 모달 open 버튼을 넣을 컴포넌트

 

 

Step 1

Context 간단 사용법
1. createContext 메서드를 사용해 context를 생성합니다.
2. 생성된 context를 가지고 context provider로 컴포넌트 트리를 감쌉니다.
3. value prop을 사용해 context provider에 원하는 값을 입력합니다.
4. useContext로 구독할 Context를 호출하여 value prop으로 입력했던 값을 사용합니다.

 

모달을 열고 닫는 기능을 하는 Dispatch Context와 모달 컴포넌트를 관리할 ModalsStateContext를 생성합니다.

import { createContext } from 'react';

export const ModalsDispatchContext = createContext({
  open: (Component, props) => {},
  close: (Component) => {},
});

export const ModalsStateContext = createContext([]);

 

Step 2

생성된 Context의 Provider로 원하는 컴포넌트 트리를 감싸고 관리할 값을 value prop에 입력합니다.

여기서는 컴포넌트와 props를 인수로 받아서 openModals라는 배열에 넣는 open함수와 배열에서 제거하는 close함수를 dispatch로 묶어서 DispatchContext의 value prop으로 입력하였고,

openedModals배열을 ModalsStateContext의 value prop으로 입력했습니다.

Modals에서 openedModals를 렌더링하는 로직을 사용할 것이기 때문에 Modals를 감싸주었습니다.

import React, { useMemo, useState } from 'react';
import { ModalsDispatchContext, ModalsStateContext } from './ModalsContext';
import Modals from './Modals';

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

  const open = (Component, props) => {
    setOpenedModals((prevModals) => {
      return [
        ...prevModals,
        {
          Component,
          props,
          isOpen: true,
        },
      ];
    });
  };

  const close = (Component) => {
    setOpenedModals((prevModals) => {
      return prevModals.filter((item) => item.Component !== Component);
    });
  };

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

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

export default ModalsProvider;

 

위에서 생성한 ModalsProvider는 main.jsx에서 다음과 같이 사용됩니다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import ModalsProvider from "./ModalsProvider";

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

 

Step 3

Modals컴포넌트에서 ModalsStateContext value prop으로 입력했던 값을 가져와서 사용할 것입니다.

openedModals라는 배열과 close함수를 가져왔고, createPortal를 이용해서 모달을 document.body에 렌더링하게 해줍니다.

 

createPortal의 역할
부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 일부 자식 요소들을 렌더링할 수 있게 해줍니다. 

 

import React, { useContext } from 'react';
import ReactDom from 'react-dom';
import { ModalsDispatchContext, ModalsStateContext } from './ModalsContext';

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

  return ReactDom.createPortal(
    <div className={'modal-wrapper'}>
      {openedModals.map((modalInfo, index) => {
        const { Component, props, isOpen } = modalInfo;
        const onClose = () => {
          console.log('닫기');
          close(Component);
        };
        return (
          <Component key={index} isOpen={isOpen} onClose={onClose} {...props} />
        );
      })}
    </div>,
    document.body
  );
};

export default Modals;

 

 

Step 4

모달을 열고 닫는 기능을 하는 useModals라는 hook을 따로 작성합니다.

openModal을 실행하면 해당 인수들을 객체로 하여 openedModals 배열에 추가되는 것이고, close를 실행하면 배열에서 제거됩니다.

import { useContext } from 'react';
import { ModalsDispatchContext } from '../ModalsContext';

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

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

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

  return { openModal, closeModal };
}

 

 

Step 5

설정은 이제 끝났습니다. 사용해봅시다! 모달을 오픈하는 버튼에 onClick 값으로 CustomModal과 관련 props를 전달합니다.

이렇게 하면 열기 버튼을 클릭하면 모달이 열립니다.

import useModals from './hooks/useModal';
import React from 'react';
import CustomModal from './components/CustomModal.jsx';

export default function App() {
  const { openModal } = useModals();

  const handleOnClick = () => {
    openModal(CustomModal, { name: 'World' });
  };

  return (
    <div className="App">
      <h1>context api로 모달 열기</h1>
      <button onClick={handleOnClick}>열기 버튼</button>
    </div>
  );
}

 

 

CustomModal은 react-modal을 이용해 만들었습니다. 

import ReactModal from 'react-modal';
import React from 'react';

const CustomModal = ({ isOpen, onClose, ...props }) => {
  const handleClose = () => {
    onClose();
  };

  return (
    <ReactModal isOpen={isOpen} ariaHideApp={false}>
      <h1>Hello CustomModal</h1>
      <button onClick={handleClose}>닫기</button>
    </ReactModal>
  );
};

export default CustomModal;

 

 

⬇️ 아래 링크로 자세한 소스 코드를 볼 수 있습니다.

https://stackblitz.com/edit/vitejs-vite-yjqybo?file=README.md

 

 

 

 

2024년 3월 원티드 프리온보딩 강의 내용을 바탕으로 이해한 내용을 정리한 글입니다.