Portals 는 리액트 프로젝트에서 컴포넌트를 렌더링하게 될 때, UI 를 어디에 렌더링 시킬지 DOM 을 사전에 선택하여 부모 컴포넌트의 바깥에 렌더링 할 수 있게 해주는 기능입니다.
Portals 가 리액트에 도입된지는 꽤 됐지만 따로 정리한적이 없어서 늦게나마 정리해보겠습니다.
이 기능은 리액트 v16 에서 도입된 기능인데요, 기존의 리액트에서 컴포넌트를 렌더링 하게 될 때, children 은 부모컴포넌트의 DOM 내부에 렌더링 되어야 했었습니다:
const Wrapper = ({ children }) => {
return <div>{children}</div>;
}
Portals 를 사용하면 DOM 의 계층구조 시스템에 종속되지 않으면서 컴포넌트를 렌더링 할 수 있습니다.
const MyPortal = ({ children }) => {
const el = document.getElementById('my-portal');
return ReactDOM.createPortal(children, el);
}
리액트 프로젝트의 엔트리 파일인 src/index.js 파일을 열어보시면 다음 같은 코드가 있습니다:
ReactDOM.render(<App />, document.getElementById('root'));
document.getElementById
함수를 사용하여 id 값이 root 인 DOM 을 찾고, App 컴포넌트를 여기에 렌더링하고 있습니다. 이 DOM 은 public/index.html 을 열어보면 찾아보실 수 있습니다:
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
한번 모달을 렌더링 할 수 있는 ModalPortal 을 구현해보겠습니다. 이 실습은 다음 CodeSandbox 에서 진행하세요:
혹은, create-react-app 으로 프로젝트를 새로 만드셔서 진행하셔도 무방합니다.
만약 create-react-app 으로 직접 프로젝트를 만드는 경우, App.js 와 App.css 를 다음과 같이 준비해주세요:
import React, { Component } from 'react';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<h1>안녕하세요 리액트!</h1>
</div>
);
}
}
export default App;
.App {
text-align: center;
color: #61dafb;
}
프로젝트를 준비하셨으면 public 디렉토리의 index.html 를 열으셔서 <div id="root"></div>
하단에 새로운 DOM 하나를 만들어주세요:
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<div id="modal"></div>
</body>
그리고, ModalPortal 컴포넌트를 생성합니다:
import ReactDOM from 'react-dom';
const ModalPortal = ({ children }) => {
const el = document.getElementById('modal');
return ReactDOM.createPortal(children, el);
};
export default ModalPortal;
나중에 우리가 이 컴포넌트를 사용하면 우리가 원하는 JSX 를 id="modal"
을 가진 DOM 엘리먼트 에 렌더링을 할 수 있게 됩니다.
이제, 모달을 만들어보겠습니다. MyModal.js 와 MyModal.css 를 다음과 같이 작성해주세요:
import React from 'react';
import './MyModal.css';
const MyModal = () => {
return (
<div className="MyModal">
<div className="content">
<h3>이것은 모달</h3>
<p>궁시렁 궁시렁 내용입니다.</p>
<button>닫기</button>
</div>
</div>
);
};
export default MyModal;
.MyModal {
background: rgba(0, 0, 0, 0.25);
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.MyModal .content {
background: white;
padding: 1rem;
width: 400px;
height: auto;
}
MyModal 컴포넌트를 만들고나서, 한번 App.js 에서 렌더링해보세요:
import React, { Component } from 'react';
import MyModal from './MyModal';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<h1>안녕하세요 리액트!</h1>
<MyModal />
</div>
);
}
}
export default App;
그러면, 위와 같은 결과가 나타나게 됩니다. MyModal 컴포넌트가 App 컴포넌트 내부에 렌더링이 되면서 기존에 App 이 지니고 있던 스타일들을 그대로 전달받게 됐습니다.
이번엔, 아까전에 만들었던 ModalPortal 을 사용하여 App 에서 MyModal 을 렌더링할때 한번 감싸도록 코드를 수정해보겠습니다. 과연 어떤 결과가 나타날까요?
이제는 MyModal 컴포넌트가 App 내부가 아니라, 그 바깥의 <div id="modal"></div>
안에 렌더링되었습니다.
이렇게 Portal 를 사용하여 불필요한 스타일을 상속받는 문제를 해결할수도있고 그 외에도 엘리먼트의 레이어 위치를 관리하는 z-index
스타일을 관리 할 때 유용하게 사용 될 수도 있습니다.
닫기 버튼을 누르면 모달이 사라지게끔 구현해보겠습니다.
App 컴포넌트에서 모달이 보여질지말지 정해주는 modal 값을 state 에 넣고, 이 값을 설정시켜줄 handleOpenModal 과 handleCloseModal 을 작성하세요:
import React, { Component } from 'react';
import MyModal from './MyModal';
import ModalPortal from './ModalPortal';
import './App.css';
class App extends Component {
state = {
modal: false
};
handleOpenModal = () => {
this.setState({
modal: true
});
};
handleCloseModal = () => {
this.setState({
modal: false
});
};
render() {
return (
<div className="App">
<h1>안녕하세요 리액트!</h1>
<button onClick={this.handleOpenModal}>모달 열기</button>
{this.state.modal && (
<ModalPortal>
<MyModal onClose={this.handleCloseModal} />
</ModalPortal>
)}
</div>
);
}
}
export default App;
onClose 를 MyModal 한테 전달해주었으니, MyModal 내부에서 버튼 클릭시 호출도 해주어야겠지요?
import React from 'react';
import './MyModal.css';
const MyModal = ({ onClose }) => {
return (
<div className="MyModal">
<div className="content">
<h3>이것은 모달</h3>
<p>궁시렁 궁시렁 내용입니다.</p>
<button onClick={onClose}>닫기</button>
</div>
</div>
);
};
export default MyModal;
이렇게 하고나면 이제 모달을 열고 닫는게 가능해집니다. 바깥 DOM 에 렌더링되어있지만, 상태관리는 평상시 하던 것 처럼 똑같이 처리 할 수 있습니다.
리액트의 Portals 기능을 사용하게되면, 렌더링을 원하는 DOM 에 자유자재로 할 수 있습니다. 참고로 타겟 DOM 이 꼭 App 바깥이 아니여도 됩니다. 리액트 앱내부에서 원래 있어야하는 곳 말고 다른곳에 렌더링하고 싶을때도 동일한 방식으로 하시면 됩니다. 모달 같은 컴포넌트를 만들게 될 때 스타일 관련해서 문제를 겪게 된다면 Portal 을 활용해보시길 바랍니다.
Context + portal에 애니메이션 라이브러리 react-flip-toolkit을 사용해서 모달을 만들어봤는데 이제 단순 UI 상태 공유에는 리덕스를 안써도 되겠다는 생각이 들더라구요 ㅋㅋ 글 잘 봤습니다.