모달 창을 구현하다보면 z-index와 overflow 속성에 의해 모달이 전체 화면으로 나타나지 않거나 원하는대로 구현이 안되는 경우가 종종 있다.
필자는 모달이 카드 안에서 뜨는 문제가 발생했다. 이를 해결하기 위해 찾아보다가 React의 portals
라는 기능을 발견했다.
처음에는 컴포넌트 구조가 잘못된 건 아닌지, CSS 속성 때문은 아닌지 고민했지만, 문제는 렌더링되는 DOM 위치에 있었다.
모달이 특정 컴포넌트 내부의 DOM 구조 안에서 렌더링되다 보니, 부모 요소들의 overflow: hidden
이나 z-index
값에 의해 제한된 영역 안에서만 보이게 되는 문제가 발생했던 것이다.
Portal
을 사용하면 이러한 구조적 제약에서 벗어나, 루트 DOM이나 body와 같은 최상위 위치에 모달을 렌더링할 수 있게 되어 이러한 문제를 간단하게 해결할 수 있다.
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다. (공식문서)
React의 Portal은 컴포넌트를 DOM 계층 구조 밖에 렌더링할 수 있도록 도와주는 기능이다.
일반적으로 React는 부모 컴포넌트의 DOM 구조 안에 자식을 렌더링하지만, portal을 사용하면 그걸 탈출해서 원하는 DOM 노드에 렌더링할 수 있는 것이다.
즉, CSS 계층 구조의 제약을 우회하는 실용적인 방법
createPortal
함수를 사용하여 매우 쉽게 구현할 수 있다.
<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>
React Portal은 시각적으로는 잘 보이지만, 보조 기술(스크린리더, 키보드) 사용자에게는 안 보일 수 있다. 따라서 WAI-ARIA 가이드라인을 따르고, a11y를 테스트하는 것이 중요하다.
앞으로 portal을 사용할 땐 다음과 같은 웹 접근성을 고려하도록 하자.
포커스 트랩
모달 안에서 Tab 이동이 가능하게 하기
ESC로 닫히게 하기
키보드만으로 모달을 닫을 수 있도록 하기
열릴 때 포커스 이동
열리면 모달 내부 첫 포커스 요소로 이동하게 하기
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>
모달을 구현할 때 일반적으로 모달 컴포넌트는 최상위 컴포넌트의 자식으로 추가해야 하지만, portal을 사용하면 모달 컴포넌트를 원하는 위치로 이동시킬 수 있으므로 복잡한 UI도 쉽게 구현할 수 있다.
Portal로 이동된 컴포넌트가 DOM 상으로는 React 트리 밖에 있지만, 이벤트 버블링은 React 트리 기준으로 작동한다.
const App = () => {
const handleClick = () => console.log('App 클릭됨');
return (
<div onClick={handleClick}>
<Page />
{ReactDOM.createPortal(
<Modal />,
document.getElementById('modal-root'))
}
</div>
);
};
<Modal />
은 DOM 상으로 #modal-root
에 있지만,위는 body
엘리먼트로 렌더링되도록 portal을 적용한 모달이다.
이 모달을 열고 닫을 때마다, 개발자 도구 요소 탭에서 body
DOM 요소 아래에 이 모달이 추가되고 사라지는 현상을 볼 수 있다.
(참고)
react 공식문서 createPortal
메모리를 절약하면서 효율적으로 React Portals를 사용해 보자