티스토리 뷰

사이드 프로젝트/🩺 바디토리 : 건강기록 및 병원추천 웹서비스

hydration error는 뭐길래 자꾸 발생하나

2022. 12. 8. 20:41

👿 이슈사항

프로젝트를 진행하면서 hydration-error라는 에러창이 나타났다.

 

신체부위별 차트를 보여주는 화면에서 신체 포지션(front, back, face) 값을 recoil로 관리하고 있는데 새로고침하면

신체 포지션 값이 front(디폴트값)로 바뀌어버리는 현상이 발생했다.

 

위 현상을 해결하기 위해 신체 포지션을 관리하는 atom에 persist를 설정했다.

다시 새로고침을 해보니 현재 신체 포지션이 front일 때는 문제가 없는데 현재 신체 포지션이 back이나 face일 경우

새로고침하면 하이드레이션 오류가 발생했다.

 

 

 

 

 

 

 

💡 해결과정

 

1차, 2차 프로젝트를 하면서 React를 사용할 때는 보지 못했던 종류에 에러라 서치를 해보니 Next.js를 사용하면서 발생하게 된 오류였다.

 

Next.js Docs를 보면 Hydration Error는 앱을 랜더링하는 동안 pre-rendring된 SSR/SSG의 React tree와 브라우저에서

첫번째 렌더링하는 동안 렌더링된 React tree 간의 차이가 있어서 발생하는 오류라고 한다.

 

여기서 첫번째 랜더링이라는 것이 React의 기능인 Hydration인데 이로 인해

React tree가 DOM과 동기화 되지않아 발생하는 오류라 Hydration Error인 것이다.

 

간단하게는 SSR과 CSR이 달라서 발생하는 오류라고 생각된다.

 

 

신체포지션 값을 저장하는 currentPosition을 console.log로 찍어보면

현재 신체 포지션이 back인 상태에서 새로고침을 했을 때 브라우저의 콘솔에서는 back이 찍히고

서버의 콘솔에서는 디폴트값인 front가 찍히는 것을 확인할 수 있다.

 

 

 

 

이 문제를 해결하기 위해 우선 공식문서에서 제공하는 에러 해결 방법을 먼저 시도해보기로 했다.

 

 

1) hydration(첫번째 랜더링)이 다른 것을 막기 위해 브라우저에서만 실행되고 hydration 중에 실행되는 useEffect를 사용한다.

(useEffect 외부에 작성한 console.log는 서버 콘솔과 브라우저 콘솔 모두에서 보이고, useEffect 내부에 작성한 console.log는 브라우저 콘솔에서만 보이는걸 보면 useEffect는 브라우저에서만 실행된다는 것을 확인해볼 수 있다.)

 

2) color라는 state의 기본값은 useState에 초기값으로 설정하는 ‘blue’이고 이는 SSR의 pre-rendering과 브라우저에서의 hydration(첫번째 렌더링)에 사용된다.

 

3) hydration이 진행되는 동안 useEffect가 call된다.

(useEffect 안에서는 window객체가 undefined인지 확인할 필요없이 사용 가능하다. window.~들도 전부 가능)

 

4) useEffect 안에서 setColor를 call함으로써, hydrating 이후에 렌더가 트리거되어 위 코드의 경우 setColor의 인자인 ‘red’라는 browser specific 값을 사용할 수 있다.

 

5) prop으로 전달된 color라는 state는 서버사이드에서 랜더된 것과 hydration된 것 사이에 불일치가 없다. useEffect 실행 후 color는 ‘red’로 set된다.

 

 

 

위 내용만으로는 4)번부터 잘 이해가 가지 않았다.

 

useEffect는 렌더링 이후에 실행되는걸로 알고 있는데 hydrating 이후 렌더가 트리거 된다는 것은

hydration → 렌더링 → useEffect 순으로 실행된다는 건가?

 

맨 처음에는 서버단과 브라우저 모두 ‘blue’라는 같은 초기값을 갖고, useEffect를 사용하면 hydration 이후에 브라우저 렌더링이 진행되기 때문에 서버단과 브라우저 모두 ‘red’로 set되어 불일치하는 부분이 없다는 건가?

 

 

 

우선 hydration 개념을 좀 더 알아야할 것 같아서 여러 사람의 블로그나 문서를 찾아보며 hydration이라는 개념에 대해 설명한 글들을 다양하게 읽어보았다.

 

-  hydrate는 Server Side에서 렌더링된 정적 페이지와 번들링된 JS파일을 Client Side에 보낸 뒤, Client Side에서 HTML코드와 JS코드(React)를 서로 매칭시키는 과정을 말한다.

 

-  Next.js는 Server Side에서 미리 웹페이지는 pre-rendring하고 이로 인해 생성된 HTML document를 Client에 보낸다.

이 시점에서 Client가 받은 웹 페이지는 단순히 화면만 보여주고 자바스크립트 요소는 하나도 없는 상태이다. 즉, 이벤트 리스너 등이 DOM 요소에 적용되어있지 않은 상태이다.

이후 React가 번들링된 JS파일들을 Client로 보낸다. 이 자바스크립트 코드들이 이전에 보내진 HTML DOM 요소 위에서 다시 한번 렌더링하며 각 요소에 매칭되고 이 과정을 Hydrate라고 한다.

이렇게 2번 렌더링하는 것이 비효율적일 수도 있지만 서버단에서 빠르게 pre-rendering된 웹 페이지가 유저에게 빠르게 보여지는 것이 큰 장점이다. 또한 Client단에서 자바스크립트가 렌더링할 때 각 DOM요소에 자바스크립트 속성을 매칭시킬 뿐 실제 웹페이지를 다시 그리는 Paint함수 호출 과정을 하지는 않는다고 한다.

(💡 React는 js만을 이용해 웹화면을 구성하기 때문에 실제 HTML코드는 존재하지 않는다.)

 

-  hydrate는 서버에 의해 pre-render된 HTML의 DOM tree를 렌더하고 이 tree에 이벤트 핸들러를 바인딩한다. 이후 사용자는 interactive가 가능하게 된다.

 

-  hydrate는 랜더링을 통해 웹페이지를 구성할 DOM을 생성하는 것이 아니라 기존 DOM Tree에 해당되는 DOM 요소를 찾아 정해진 이벤트 리스너 등을 부착시키는 것이다.

 

-  Next.js는 서버에서 HTML을 문자열로 가져온 후, 서버에서 보내준 HTML을 클라이언트에서 hydrate() 혹은 render()하여 브라우저에 렌더링한다. 이 일련의 과정을 Hydration이라고 한다.

 

 

 

그렇다면 공식문서에서 말한 hydration이 트리거하는 렌더링이란 pre-rendering된 정적 HTML 웹페이지에 자바스크립트를 입히는 과정을 얘기하는 것이려나..?

 

setColor는 useEffect 안에 있어서 hydration 이후에 동작하는 거고, 만약 setColor가 useEffect 밖에 있다면 hydration 중에 동작해서 color를 red로 바꾸는건가..?

 

 

 

 

hydration error를 해결하는 방법이 완벽하게 이해가 가진 않았지만 우선 프로젝트에서 에러난 부분을 고쳐보기로 했다.

 

recoil로 받아온 currentPosition 값을 그대로 사용하지 않고 useState를 사용해 currentPos라는 state에 넣어서 사용하였다.

 

useEffect 안에서 현재 신체 포지션 값인 currentPosition을 set하여 currentPos로 관리하도록 하고,

currentPos는 currentPosition이 변경될 때마다 함께 변경되어야 해서 useEffect에 currentPosition을 dependency로 넣어주었다.

 

아래와 같이 코드를 작성하니 에러가 해결되었다.

 

 

 

 

에러는 해결되었지만 아직 이해가 잘 되지않아서 이런저런 상황들을 만들어서 정리해보았다.

 

1) 에러를 해결한 위의 코드에서 currentPos의 초기값에 currentPosition을 넣어주면 여전히 hydration error가 발생한다.

const [currentPos, setCurrentPos] = useState(currentPosition);
// 이렇게 작성하면 여전히 hydration error 발생

 

 

2) useEffect 바깥쪽에 currentPosition를 console.log 찍어보면 서버 쪽 콘솔메세지에는 현재 신체 포지션이 무엇이든

무조건 front만 찍히는데 같은 console.log가 브라우저에서는 현재의 신체 포지션(front, back, face 중 한가지)으로 잘 찍힌다.

 

3) useState의 초기값과 상관없이 신체 포지션이 front일 때는 서버와 브라우저 모두 콘솔에 front가 찍히고 hydration error가 나지 않는다.

 

 

 

위의 3가지 상황들과 앞서 찾아본 에러 해결방법을 종합해서 생각해보았을 때 아래와 같은 결론을 내렸다.

-  서버단에서 pre-rendering 시 사용하는 currentPosition값은 recoil에서 설정한 초기값인 front이다.

-  useEffect 안에 작성하지 않은 코드들은 hydration 과정에서 동작하는데 이 때 서버단에서 pre-rendering된 내용과 다르면 오류가 난다.

(서버단에서 pre-rendering한 currentPosition는 무조건’front’, hydration 과정에서 currentPosition는 recoil이 가지고 있는 현재 currentPosition값)

-  useEffect 안에 작성한 내용은 hydration이 끝나고 렌더링까지 끝난 후에 동작하기 때문에 hydration error를 피할 수 있다.

 

 

 

 

에러를 해결하고 추후 오피스아워에서 해당 이슈에 대해 질문을 하고 코치님의 답변을 받았다.

(오피스아워란 엘리스 부트캠프에서 프로젝트 시 팀당 포지션별 담당 코치님을 배정해주고 정해진 요일에 1시간씩 질의응답과 코칭을 진행하는 시간이다. 1시간의 코칭시간동안 다른 질문들도 많아서 내 질문에 대해서는 충분한 답변을 듣진 못했다ㅠㅠ)

 

hydration error는 SSR과 CSR이 달라서 나는 오류로 SSR(페이지 컴포넌트)구성을 CSR(자식 컴포넌트)을 가져다

사용하니까 SSR이 끝나기 전에는 CSR이 끝나면 안되고 페이지 컴포넌트(부모)가 먼저 마운트되어야 한다.

 

recoil persist를 사용하면 로컬스토리지에 저장되는 것(본인컴퓨터-구글-크롬-…에 어떤 파일로 저장되는 것)이라

이 값을 가져오기 전까지는 undefined가 뜬다고 한다. 따라서, 아래와 같이 if문을 더 추가해주었다.

 

(if문이 없을 때도 에러가 생기진 않았었는데 어떤 상황에서 에러가 발생할지 추가로 의문의 생겼다..)

 

 

 

코치님 답변을 듣고 내가 생각하지 못했던 부분을 발견했다.

 

recoil persist를 사용하면 localStorage에 저장이 된다는건 알고 있었지만 localStorage는

웹 브라우저에 자체에 있는 특정 저장 공간으로 SSR시점에서는 localStorage에 접근하지 못한다는 점은 생각하지 못했다.

 

그래서 persist를 사용하니 hydration오류가 발생했던 것이었다.

Next.js에서는 localStorage나 sessionStorage를 사용하려면 렌더링이 이루어진 후에 사용할 수 있다.

 

 

따라서,

1)  window 객체 존재 유무를 확인한 후 접근하거나

2)  함수형 컴포넌트일 경우에는 useEffect 안에, 클래스형 컴포넌트일 경우에는 componentDidMount 안에 사용 가능하다.

반응형
프로필사진
개발자 삐롱히

프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그