Modal 컴포넌트를 위한 Portal 기능 - TEAM-ARK/inflearn-clone-front GitHub Wiki

React의 기본 구조

React의 기본적인 구조는 Tree 형태이며 HTML과 같은 구조를 갖고 있다.

리엑트 노드 구조

부모와 자식 컴포넌트의 관계를 절대적이며 어떠한 경우에 자식 컴포넌트에서 다른 자식 컴포넌트의 이동이 필요한 경우가 있을 수 있다. 예를 들어, 하단 컴포넌트의 내부 메소드를 사용하여 중앙에 위치한 컴포넌트에 자식 컴포넌트를 추가하고 싶을 경우가 있다. (Modal)

이러한 경우에는 App에서 state와 method를 만들고 자식 컴포넌트를 만들곳과 Action을 넣을 곳에 분배를 시켜야 한다. 그리고 우측 하단 컴포넌트에서 Action을 취하면 App 밑의 컴포넌트에 자식 컴포넌트를 생성해야 한다 (이러한 서비스는 지양하는것이 좋다)

이러한 경우에는 re-render 구조를 갖고 있는 React의 특성을 생각해보면 좋지 않은 사례이다. Action을 보내면 App의 state가 변경되기 때문에 최상의 계층부터 Update가 일어나고 모든 자식 컴포넌트의 Update를 발생시킨다.

Portal의 구조

리엑트 포탈 구조

기본 React에서 포탈의 사용방법은 다음과 같다.

// index.html
<html>
    <head>...</head>
    <body>
        <div id="root"/>
        <div id="global-portal"/> <!-- 추가 -->
    </body>
</html> 

id="global-portal"의 속성을 가진 div를 추가한다. div tag가 portal의 도착점이 될 지점이다.

// App.tsx
import React from "react"
import ReactDOM from "react-dom"

const Portal = ({ children }) => {
    const globalPortal = document.getElementById("#global-portal");
    return ReactDOM.createPortal(children, globalPortal)
}

function App () {
    return (
        <div>
            <Portal>
                #global-portal로 ! 하고 이동함
            </Portal>
        </div>
    )
}

컴포넌트를 만들고 App에서 렌더링 시킨다.

위의 코드들은 다음과 같은 흐름을 가지게 된다.

리엑트 포탈 흐름

의 children으로 컨텐츠가 전달된다. 이렇게 전달된 컨텐츠는 Portal에서 children으로 변환되고 이전에 정의하였던 #global-portal을 탐색하여 .createPortal의 매개변수로 들어가 실행된다. 이러한 과정 이 후 global-portal의 children이 렌더링되는 것이다.

Nextjs에서의 사용

Modal의 주된 목적은 페이지 전체 페이지에서 어떠한 컨텐츠를 보여주는 것이다. 이러한 컨텐츠는 전체 UI에서 분리된 DOM 노드에 정의를 하는것이 좋으며 Nextjs에서 root DOM 구조를 수정 할 수 있는 곳은 _document.js 페이지이다. 이곳에 Portal을 정의하고 사용하면 우리가 원하는 Modal을 쉽게 적용 할 수 있다.

// _document.js
import Document, { Html, Head, Main, NextScript } from "next/document";

class MainDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
          {/*Below we add the modal wrapper*/}
          <div id="modal-root"></div>
        </body>
      </Html>
    );
  }
}

export default MainDocument;

_document.js 페이지안에 Portal을 적용하고자하는 div tag를 정의한다.

import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";

function Modal() {
    const [isBrowser, setIsBrowser] = useState(false);
  
    useEffect(() => {
      setIsBrowser(true);
    }, []);

    if (isBrowser) {
        return ReactDOM.createPortal(
            <div>Hello from modal</div>, 
            document.getElementById("modal-root")
        );
      } else {
        return null;
      }    
  
}

export default Modal;

Nextjs의 경우에는 Server Side Rendering으로 동작하므로 렌더링이 프론트 서버에서 일어나게 된다. 페이지가 초기에 프론트 서버에서 로드 될 때 브라우저에 렌더링 되지 않은 상황이므로 window.document가 undefined이기 때문에 Portal의 동작이 되지 않는다. 그러므로, 브라우저에 렌더링 된 후 Portal의 동작이 이루어져야 하며 이러한 보장은 useEffect를 사용하여 보장이 가능하다. (useEffect는 dependencies가 empty인 경우 브라우저 painting 이 후 동작한다)

useState를 사용하여 useEffect에서 useState의 값을 변경하여 Portal의 사용 시점을 보장 할 수 있게 된다.

// Modal component
const Modal = ({ show, onClose, children, title }) => {
  const [isBrowser, setIsBrowser] = useState(false);

  useEffect(() => {
    setIsBrowser(true);
  }, []);

  const handleCloseClick = (e) => {
    e.preventDefault();
    onClose();
  };

  const modalContent = show ? (
    <StyledModalOverlay>
      <StyledModal>
        <StyledModalHeader>
          <a href="#" onClick={handleCloseClick}>
            x
          </a>
        </StyledModalHeader>
        {title && <StyledModalTitle>{title}</StyledModalTitle>}
        <StyledModalBody>{children}</StyledModalBody>
      </StyledModal>
    </StyledModalOverlay>
  ) : null;

  if (isBrowser) {
    return ReactDOM.createPortal(
      modalContent,
      document.getElementById("modal-root")
    );
  } else {
    return null;
  }
};

const StyledModalBody = styled.div`
  padding-top: 10px;
`;

const StyledModalHeader = styled.div`
  display: flex;
  justify-content: flex-end;
  font-size: 25px;
`;

const StyledModal = styled.div`
  background: white;
  width: 500px;
  height: 600px;
  border-radius: 15px;
  padding: 15px;
`;
const StyledModalOverlay = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.5);
`;

export default Modal;
// page which we want to apply the Modal component
export default function Home() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
        <button onClick={() => setShowModal(true)}>Open Modal</button>
        <Modal
          onClose={() => setShowModal(false)}
          show={showModal}
        >
          Hello from the modal!
        </Modal>
    </div>
  )
}

위와 같이 코드를 적용하면 Modal을 쉽게 전체 페이지의 UI의 상관관계를 신경쓰지 않고 적용이 가능하다.

⚠️ **GitHub.com Fallback** ⚠️