Nextjs에서 React Portal 사용해보기
포스트
취소

Nextjs에서 React Portal 사용해보기

React Portal

웹을 제작하다보면 대부분의 경우 계층 구조로 컴포넌트를 배치하여 렌더링하는 방식으로 문제를 해결할 수 있습니다.
그런데 간혹 n레벨에 배치 되어야 하는 컴포넌트가 실제로 렌더링 될 때는 Document의 최상단에 배치하고 싶을 수 있습니다.

Tooltip Tooltip Modal Modal

Tooltip이나 Modal 컴포넌트가 바로 그 대표적인 예시입니다.
위 두 개의 컴포넌트는 웹 페이지에 존재하는 어떤 요소보다 위에 존재해야 하기 때문에 <body> 태그의 바로 하단으로 옮겨줄 필요가 있습니다.
이런 상황에 간단하게 사용할 수 있는 방법이 바로 ReactDOM.createPortal() API입니다.

ReactDOM.createPortal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Modal = (props: ModalProps) => {
  const Component = React.createComponent('div', { ...props }, props.children);
  const portal = ReactDOM.createPortal(Component, document.body);

  return Portal
};

const App = () => {
  return (
    <Modal />
  );
}

ReactDOM.createPortal은 두 개의 인자를 받습니다.
첫번째 인자는 ReactNode, 즉 배치하고 싶은 컴포넌트를 넘겨주시면 됩니다.
두번째 인자는 첫번째 인자에서 넘겨준 ReactNode를 배치하고 싶은 Element를 넘겨주시면 됩니다.

Next

정말 사용하기 쉬운 API지만 Next에서 사용할 때 한가지 문제가 있습니다.

1
2
3
4
5
6
7
8
const Modal = (props: ModalProps) => {
  const Component = React.createComponent('div', { ...props }, props.children);
  const portal = ReactDOM.createPortal(Component, document.body);

  return Portal
};

위 코드를 Next.js에서 사용하면 아래와 같은 이슈가 표시될 것입니다.

document is not defined

Next는 별도의 설정을 하지 않는다면 SSR / CSR을 동시에 진행하기 때문에 SSR 과정에서 document를 탐색할 수 없기 때문입니다.
그렇다면 아래와 같이 코드를 수정해봅시다.

1
2
3
4
5
6
7
8
9
10
const Modal = (props: ModalProps) => {
  if (typeof window === 'undefined') return <></>;

  const Component = React.createComponent('div', { ...props }, props.children);
  const portal = ReactDOM.createPortal(Component, document.body);

  return Portal
};

CSR 과정에서는 window 객체가 생성될 터이니 window 객체가 없다면 렌더링을 진행하지 않는 로직이 추가되었습니다.
이러면 별 문제가 없어보이지만 아래와 같은 메시지가 나오면서 Hydration 이슈가 발생합니다.

Hydration failed because the initial UI does not match what was rendered on the server

Hydration

Hydration의 상세한 내용은 이 문서를 참고해주세요!
문제점만 간단하게 요약하자면 SSR에서 생성한 정적 페이지와 CSR에서 초기에 읽어낸 웹페이지의 코드가 서로 상이하여 생기는 문제입니다.

해결 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Modal = (props: ModalProps) => {
  const [isCSR, setIsCSR] = useState<boolean>(false);

  useEffect(() => {
    setIsCSR(true);
  }, [])
  
  if (typeof window === 'undefined') return <></>;
  if (!isCSR) return <></>;

  const Component = React.createComponent('div', { ...props }, props.children);
  const portal = ReactDOM.createPortal(Component, document.body);

  return Portal
};

isCSR state를 생성하고, useEffect Hook으로 isCSR을 수정하여 isCSRtrue일 때 렌더링을 진행하면 Hydration 문제가 해결됩니다.

레퍼런스

[Next.js] Hydration failed because the initial UI does not match what was rendered on the server 에러

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.