프로젝트를 하면서 Modal을 구현할 일이 있었고, 팀원이 Portal
이라는 것을 사용해서 모달을 구현해보라고 제안했다.
공식 문서에서는
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.
ReactDOM.createPortal(child, container)
첫 번째 인자(child)는 엘리먼트, 문자열, 혹은 fragment와 같은 어떤 종류이든 렌더링할 수 있는 React 자식입니다. 두 번째 인자(container)는 DOM 엘리먼트입니다.
라고 이야기 하고 있다.
React는 부모 컴포넌트가 렌더링 되고 나서 자식 컴포넌트가 렌더링 되는 구조를 가지고 있는데, Portal을 이용하면 이런 tree 구조를 벗어나 독립적인 구조에서 렌더링 되게 할 수 있다.
child 라는 컴포넌트를 container는 child를 넣어줄 DOM이다.
독립되길 원하는 child
독립시킬 곳 container
일단 container를 설정해주기 위해서 index.html
에 독립시킬 공간을 만들어준다.
...
<body id="body">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal"></div> // 독립 공간
</body>
...
그리고 Portal.jsx
를 생성해준다.
이 Portal이 내가 원하는 컴포넌트를 독립시켜줄 것이다.
import reactDom from "react-dom";
const ModalPortal = ({ children }) => {
const el = document.getElementById("modal"); // 독립 공간
return reactDom.createPortal(children, el);
};
export default ModalPortal;
Modal.jsx
를 생성한다.
이 컴포넌트가 내가 직접적으로 사용하게 될 모달창이다.
띄워지는 모달창과 그 외의 배경을 담당한다.
import React, { useEffect } from "react";
import ModalPortal from "./Portal";
import styled from "styled-components";
import modalCloseIcon from "../../images/modal-close-icon.svg";
const Modal = ({ onClose, Body, width }) => {
// 모달창이 띄워지면 스크롤 금지
useEffect(() => {
// 현재 위치에 고정시킴
document.body.style.cssText = `
position: fixed;
top: -${window.scrollY}px;
overflow-y: scroll;
width: 100%;`;
return () => {
// 모달이 사라지면 style 코드 지워주고 원래 있던 위치로 돌려주기
const scrollY = document.body.style.top;
document.body.style.cssText = "";
window.scrollTo(0, parseInt(scrollY || "0", 10) * -1);
};
}, []);
return (
<ModalPortal>
<Background>
// 모달창 부분
<ModalContainer width={width}>
{Body}
<CloseButton onClick={onClose}>
<img src={modalCloseIcon} alt="close-modal" />
</CloseButton>
</ModalContainer>
</Background>
</ModalPortal>
);
};
export default Modal;
// 하단에는 styled-components 코드가 있음
const Background = styled.div`
z-index: 999; /* 의문이 들었던 부분 */
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 0;
top: 0;
text-align: center;
background-color: #00000033;
`;
...
onClose
모달창을 여닫을 수 있는 함수
Body
모달창 안의 내용
재사용성을 높이기 위해 모달창 안에 넣을 내용은 컴포넌트로 만들어 props로 넘겨준다.
width
프로젝트에서 사용할 때 크기가 다른 모달창이 있어서 width를 props로 넘겨줄 수 있게 했다.
재사용성을 높이기 위해 Body라는 모달창 내용 컴포넌트를 넘겨줄 수 있게 했다.
그리고 x 버튼을 모달 자체에 넣어두고, 어떤 모달이든 x룰 누르면 닫힐 수 있게 구현했다.
React Portal을 사용하면 부모 컴포넌트의 overflow: hidden
이나 z-index
의 방해를 받지 않기 위해서 사용한다고 생각했다.
그래서 Modal 컴포넌트에 z-index를 주지 않아도 알아서 혼자 띄워질 줄 알았는데, 다른 z-index가 주어져있는 컴포넌트들이 모달창을 뚫고 나오는 것이다.
회색 부분이 Background이고, 뒤의 달력 input창은 내가 z-index를 1로 준 어떤 컴포넌트이다.
그래서 모달 자체에 z-index를 주니 잘 적용되긴 했다. 하지만 의문은 풀리지 않았고 구글링을 통해 이유를 찾을 수 있었다.
여기서 주의할 점은 div#modal-root 내부에 있는 modal에 z-index 값을 주지 않으면, div#root 내부에 있는 z-index가 더 높은 element가 더 상위에 보이게 된다. div#modal-root 내부에 있는 modal이 부모 element의 쌓임맥락에 동화되지 않을 뿐이지 z-index 비교는 그대로 하기 때문이다.
출처
tree 구조에서 벗어나 독립적으로 있긴 하지만, z-index 비교는 그대로 하기 때문에 값이 있어야 내가 원하는 구현이 가능한 것이다.
출처