React portal을 이용하면 UI 를 어디에 렌더링 시킬지 DOM 을 사전에 선택하여
부모 컴포넌트의 바깥에 렌더링 할 수 있게 해줄 수 있다.
즉, DOM 의 계층구조 시스템에 종속되지 않으면서 컴포넌트를 렌더링 할 수 있게 된다.
프로젝트를 진행하면서 부모 컴포넌트의 스타일링 속성에 제약을 받아서 모달이 가려지는 현상으로 z-index값을 조정해줘야 하는 경우가 있었다.
또 최상위에서 전체 스타일에 width:80%를 스타일을 줬다고 가정한다면,
그 안에 modal 이 들어갈 경우 전체 스타일의 영향을 받을 수 밖에 없기 때문에
portal로 분리하도로 했다.
export default class MyDocument extends Document {
//코드 생략...
render() {
return (
<Html>
<Head />
<body>
<Main />
<div id="modal-root" />
<NextScript />
</body>
</Html>
);
}
}
react에서는 index.html에 넣어주지만,next는 html 파일이 없으므로 _document.tsx파일에
<div id="modal-root" />
를 추가해주었다.
모달을 띄울 때 modal-root 노드가 portal의 도착지점이 된다.
// front/components/modal/ModalPortal.tsx
import React, { ReactElement, useEffect, useState } from "react";
import ReactDom, { createPortal } from "react-dom";
const Portal = ({ children }: { children: ReactElement }) => {
const [mounted, setMounted] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
if (typeof window === "undefined") return <></>;
return mounted ? createPortal(children, document.getElementById("modal-root") as HTMLElement) : <></>;
};
export default Portal;
const Login = () => {
return (
<LoginWrap>
<LoginContainer>
//일부코드 생략...
<Portal>
<FindPasswordModal />
</Portal>
</LoginContainer>
</LoginWrap>
);
};
Portal 의 children은 FindPasswordModal로
모달이 modal-root안에서 랜더링된다.
const [mounted, setMounted] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
Next는 별도의 설정을 하지 않는다면 SSR과 CSR을 동시에 진행한다.
SSR 과정에서 document에 접근하려고 하면 접근할 수 없어서 에러가 난다.
따라서 CSR을 마친 후, window객체가 있을때만 동작하게 작성해야 한다.
useEffect()를 통해서 mount되면 createPortal이 작동되도록 구현하였다.
if (typeof window === "undefined") return <></>;
이 코드를 추가하여도 해결된다.
이 전에서는 __next 최상위 루트 안에 포함되어있었다면,
modal-root로 빠져나와 그 안에 포함되어있는 것을 볼 수 있다.