개발 지식/next.js

Next.js 에서 ReferenceError: document is not defined 에러 해결하기

sisun 2024. 9. 21. 23:51

문제 상황

저는 평소에 컴포넌트를 만들 때 document나 window를 useEffect 밖에 선언해서 사용을 하지 않는 편입니다.

근데 이번에 모달 컴포넌트를 만들 때 React 18 공식 문서에 나와있는 createPortal 예제(링크)를 보면서 만들다가 document를 사용하게 되었습니다. 이 때  ReferenceError: document is not defined  에러를 만나게 되었죠. 

 

기존에 모달 컴포넌트를 만들 때도 createPortal을 사용했었는데, 항상 ref에 document를 저장해서 사용했었기 때문에 createPortal에 바로 document를 넣어서 사용한 것은 처음이었습니다.

(참고로 아래 코드도 초창기 React 16 버전 공식 문서를 참고해서 만들었던 걸로 기억하는데, 예제 코드를 찾지 못하겠네요ㅠㅠ 찾으면 링크를 남겨두겠습니다.)

// 기존에 ModalPortal을 만들던 방식

import React, { useEffect, useRef, useState, FC } from 'react';
import ReactDOM from 'react-dom';

type ModalPortalProps = {
  children: React.ReactChild | React.ReactChild[] | string;
  selector: string;
};

const ModalPortal: FC<ModalPortalProps> = ({ children, selector }) => {
  const ref = useRef();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    ref.current = document.querySelector(selector);
    setMounted(true);
  });

  return mounted ? ReactDOM.createPortal(children, ref.current) : null;
};

export default ModalPortal;

 

이번에 모달을 만들면서 document를 바로 사용하게 된 코드는 다음과 같습니다. 

const Modal: FC<ModalProps> = (props) => {
  const { children, isOpen } = props;

  return createPortal(
    <div {...stylex.props(Styles.container)}>
      <div {...stylex.props(Styles.wrapper)}>{children}</div>
    </div>,
    document.body // 🔥 문제가 발생한 지점
  );
};

export default Modal;

문제가 발생한 원인

문제 원인은 SSR 처리시 document를 쓰려고 해서 입니다. SSR에서는 아직 html, css, js 등의 리소스와 UI가 형태를 갖추지 않은 상태라서 document에 접근할 수 없습니다. document 뿐만이 아니라 window 객체도 마찬가지입니다.

 

그럼 언제 SSR이 실행된걸까요? 페이지를 새로고침을 하거나 외부에서 url을 클릭해서 접속했거나, 주소 입력창에에 직접 url을 입력했을 때 입니다. 초기 요청 시 서버에서 HTML을 렌더링을 하기 때문이죠.

hot reload 상태에서는 에러가 당장 보이지는 않아요. 이 때는 이미 document가 정의되어 있는 상태이기 때문입니다.

 

해결 방법

createPortal(<div />, document.body)와 같은 코드 사용 방식을 유지하면서 Next.js 에서 ReferenceError: document is not defined  에러를 해결할 수 있는 방법은 Next.js에서 제공하는 dynamic 사용입니다.

 

Next.js에서는 서버에서 렌더링하지 않고 클라이언트에서만 컴포넌트를 렌더링할 수 있게 하는 기능을 제공하는데요. 해당 기능에 관해서 공식 문서(링크)는 아래와 같이 설명하고 있습니다.

클라이언트 측에서 컴포넌트를 동적으로 로드하려면 ssr 옵션을 사용하여 서버 렌더링을 비활성화할 수 있습니다. 이 옵션은 외부 종속성이나 컴포넌트가 window와 같은 브라우저 API에 의존하는 경우에 유용합니다.

 

 

사용 방법은 아래와 같습니다. 

// Next.js에서 dynamic 사용하기
import dynamic from 'next/dynamic';

// document가 사용된 컴포넌트는 dynamic으로 감싸서 import안에 컴포넌트 경로 입력
// ssr은 false로 지정!
const Modal = dynamic(() => import('@src/components/ui/Modal'), {
  ssr: false,
});

// 기존에 쓰던대로 컴포넌트처럼 사용
const Home = () => {
  return (
    <Modal isOpen={true} onOk={null}>
      테스트
    </Modal>
  );
};

export default Home;

 

마치며

사실 이번 에러는 ApexChart 라이브러리를 Next.js에 사용할 때 만난적이 있어서 해결방법은 알고 있었지만, 문제가 발생한 원인을 어렴풋이 느낌으로만 알고 있었습니다. 

이번에 다시 에러를 마주하게 되었을 때는 확실히 정리해놔야겠다고 생각이 들어서 블로그에 기록하게 되었습니다.

도움이 되었기를 바랍니다 :)

참고

https://ko.react.dev/reference/react-dom/createPortal

https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-no-ssr

https://nextjs.org/docs/app/building-your-application/rendering

https://helloinyong.tistory.com/248