리액트 Portal 사용하기

김세현·2022년 7월 7일
1

React

목록 보기
6/10
post-thumbnail

연습 프로젝트에서 모달을 구현하던 중 리액트 포털이라는 개념을 알게되었다.

요소, 컴포넌트에 대한 실제 HTML 코드를 JSX 코드가 작성된 위치를 기준으로 렌더링하지 않고, DOM의 다른 위치로 렌더링되게 한다.

프로젝트의 컴포넌트 구조는 다음과 같다.

// App.tsx
return (
  <>
    <AddUser/>
    <UserList/>
  </>
)
// AddUser.tsx
return (
  <>
    {error && <ErrorModal/>}
    <div>
      ...
    </div>
  </>
)

루트 컴포넌트에서 UserList , AddUser 컴포넌트를 사용하고, AddUser 컴포넌

트에서 에러 상태에 따라 ErrorModal 컴포넌트를 렌더링한다.

개발자 도구에서 살펴보면, 에러가 발생하는 상황에서 모달이 DOM에 렌더링 되고 정상적으로 동작한다.

하지만, 의미적인 관점이나 간결한 HTML 구조를 갖췄는지의 관점에서 살펴본다면 이 코드, 구조는 완벽하지는 않다.

왜냐하면, 모달은 기본적으로 전체 페이지 위에 표시되는 오버레이이기 때문이다.

따라서 다른 모든 요소 위에 존재해야 한다.

그런데, 현재의 모달은 다른 HTML 요소 안에 중첩되어 있다. 기술적으로 CSS 스타일 덕분

에 동작은 하지만 좋은 코드, 구조를 가지고 있지는 않다.

모달이 다른 요소 안에 중첩되어 있기 때문에 모달이 다른 모든 페이지에 대한 오버레이인지 명확하지 않다.
(모달 뿐만 아니라 사이드 드로어, Dialog 같은 모든 종류의 오버레이도 마찬가지이다)

(현재에는 단순한 프로젝트이기 때문에ErrorModal 컴포넌트가 root 컴포넌트 바로 아래(상단)에서 AddUser 컴포넌트와 함께 존재하고 있지만, 복잡한 프로젝트의 DOM 구조에서는ErrorModal이 더 중첩된 곳에 존재할 수도 있다. 이럴 경우에도 당연히 의미적으로나 구조적으로 옳지 않다.)


리액트 Portal

리액트 포털을 사용하면 기존의 JSX 코드를 거의 유지하면서 렌더링된 모달의 HTML 코드를

다른 위치로 옮길 수 있다. (전체 페이지의 최상단 : body의 자식으로)

즉 모달과 같은 전체 페이지에 대한 오버레이가 다른 요소들 안에 중첩되어 있는 구조적인 문제를 해결할 수 있다.


리액트 포털을 사용하기 위해 두 단계가 필요하다.

  1. 컴포넌트가 이동할 장소
  2. createPortal을 사용해 컴포넌트를 이동시키기

컴포넌트가 이동할 장소를 표시하기 위해 index.html에 다음과 같이 작성해 준다.

backdrop-rootmodal-root은 각각 모달 바깥쪽에 대한 HTML 코드와 모달 자체에 대한 HTML 코드가 이동할 위치이다.

이렇게 작성해주면 나중에 body의 자식 위치에(페이지 최상단에) 모달의 HTML 코드가 렌더링 될 것이다.

위치를 정했으면 리액트에게 이 위치로 이동되어야 한다고 알려줘야 한다.

먼저, react-dom 라이브러리의 createPortal메소드를 사용하기 위한 import를 해준다.

그리고 기존에 모달 코드가 작성되었던 JSX 코드에서 createPortal메소드를 호출한다.

createPortal 메소드는 두 개의 인수를 취한다.

  1. 렌더링 되어야 하는 리액트 노드
  2. 해당 노드가 렌더링되어야 하는 실제 DOM 컨테이너를 가리키는 포인터

참고 : 2번 째 인수를 작성할 때 DOM API를 이용해 실제 렌더링 되어야 하는 요소를 선택해야 한다.
document.getElementById()는 일반적으로 리액트 프로젝트에서는 직접 사용하진 않지만, 리액트 포털을 사용하기 위해선 실제 DOM 요소에 접근해야 하므로 명시적으로 코드를 작성한다.


  • createPortal을 사용하지 않은 기존 코드
 return (
    <>
      <div className={styles["backdrop"]} onClick={props.onCloseModal}></div>
      <Card className={styles["modal"]}>
        <header className={styles["header"]}>
          <h2>{props.title}</h2>
        </header>
        <div className={styles["content"]}>
          <p>{props.message}</p>
        </div>
        <footer className={styles["actions"]}>
          <Button onClick={props.onCloseModal}>닫기</Button>
        </footer>
      </Card>
    </>
  );
  • createPortal 메소드를 사용하는 코드
 return (
    <>
      {ReactDOM.createPortal(
        <div className={styles["backdrop"]} onClick={props.onCloseModal}></div>,
        document.getElementById("backdrop-root")!
      )}
      {ReactDOM.createPortal(
        <Card className={styles["modal"]}>
          <header className={styles["header"]}>
            <h2>{props.title}</h2>
          </header>
          <div className={styles["content"]}>
            <p>{props.message}</p>
          </div>
          <footer className={styles["actions"]}>
            <Button onClick={props.onCloseModal}>닫기</Button>
          </footer>
        </Card>,
        document.getElementById("overlay-root")!
      )}
    </>
  );

기존 : Modal이 렌더링될 때 다른 요소 안에 중첩되어 있다.

createPortal 사용 : Modal이 렌더링될 때 페이지의 최상단에 위치한다.

profile
under the hood

0개의 댓글