카테고리 없음

[Next.js]"Text content does not match server-rendered HTML" 에러

봄나물소년 2024. 3. 27. 21:48

상황

특정 페이지에서 새로고침을 하니

처음보는 에러가 떴다.

두 에러메시지를 직역해보면

"서버에서 렌더링하는 html과 text content가 매치되지 않는다" 

"hydrating을 하는 중에 에러가 있습니다. 결정이 되지않는 범위 바깥에서 일어났기 떄문에

전체 루트가 클라이언트 렌더링으로 전환됩니다."

이다.

text content와 hydrating, entire root라는 단어가 어떤 의미인지 먼저 알아야 할 것 같다.

에러를 구글링했더니 

친절하게도 

next.js 공식문서에서

이 에러를 다루고 있었다.

 

분석

 

쉽게 말하면

서버로부터 pre-rendered되는 리액트 트리와

브라우저에서 first-render 중에 렌더링되는 리액트 트리 사이에 

차이가 있어서 해당 에러가 발생했다.

내가 궁금했던 hydration 역시 친절한 설명과 함께 부연설명이 되어있다.

hydration이란

React가 이벤트 핸들러를 연결하여 서버에서 사전 렌더링된 HTML을 완전한 인터렉티브한 애플리케이션으로 변환하는 것이다.

즉, 사전 렌더링된 html에 살을 붙이는 과정이다.

 

 

해당 에러가 발생하는 일반적인 상황은 이렇다

  1. html태그가 잘못 nesting되어 있는 경우
  2. 렌더링 로직에 typeof window !== 'undefined' 이게 있는지 확인
  3. 렌더링 로직에 브라우저에서만 쓸 수 있는 window, localstorage API가 있는지 확인
  4. 렌더링 로직에 Date() 같은 시간 의존적인 API를 쓰는가?
  5. html을 수정하는 브라우저 익스텐션(이건 뭐지?)
  6. 잘못 설정된 css-in-js(내 경우엔 styled component 설정을 확인해보면 되겠다)
  7. Cloudflare Auto Minify와 같이 html 응답을 수정하려고 시도하는 잘못 구성된 Edge/CDN(이건 뭐지?2222)

보다보니

하나 걸리는 부분이 보였다.

바로 에러가 난 페이지에서

로컬스토리지에 저장된 유저의 이름을 가져오기 위해 

이름을 local 변수에 할당해 사용하는 코드를 썼었다.

위는 문제의 그 코드이다.

 

내가 typeof window !== 'undefined'를 쓴 이유는 다음과 같다.

localstorage API 앞에는 global 객체인 window가 생략되어있다.

그러므로 이를 사용하기 위해선

window객체가 존재하는지 먼저 체크가 선행이 되어야하기 때문이다.

다행히 위 코드를 주석처리하니

에러가 사라졌다.

 

해결

이런 저런 블로그들을 뒤져보다 정리해보니 자연스럽게 분석이 되었다.

코드에서 typeof window !== 'undefined' ?? localstorage.getItem('user') 이 조건을 사용하는데,

이 조건은 클라이언트 사이드에서만 window 객체가 정의되어 있기 때문에, 서버 사이드 렌더링에서는 항상 false가 되어 null을,

클라이언트 사이드에서는 true가 되어  localstorage.getItem('user')의 값을 반환한다.

결과적으로, 서버에서 렌더링된 localstorage의 값과 클라이언트에서 렌더링된 localstorage의 값이 달라져서,

Next.js가 초기에 서버에서 생성된 HTML을 클라이언트에서 다시 사용하려고 할 때,

두 상태가 일치하지 않아 hydration 에러가 발생하게 된다.

 

그러므로 dom의 render를 보장해주는 useEffect 안에 window객체를 사용하여

에러를 깔끔히 해결하였다.


마무리하며..

hydration이라는 말을 얼핏 알고는 있었는데

이번 에러를 해결하면서 hydration이 어떤 때 쓰이는 과정인지 명확하게 알게되었다.

공식문서의 중요성도 다시한번 맘 속에 새기며,

블로그를 마무리한다.