"React Portal"
은 컴포넌트를 우리가 원하는 돔 엘리먼트(실제 돔 엘리먼트)에 렌더링하고 싶은 경우에 사용하는 개념입니다.
// index.js, React v18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootNode = document.getElementById('root');
ReactDOM.createRoot(rootNode).render(<App />);
위 코드처럼 지금까지는 ReactDOM.createRoot().render()
을 사용하여 HTML 문서의 <div id="root"></div>
라는 최상위 돔 엘리먼트 안에 "모든 리액트 컴포넌트를 렌더링"했습니다.
하지만 Modal이나 Dialogue 같은 컴포넌트를 렌더링할 때는 이러한 구조를 벗어나 "또 다른 최상위 돔 엘리먼트에 위치"시키도록 하고 싶은 경우가 존재할 수 있습니다.
즉, root 돔 요소 노드가 아닌 다른 돔 요소 노드에 렌더링하고자할 때 사용합니다.
Modal이나 Dialogue와 같은 것들을 <div id="root"></div>
최상위 돔 엘리먼트에 렌더링하는 것은 기술적으로 잘못된 부분은 아니지만 이상적이지 않습니다. HTML 문서는 Semantic하게 작성해야 하는데 이러한 관점에서는 이상적이지 않다고 판단이 됩니다.
Modal을 예로 들면 Modal은 Overlay로써 논리적으로 "모든 것 위에" 존재해야 합니다. 하지만 Modal을 나타내는 컴포넌트가 다른 컴포넌트 내부에 깊숙히 중첩되어 렌더링되는 경우 Sementic한 HTML 구조에 맞지 않다고 판단됩니다.
이는 리액트의 "Portal" 개념을 사용하여 컴포넌트를 "다른 돔 엘리먼트 아래에 렌더링"하여 해결할 수 있습니다.
주의할 점으로는 "다른 돔 엘리먼트 아래에 렌더링"될 뿐이며 구조적으로 상위 컴포넌트에서 props를 통해 데이터를 전달받는 것은 이전과 동일하게 동작합니다.
"Portal"은 "react-dom/client" 라이브러리를 사용해야 하며, 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>
위 HTML 문서처럼 리액트 앱이 렌더링될 root 돔 요소 노드와 BackDrop과 Modal이 렌더링될 돔 요소 노드를 생성합니다.
// ErrorModal.js
import ReactDOM from 'react-dom';
const Backdrop = props => { // -> 백드롭 컴포넌트
return <div></div>;
};
const Modal = props => { // -> 모달 컴포넌트
return (
<div>
<ModalContnet />
</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
메서드는 두 개인 인수를 전달받습니다.
첫 번째 인수로는 "리액트 엘리먼트"를 전달받습니다.
두 번째 인수로는 렌더링될 위치인 "돔 요소 노드"를 전달합니다.
ErrorModal 컴포넌트로 JSX 문법 사용시 Backdrop 컴포넌트와 Modal 컴포넌트는 ReactDOM.createPortal
메서드의 두 번재 인수로 전달된 돔 엘리먼트 아래에 렌더링됩니다.
즉, Backdrop 컴포넌트는 <div id="backdrop"></div>
의 Content에 렌더링되고, Modal 컴포넌트는 <div id="modal"></div>
의 Content에 렌더링됩니다.
const MainContent = () => {
const [hasError, setHasError] = useState(false);
const validHandler = () => {
setHasError(true);
}
return (
<>
,,,
{hasError && <ErrorModal/>}
<InputFormContent onValid={validHandler}/>
,,,
</>
);
};
예를 들어, 위 코드처럼 InputFormControl 컴포넌트에서 발생한 에러에 대해서 모달창을 표시해주기 위해 ErrorModal 컴포넌트를 작성하더라도 실제로는 다른 돔 요소 노드 아래에 렌더링되므로 HTML 구조를 Sementic하게 유지할 수 있습니다.
React v18부터는 더이상 react-dom 패키지의 render
을 사용하지 않고, react-dom/client 패키지의 createRoot
를 사용해야 합니다.
자세한 내용은 여기에 나와있습니다.
// react 18v 이전
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
메서드에 루트 컴포넌트를 전달해줍니다.