
마커를 클릭하고 게시글 상세를 볼때는 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이다.
각 인자에 대해서 설명을 잠깐 해보자면,
렌더 흐름은 이렇게 진행 된다.
getServerSnapshot()이 사용된다.isHydrated === false
그래서:
return null;
getServerSnapshot() 값을 씁니다.isHydrated === false
그래서 클라이언트 첫 렌더도 서버와 똑같이:
return null;
getClientSnapshot()을 사용한다. isHydrated === true
그래서 아래와 같이 렌더링이 되어서 원하는 화면이 나오게 되는 것이다.
return <FullScreenModalContainerInner>{children}</FullScreenModalContainerInner>;