dwook.record
Published on

React Portal로 Modal 만들기. useModal 훅 만들기

Authors
  • avatar
    Name
    dwook

Table of Contents

React Portal

  • Portal은 부모 컴퍼넌트의 DOM 계층 외부에 있는 DOM 노드로 자식을 렌더링하는 방법
  • 첫 번째 인자로 리액트 컴퍼넌트를 받고, 두 번째 인자로 리액트 컴퍼넌트를 넣은 DOM을 받음
ReactDOM.createPortal(child, container)

일반적인 모달 만들기

pages/_app.tsx
const app = ({ Component, pageProps }: AppProps) => {
  return (
    <>
      <GlobalStyle />
      <Header />
      <Component {...pageProps} />
      <div id="root-modal" />
    </>
  );
};
components/Mo
import React, { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import styled from "styled-components";

const Container = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 11;
  .modal-background {
    position: absolute;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.75);
  }
`;

interface ModalProps {
  children: React.ReactNode;
  closePortal: () => void;
}

const ModalPortal: React.FC<ModalProps> = ({ children, closePortal }) => {
  const ref = useRef<Element | null>();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
    if (document) {
      const dom = document.querySelector("#root-modal");
      ref.current = dom;
    }
  }, []);

  if (ref.current && mounted) {
    return createPortal(
      <Container>
        <div
          className="modal-background"
          role="presentation"
          onClick={closePortal}
        />
        {children}
      </Container>,
      ref.current
    );
  }
  return null;
};

export default ModalPortal;
components/Header.tsx
import ModalPortal from "./ModalPortal";
import SignUpModal from "./auths/SignUpModal";

{
    modalOpened && (
        <ModalPortal>
            <SignUpModal />
        </ModalPortal>
    )
}

재사용성을 높인 useModal

  • 모달을 사용하기 위해 부모에 상태를 하나 만들어야하고, ModalPortal에 props로 모달을 닫는 함수를 전달하는 일은 번거롭기 때문에 커스텀 훅으로 만들기.
  • ModalPortal 컴퍼넌트를 리턴하기 때문에 .tsx 파일
hooks/useModal.tsx
import React, { useRef, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import styled from "styled-components";

const Container = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 11;
  .modal-background {
    position: absolute;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.75);
  }
`;

const useModal = () => {
  const [modalOpened, setModalOpened] = useState(false);

  const openModal = () => {
    setModalOpened(true);
  };

  const closeModal = () => {
    setModalOpened(false);
  };

  interface ModalProps {
    children: React.ReactNode;
  }

  const ModalPortal: React.FC<ModalProps> = ({ children }) => {
    const ref = useRef<Element | null>();
    const [mounted, setMounted] = useState(false);

    useEffect(() => {
      setMounted(true);
      if (document) {
        const dom = document.querySelector("#root-modal");
        ref.current = dom;
      }
    }, []);

    if (ref.current && mounted && modalOpened) {
      return createPortal(
        <Container>
          <div
            className="modal-background"
            role="presentation"
            onClick={closeModal}
          />
          {children}
        </Container>,
        ref.current
      );
    }
    return null;
  };

  return {
    openModal,
    closeModal,
    ModalPortal,
  };
};

export default useModal;
components/HeaderAuths.tsx
import useModal from "../hooks/useModal";

const HeaderAuths: React.FC = () => {
  const { openModal, ModalPortal, closeModal } = useModal();

  return (
    <>
      <div className="header-auth-buttons">
        <button
          className="header-sign-up-button"
          onClick={() => {
            dispatch(authActions.setAuthMode("signup"));
            openModal();
          }}
          type="button"
        >
          회원가입
        </button>
        <button
          className="header-login-button"
          type="button"
          onClick={() => {
            dispatch(authActions.setAuthMode("login"));
            openModal();
          }}
        >
          로그인
        </button>
      </div>
      <ModalPortal>
        <AuthModal closeModal={closeModal} />
      </ModalPortal>
    </>
  );
};

export default HeaderAuths;
import React, { FC, PropsWithChildren, useCallback } from 'react';

interface Props {
  show: boolean;
  onCloseModal: () => void;
}

const Modal: FC<PropsWithChildren<Props>> = ({ show, children, onCloseModal }) => {
  const stopPropagation = useCallback((e) => {
    e.stopPropagation();
  }, []);

  if (!show) {
    return null;
  }
  return (
    <div onClick={onCloseModal}>
      <div onClick={stopPropagation}>
        <button onClick={onCloseModal}>&times;</button>
        {children}
      </div>
    </div>
  );
};

export default Modal;