React의 portal은 컴포넌트를 렌더링 할 때 UI를 어디에 렌더링 시킬지 DOM을 사전에 선택할 수 있도록 합니다.
이를 통해 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드에 렌더링을 할 수 있습니다.
모달의 경우 일반적으로 최상위에 나타납니다. 따라서 portal을 이용하여 모달이 다른 컴포넌트의 최상위에 나타날 수 있도록 모달이 렌더링 될 위치를 지정했습니다.
먼저 index.html에 다음과 같이 모달이 렌더링 될 DOM 노드를 추가해줍니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Payhere Frontend 과제</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- NOTE: 모달 루트 추가 -->
<div id="modal-root"></div>
</body>
</html>
Portal을 생성하기 위해 createPortal을 다음과 같이 호출합니다.
createPortal(children, domNode, key?)
children
: Portal을 통해 렌더링 할 JSX와 같은 React로 렌더링할 수 있는 모든 것domNode
: children
이 렌더링 될 DOM 노드key?
: Portal의 키로 사용할 고유한 문자열 또는 숫자(optional)createPortal
의 반환값children
으로 전달 받은createPortal
의 반환값을 domNode
에 위치시킵니다. 다음과 같이 createPortal을 이용하여 모달을 적용했습니다.
interface ModalProps {
children: ReactNode;
isVisible: boolean;
}
const Modal = ({ children, isVisible }: Props) => {
const modalRoot = document.getElementById('modal-root') as HTMLElement;
return createPortal(
isVisible ? (
<ModalContainer>
<ModalOverlay />
<ModalContent>{children}</ModalContent>
</ModalContainer>
) : null,
modalRoot
);
}
export default Modal;
Next.js에는 index.html 파일이 없기 때문에 _document.tsx
에 모달 루트를 추가합니다.
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
// NOTE: 모달 루트 추가
<div id="modal-root" />
<NextScript />
</body>
</Html>
);
}
_app.tsx
에 모달 루트를 추가해도 동작합니다. 다만 그 위치가 다음과 같이 차이가 있습니다.
_document.tsx
에 <div id="modal-root" />
추가<div id="__next">
와 같은 계층에 위치_app.tsx
에 <div id="modal-root" />
추가<div id="__next">
의 하위 계층에 위치document is not defined
React와 동일하게 코드를 적용할 경우 Next.js에서는 document is not defined
에러가 발생합니다.
그 이유는 많은 분들이 알고 있듯 서버 사이드 렌더링을 먼저 수행하기 때문입니다.
따라서, 클라이언트 사이드에 존재하는 window
, document
같은 브라우저 전역 객체를 사용할 수 없습니다.
useEffect
를 이용하여 document
객체가 정의된 후 사용할 수 있도록 다음과 같이 코드를 작성합니다.
interface ModalProps {
children: ReactNode;
isVisible: boolean;
}
const Modal = ({ children, isVisible }: Props) => {
const [modalElement, setModalElement] = useState<HTMLElement | null>(null);
useEffect(() => {
const modalRoot = document.getElementById('modal-root');
setModalElement(modalRoot);
}, []);
if (modalElement === null) return null;
return createPortal(
isVisible ? (
<ModalContainer>
<ModalOverlay />
<ModalContent>{children}</ModalContent>
</ModalContainer>
) : null,
modalRoot
);
}
export default Modal;
참고