React Portal로 모달 띄우는 법

Soyeon·2025년 6월 8일
4
post-thumbnail

모달 창을 구현하다보면 z-index와 overflow 속성에 의해 모달이 전체 화면으로 나타나지 않거나 원하는대로 구현이 안되는 경우가 종종 있다.

필자는 모달이 카드 안에서 뜨는 문제가 발생했다. 이를 해결하기 위해 찾아보다가 React의 portals라는 기능을 발견했다.

처음에는 컴포넌트 구조가 잘못된 건 아닌지, CSS 속성 때문은 아닌지 고민했지만, 문제는 렌더링되는 DOM 위치에 있었다.
모달이 특정 컴포넌트 내부의 DOM 구조 안에서 렌더링되다 보니, 부모 요소들의 overflow: hidden이나 z-index 값에 의해 제한된 영역 안에서만 보이게 되는 문제가 발생했던 것이다.

Portal을 사용하면 이러한 구조적 제약에서 벗어나, 루트 DOM이나 body와 같은 최상위 위치에 모달을 렌더링할 수 있게 되어 이러한 문제를 간단하게 해결할 수 있다.

Portal?

Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다. (공식문서)

React의 Portal은 컴포넌트를 DOM 계층 구조 밖에 렌더링할 수 있도록 도와주는 기능이다.

일반적으로 React는 부모 컴포넌트의 DOM 구조 안에 자식을 렌더링하지만, portal을 사용하면 그걸 탈출해서 원하는 DOM 노드에 렌더링할 수 있는 것이다.

즉, CSS 계층 구조의 제약을 우회하는 실용적인 방법


사용법

createPortal 함수를 사용하여 매우 쉽게 구현할 수 있다.

<div>
  <SomeComponent />
  {createPortal(children, domNode, key?)}
</div>
  • children: 렌더링 될 리액트 컴포넌트
  • domNode: children이 렌더링 될 DOM 엘리먼트, portal의 목적지
  • key?: 리액트의 key prop과 유사

Portal을 사용할 땐 "웹 접근성"에 신경 써야 한다.

React Portal은 시각적으로는 잘 보이지만, 보조 기술(스크린리더, 키보드) 사용자에게는 안 보일 수 있다. 따라서 WAI-ARIA 가이드라인을 따르고, a11y를 테스트하는 것이 중요하다.

앞으로 portal을 사용할 땐 다음과 같은 웹 접근성을 고려하도록 하자.

  1. 포커스 트랩
    모달 안에서 Tab 이동이 가능하게 하기

  2. ESC로 닫히게 하기
    키보드만으로 모달을 닫을 수 있도록 하기

  3. 열릴 때 포커스 이동
    열리면 모달 내부 첫 포커스 요소로 이동하게 하기

  4. WAI-ARIA 속성 사용하기
    role='dialog'
    aria-modal
    aria-labelledby

 <section
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      aria-describedby="modal-description"
      onClick={closeModal}
      ref={modalRef}
      tabIndex={-1}
      className="max-w-[520px] m-auto column-center fixed inset-0 z-50 bg-black/90 focus:ring-0 focus:border-none "
    >
 </section>

장점

1. UI 구조 분리

모달을 구현할 때 일반적으로 모달 컴포넌트는 최상위 컴포넌트의 자식으로 추가해야 하지만, portal을 사용하면 모달 컴포넌트를 원하는 위치로 이동시킬 수 있으므로 복잡한 UI도 쉽게 구현할 수 있다.

2. React 트리 상의 이벤트 버블링 유지

Portal로 이동된 컴포넌트가 DOM 상으로는 React 트리 밖에 있지만, 이벤트 버블링은 React 트리 기준으로 작동한다.

  • 이벤트 버블링이란? 이벤트가 발생했을 때 자식 -> 부모 요소로 차례대로 이벤트가 전파되는 과정
    즉, DOM 위치가 바뀌어도 React 트리 기준으로 버블링된다.

Example

const App = () => {
  const handleClick = () => console.log('App 클릭됨');

  return (
    <div onClick={handleClick}>
      <Page />
      {ReactDOM.createPortal(
        <Modal />, 		
        document.getElementById('modal-root'))
      }
    </div>
  );
};
  • 이 때 <Modal />은 DOM 상으로 #modal-root에 있지만,
  • React 트리 상에서는 App > Page > Modal 순서로 존재한다.
  • 즉, 이벤트가 발생하면 React는 이 컴포넌트를 React 내부 계층 구조로 판단해서 React 트리 기준으로 이벤트 버블링을 실행한다.

위는 body 엘리먼트로 렌더링되도록 portal을 적용한 모달이다.
이 모달을 열고 닫을 때마다, 개발자 도구 요소 탭에서 body DOM 요소 아래에 이 모달이 추가되고 사라지는 현상을 볼 수 있다.




(참고)
react 공식문서 createPortal
메모리를 절약하면서 효율적으로 React Portals를 사용해 보자

profile
탄탄한 개발자로 살아남기🗿

0개의 댓글