이번 편에서는 React Portals을 소개하고 작성하는 방법을 알아보겠습니다.

React Portals는 React에서의 특수한 개념이므로 다른 기술 혹은 분야에서의 Portal과는 의미가 다를 수 있습니다.

React의 고급 기능이므로 React를 익히는 목적이라면 어떤 것을 위한 기능인지 목적만 확인하는 정도로 충분합니다.

React 진입점

<!-- /public/index.html -->
<body>
  <div id='root'></div>
</body>
import React from 'react'
import ReactDOM from 'react-dom'

const App = () => (
  <div>App</div>
)

ReactDOM.render(<App />, document.getElementById('root'))

지금까지 소개한 내용으로 볼 떄에 React는 위 예시와 같이 최상위 태그 아래에 모든 컴포넌트를 '집어넣는' 구조입니다.

로그인창, 결제창 등을 위한 다이얼로그와 같은 컴포넌트를 작성하다 보면 이러한 구조를 벗어나 또 다른 최상위 태그에 위치하게 하고 싶을 때가 있습니다.

<!-- /public/index.html -->
<body>
  <div id='root'></div>
  <div id='other'></div>
</body>
import React from 'react'
import ReactDOM from 'react-dom'

const App = () => (
  <div>App</div>
)

ReactDOM.render(<App />, document.getElementById('root'))

const Other = () => (
  <div>Other</div>
)

ReactDOM.render(<Other />, document.getElementById('other'))

이런 형태를 상상해볼 수 있지만 App 컴포넌트와 Other 컴포넌트 사이에 State를 공유하는 등 복잡한 동작이 늘어날수록 점점 구조가 이상하게 변합니다.

더 나은 방법이 필요한 순간입니다.

React Portals

<!-- /public/index.html -->
<body>
  <div id='root'></div>
  <div id='other'></div>
</body>
import React from 'react'
import ReactDOM, { createPortal } from 'react-dom'

const App = () => (
  <>
    <div>App</div>
    <Other />
  </>
)

const Other = () => {
  return createPortal(<div>Other</div>, document.getElementById('other'))
}

ReactDOM.render(<App />, document.getElementById('root'))

React Portals를 이용하면 위에서 언급한 요구사항을 우회책 없이도 해결할 수 있습니다.

익숙하지 않으므로 하나씩 살펴보겠습니다.

  • React 라이브러리에서 createPortal을 불러오기.

  • App 컴포넌트에 Other 컴포넌트 포함하기.

  • Other 컴포넌트 안에서 createPortal 함수를 실행한 값을 return 하기.

    • createPortal 함수의 1번째 파라미터는 화면에 나타낼 JSX, 2번째 파라미터는 1번째 파라미터가 위치할 태그입니다.

해당 컴포넌트의 아래가 아니라 다른 태그의 하위에 위치한다는 것이 처음에는 헷갈릴 수 있지만 금방 적응할 수 있습니다.

import React, { useMemo } from 'react'
import ReactDOM, { createPortal } from 'react-dom'

const App = () => (
  <>
    <div>App</div>
    <Other />
  </>
)

const Other = () => {
  const rootElement = useMemo(() => document.getElementById('other'))

  return createPortal(<div>Other</div>, rootElement)
}

ReactDOM.render(<App />, document.getElementById('root'))

상태가 바뀌면 컴포넌트를 다시 그리는데, 그 때 마다 document.getElementById 메서드를 사용하는 것은 효율이 좋지 않으므로 React Hooks의 useMemo 함수를 이용하여 간단하나마 최적화를 해주는 것이 좋습니다.

추상화

<!-- /public/index.html -->
<body>
  <div id='root'></div>
  <div id='other'></div>
</body>
import React, { useMemo } from 'react'
import ReactDOM, { createPortal } from 'react-dom'

const App = () => (
  <>
    <div>App</div>
    <Portal>
      <Other />
    </Portal>
  </>
)

const Portal = ({ children }) => {
  const rootElement = useMemo(() => document.getElementById('other'))

  return createPortal(children, rootElement)
}

const Other = () => (
  <div>Other</div>
)

ReactDOM.render(<App />, document.getElementById('root'))

위 예시처럼 Portal 컴포넌트로 분리하면 보다 더 읽기 편하게 작성할 수 있습니다.