오늘 인턴 수행 중 두 번째 선임 호출을 하게 되었다.. 고객사 디자인 업그레이드 프로젝트가 추석 연휴 전으로 하여 모두 마무리가 되어야 했다. 웬만한 퍼블리싱 작업이 완료가 되었고, 혹시나 스타일이 깨진 부분이 없나 확인하던 중이었다. 그때, 모달 내에 위치한 달력 위젯이 모달의 하단 버튼에 가려지고 무슨 이유 때문인지 스크롤이 내려지지 않는 현상이 발생했다.
모달 내 본문은 overflow: scroll
, 모달 하단 버튼 영역에는 position: sticky
속성이 부여되어 있다. 처음에는 늘 그랬듯 쌓임 맥락이 문제인가 싶어서 z-index를 손봤다. 하지만 position이 sticky인 요소의 쌓임 맥락은 새로 형성된다. 자, 그럼 이제 어찌해야 할까.. 도저히 문제해결의 감이 잡히질 않았고 선임 찬스를 쓸 순간이었다. z-index 문제가 아닐 것 같고 portal을 활용하면 될 것 같다고 하셨다. 그러고 바로 react-datepicker 라이브러리의 공식문서로 향하셨고 몇 번 뚝딱뚝딱 하시고는,, 잘 되지 않았다. 프로젝트의 라이브러리 버전이 너무 오래됐다며, 버전 업그레이드 후 정상적으로 적용되는지 확인해보라고 하시고 떠나셨다. 다행히 버전 업데이트 하고 나니 정상적으로 동작하였다!
결론은, index.html에 datepicker가 위치할 곳을 지정하고 그 곳에 달력을 위치시키는 방식으로 해결할 수 있었다. 이전에 이정환 님의 '한 입 크기로 잘라먹는 Next.js' 강의에서 createPortal
을 사용하여 모달창을 띄우는 스킬을 본 적이 떠올랐다. 혹시나 해서 그 개념과 관련있어서 라이브러리에서도 portal이라고 표현하는 것인가 궁금했다. 역시나 이전 React 버전부터 있는 개념이었고 이러한 용도로 사용되는게 옳았다.
서론이 길었지만, 이번 포스팅은 React 공식문서에 나와있는 createPortal
의 개념에 대해 정리하고 react-datepicker와 react-select 라이브러리의 가림 현상에 대해 정리하고자 한다.
Portals
[사진 출처: 메이플스토리 인벤]
Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링할 수 있는 방법을 제공한다.
portal: 문
리액트 공식문서에는 createPortal가 다음과 같은 상황에서 유용하게 쓰일 수 있다고 한다.
portal의 전형적인 유스케이스는 부모 컴포넌트에 overflow: hidden이나 z-index가 있는 경우이지만, 시각적으로 자식을 “튀어나오도록” 보여야 하는 경우도 있습니다. 예를 들어, 다이얼로그, 호버카드나 툴팁과 같은 것입니다.
createPortal(children, domNode, key?)
Portal을 생성하려면
createPortal` 을 호출하여 일부 JSX와 렌더링할 DOM 노드를 전달해야 한다.
import { createPortal } from 'react-dom';
// ...
<div>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>
<div />
또는 <SomeComponent />
), <Fragment>
(<>...</>
), 문자열이나 숫자 또는 이들의 배열과 같이 React로 렌더링할 수 있는 모든 것이다.document.getElementById()
가 반환하는 것과 같은 일부 DOM 노드로, 노드가 이미 존재해야 한다. 업데이트 중에 다른 DOM 노드를 전달하면 Portal 콘텐츠가 다시 생성된다.// 리액트 공식문서 중
import { createPortal } from 'react-dom';
export default function MyComponent() {
return (
<div style={{ border: '2px solid black' }}>
<p>This child is placed in the parent div.</p>
{createPortal(
<p>This child is placed in the document body.</p>,
document.body
)}
</div>
);
}
개발자 도구로 확인해보면, 아래와 같이 두번째 <p>
는 React의 진입점인 <div id="root"/>
를 벗어나 document.body
하위에 위치한 것을 확인할 수 있다.
이번에 접한 문제와 동일한 예시이다.
import { useState } from 'react';
import { createPortal } from 'react-dom';
import ModalContent from './ModalContent.js';
export default function PortalExample() {
const [showModal, setShowModal] = useState(false);
return (
<>
<button onClick={() => setShowModal(true)}>
Show modal using a portal
</button>
{showModal && createPortal(
<ModalContent onClose={() => setShowModal(false)} />,
document.body
)}
</>
);
}
index.html
내에 아래와 같이 datepicker가 띄워질 위치를 지정한다.
// index.html
<body>
<div id="portal"></div>
</body>
// Modal.jsx
<RDatePicker
{...this.props}
portalId="portal"
/>
import Select from 'react-select';
<Select
menuPortalTarget={document.getElementById('portal')}
styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }}
/>
공식문서의 중요성을 깨달았다. 그냥 주제로 다루고 있었던 거다.. 이전에 리액트 공식문서 번역 기여를 하면서 생각보다 개발 중에 맞닥뜨릴 수 있는 문제들이 예제로 있다는 것을 알 수 있었다. 모르는 것이 나올 때 적어도 해당 개념의 페이지는 모두 읽어보는 것을 습관화하는 것이 좋겠다.