
리액트 공식문서에 따르면, Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법이라고 말한다.
예시를 한번 보자

보면 현재 <div id="root".>와 <div id ="backdrop-root".>, <div id = "root".>는 동등관계 같이 보이지만, 사실은 root가 부모고 나머지 두 개는 자식 컴포넌트다.
대신 렌더링이 부모 컴포넌트인 root 밖에서 이루어진다.
정리하면 부모와 자식관계이지만, root가 렌더링 된다고 해서 자식 컴포넌트들도 렌더링이 일어나지는 않는다. 왜냐면 독립돼 있으니까.
===
일반적으로 react는 부모 컴포넌트가 렌더링 되면 자식 컴포넌트도 렌더링 되는 tree 구조다.
그런데 이런 tree 구조가 불필요한 렌더링을 발생시키는 문제점을 가져온다. 그래서 부모와 자식 관계는 유지하지만 부모의 렌더링에 영향을 받지 않는 독립적인 위치를 가지고자 할 때 주로 사용한다.
특히 지금 내가 만들어놓은 모든 modal은 Portal을 통해 만들었는데 그 이유는, 만약 내가 modal을 portal을 통해 만들지 않고 <div id="root".> 안에 만든다면, 스크린리더가 렌더링되는 HTML 코드를 해석할 떄 모달이라는 존재를 인식 못한다. 그리고 구조적인 관점에서 모달이 모든 영역 위에 깔린다는 것도 알지 못한다.
그래서 만약 <div id="root".> 안에 만든다면 부모 컴포넌의 스타일링 속성에 제약을 받아 z-index 등으로 번거로운 후처리를 해줘야한다.
무엇보다 modal은 페이지 위에 표시되는 오버레이다. 그럼 당연히 다른 모든 것의 위에 붕 떠 있는 건데 다른 HTML 코드와 고구마줄기처럼 엮여있다면, 기술적으로는 z-index 같은 것으로 원하는대로 작동할 지는 몰라도, 개인적으로 좋은 구조는 아니라고 생각한다.
그래서 뭐 모달이나, 부모 컴포넌트로부터 독립적으로 위치시켜야 할 게 있따면 default 값인 <div id="root".> 안이 아니라, 렌더링 될 위치를 심어 그곳에서 실행시키면 된다.
그럼 부모-자식간의 관계는 유지하되, 부모 컴포넌트로부터 영향을 받지 않는 독립적일 수 있다.
===

import React from 'react';
import ReactDOM from 'react-dom';
import ConfirmOverlay from '../modalSetting/overlay/confrimOverlay/ConfirmOverlay';
import { IConfirm } from './confirmModal.types';
const ConfrimModal: React.FC<IConfirm> = ({
onClickGotoMain,
onClickGotoPost2,
postDiaryItem,
}) => {
return (
<div>
{ReactDOM.createPortal(
<ConfirmOverlay
onClickGotoMain={onClickGotoMain}
onClickGotoPost2={onClickGotoPost2}
postDiaryItem={postDiaryItem}
/>,
document.getElementById('overlay-root') as HTMLElement
)}
</div>
export default ConfrimModal;
포탈이 정의돼 있는 리액트 돔을 불러온다.
리액트 돔의 createPortal을 사용하는데, 이것은 두 가지 매개변수를 가져야한다.
위의 코드를 보면 이제 createPortal을 통해 ConfirmOverlay가 overlay-root 라는 DOM 영역에서 렌더링이 되게끔 로직이 구성됐다.
근데 여기서 보면, 지금 저기 portal 시키는 컴포넌트는 가만 보면 로직이 거의 비슷하다. 그래서 나는 하나의 컴포넌트로 분리시켰다
//Portal.ts
import ReactDom from 'react-dom';
import { IPortal } from './Portal.types';
const Portal : React.FC<IPortal> = ({ children }) => {
const overlayRoot = document.getElementById('overlay-root')!;
return ReactDom.createPortal(children, overlayRoot);
};
export default Portal;
//DeleteOverlay.tsx
import React, { MouseEvent } from 'react';
import * as S from './DeleteOverlay.styles';
import Animation3 from 'src/components/commons/utills/Animation/Animation3';
import { IDelete } from './DeleteOverlay.types';
import Portal from 'src/components/commons/utills/Portal/Portal';
const DeleteOverlay: React.FC<IDelete> = ({ onOk, onClose }) => {
const onClickCancel = () => {
onClose();
};
const onClickModalDiv = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const onClickOk = () => {
onOk();
};
return (
<Portal>
<S.ContainerDiv className='modal' onClick={onClose}>
<Animation3>
<S.ModalContentDiv onClick={onClickModalDiv}>
<img src='/cloud.png.png' alt='구름' />
<S.ContentsDiv>
<S.TitleSpan>나인 클라우드를 정말 떠나시겠어요?</S.TitleSpan>
<S.SubTitleSpan>
떠나시면 작성하신 마음 일기는 모두 사라져요!
</S.SubTitleSpan>
</S.ContentsDiv>
<S.ButtonWrapperDiv>
<S.CancleButton onClick={onClickCancel}>
다시 생각해볼게요
</S.CancleButton>
<S.OkButton onClick={onClickOk}>네 떠날래요</S.OkButton>
</S.ButtonWrapperDiv>
</S.ModalContentDiv>
</Animation3>
</S.ContainerDiv>
</Portal>
);
};
export default DeleteOverlay;
보면 내가 만든 Portal이라는 컴포넌트로 DeleteOverlay jsx 섹션을 전부 감쌌다. 그러면 이게 지금 children으로 들어가는 것.
아까 말한 createPortal의 첫 번째 인자가 되는 것이다
두 번째 인자인, getElementbyId('장소')는 overlayRoot 라는 변수로 미리 선언해서 그냥 children 뒤에 overlayRoot로 표시했다 이러면 두 개의 인자는 전달이 돼 조건이 충족되고 , Portal을 통해 만들어질 모든 modal들은 이 Portal이라는 컴포넌트로 감싸지기만 하면 된다.
똑같은 일을 두 번씩 하고 있었는데, 아주 간단하게 리팩토링 시켰다.