Next.js 13 createPortal 로 모달 구현

버건디·2023년 6월 27일
3

Next.js

목록 보기
40/52
post-thumbnail
post-custom-banner

1. layout.tsx 에서 포탈 div 만들어주기

<html lang="en" className={nanumGothicFont.className}>
      {/*
        <head /> will contain the components returned by the nearest parent
        head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
      */}
      <head />
      <body suppressHydrationWarning={true}>
        <script
          dangerouslySetInnerHTML={{
            __html: themeInitializerScript,
          }}
        ></script>
        <Card>
          <Header />
          {children}
			// 포탈 위치
          <div id="portal"></div>
        </Card>
      </body>
    </html>

2. 해당 모달을 띄어줄 컴포넌트 만들기

"use client";

import classes from "./Login.module.css";
import { BiLogIn } from "react-icons/bi";
import { createPortal } from "react-dom";
import { useState, useEffect } from "react";
import LoginModal from "@/components/Modal/LoginModal";

export default function Login() {
  let [loginModal, setLoginModal] = useState(false);
  let [portalElement, setPortalElement] = useState<Element | null>(null);

  useEffect(() => {
    setPortalElement(document.getElementById("portal"));
  }, [loginModal]);

  const loginModalHandler = () => {
    setLoginModal(!loginModal);
  };

  return (
    <>
      <div className={classes.login_div} onClick={loginModalHandler}>
        <h2>로그인</h2>
        <BiLogIn className={classes.login_icon} />
      </div>
      {loginModal && portalElement
        ? createPortal(<LoginModal />, portalElement)
        : null}
    </>
  );
}
  1. useState를 통해서 초깃값을 false로 만들어 준후, 로그인 버튼을 누르면 loginModal 상태값이 true가 되면서 createPortal이 작동된다.

저 useEffect를 사용해서 portalElement에 document.getElementById("portal")를 넣어주는것이 중요하다.

그렇지 않으면 document is not defined 라는 에러가 발생한다.

document는 브라우저에 의존적인 요소이므로 SSR 에서는 렌더링 될수가 없다.

그렇기 때문에 useEffect 를 통해 렌더링 후에 portalElement를 업데이트 해주면 된다.

- Backdrop.tsx

import classes from "./Backdrop.module.css";

type BackDropType = {
  children: React.ReactNode;
  loginModalHandler: () => void;
};

export default function Backdrop({
  children,
  loginModalHandler,
}: BackDropType) {
  return (
    <div className={classes.backdrop} onClick={loginModalHandler}>
      {children}
    </div>
  );
}

- loginModal.tsx

import classes from "./LoginModal.module.css";
import Backdrop from "./Backdrop";

type LoginModalProps = {
  loginModalHandler: () => void;
};

export default function LoginModal({ loginModalHandler }: LoginModalProps) {
  return (
    <>
      <Backdrop loginModalHandler={loginModalHandler}>
        <div className={classes.login_modal_container}></div>
      </Backdrop>
    </>
  );
}

정상적으로 모달창이 등장했다.


로그인창과 환경설정 모달이 총 2개가 있어서, useModal 이라는 커스텀 훅을 만들어주었다.

- use-modal.ts

"use client";

import { useState, useEffect } from "react";

type UseModalReturn = [boolean, () => void, Element | null];

const useModal = (): UseModalReturn => {
  let [modal, setModal] = useState(false);
  let [portalElement, setPortalElement] = useState<Element | null>(null);

  useEffect(() => {
    setPortalElement(document.getElementById("portal"));
  }, [modal]);

  const modalHandler = () => {
    setModal(!modal);
  };

  return [modal, modalHandler, portalElement];
};

export default useModal;

또한 모달이 생성 됐을때, 백그라운드 스크롤을 막아주었다.

// utils/scroll.ts

export const preventScroll = () => {
  const currentScrollY = window.scrollY;
  document.body.style.position = "fixed";
  document.body.style.width = "100%";
  document.body.style.top = `-${currentScrollY}px`; // 현재 스크롤 위치
  document.body.style.overflowY = "scroll";
  return currentScrollY;
};


export const allowScroll = (prevScrollY: number) => {
  document.body.style.position = "";
  document.body.style.width = "";
  document.body.style.top = "";
  document.body.style.overflowY = "";
  window.scrollTo(0, prevScrollY);
};

- use-preventScroll.ts

import { useEffect } from "react";
import { preventScroll, allowScroll } from "@/utils/scroll";

const usePreventScroll = () => {
  useEffect(() => {
    const prevScrollY = preventScroll();
    return () => {
      allowScroll(prevScrollY);
    };
  }, []);
};

export default usePreventScroll;
profile
https://brgndy.me/ 로 옮기는 중입니다 :)
post-custom-banner

0개의 댓글