React Portal

김동현·2021년 12월 7일
1

React

목록 보기
9/27
post-thumbnail
post-custom-banner

React Portal

"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 사용하기

"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 메서드는 두 개인 인수를 전달받습니다.

  1. 첫 번째 인수로는 "리액트 엘리먼트"를 전달받습니다.

  2. 두 번째 인수로는 렌더링될 위치인 "돔 요소 노드"를 전달합니다.

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하게 유지할 수 있습니다.

createRoot

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 메서드에 루트 컴포넌트를 전달해줍니다.

profile
Frontend Dev
post-custom-banner

0개의 댓글