Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

coding etude

[ReactJs(Ts)] NextJs : error : Hydration failed because the initial UI does not match what was rendered on the server 해결 본문

ReactJs

[ReactJs(Ts)] NextJs : error : Hydration failed because the initial UI does not match what was rendered on the server 해결

코코리니 2024. 4. 9. 22:08

ReactJs, NextJs, Recoil을 기반으로 작업중 발생한 에러로 

Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <header> in <html>.

 

난감한 상황이다.. 분명 header가 존재하지만 서버에서 랜더링 된 내용과 매치가 안되다는 오류...

 

생각보다 흔한 증상이였는지 검색하면 여러가지 해결 방법이 나온다. 

하지만 우선 발생 원인을 분석해보고 싶어졌다.. 일반적인 오류는 아니라고 생각했다.

 

발생 원인

처음에는 initail UI가 서버에서 랜더 된 내용과 매칭이 안되고 있다는 내용을 봤을때, tsx 파일의 html 부분이 뭔가 잘 못 됐다고 생각했다.

그래서 NextJs의 랜더링 방식을 살펴보기로 했다. 오류에서 나오는 Hydration이 과연 무엇인가.. 다행이 미리 정리해 둔 블로그를 찾았다.

https://blog.hwahae.co.kr/all/tech/13604

 

React의 hydration mismatch 알아보기 – 화해 블로그 | 기술 블로그

React의 hydration mismatch 알아보기 화해는 에러 트래킹 서비스인 Sentry를 사용하고 있는데요. 지난해 Next.js의 static export로 배포한 이벤트 페이지에서 hydration mismatch 에러가 쌓이기 시작했습니다.

blog-wp.hwahae.co.kr

생각보다 단순한 내용이지만 NextJs의 기본적인 작동원리를 어느정도 파악이 가능하다고 생각이 됐다.

하지만, 문제는 Hydration에서 문제가 발생하는것은 아니라 서버에 미리 Hydration 된 내용과(SSR) 랜더 시 클라이언트에서 HTML이 랜더링 된 내용이 다르다는 것이다.

단순한 이야기지만, 그렇다면 문제는 결국 나의 HTML 어딘가에 문제가 있다는 이야기라고 생각했다. 

 

시도 방법

1. NextJs와 Recoil을 처음 사용해 보는 입장에서 우선 NextJs 쪽의 뭔가를 잘 못 건드렸다고 생각해서 기존의 layout.tsx에 포함 되어 있던 <Header />와 <Footer /> component 를 다로 분리 해주고 .next/server 와 node_modules 폴더를 삭제 후 npm install로 다시 셋팅 해주었지만 동일했다.

2. Recoil을 사용할 때 NextJs가 Hydration 하면서 서버에 저장된 될때  <RecoilRoot>를 인식하지 못하는 경우가 있다는 내용이 있어서 Recoil의 이 사용된 곳을 하나씩 따로 때어내서 다시 점검했지만 모두 정상적으로 구동 되고 있었다.

3. 클라이언트에서 랜더되기 전에 변수 또는 상수의 값이 정해 지면서 UI값이 정상적으로 랜더링 되지 못해서 나오는 문제가 있었다. 그래서  랜더 이후에 나와야하는 모든 값들을 useEffect를 사용하여 생명주기를 조금더 명확하게 만들어 주었지만 동일한 증상.

4. 클라이언트에서 랜더링 되기 전에 서버에 저장되어 있는 내용을 받아서 클라이언트와 비교하는 방법이 있었지만 .. 굳이 진행해 보진 않았다. 서버에 저장된 내용을 받아와서 랜더링 된 클라이언트와 비교 후에 서버의 내용을 사용한다... 생각보다 코드는 간결했지만 복잡하고 가장 불편한 방법이라고 생각되서 최후의 수단으로 남기고 패스했다.

 

결론 및 해결

시간이 꽤 지나서 지쳐 갈때쯤.. matching <header> in <html> 이부분이 무조건 답이라는 생각이 들었다.

검색했을 때 모두 동일한 오류 였지만, 이부분만 모두 달랐다. <p> in <div> / <div> in <div> 등등.. 너무 다른 테그들 에서 발생하는 오류..

html 내의 header의 위치 또는 무언가 잘 못 됐다고 생각해서 조금더 집중에서 보았고 드디어 문제점을 찾았다.

 

// layout.tsx
//(에러발생 코드)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={notoSansKR.className}>
      <head>
        <link
          href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,1,200"
          rel="stylesheet"
        />
        <meta title="KingPigShop" />
      </head>
        <React.Fragment>
          <RecoilRoot>
            <Header />
            <SignModal />
            <body>
            {children}
            </body>
            <Footer />
          </RecoilRoot>
        </React.Fragment>
    </html>
  );

//(수정된 코드)
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={notoSansKR.className}>
      <head>
        <link
          href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,1,200"
          rel="stylesheet"
        />
        <meta title="KingPigShop" />
      </head>
      <body>
        <React.Fragment>
          <RecoilRoot>
            <Header />
            <SignModal />
            {children}
            <Footer />
          </RecoilRoot>
        </React.Fragment>
      </body>
    </html>
  );

 

 

위 코드 처럼 html은 <body> 테그 내에 모든 내용이 담겨야 하는데 그 순서가 잘못 되어서 header가 랜더링 되지 못했던 정말 가장 단순하고 멍청한 실수였다.. 그래서 나온 결론은 해당오류가 발생하는 지점의 html 코드에 무언가 분명 실수가 존재한다는 것이다.

예를 들어 <p><div></div></p> 이렇게 사용할 수 없는 테그들이 발생하거나 <div <- ->></div> 이런식의 테그 내에 공백이 발생 한다거나 하게 되면 나오는 단순한 문법상의 오류였다는 것이다.

component를 사용하면서 조금 더 꼼꼼하게 확인 후 삽입해야 겠다...

 

끝.