치춘짱베리굿나이스
Hydration failed because the initial UI does not match what was rendered on the server. 본문
Hydration failed because the initial UI does not match what was rendered on the server.
치춘 2023. 8. 20. 17:01Hydration 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로 수정해도 오류가 계속 발생하길래 한참을 해멨는데
바보같게도 div
를 body
안에 넣지 않고 html
에 넣어서 발생하는 이슈였다
왜 div
를 body
밖에 넣으면 오류가 발생할까? 라는 질문에 팀원 한 분이 ‘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 |