3.1Suspense Boundaries
Suspense 영역에 업데이트가 발생하여 생긴 하이드레이션 문제 해결
TL;DR
추억의 쪽지 시험
문제 발견

활성화된 웹사이트에 Language Selector 기능을 추가하던 중, 위와 같은 에러가 떴다. 어디서 에러가 떴는지 구체적으로 나오지 않았지만, Suspense Boundary라는 말에서 <Suspense>
와 관련된 에러로 보였고 짐작 가는 곳을 살펴보니 <Suspense>
가 있었다. 하지만 여태까지 잘 작동하다가 왜 에러가 발생한걸까?
문제 분석
에러의 문장을 하나하나 분석해봤다. 우선, 첫번째 문장.
This Suspense boundary received an update before it finished hydrating.
하이드레이션이 끝나기 전에 <Suspense>
로 둘러싼 코드가 업데이트를 받았다. <Suspense>
는 lazy-loading되거나 async/await으로 받아오는 비동기적인 데이터를 기다릴 때 렌더링을 멈추게 하는 용도로 쓰는 컴포넌트인데, 데이터나 컴포넌트가 준비되기 전에 fallback UI(예를 들면 Loading Spinner)를 보여줘서 보다 자연스러운 사용자 경험을 가능하게 해준다.
즉, 위의 에러 문장은
- 서버에서 렌더된 HTML에 React가 상호작용 기능들(이벤트 리스너들, state등)을 연결하는 중인 과정(하이드레이션 중)인데,
- 그 과정에선 서버에서 렌더된 HTML의
<Suspense>
는 fallback UI를 띄운다. - 그런데 클라이언트에서는 fallback UI 말고 실제 데이터를 반영한 업데이트가 발생하였다.
이렇게 클라이언트에서 하이드레이션 과정 중에 업데이트가 발생하면 생기는 문제에 대해선 두번째 문장에 나온다.
This caused the boundary to switch to client rendering.
React는 하이드레이션을 할 때 서버에서 렌더된 HTML이라는 껍데기를 바라보며 클라이언트에서 기능을 붙여 상호작용 가능한 애플리케이션을 만드는데, 서버에서는 애플리케이션이 이렇고 저렇게 생겼다고 보여주는데 클라이언트에선 이미 업데이트가 발생해 전혀 다르게 생겼다고 보여주니, React에선 해당 부분은 서버의 HTML을 따르지 않고 클라이언트 쪽에서 완전히 새롭게 그린다. 즉, 클라이언트에서만 해당 컴포넌트를 렌더한다.
우선, SSR의 성능 이점이 없어진다. React가 애플리케이션을 렌더링할 때 가장 먼저 하는 것은 서버에 컴포넌트들을 정적인 HTML로 렌더링 하는 것이다(Server-Side Rendering). 복잡한 자바스크립트 코드들은 전혀 실행하지 않고 우선 껍데기들을 그려놓기 때문에 속도가 아주 빠르고, 여기에 상호작용 기능들을 붙이는 것은 그 이후에 하기 때문에 React는 아주 빠르게 화면을 보여주고 상호작용도 가능하게 한다. 클라이언트에서만 컴포넌트를 렌더하면 해당 부분은 따로 그려야 하기 때문에 성능에서 손해를 본다.
다음으로, 처음에 fallback UI를 보여줬다가 완전히 다시 클라이언트에서 컴포넌트를 그리는 것이라면 컨텐츠가 업데이트될 때 깜빡거리는 현상(flickering)이 나타난다. 이는 유저 경험에 좋지 못하다.
또, SEO 및 접근성에 손해를 본다. 서버에 정보가 없고 클라이언트 단에서만 정보가 있기 때문에 Search engine들의 indexing이나 접근이 어려워진다.
그리고 마지막 문장.
The usual way to fix this is to wrap the original update in startTransition.
React의 startTransition
함수는 특정 업데이트들을 '급하지 않은' 것으로 표시할 수 있게 해준다. 즉, 하이드레이션 과정에 어떤 업데이트가 진행되려고 하는데 startTransition
으로 싸여 있는 업데이트라면 React에선 그리 급한 업데이트가 아닌 하이드레이션 이후에 처리해도 될 것으로 이해하고 기존 서버 사이드의 <Suspense>
의 fallback UI에 맞추어 하이드레이션을 진행한다.
문제 해결
즉, 이 문제는 지금까지 잘 작동하던 컴포넌트 중 하나에 <Suspense>
가 있었고, 해당 컴포넌트에 있는 텍스트에 번역 기능을 넣다보니 처음에 언어 설정이 default 값(영어)이 아닌 다른 값이면 업데이트를 하는 코드 때문에 업데이트가 빠르게 발생했는데 이것이 해당 컴포넌트 전체를 리렌더하면서 서버 사이드에선 기본 언어로, 클라이언트 사이드에선 변경 언어로 업데이트되며 생긴 하이드레이션 문제였던 것이다.
문제를 해결할 수 있는 방법은 여러가지가 있었다. 우선, 쓰여있는대로 startTransition
을 써서 해당 업데이트를 미뤄도 되었고, <Suspense>
를 없애는 것도 방법이었다.
하지만 근본적으로 생각해보면, startTransition
을 쓴다는 것은 원래 적용하려던 번역을 미뤄도 된다는 표시를 한다는 의미이고, 그러면 애초에 번역을 도입하려고 했던 이유 중 하나인 여러 언어별 SEO는 애초에 적용이 안 되고 있다는 의미가 아닐까?
즉, 서버에 '이 웹페이지는 이런 저런 기능이 있습니다'를 여러 언어로 알려야 하는 상황인데 '이 웹페이지는 이런 저런 기능이 있습니다'를 영어로만 알리고, 영어로 들어오면 다른 언어로 바꿀 수 있는 버튼 정도만 제공되는 상황인 것이 아닐까?
그렇게 해서 현재 작업중이던 번역 체계가 좋지 못한 것을 발견했고, 이 문제의 해결은 <Suspense>
도, startTransition
도 아닌 next-i18next
로 하게 되었다(..)