왜 새로운 게 끝도 없이 나오는거지...? React Portal은 진짜 생전 첨 들어보는데 인강 듣다가 알게돼서 기록해두기 위해 끄적여본다...
공식 문서에 따르면 Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공한다고 되어있다.
즉 외부에 존재하는 DOM 노드가 React App DOM 계층 안에 존재하는 것처럼 연결을 해주는 포탈 기능을 제공합니다.
이 말은, 컴포넌트들의 상하 관계, 구조를 가지고 있는 문서인 DOM 상에서, 자식 컴포넌트를 부모 컴포넌트 바깥에 있는 다른 컴포넌트에 전달할 수 있다는 뜻이다.
위의 이미지를 보면 root와 modal이 형제 element처럼 보이지만, 사실은 modal div는 root div 안에 존재하는 자식 컴포넌트 이다. 단지 렌더링만 root의 바깥에서 이뤄지고 있는 것이다.
Portal은 보통, React의 index.html의 #root div 외에, 다른 루트에 Modal, Dialog, Tooltip 등을 띄우기 위해 많이 사용된다.
➡️ React 의 tree 구조에 따라서 부모 컴포넌트가 렌더링 되면 자식 컴포넌트도 그 영향을 받아서 같이 렌더링이 된다. 그래서 자식 컴포넌트 같은 경우는 쓸데없는 렌더링이 일어날 수도 있고 부모 컴포넌트의 스타일링에 제약을 받아 z-index 와 같은 후처리를 해야한다는 점도 있다.
이러한 DOM tree 상의 부모-자식 컴포넌트 간의 제약에서 Portal을 통해 독립적인 구조와 부모-자식 관계를 동시에 유지할 수 있기 때문에 Portal을 사용하는 것이다.
➡️ 또 다른 이유로는 modal이 독립적이지 않고 부모의 DOM노드 안에서 렌더링 되는 것이 의미적인 관점이나 간결한 HTMl 구조를 갖췄는지의 관점으로 보면 별로 좋지 않기 때문이다.
왜냐, 기본적으로 모달은 페이지 위에 표시되는 오버레이이기 때문이다. 모달은 전체페이지에 대한 오버레이이기 때문에 당연히 다른 모든 것의 위에 있다. 그래서 모달이 만약 다른 HTML 코드 안에 중첩되어 있다면, 기술적으로는 스타일링 덕분에 작동할지 몰라도 좋은 코드가 아니고 좋은 구조 또한 아니게 된다.
예를들어 스크린 리더가 렌더링 되는 HTML 코드를 해석할 때 일반적이 오버레이라고 인식하지 못할 수도 있다. 또한 의미적인 관점이나 구조적인 관점으로 보면 이것은 HTML 코드 안 깊은 곳에 자리잡고 있기 때문에 이 모달이 다른 모든 내용에 대한 오버레이인지 명확하지 않다.
그래서 리액트 개념에서 오버레이 내용이 있는 모달이 깊게 중첩되면 안 되는 문제를 해결할 수 있게끔 한 방법이 React Portal인 것이다.
ReactDom.createPortal(child, container)
최근에 완성했던 오픈마켓 프로젝트에 적용시켜 보았다.
public/index.html
<body>
<div id="root"></div>
<div id="modal"></div>
</body>
src/helpers/Portal.js
import ReactDom from "react-dom";
const ModalPortal = ({ children }) => {
const el = document.getElementById("modal")
return ReactDom.createPortal(children, el)
}
export default ModalPortal
src/pages/ProductDetail.js (상품상세페이지)
//components
import UserModal from '../components/UserModal';
//helpers
import ModalPortal from '../helpers/Portal';
function ProductDetail() {
return (
<div>
<ModalPortal>
{
modal === 1 ?
<UserModal modal_to_check
display="none"
children2="이미 장바구니에 있는 상품입니다."
children3="장바구니로 이동하시겠습니까?"
btn_children_1="아니오"
btn_children_2="예"
margin="40px 0 0 0"
_onClick={() => setModal(0)}
_onClick2={modalAddCart}
_onClickBg={() => setModal(0)}
/> :
modal === 2 &&
<UserModal modal_to_check
_disabled={true}
children2="로그인이 필요한 서비스입니다."
children3="로그인 하시겠습니까?"
btn_children_1="아니오"
btn_children_2="예"
margin="30px 0 0 0"
_onClick={() => setModal(0)}
_onClick2={() => {
navigate("/login")
}}
_onClickBg={() => setModal(0)}
/>
}
</ModalPortal>
</div>
}
출처