Next.JS hydration 스타일 이슈 파악하기

Next.JS를 사용해 웹을 만들어가다보면, 어느 순간 Hydration 이슈를 마주치게 된다. 이번엔 그 상황이 언제, 왜 생겨나는지를 파악해보고, 이걸 피해가를 방법을 알아보자.

Problem of Hydration

하이드레이션이란, 리엑트에서 서버사이드 렌더링 혹은 SSG(스태틱 사이트 제네레이션)을 실행한 HTML 결과물을 받아온 뒤, 브라우저에서 이것을 다시 리액트 트리에 맞게 파싱하는 행위이다. 이 단계에서,

  1. 렌더링한 결과물이 어떤 컴포넌트인지 확인하고,
  2. 각 컴포넌트에 걸린 이벤트 들을 실제 DOM에 걸어주는 동작을 하게 된다.

하이드레이션이 잘못되었을 때, 우리가 마주하는 문제들은 거의 1번 과정이 잘못되어서 일어난다. 긴 말 하지 않고, 코드를 보자.

SSR 시에만 렌러링되는 어떤 컴포넌트가 있고

CSR 시에만 렌더링되는 다른 컴포넌트가 있다

그리고 항상 렌더링되는 또다른 컴포넌트가 있다.

이 페이지에 접속하면 어떻게 보일까?

이게 대체 무슨 일이람!

Why this happened?

문제를 파악하기 위해, 서버사이드에서 렌더링되어서 내려온 html을 살펴보자. 브라우저에서 자바스크립트를 비활성화 시키거나, 네트워크 탭으로 서버에서 온 응답을 확인해보자.

서버사이드 렌더링은 잘 된것 같다.

대체 무슨 일이 발생한걸까?

Next.JS에서 내부적으로 사용하는 ReactDOM.hydrate 함수는 다음과 같은 일을 한다.

  1. 서버에서 받아온 DOM tree와 자체적으로 렌더링한 tree를 비교한다.
  2. 두 tree 사이의 diff를 얻어낸 뒤, 자체적으로(클라이언트사이드) 렌더링 한 tree에 맞춰 patch를 적용한다.

이 스텝을 따라가보자.

Digging — part 1.

리액트가 두 개의 트리를 어떻게 비교하는걸까? 일단 렌더링된 결과물을 비교한다면 아래 두 트리를 비교하는게 맞을 것 같다.

땡!

리액트는 렌더링 된 트리가 아닌, 마운트 되기 전의 트리와 비교한다.
(훅을 사용한다면, useEffect가 실행되기 전의 상태를 가지고 비교한다.)
아마도 서버사이드 렌더링 결과와 환경을 맞춰 비교하기 위해서- 인 듯 하다.
때문에, 실제 비교가 일어나는 환경은 다음과 같다.

땡!

하이드레이트 함수의 설명을 잘 읽어보면 경고문이 있다.

There are no guarantees that attribute differences will be patched up in case of mismatches.

그러니까 저 경고의 함의는, 리액트 하이드레이션은 텍스트나 속성값은 비교하지 않는다. 리액트가 비교하는 트리는 아래와 같다. 결국 렌더링된 엘리먼트 타입과 순서만 비교한다는 뜻이다.

이제 조금 느낌이 오는가? 여기에 추가적인 논의를 이어가기 위한 표지를 좀 추가해보자.

그러니까 리액트의 입장에서는, 아래와 같은 판단을 하게 된다.

  1. $S0 === $C0
  2. $S1 는 제거되었군.
  3. $C0 는 Always 컴포넌트가 렌더링 된 결과물이군.

Digging — part 2.

그러면 리액트의 판단 대로 하나씩 적용시켜보자.

  1. $S0에는 Always 컴포넌트를 적용시킨다.
    — 이때 $S0의 attribute(style)는 손대지 않는다. 왜냐하면, 하이드레이션 스텝은 이미 렌더링된 돔 트리가 어떤 컴포넌트에 해당하는지 파악하고 이벤트를 걸어주기 위한 스텝으로 의도되었기 때문이다. style=“color:red” 값이 그대로 살아있게 된다.
  2. componentDidMount() 를 호출한다 — 혹은 useEffect() 를 호출한다.
    <Always /> 컴포넌트의 앞에, <CSR /> 컴포넌트가 렌더링된다.

사실, 이 상태에서 클라이언트 사이드에서 재 렌더링이 발생할 경우, props를 업데이트 하게 되면서 바람직한 상태로 렌더링이 된다. 이 문제는 서버사이드 렌더링 후 하이드레이션으로 돔 렌더링이 끝나버리는 페이지에서만 발생한다.

코드는 멍청하지 않다. 그냥 로직을 따랐을 뿐.

SO HOW TO SOLVE IT?

두 가지의 해결 방법을 제시할 수 있다.

정석적인 해결 방법

클라이언트 사이드에서만, 혹은 서버 사이드에서만 렌더링되는 로직을 제거한다. 단순하게 말하자면, return null을 하지 않는다.

여기에도 두 가지 방법이 있다.

  1. return null 대신 return <div />
  2. return null 대신 visibility: hidden; 또는 display: none;

꼼수같은 해결 방법

이 문제가 발생하는 컴포넌트가 비동기로 받아올 특정 데이터를 꼭 필요로 한다거나, return null이 아니면 레이아웃이 깨진다거나, 컴포넌트 로직이 복잡해 위 방법으로 고치기엔 시간이 없다거나 할 때 다음과 같은 방법을 사용할 수 있다.

리액트가 두 컴포넌트를 어떻게든 구분할 수 있게 해준다.

  1. 서로 다른 엘리먼트를 사용한다. 특히 이렇게 렌더링이 되다 말다 하는 컴포넌트의 루트를 div, span, section, p, 같은 대체 가능한 엘리먼트로 변경한다.
  2. 혹은, 사이 사이에 <div />가 아닌 요소를 삽입해준다.

2번은 이런 방식이다.

Conclusion

리액트는 충분히 똑똑하다.
문제는, 내가 짠 코드가 리액트를 햇갈리게 했기 때문이다.

이런 데이터에 오락가락하는것까지 탓할 순 없다.

--

--

--

Frontend Developer, mainly using React.js

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
HyeonSeok Yang

HyeonSeok Yang

Frontend Developer, mainly using React.js

More from Medium

Next.js Redirection without Flashing Content

[Nextjs Tip] How to resize image URL dynamically with next/image component

How to update Next.js' old version to the latest version?

NextJs. What Next?