치춘짱베리굿나이스

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

ClientSide/Next.js

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

치춘 2023. 8. 20. 17:01

Hydration Failed…

React Portal을 이용하여 토스트를 구현하던 중에 이런 오류를 마주했다

서버에서 렌더링한 UI와 초기 UI가 달라서 Hydration을 실패했다는 오류이다

이 이슈는 왜 발생하는 것일까? 한번 알아보도록 하자

이유와 해결법

기존 코드

const element = document.getElementById('toast-root')

if (!element) return <></>;

return ReactDOM.createPortal( ... );

컴포넌트 내부에 document 에 접근하는 코드가 존재하는데, 서버사이드에서 document 가 존재하지 않을 경우 element가 존재하지 않을 것이기 때문에 예외를 처리해주었다

문제는 이 예외 처리에서 발생한다… 먼저 Next.js 공식 문서에서의 Pre-rendering 섹션을 읽어보자

 

By default, Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.

Each generated HTML is associated with minimal JavaScript code necessary for that page. When a page is loaded by the browser, its JavaScript code runs and makes the page fully interactive. (This process is called hydration.)

Next.js 에서는 모든 페이지를 Pre-rendering한다 ⇒ 클라이언트 사이드 자바스크립트에서 모든 렌더링이 진행되는 대신, 각 페이지에 대한 HTML을 미리 생성하여 성능상의 이점과 SEO를 모두 잡는다

이 과정에서 생성된 HTML은 해당 페이지에서 필요로 하는 최소한의 자바스크립트 코드와 연결되어 있으며, 페이지가 브라우저에 의해 로드되면 해당 자바스크립트 코드가 실행되어 페이지에서 상호작용을 수행할 수 있게 된다

일련의 과정을 Hydration이라고 부르며, 메마른 HTML 불모지에 자바스크립트를 얹어 촉촉하고 생기있게 만들어준다고 생각하면 이해가 좀 된다 (?)

 

실제로 네트워크 탭에 들어가보면 use client 페이지임에도 불구하고 기본적인 html은 서버사이드에서 내려주는 것을 볼 수 있다

이 상태의 문서는 추가적인 자바스크립트 미함유 상태이기 때문에 인터렉션이 불가능하다 (궁금하면 여기의 방법으로 자바스크립트를 끄고 문서를 실행해 보자)

 

if (!element) return <></>;

다시 우리의 코드로 돌아가 보자

서버사이드에서는 document 가 없기 때문에 (document는 브라우저 전용 API 이다) 위의 if절로 들어간다

 

return ReactDOM.createPortal( ... );

반면 클라이언트사이드에선 document가 존재하기 때문에 하기의 createPortal 을 통해 포탈을 생성한다

여기서 서버사이드와 클라이언트사이드의 렌더 트리의 차이가 발생하기 때문에 Hydration을 수행하는 데에 문제가 생긴다

서버사이드에서는 분명 Pre-rendering 으로 구축한 렌더 트리가 A처럼 생겼으니 이렇게 Hydration 수행하면 되겠다~ 했는데 갑자기 클라이언트 사이드에서는 B처럼 생긴 렌더 트리가 구성되니 Hydration을 어떻게 수행할지 혼란이 오는 것이다

 

몬스터헌터로 치면 리오레우스 잡으러 간다고 공략과 패턴 다 보고 왔더니 바젤기우스가 난입한 느낌이랄까… (아님)

해결법

크게 두 가지 해결법이 있어보인다

  • document 에 접근하는 코드를 useEffect() 에 넣어서 브라우저 API를 숨기는 방법
  • next/dynamic 을 사용하여 완전 클라이언트 사이드로 Lazy Loading 하는 방법

기존 코드가 어차피 클라이언트 사이드에서 렌더링되는 것을 전제로 하기 때문에 useEffect() 를 사용하는 방법으로 수정하였고 결과적으론 잘 되었다,,

useEffect() 를 사용하는 방법

const [element, setElement] = useState<HTMLElement | null>(null);

useEffect(() => {
  setElement(document.getElementById('toast-root'));
}, []);

if (!element) return <></>;

return ReactDOM.createPortal( ... );

document API 를 useEffect() 내부로 숨겨 완벽하게 클라이언트 사이드로 보내버리는 방법이다

useEffect() 는 Hydration 시점부터 실행 가능해지므로 브라우저 API에서 발생하는 Hydration Mismatch를 방지할 수 있다

Lazy Loading을 사용하는 방법

import dynamic from 'next/dynamic';

const ToastList = dynamic(
  () => import('@/_shared/components/Toast/ToastList'),
  { ssr: false }, // SSR False 를 걸어주자
);

next/dynamic을 사용한 Lazy Loading에서는 ssr: false를 걸어줌으로써 프리렌더링을 막아줄 수 있다

실수 (여담)

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body
        className={clsx(inter.className, 'w-screen h-screen flex flex-col')}
      >
        {children}
      </body>
      <div id='toast-root' /> {/* 바보같은 실수 */}
    </html>
  );
}

위의 방법으로 고치고 Dynamic Import로 수정해도 오류가 계속 발생하길래 한참을 해멨는데

바보같게도 divbody 안에 넣지 않고 html 에 넣어서 발생하는 이슈였다

divbody 밖에 넣으면 오류가 발생할까? 라는 질문에 팀원 한 분이 ‘React Hydration 범위가 body 내부로 한정되어 있어서 그런 게 아닌지?’ 라는 추측을 제기하셨다

그럴듯 하기도 하고… 일단 body 밖에 div를 넣는 행위 자체가 1 + 1 = 3이라고 하는 거랑 비슷한 느낌인 것 같아서 검색해도 잘 안 나오는데 이유를 아시는 분 댓글 주시면 감사드리겠읍니다.


참고 자료

https://nextjs.org/learn/basics/data-fetching/pre-rendering

https://nextjs.org/docs/messages/react-hydration-error

https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading#skipping-ssr

'ClientSide > Next.js' 카테고리의 다른 글

App 라우터와 Pages 라우터  (0) 2023.05.13
Next.js - 첫 Next.js 앱  (0) 2023.04.08
Comments