React의 portal를 사용해보자

Devookim·2022년 7월 31일
1
post-thumbnail

왜 portal를 포스팅 할까?

이번에 공용 컴포넌트를 개발하면서 발생했던 이슈를 portals로 해결 하였다.
react를 사용하면서 portals을 사용한 것은 처음이기 때문에 간단히 정리해보려고 한다.

Portal가 뭐지?

Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.

리액트 docs에 나와있는 설명이다.

ReactDOM.createPortal(child, container)

이렇게 하면 container에 child를 렌더링 할 수 있다고 한다.
사실 개념 자체는 어려운게 아니어서 내부 로직까지는 아니더라도 어떻게 동작되는지는 쉽게 알 수 있다.
(포탈2를 즐겨했던게 도움이 된건가..?)

Portal를 왜 사용했을까?

이번에 겪은 이슈의 핵심만 간단히 설명하겠다.

ISSUE

const DropdownComponent = ({ id }) => {
  const [isOpen, setIsOpen] = useState(false);
  const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
  
  return (
    <Dropdown isOpen={isOpen} toggle={toggle}>
      <DropdownToggle>{id} - dropdown</DropdownToggle>
      <DropdownMenu>
        <DropdownItem>foobar</DropdownItem>
        <DropdownItem>foo</DropdownItem>
        <DropdownItem>bar</DropdownItem>
      </DropdownMenu>
    </Dropdown>
  );
};  

const 위_이미지_컴포넌트 = {list.map((v, index) => (
  <ZIndexComponent key={index}>
    <DropdownComponent id={index} />
  </ZIndexComponent>
))}

각 부모(파란박스)에는 z-indexsticky가 적용되어있고Dropdown이 각각 컴포넌트 안에 구현되어 있는 구조이다.
이렇게 했더니 DropdownMenu들이 부모를 벗어나지 못한채 안에 갇혀버리는 문제가 발생하였다.

정말 그런지 확인해보자.

정말 그렇다.

왜 그런지 먼저 알아보자

The stacking context

The stacking context MDN은 쌓임 맥락으로 변역하고 있다.
쌓임 맥락을 간단히 요약하자면 z-index가 쌓이는 공간(?)이라고 할 수 있을 것 같다.
즉, 쌓임 맥락이 형성된 z-index는 형성된 쌓임 맥락 안에서만 유효한 것이다.

쌓임 맥락이 생성되는 조건을 살펴보자

  • 문서의 루트 요서 (<html>)
  • positionabsolute 또는 relative 이고 z-indexauto 가 아닌 요소
  • positionfixed 또는 sticky 인 요소
  • flexbox 컨테이너의 자식 중 z-indexauto 가 아닌 요소
  • 이하 생략

ISSUE 원인

쌓임 맥락이 생성되는 조건 세번째를 다시 읽어보자.
즉, sticky안에서 렌더링 되는 z-index들은 sticky가 적용된 부모를 벗어나지 못한다는 것이다.
bootstrap의 dropdown의 z-index는 1000인데도 불구하고 sticky가 적용된 부모를 벗어나지 못하는 ISSUE가 발생한 것이다.

해결

부모 컴포넌트 바깥에서 Dropdown이 렌더링 되도록 하면 간단히 해결되는 문제였다. (portal를 알았다면 😤)

portal를 이용하여 위 예제 코드를 수정해보자.

먼저 포탈이 받은 컴포넌트를 렌더링할 엘리먼트를 만들어보자.

const 최종_렌더될_컴포넌트 = (
  <>
    {list.map((v, index) => (
      <ZIndexComponent key={index}>
        <DropdownComponent id={index} />
      </ZIndexComponent>
    ))}
    <div id="portal-target" />	// 포탈의 컴포넌트가 렌더링 될 element
  </>
)

포탈을 만들어보자.

const Portal = ({ children }) => {
  const portalTarget = document.getElementById("portal-target");

  if (!portalTarget) {
    return null;
  }
  return ReactDOM.createPortal(<div>{children}</div>, portalTarget);
};

포탈을 사용해보자.

const DropdownComponent = ({ id }) => {
  const [isOpen, setIsOpen] = useState(false);
  const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
  
  return (
    <Dropdown isOpen={isOpen} toggle={toggle}>
      <DropdownToggle>{id} - dropdown</DropdownToggle>
        <Portal>
          <DropdownMenu>
            <DropdownItem>foobar</DropdownItem>
            <DropdownItem>foo</DropdownItem>
            <DropdownItem>bar</DropdownItem>
          </DropdownMenu>
      </Portal>
    </Dropdown>
  );
};

포탈은 sticky가 적용된 부모 바깥에서 z-index가 적용된 DropdownMenu가 렌더링 되기 때문에 의도한대로 동작할 것이다.

의도되로 구현 되었는지도 확인해보자.

portal을 사용한 의도대로 stikcy부모 바깥에 Dom tree도 잘 생성 되었다.

profile
I am Woo

1개의 댓글

comment-user-thumbnail
2022년 8월 25일

오래간만이구만~
2015년에 컨퍼런스가서 HTML 신기술이라고 발표했던걸 들은 것 같은데 ㅎㅎ 리액트에서 잘 쓰고 있나보네

답글 달기