"React Portal"
은 컴포넌트를 우리가 원하는 돔 엘리먼트(실제 돔 엘리먼트)에 렌더링하고 싶은 경우에 사용하는 개념입니다.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
// index.html의 root라는 요소에 모든 컴포넌트를 렌더링
ReactDOM.render(<App />, document.getElementById('root'));
위 코드처럼 지금까지는 ReactDOM.render
을 사용하여 <div id="root"></div>
라는 최상위 돔 엘리먼트 안에 "모든 리액트 컴포넌트를 렌더링"했습니다. 하지만 Modal이나 Dialogue 같은 컴포넌트를 렌더링할 때는 이러한 구조를 벗어나 "또 다른 최상위 돔 엘리먼트에 위치"시키도록 하고 싶은 경우가 있습니다.
<div id="root"></div>
최상위 돔 엘리먼트에 렌더링하는 것은 기술적으로 잘못된 부분은 아니지만 이상적이지 않습니다. HTML 문서는 Semantic하게 작성해야 하는데 이러한 관점에서는 이상적이지 않다고 판단이 됩니다.
Modal을 예로 들면 Modal은 Overlay로써 논리적으로 "모든 것 위에" 있다고 말할 수 있습니다. 하지만 Modal을 나타내는 컴포넌트가 다른 컴포넌트 내부에 깊숙히 중첩되어 렌더링되는 경우 의미가 맞지 않다고 판단됩니다.
이는 리액트의 "Portal" 개념을 사용하여 컴포넌트는 "다른 돔 엘리먼트 아래에 렌더링"하여 해결할 수 있습니다. "다른 돔 엘리먼트 아래에 렌더링"만할 뿐이지 구조적으로 상위 컴포넌트에서 props를 통해 데이터를 전달받는 것은 이전과 동일하게 동작합니다.
"Portal"은 react-dom
라이브러리를 사용해야 하며, HTML 문서에서 컴포넌트를 렌더링할 돔 루트 엘리먼트(root 엘리먼트가 아닌 엘리먼트)가 필요합니다.
참고로 react 라이브러리는 상태 관리 등을 비롯한 리액트의 모든 기능이 존재하는 라이브러리 이며, react-dom 라이브러리는 리액트를 사용해 로직과 각종 기능들을 웹 브라우저와 연결시켜 줍니다. 즉, DOM과 호환되도록 만들어주는 역할을 합니다.
// index.html
<!DOCTYPE html>
<html lang="ko">
<head>
,,,,
<heady>
<body>
<!-- 백드롭이 렌더링될 위치 -->
<div id="backdrop"></div>
<!-- 모달이 렌더링될 위치 -->
<div id="overlay"></div>
<!-- 일반적인 컴포넌트들이 렌더링될 위치 -->
<div id="root"></div>
</body>
</html>
// ErrorModal.jsx
import React from 'react';
import ReactDOM from 'react-dom';
// 백드롭 컴포넌트
const Backdrop = props => {
return <div onClick={props.onConfirm}></div>;
};
// 모달 컴포넌트
const Modal = props => {
return <div></div>;
};
// 에러 모달 컴포넌트
const ErrorModal = props => {
return (
<>
// Backdrop 컴포넌트를 backdrop 엘리먼트 아래에 렌더링
{ReactDOM.createPortal(<Backdrop />, document.getElementById('backdrop')}
// Modal 컴포넌트를 modal 엘리먼트 아래에 렌더링
{ReactDOM.createPortal(<Modal />, document.getElementById('modal')};
</>
);
};
export default ErrorModal;
ReactDOM.createPortal
메서드는 두 개인 인수를 전달받습니다. 첫 번째 인수로는 "JSX 문법"을 전달하고, 두 번째 인수로는 컴포넌트를 렌더링할 위치인 "돔 요소 노드"를 전달합니다.
ErrorModal 컴포넌트가 렌더링될 때 Backdrop 컴포넌트와 Modal 컴포넌트는 ReactDOM.createPortal
메서드의 두 번재 인수로 전달된 돔 엘리먼트 아래에 렌더링됩니다.
이후에 ErrorModal 컴포넌트를 "어디서 사용하던 언제나" HTML 문서(index.html) 기준으로 Backdrop 컴포넌트는 <div id="backdrop"> </div>
의 Content에 렌더링되고, Modal 컴포넌트는 <div id="modal"></div>
의 Content에 렌더링됩니다.
즉, ReactDOM.createPortal
메서드를 사용하여 컴포넌트를 우리가 원하는 "특정 HTML 엘리먼트로 옮길 수 있습니다".
React 18부터는 더이상 react-dom 패키지의 render
을 사용하지 않고, react-dom/client 패키지의 createRoot
를 사용해야 합니다.
자세한 내용은 여기에 나와있습니다.
// react 17v
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
위 코드는 18v 이전에 작성하던 코드이며 아래는 18v 이후 코드입니다.
// react 18v
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
react-dom/client 패키지의 createRoot
메서드에 인수로 루트 돔 엘리먼트를 전달하고, 반환되는 객체의 render
메서드에 루트 컴포넌트를 전달해줍니다.