💣 겪고 있는 문제...
아래와 같이 모달을 포함한 코드가 있다.
// App.js
const App = () => {
return (
<div>
<h1>Title</h1>
<Modal />
</div>
);
};
// Modal.js
const Modal = () => {
return (
<>
<div id="overlay"></div>
<aside id="modal">
<h1>modal</h1>
<p>contents</p>
</aside>
</>
);
};
위 코드를 실제 돔으로 렌더링하면 아래와 같은 결과를 얻을 수 있다.
모달 컴포넌트에 스타일을 제대로 부여한다면 화면 상에서 문제는 없어 보인다.
하지만 스크린리더가 렌더링되는 HTML 코드를 해석할 때 모달이라는 존재를 인식할 수 없게 된다. 또한 의미적이나 구조적인 관점에서 모달이 모든 영역 위에 있는 지 알 수 없다.
이러한 문제는 모달 뿐만 아니라 side drawer, dialog, overlay 등에서도 나타날 수 있다.
이 문제는 Portal을 사용하면 싹- 해결할 수 있다.
Portal이 뭔데?
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드 자식을 렌더링하는 최고의 방법을 제공한다. 무슨 말인지 잘 모르겠어요!!
React는 부모 컴포넌트가 렌더링되면 자식 컴포넌트가 렌더링되는 Tree 구조를 따른다. 이런 구조는 자식 컴포넌트가 모달일 경우에 부모 컴포넌트의 스타일 속성에 제약을 받아 번거로운 후처리를 해줘야 한다는 단점이 있다. (💣에서 설명한 것과 같이.)
이때 부모-자식 관계를 유지하면서 자식이 독립적인 위치에서 렌더링 하도록 도와주는 것이 Portal이다. (탈캥거루족🦘)
🎢 어떻게 사용하는 건데?
렌더링 될 위치(root) 만들기
<!-- index.html --> <body> <div id="backdrop-root"></div> <div id="modal-root"></div> <div id="root"></div> <!-- 중략 --> </body>
배경 오버레이 영역과 모달 영역을
body
태그 바로 아래에 위치 시키고자 위와 같은 위치에div
요소를 추가한다.Portal 컴포넌트 만들기
import ReactDom from "react-dom"; const Portal = ({ children, root }) => { const el = document.getElementById(root) return ReactDom.createPortal(children, el) } export default ModalPortal
children
과root
라는 props를 만들어 재사용성을 높인다.children
은 모달이나 오버레이와 같은 콘텐츠 정보가 들어있고,root
에는 렌더링 될 위치에 대한 id 정보가 들어가게 된다.참고로
ReactDom.createPortal
의 첫 번째 매개변수는 렌더링 할 자식 컴포넌트를, 두 번째 매개변수에는 렌더링 될 DOM Element를 넣어준다.
Modal 및 Backdrop 컴포넌트에 Portal 연결하기
const Modal = () => { return ( <Portal root="modal-root"> <aside id="modal"> <h1>modal</h1> <p>contents</p> </aside> </Portal> ) }
const Backdrop = () => { return ( <Portal root="backdrop-root"> <div id="overlay"></div> </Portal> ) }
각자의
root
안에 들어갈 콘텐츠를 넣고Portal
로 감싼다.
🌷 아름답게 바뀌었다
마치며
단순히 화면에 보이는 스타일만 제대로 적용하면 끝이라는 생각이 완전히 바뀌게 되는 계기가 되었다. 컴포넌트 트리 구조와 이벤트 전파에 대해 배우게 되는 시간이었다.