react로 개발을 진행하다보니 react-create-portal 즉, 리액트 v16 에서 도입된 기능인 Portals를 접하게 되었다. 사실 나는 vue.js 개발자다보니 react가 조금 생소하긴 했지만 개발을 해나가는데에는 큰 어려움이 없었다. 하지만 Portals 기능은 조금 생소하였는데 써보게 되니 '굉장히 편리하구나! 유용하게 써먹을 수 있겠네!' 라는 생각이 들게 되었다.
Portals 가 대체 뭐야?!?!
리액트 v16에서 도입된 'Portals'는 컴포넌트를 랜더링 시킬 때 랜더링 시킬 DOM을 선택하여 부모 컴포넌트의 바깥에서도 렌더링 할 수 있게 해주는 기능이다.
이러한 기능을 써먹을 수 있는 가장 큰 예로는 'Modal'이나 'Popup'이다.
생각해보자! 공통된 모달창이나 팝업창을 지속적으로 띄워야 하는데 그 안에 들어갈 내용들은 페이지마다 다르다고 가정해보자.
원래는 모달 컴포넌트와 팝업 컴포넌트를 만들어 놓고 해당 페이지를 보여주는 상위 컴포넌트에서 모달 및 팝업 컴포넌트를 넣어주어야 한다. 사실.. Portals도 크게 다르지는 않다. 하지만 position 스타일 요소를 활용하여 모달이나 팝업을 띄워줄때 상위 노드들의 overflow 요소나 z-index에 대한 우선순위가 뒤로 밀릴 수 있어 작업을 하는데 방해가 될수도 있다.
이럴때!! index.html이나 App.js 같은 부분에 모달창이나 팝업창이 뜰 수 있는 Element가 이미 지정이 되어 있다면 위와 같은 부분들이 모두 손쉽게 해결될 수 있다.
누가 그러더라 개발자는 글이 아닌 소스로 이해 해야한다고... 그럼.. 만들어볼까?
.App {
font-family: sans-serif;
text-align: center;
}
.modal-wrap {
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
visibility: hidden;
opacity: 0;
}
.modal-wrap.active {
visibility: visible;
opacity: 1;
transition: opacity ease 0.25s;
}
.modal-wrap .overlay {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal-wrap .modal-con {
position: relative;
z-index: 10;
max-width: 500px;
min-width: 300px;
min-height: 100px;
width: 33%;
padding: 20px;
border-radius: 5px;
background: #fff;
transform: translateY(80px);
transition: transform ease 0.3s 0.1s;
}
.modal-wrap.active .modal-con {
position: relative;
z-index: 10;
max-width: 500px;
min-width: 300px;
min-height: 100px;
width: 33%;
padding: 20px;
border-radius: 5px;
background: #fff;
transform: translateY(0px);
}
.modal-wrap .modal-con .contents {
padding: 20px 0;
}
.modal-wrap .modal-con .bottom {
text-align: right;
margin-top: 10px;
}
일단 모달이 작동될 수 있도록 css를 작성한 css 소스이다. 모달 창이 보여져야할때는 .modal-wrap에 active 클래스를 부여하고 active클래스가 있을 경우 visiblity는 visible, active클래스가 존재하지 않을 때는 hidden을 주어 모달창의 보여짐을 설정하고 animation을 위해 opacity와 tranform에 transition 효과를 주었다.
이제 modal 컴포넌트를 작성해보자
export default function Modal() {
return (
<div className="modal-wrap">
<div className="overlay"></div>
<div className="modal-con">
<div className="contents">모달이 열렸다!</div>
<div className="bottom">
<button type="button">
모달 닫기
</button>
</div>
</div>
</div>
);
}
그리고 Portals 기능을 써보기 위해 ./public/index.html과 Portals 기능 구현을 담당하기 위한 ModalPortals 컴포넌트를 만들어보자
./public/index.html
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<div id="modal"></div>
</body>
id를 modal로 하는 element를 만들고
import ReactDOM from "react-dom";
export default function ModalPortals({ children }) {
const modalElement = document.querySelector("#modal");
return ReactDOM.createPortal(children, modalElement);
// createPortal(ModalPortals안에서 랜더링될 컴포넌트, 랜더링 시킬 상위 DOM Element)
}
id가 modal인 element를 선택하고 해당 element에 ModalPortals 컴포넌트 안에 랜더링될 컴포넌트를 createPortal 메소드의 첫번째 인자로 넣어준다. 여기까지 해주었다면 최상위 컴포넌트인 App.js를 해당 모달 기능이 잘 돌아갈 수 있게끔 소스코드를 작성해준다.
import "./styles.css";
import { useState } from "react";
import ModalPortals from "./ModalPortals";
import Modal from "./Modal";
export default function App() {
const [modal, setModal] = useState(false);
const handleModalShow = (status) => {
setModal(status);
};
return (
<div className="App">
<h1>쫑's의 Portals 모달 만들기</h1>
<h2>밑에 모달 버튼을 눌러보세요!</h2>
<div>
<button
type="button"
onClick={() => {
handleModalShow(true);
}}
>
모달 버튼
</button>
</div>
<ModalPortals>
<Modal show={modal} handleModalShow={handleModalShow} />
</ModalPortals>
</div>
);
}
useState Hook을 활용하여 modal이 활성화 되었는지 비활성화 되어 있는지를 구분할 수 있도록 해주고 handleModalShow()라는 Modal상태를 핸들링하는 이벤트 메소드를 만들어 setModal을 이용하여 modal state의 값을 변경할 수 있게 해준다. Modal 컴포넌트에는 modal state를 props로 전달하고 Modal상태를 핸들링하는 이벤트 메소드인 handleModalShow도 props로 전달한다.
이제는 App.js에서 props로 전달하는 state와 method를 Modal 컴포넌트에서 받을 수 있도록 Modal.js에서 설정을 해주도록 하자.
export default function Modal({ show, handleModalShow }) {
return (
<div className={"modal-wrap " + (show ? "active" : "")}>
<div
className="overlay"
onClick={() => {
handleModalShow(false);
}}
></div>
<div className="modal-con">
<div className="contents">모달이 열렸다!</div>
<div className="bottom">
<button
type="button"
onClick={() => {
handleModalShow(false);
}}
>
모달 닫기
</button>
</div>
</div>
</div>
);
}
App.js에서 전달하는 props를 Modal.js에서 받을 수 있도록 설정해준 뒤 .overlay와 '모달 닫기' 버튼에 이벤트 메소드를 작동할 수 있도록 onClick 이벤트를 설정해준다.
이제 모든 기능 구현이 완료되었다. 한번 실행해보도록 하자.
index.html에서 id가 modal인 element 안에서 Modal 컴포넌트가 랜더링이 되고 있음을 확인 할 수 있으며 작동이 잘 되고 있음을 확인 할 수 있다. 이 처럼 부모 컴포넌트 바깥에서 Portals 기능을 활용하여 모달창을 구현할 수 있음을 확인할 수 있다.
결론
Portals 기능은 모든 페이지에서 공통적으로 자주 사용되는 모달창이나 팝업창, 알림창 등을 구현할 때 활용하기 좋은 기능인것 같다. 특히 커스터마이징된 alert나 confirm 기능을 구현할때 활용도가 높을 것으로 생각되며 알아두면 좋을 기능 중의 하나로 생각된다.
P.S Portals기능의 더 좋은 활용 사례를 알고계신다면 피드백 주시면 너무 감사할거 같아요! 혹시나 제가 잘못된 설명을 한게 있다면 그것 또한 피드백 감사히 받도록 하겠습니다 ^^
다른 root를 가진 컴포넌트를 불러올 수 있는 기능이 Portal 이군요.
랜더링 시에 기존 한 곳에서 모든 것들을 처리하는 부담을 최소화 할 수 있기에 더욱 좋은 기능같네요. 감사합니다.