React Portal 적용기
안녕하세요. 🙂
식스샵 프론트엔드 개발팀입니다.
최근에 Popover 컴포넌트를 적용하는 중에 디자인 시안처럼 구현하기 어려운 문제가 발생했었습니다. 해당 문제를 해결하는 방법으로 Portal이라는 기술을 접하고 다뤄본 부분을 간단하게 소개하려고 합니다.
Portal?
리액트 공식문서에 의하면 Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법이라고 합니다.
쉽게 요약하자면 특정 컴포넌트를 부모 컴포넌트 외부에서 렌더링할 때 사용할 수 있는 기술입니다. 말 그대로 Portal …! 마치 영화나 게임에서 두 공간을 이어주는 차원의 문과 같은 개념이죠.
Portal은 react-dom 패키지의 createPortal를 사용하여 생성합니다. 첫 번째 인자(child)는 차원의 문으로 보낼 컴포넌트(컴포넌트 말고도 엘리먼트, 문자열, 혹은 fragment와 같은 어떤 종류이든 렌더링할 수 있는 요소면 가능해요.), 두 번째 인자(container)는 첫번째 인자를 받아줄 부모요소입니다.
어디에 사용할까?
아래 이미지와 같이 페이지 리스트의 더보기 아이콘을 클릭하면 팝오버가 노출되는 코드를 작성했으나 페이지탭과 섹션 탭의 z-index, overflow: hidden으로 처리된 부모 컴포넌트의 스타일 방해를 받아 팝오버가 잘려 보이는 문제가 발행했습니다.
여기서 단순히 페이지 탭 요소의 z-index를 높이게 되면 오히려 페이지 텝이 섹션 탭 요소를 가려버리는 문제가 발생하게 되는데요. 해결방법을 찾는 도중 팀원의 도움으로 Portal을 사용해 볼 것을 제안했습니다.
Portal의 전형적인 사례는 부모 컴포넌트의 position, z-index, overflow 속성으로 인해 자식 컴포넌트가 시각적으로 가려지는 경우 등을 해결하기 위해 주로 사용합니다. 예를 들면 Modal, Popover, Tooltip 등에서 사용할 수 있는데요. 참고로 이 글에서는 Popover 컴포넌트를 예시로 가져왔습니다.
이제 해당 문제를 해결하기 위한 방법으로 Portal을 사용해보겠습니다.
구현하기
1. 렌더링할 위치 심어주기
우선 Portal의 위치를 어디에 설정할 지 정해야 합니다. CRA기반이면 index.js에 심어주게 되지만 식스샵에서는 next.js와 typescript를 기반으로 개발하고 있어 _document.tsx에 추가합니다. (1–1)
혹은 next.js를 사용하지 않는다면 CRA 환경에서 index.html 문서에 1–2처럼 적용시키야 합니다.
_document.tsx는 next.js에서 제공하는 파일로 html 문서를 커스텀할 수 있습니다. 공통으로 사용할 head, meta 정보, 웹 접근성 설정, body 등을 커스텀 할 때 활용합니다.
_document.tsx 파일은 항상 서버에서 실행되는 파일이기 때문에 브라우저 api, 이벤트 핸들러 등을 포함된 비즈니스 로직을 추가하지 않도록 주의해주세요.
1–1, 1–2와 같이 작성하고 서버를 구동시키면 root에서 1–3과 같이 DOM tree에 Portal 엘리먼트 생성된걸 볼 수 있습니다.
2. 포탈 컴포넌트 구현하기
다음으로 컴포넌트를 children prop으로 받아 Portal을 생성하는 Portal 컴포넌트를 구현합니다.
앞서 설명해 드린 대로 Portal 컴포넌트는 자식 컴포넌트와 자식 컴포넌트를 이동시킬 Portal 엘리먼트를 props로 받고 있습니다.
2–1처럼 작성한 Portal 컴포넌트는 createPortal에 의해 결과 값으로 ReactNode를 반환하게 되는데 이 ReactNode를 DOM에 렌더링할 때 selector로 받은 두 번째 인자로 지정된 DOM Element에 자식 컴포넌트를 전달합니다. 쉽게 말하면 자식 컴포넌트를 selector로 지정된 부모 Element에 렌더링 시킨다는 의미가 됩니다.
3. Popover 컴포넌트에 Portal 적용하기
이제 우리가 사용할 Popover 컴포넌트를 Portal 시켜보겠습니다.
3–1의 line 9~11와 같이 렌더링 하는 부분에 Popover 컴포넌트를 Portal 컴포넌트로 감싸주었습니다. 이렇게 하면 Portal 컴포넌트에 의해 Popover 컴포넌트는 selector로 전달한 #portal-root 엘리먼트에 렌더링 됩니다.
결과로 3–2와 같이 부모 컴포넌트 내부가 아닌 상위 계층에 Popover 컴포넌트가 생성되고 3–3과 같이 렌더링 되었습니다. 그렇기 때문에 Popover 컴포넌트는 z-index, overflow:hidden 등의 의도하지 않은 제약으로부터 자유롭게 스타일이 가능하게 됩니다.
Portal을 통한 이벤트 버블링
Portal을 사용할 때 주의할 점도 있습니다. 공식문서에 설명하고 있지만 Portal의 위치가 DOM tree 어디에 위치해도 이벤트 버블링이 발생합니다. 그 이유는 실제 Portal은 DOM tree의 위치에 상관없이 React tree에 존재하기 때문입니다. 그래서 portal 내부에서 발생한 이벤트는 React tree에 포함된 상위로 이벤트가 전파됩니다.
첫 번째 이미지를 보면 Portal 컴포넌트를 PageList 내부에서 호출했음에도 DOM tree에서는 <div id=’portal-root’ />에 렌더링 되고 있습니다. 그러나 실제 React tree에서는 기존 위치에 Portal 컴포넌트가 있다는 것을 확인할 수 있습니다. 그렇기 때문에 Popover에서 발생한 이벤트는 부모 컴포넌트인 PageList 이상으로 버블링이 발생하게 됩니다.
마치며
가장 만족스러웠던 부분은 부모 컴포넌트의 스타일에 영향을 받지 않고 작업할 수 있다는 것이었습니다. 포스팅하는 시점에는 Popover 컴포넌트만 적용했으나 점차 Modal, Popup, Tooltip등으로 확장하면 쉽게 스타일을 구현할 수 있을 것이라 예상합니다!
이상으로 Portal 사용기 포스팅을 마치겠습니다.
긴 글 읽어주셔서 감사합니다.
Reference
식스샵은 올인원 이커머스 플랫폼을 만들고 있는 팀입니다.
B2B | E-Commerce | SaaS 시장에 대해 함께 고민하고, 빠른 실험과 검증을 통한 사업의 성장을 경험하고 싶은 분을 모시고 있습니다.
채용 중인 포지션은 이곳에서 확인하실 수 있습니다 🙂