/post/[id] 응답이 500으로 오는 현상

최정은·2026년 9월 1일

pin-plate

목록 보기
7/7

마커를 클릭하고 게시글 상세를 볼때는 500에러가 나지 않는다. 왜냐면 post/28으로 이동하고 그 위에 모달이 뜨기 때문이다.

근데 해당 주소로 바로 진입하게 되면 Next.js 서버가 HTML을 만드려고 한다. 이때 Modal을 react portal을 이용해서 띄우게 되어 있다. 이때 document.body에 portal이 띄워지도록 설정을 해놨다.

하지만 서버에서는 document를 건들일 수 없기 때문에 서버에서 500 에러로 내는 것이다.

해결방법

클라이언트에서 document가 존재하지 않으면 해당 컴포넌트를 보여주지 않기로 했다.

if (!isOpen || typeof document === 'undefined') return null;

하지만… hydration에 문제가 생겨버렸다. hydration mismatch가 생겨버린것이다.

서버에선 해당 컴포넌트가 null을 반환하는데 클라이언트에서는 아래와 같이 렌더링이 된다.

<FullScreenModalContainerInner>{children}</FullScreenModalContainerInner>

그래서 서버 HTML과 클라이언트 첫 렌더 결과가 달라져서 hydration mismatch가 발생하게 되는거다.

mount 여부에 따라서 서버와 hydration 첫 렌더에서는 모달 portal을 렌더하지 않고, hydration 이후 클라이언트 렌더에서만 portal을 렌더하도록 수정을 했다.

const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
  setIsMounted(true);
}, []);

if (!isOpen || !isMounted) return null;

하지만 이것도 문제가 발생했다.

Error: Calling setState synchronously within an effect can trigger cascading renders

최근 리액트에서 권장하는 방향은 props나 다른 state로부터 계산할 수 있는 값이면, useEffect + useState로 사용하지 말라고 한다.

그래서 찾아보니 useSyncExternalStore을 사용해서 server rendering을 서포트 받을 수 있다. https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering

import { PropsWithChildren, useSyncExternalStore } from 'react';

const subscribeToHydration = () => () => {};
const getClientSnapshot = () => true;
const getServerSnapshot = () => false;

const useIsHydrated = () =>
  useSyncExternalStore(
    subscribeToHydration,
    getClientSnapshot,
    getServerSnapshot,
  );

useSyncExternalStore 같은 경우엔 외부 store를 React 내부에 안전하게 연결하기 위한 hook이다.

각 인자에 대해서 설명을 잠깐 해보자면,

  • subscribe : 외부 store가 바뀌었을 때 React에게 다시 렌더하라고 알려주는 구독 함수이다.
  • getSnapshot : 클라이언트에서 현재 store 값을 읽는 함수이다.
  • getServerSnapshot : 서버 렌더링과 hydration 시점에 사용할 초기 snapshot을 읽는 함수이다.

렌더 흐름은 이렇게 진행 된다.

  1. 서버 렌더링getServerSnapshot()이 사용된다.
isHydrated === false

그래서:

return null;
  1. 브라우저 hydration 첫 렌더여기서도 React는 mismatch를 막기 위해 getServerSnapshot() 값을 씁니다.
isHydrated === false

그래서 클라이언트 첫 렌더도 서버와 똑같이:

return null;
  1. hydration 이후 클라이언트 렌더이제 React는 클라이언트 snapshot인 getClientSnapshot()을 사용한다.
isHydrated === true

그래서 아래와 같이 렌더링이 되어서 원하는 화면이 나오게 되는 것이다.

return <FullScreenModalContainerInner>{children}</FullScreenModalContainerInner>;

0개의 댓글