이번에 공용 컴포넌트를 개발하면서 발생했던 이슈를 portals
로 해결 하였다.
react를 사용하면서 portals
을 사용한 것은 처음이기 때문에 간단히 정리해보려고 한다.
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.
리액트 docs에 나와있는 설명이다.
ReactDOM.createPortal(child, container)
이렇게 하면 container에 child를 렌더링 할 수 있다고 한다.
사실 개념 자체는 어려운게 아니어서 내부 로직까지는 아니더라도 어떻게 동작되는지는 쉽게 알 수 있다.
(포탈2를 즐겨했던게 도움이 된건가..?)
이번에 겪은 이슈의 핵심만 간단히 설명하겠다.
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-index
와 sticky
가 적용되어있고Dropdown
이 각각 컴포넌트 안에 구현되어 있는 구조이다.
이렇게 했더니 DropdownMenu
들이 부모를 벗어나지 못한채 안에 갇혀버리는 문제가 발생하였다.
정말 그런지 확인해보자.
정말 그렇다.
왜 그런지 먼저 알아보자
The stacking context
MDN은 쌓임 맥락으로 변역하고 있다.
쌓임 맥락을 간단히 요약하자면 z-index
가 쌓이는 공간(?)이라고 할 수 있을 것 같다.
즉, 쌓임 맥락이 형성된 z-index
는 형성된 쌓임 맥락 안에서만 유효한 것이다.
쌓임 맥락이 생성되는 조건을 살펴보자
<html>
)position
이 absolute
또는 relative
이고 z-index
가 auto
가 아닌 요소position
이 fixed
또는 sticky
인 요소flexbox
컨테이너의 자식 중 z-index
가 auto
가 아닌 요소쌓임 맥락이 생성되는 조건 세번째를 다시 읽어보자.
즉, 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도 잘 생성 되었다.
오래간만이구만~
2015년에 컨퍼런스가서 HTML 신기술이라고 발표했던걸 들은 것 같은데 ㅎㅎ 리액트에서 잘 쓰고 있나보네