본문 바로가기

프로젝트/세컨핸드

[기능 구현] 동적 portal 만들기

portal이란?

- 리액트(React)에서 portal은 컴포넌트 트리 내에서 다른 위치로 자식 요소를 렌더링할 수 있는 기능이다. portal은 주로 모달 창, 팝오버, 툴팁과 같은 오버레이 컴포넌트를 구현하거나, 외부 DOM 요소에 컴포넌트를 렌더링할 때 사용된다.

- portal을 사용하면 자식 요소는 DOM 트리의 지정된 위치로 이동되지만, 해당 자식 컴포넌트는 여전히 컴포넌트 트리의 일부로 유지된다.

- portal을 사용하면 컴포넌트가 렌더링되는 위치와 실제로 화면에 표시되는 위치를 분리할 수 있으며, 레이아웃, 스타일링, 이벤트 처리 및 zIndex 관리를 보다 쉽게 할 수 있게 해준다.

- portal은 보통 모달창을 구현할 때, 많이 사용된다.

 

- 프로젝트에서도 모달창을 구현할 때, portal을 사용했고 처음 portal을 사용한 코드는 아래와 같다.

// index.html
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="modal-root"></div>
  <div id="root"></div>
</body>
// portal을 사용한 Menu 컴포넌트
export const Menu = (...) => {
    return createPortal(
    <>
      <BackDrop onClick={backDropHandler} />
      <Menu>
      ...
      </Menu>
    </>,
    document.querySelector('#dropdown-root') ?? document.body
  );
};

 - index.html에 id가 "modal-root"인 div를 하나 만들고, portal을 사용해 Menu 컴포넌트를 "modal-root"로 이동시켰다. 

의문점

- 프로젝트에서 처음 portal을 사용할 때는 이전 리액트 학습자료와 구글링을 통해 portal을 사용한 다른 글들을 참고했다.

- 하지만, 한 가지 의문점이 있었다. 모달창이 렌더링되지 않는 상황에서 사용하지 않는 div("modal-root")가 남아있는 것 대신에 portal을 사용할 때만 div를 만들고 사용하지 않을 때는 없앨 수 있지 않을까?

 

- 이러한 의문을 프론트엔드 리뷰어에게 질문을 했고, 리뷰어로부터 참고 글을 받았다.

- 나는 리뷰어가 남겨준 참고자료(https://medium.com/@jc_perez_ch/dynamic-react-portals-with-hooks-ddeb127fa516)를 참고하여 동적 portal 컴포넌트를 만들기로 했다.

시도한 방법 및 해결방안

- 동적 portal의 내용은 다음과 같다.

- 첫 번째, portal 컴포넌트에 전달된 id prop과 일치하는 div가 있는지 찾고, 일치하는 div가 없으면 div를 만들고 modalDiv에 저장한다.

- 두 번째, modalDiv의 부모 요소가 있는지를 확인하여 만약 부모 요소가 없다면 useEffect 내부에서 id prop으로 modalDiv의 id를 설정하고, document.body에 추가한다.

- 세 번째, useEffect의 클린업 함수로 컴포넌트가 언마운트되면 동적으로 생성한 div를 삭제한다.

// Portal 컴포넌트
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

const Portal = ({ id, children }) => {
  const modalDiv = useRef(
    document.getElementById(id) || document.createElement('div')
  );

  const dynamicDiv = !modalDiv.current.parentElement;

  useEffect(() => {
    if (dynamicDiv) {
      modalDiv.current.id = id;
      document.body.appendChild(modalDiv.current);
      modalDiv.current.classList.add('modal-root');
    }

    return () => {
      modalDiv.current.remove();
    };
  }, [id]);

  return createPortal(
    <S.PortalWrap>
      <S.Portal>{children}</S.Portal>
    </S.PortalWrap>,
    modalDiv.current
  );
// 동적 portal을 사용한 Menu 컴포넌트
export const Menu = (...) => {
    return <Portal id="modal-root">
      <BackDrop/>
      <Menu>
       ...
      </Menu>
    </Portal>
};

 - 위와 같이 동적으로 portal을 생성하여 사용하면서, index.html에는 portal을 사용하지 않을 때에 불필요한 정적 div가 남아있지 않다.

배운 점

- Portal을 사용하여 동적으로 부모 div 요소를 생성하는 과정에서 React 컴포넌트의 생명주기에 대해 이해할 수 있었고, 컴포넌트의 마운트 및 언마운트 시점에 로직을 실행하여 동적으로 DOM 요소를 조작하는 방법을 학습할 수 있었다.

- 개발은 역시 내가 생각하는 부분을 실제로 만들어내는 과정에서 충분히 매력적인 것 같다.

- 또한, 내가 고민했던 부분에 대해서 충분히 필요한 고민이라며 참고자료를 건네준 리뷰어에게 감사했고, 다음에 누군가 나에게 질문을 한다면 참고자료와 함께 도움을 줄 수 있는 사람이 되어야 겠다. ✌️

 


참고 : https://pocoding.tistory.com/117

https://medium.com/@jc_perez_ch/dynamic-react-portals-with-hooks-ddeb127fa516

https://velog.io/@codns1223/React-React-Portal%EC%9D%B4%EB%9E%80-React-Portal%EB%A1%9C-modal-%EA%B5%AC%ED%98%84