[Nextjs] react-hydration error 원인 및 해결방법 (feat. react-calendar)

쥬롬의 코드착즙기·2023년 3월 4일
12

TIL : Today I learned

목록 보기
10/10

nextjs 프레임워크를 이용해서 개발중인 프로젝트가 있다.
화면에 달력을 띄워야 했는데, 직접 만들기 너무 귀찮아서 react-calender 라이브러리를 import해서 개발하고 있었다.
그런데 이상하게도... 메인페이지에서 해당 페이지를 클릭해서 그 경로로 들어가면 에러가 안나는데, 이 경로에서 새로고침을 하면 에러가 났다.
에러가 발생한 코드는 아래와 같다.

<Calendar onChange={onChange} value={value} />

원인을 파악하기 위해서 찬찬히 에러메시지를 읽어보자.

1) Error: Text content does not match server-rendered html.

이 에러는 Nextjs의 렌더링 기법에 의해 업데이트되었지만, React에서 페이지가 로드된 후 hydrate된 코드들은 업데이트되지 않았을 때 일어난다.

?? nextjs의 렌더링 기법이 뭔데? hydrate는 뭔데? 하시는 분들은 쭉 읽어주세요. 다 아는 놈들이군... 하시면 맨 하단에 해결방법만 제시하고 있습니다.

nextjs의 렌더링 기법 : SSR

  • rendering
    • 웹사이트를 구성하기 위한 코드(html, css, js)를 실제 대화형 페이지로 만드는 프로세스. 브라우저 내장 렌더링 엔진에 의해 실행됨.
    • 과정 : 웹서버에서 브라우저로 파일코드 전송 → 브라우저에서 코드 바이트를 문자열로 변경 → 문자열을 토큰으로, 토큰을 노드로 구문분석 → 노드를 dom tree로, css를 cssom tree로
  • dom / cssom
    • dom : 문서 객체 모델(DOM, Document Object Model). XML이나 HTML 문서에 접근하기 위한 일종의 인터페이스. 문서 내의 모든 요소를 정의하고, 각각의 요소에 접근하는 방법을 제공.

    • cssom : CSS 객체 모델(CSS Object Model). CSS 객체 모델로 자바스크립트가 CSS를 동적으로 조작할 수 있게 함.

      ⇒ dom은 html, cssom은 css를 위한 인터페이스.

  • SSR vs CSR

  • SSR

    https://jaeone94.github.io/img/1_jJkEQpgZ8waQ5P-W5lhxuQ-1784075.png

    • server side rendering. 서버측에서 클라이언트 측으로 html을 넘겨줄 때, 클라이언트가 브라우저단에서 렌더링을 하지 않아도 되도록 완성된 html을 넘겨주는 것.
    • 장점
      • seo(검색엔진 최적화)에 유리함. html이 viewable한 상태로 브라우저로 전달되니까.
      • 초기 로딩이 빠름. js를 이미 렌더링해서 클라이언트로 보내기 때문에, 브라우저가 렌더링을 수행하지 않아도 됨.
    • 단점
      • ttv≠tti (time to view와 time to interact가 다름). 상호작용을 위해 .js 파일이 필요하다면 클라이언트는 추가적으로 파일을 요청할 것이고 그것이 모두 실행되기 전까지는 상호작용 할 수 없음.
      • 서버에서 html을 전달하기 때문에 서버부하가 증가하고, 단순한 interaction에도 반응속도가 느림.
  • CSR
    https://jaeone94.github.io/img/1_CRiH0hUGoS3aoZaIY4H2yg-1784129.png

  • client side rendering. 클라이언트 측에서 렌더링을 담당하는 것.

    • 장점
      • js 렌더링을 클라이언트 사이드에서 하기 때문에 spa 만들기에 유리.
      • 화면 깜박임 이슈가 없음.
      • tti = ttv 이므로 화면에 보여질 때 바로 상호작용할 수 있음.
    • 단점
      • seo에 불리함. js가 실행되기 전에는 그저 빈 html파일이므로.
      • 초기 로딩속도가 느림. 그러나 최근 하드웨어가 점차 발전하면서 이 이슈는 거의 없다고…

2) Error : Hydration failed because the initial UI does not match what was rendered on the server.

hydration

  • 컴포넌트를 렌더링하고 이벤트 핸들러를 연결하는 프로세스를 Hydration이라고 함.

  • 인터랙션 기능 및 이벤트 핸들러를 이용해 '말라있는' HTML에 물을 주는 것.

  • Hydration 후 애플리케이션은 인터랙티브하며 클릭 등 사용자 입력에 응답함.

  • ttv → 🐳 hydrate 🐳 → tti

  • render() vs hydrate()

  • render()

    ReactDOM.render(element, container[, callback])

    ReactDOM.render() 함수는 두번째 파라미터인 지정된 DOM 요소에 하위로 추가하여 렌더링. 렌더링이 완료되면 특정 이벤트를 처리할 콜백함수를 세번째 인자로 넣어줌.

  • hydrate()

    ReactDOM.hydrate(element, container[, callback])

    ReactDOM.hydrate() 함수는 특정 컴포넌트를 두번째 파라미터인 지정된 DOM 요소에 하위로 Hydrate . 렌더링을 통해 새로운 웹페이지를 구성하는 것이 아니라 기존 DOM Tree에서 해당되는 DOM 요소를 찾아 정해진 자바스크립트 속성들만 적용.

⇒ 새롭게 요소를 만드느냐 vs 원래 있던 요소를 찾아 js만 적용하느냐

3) Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

  • Suspense
    • Suspense는 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다고 리액트에 알려주는 것. 본 에러 해결의 핵심과는 관련이 없는 것 같아... 설명을 생략한다.

4) 에러메시지에 링크된 nextjs 공식문서

일단 링크된 next 공식문서를 한번 들여다보자.

에러가 발생한 이유

애플리케이션을 렌더링하는 동안, ssr / ssg 에 의해 pre-rendered된 react tree와 브라우저에 의해 처음으로 렌더링된 react tree에 차이가 존재했다. 이러한 첫 렌더링은 react의 특성인 hydration이다.

이 경우 react tree가 dom과의 동기화에 실패할 수 있으며 예기치 못한 콘텐츠가 화면에 나타날 수 있다.

⇒ 정리하자면 이 에러는 next의 특징인 ssr 방식으로 렌더링할 경우와, 브라우저에 의해 렌더링할 경우에 서로 다른 react tree가 만들어질 때 발생한다.

해결방법 제안

보통 이 문제는 특정 라이브러리나 코드 가운데 pre-rendering과 브라우저 간 차이가 있을 수 있는 부분에 의해 발생한다. 예를 들면 컴포넌트 렌더링에 window 를 사용한 경우가 있다.

function MyComponent() {
  // This condition depends on `window`. During the first render of the browser the `color` variable will be different
  const color = typeof window !== 'undefined' ? 'red' : 'blue'
  // As color is passed as a prop there is a mismatch between what was rendered server-side vs what was rendered in the first render
  return <h1 className={`title ${color}`}>Hello World!</h1>}

color 변수가 window 객체의 존재여부에 따라 달라지는데… 사실 이 코드는 이해가 안 되서 다른 분 블로그를 참고했다.

  • node에서 실행시는 최상위 객체가 window가 아니다. 때문에 color는 blue가 된다.
  • 그러나 브라우저에서 실행시는 최상위 객체가 window다. 때문에 color는 red가 된다.
  • 이렇게 환경에 따라 다른 UI가 되면, 서버에서 만든 html파일과 브라우저에서 처음 렌더하는 html 내용이 다르므로 오류가 난다.

에러를 수정하려면, 위 코드가 window 객체에 의존하지 않도록 고쳐주면 된다.

// In order to prevent the first render from being different you can use `useEffect` which is only executed in the browser and is executed during hydration
import { useEffect, useState } from 'react'
function MyComponent() {
  // The default value is 'blue', it will be used during pre-rendering and the first render in the browser (hydration)
  const [color, setColor] = useState('blue')
  // During hydration `useEffect` is called. `window` is available in `useEffect`. In this case because we know we're in the browser checking for window is not needed. If you need to read something from window that is fine.
  // By calling `setColor` in `useEffect` a render is triggered after hydrating, this causes the "browser specific" value to be available. In this case 'red'.
  useEffect(() => setColor('red'), [])
  // As color is a state passed as a prop there is no mismatch between what was rendered server-side vs what was rendered in the first render. After useEffect runs the color is set to 'red'
  return <h1 className={`title ${color}`}>Hello World!</h1>}

color의 기본값은 blue이다. 첫번째 렌더링이 일어나면 useEffect()가 실행된다. 따라서 첫번째 렌더링에 의해 hydration이 실행되면 setColor()가 실행되며 red가 된다.

⇒ 위에서 봤던 tti와 ttv 개념과도 이어지는 것 같다. ttv 일때는 blue, tti 일때는 red 인듯!

그 외에도 html 코드가 잘못 작성된 경우에도 발생할 수 있다고 한다. 아래 두 경우 외에 더 있을 수도 있다.

  • 오류코드 → 수정코드
    • p 안에 div :
      <p><div></div><p> → <div><div></div></div>
    • ul/ol 없이 li :
      <div><li></li><div> → <div><ul><li></li></ul></div>

문제 해결

오류가 왜 일어나는지는 이해했다.
자 이제 중요한건… 이 라이브러리의 어느 부분에서 오류가 발생하는지 찾아내는 것.

일단 콘솔을 들여다보았다.

react-calendar에서 영문을 한글로 번역하는 과정에서 에러가 난 것 같다.
그럼 브라우저를 거치기 전에 미리 언어설정을 변경할 수 있나?

react-calendar 공식문서를 들여다보자.

다행히도 custom props 안에 있다!!!
IETF language tag에 의하면 한국어는 ko이다. 서버에서 렌더링할 때 미리 언어설정이 되도록 코드를 수정해주자.

<Calendar locale='ko' onChange={onChange} value={value} />

그럼 해결!!!

생각보다 ez한 해결법이라 어이없었지만, next의 렌더링 방식인 ssr에 대해서 제대로 알아볼 수 있었고 hydration에 대해서도 알 수 있어서 배움의 기회가 되었던 것 같다.

profile
코드를 짭니다...

1개의 댓글

comment-user-thumbnail
2023년 3월 8일

대단히 감사합니다. 헤매고 있었는데 큰 도움 됐어요!

답글 달기