blog.coinsect.io
🇰🇷
원문
마크다운
2024-06-27
310
공유

Hydration mismatch란 무엇이고 어떻게 해결할까?

Universal Rendering을 하는 모던 웹앱에는 Hydration이라는 개념이 존재한다. 단순한 SPA(Single-Page Application)와 달리 서버에서 일단 HTML을 온전히 렌더링해서 내려준 뒤, 클라이언트에서 자바스크립트 다운로드 & 해석이 끝나면 갖고 있는 HTML에 필요한 이벤트들을 바인딩하는 작업을 하는 것을 일컫는다. 이렇게 하면 검색엔진에도 대응할 수 있고(SEO), 첫 요청 이후로는 SPA로 작동하므로 사용자 경험도 좋아 두마리 토끼를 잡을 수 있다. 만약 Hydration이 없다면 당신의 HTML은 자바스크립트가 등장하기 이전의 정적인 웹사이트들처럼 인터액티브하지 않은 '건조한' 웹페이지가 된다. Hydration을 진행하는 과정에서 DOM을 체크하는데, 서버에서 렌더링된 DOM과 클라이언트에서 렌더링해야할 DOM이 다를 수 있다. 이것이 바로 Hydration mismatch이다. 물론 이런 경우에도 대부분 클라이언트에서 재렌더링을 해서 올바른 화면을 복구하므로 유저가 정상적으로 이용할 수는 있지만, DOM을 수정하는 것은 비싼 작업이므로 가급적 애초에 이런 상황이 발생하지 않도록 꼭 해결해주는 것이 좋다. [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html) 문서에서는 최악의 경우 이벤트 핸들러가 잘못된 DOM에 연결될 수도 있다고 경고한다. React를 기준으로, 프로덕션에서는 아래와 같은 에러들을 볼 수 있다. ![Hydration Mismatch](https://preview.redd.it/gy9w9tz18kf91.png?width=1490&format=png&auto=webp&s=a97566a495d60fcc060ed2b3339028c792f75162) 프로덕션에서 hydration 에러 발생시 마주칠 수 있는 [#418](https://react.dev/errors/418), [#423](https://react.dev/errors/423), [#425](https://react.dev/errors/425) 에러. 그러면 어떤 경우에 렌더링 결과가 다를 수 있을까? 대표적으로 날짜, 랜덤한 값 등이 문제가 된다. 예시를 보자. ```markup <!-- post.createdAt: 2024-06-13 23:00:00 --> <div>{dayjs(post.createdAt).format('YYYY-MM-DD')}</div> <div>{Math.random()}</div> ``` 위 경우, 서버의 타임존이 UTC라면 시간 부분의 `innerHTML`은 서버에서는 `2024-06-13`이고 한국에서는 `2024-06-14`가 되므로 다르다. 특히 이처럼 시간에 관한 에러의 경우, 대개 개발환경은 타임존이 내 로컬일 것이고 AWS EC2 등을 사용중이라면 프로덕션 서버의 타임존은 UTC일 것이기 때문에 로컬에서는 뜨지 않는 에러가 프로덕션에서만 계속 보이는 상황을 접할 수도 있다. 그래서 뭔가 minify를 하며 문제가 생겼나? 하며 삽질하여 애꿎은 시간만 날린 기억이 있다. 랜덤값의 경우 당연히 서버에서 생성한 랜덤값과 클라이언트에서 생성한 랜덤값이 다르므로 렌더링된 텍스트도 다르다. 이런 상황을 해결하는 방법들은 크게 다음과 같은 것들이 있다. - 가능하다면 클라이언트에서 어떻게 보일지를 서버에서 고려해 동일한 값을 서버에서 렌더링한다. - 또는 그런 부분들은 클라이언트 전용 컴포넌트임을 명시하면 된다. (e.g. `useEffect` or `lazy` + `<Suspense/>` or [nextjs의 경우 파일 최상단에 `'use client'` 적기](https://nextjs.org/docs/app/building-your-application/rendering/client-components)) - 이 상황이 사소하다고 생각한다면 그냥 `suppressHydrationWarning` 속성을 추가해서 경고만 무시하면 된다. 단, 해당 DOM들에 한해 에러를 감추는 것이지 에러가 없어진 것은 아니다. ```markup <!-- post.createdAt: 2024-06-13 23:00:00 --> <div suppressHydrationWarning>{dayjs(post.createdAt).format('YYYY-MM-DD')}</div> <div suppressHydrationWarning>{Math.random()}</div> ``` 위처럼 경고를 무시하거나 아래처럼 클라이언트에서만 실행되는 useEffect 훅을 활용할 수 있다. ```javascript const [clientOnly, setClientOnly] = useState(false) useEffect(() => { setClientOnly(true) }, []) {clientOnly && <> <!-- 단, 날짜의 경우 아마 검색엔진에 노출하기를 원할 것이다 --> <div>{dayjs(post.createdAt).format('YYYY-MM-DD')}</div> <div>{Math.random()}</div> </>} ``` 이 외에도 `<p>` 태그 안에 `<div>` 등의 블록 요소가 들어가는 등 잘못된 시맨틱 마크업을 한 경우에도 Hydration 에러를 볼 수 있다. 이런 것은 올바르게 수정해주면 된다. 사실 React의 hydration 에러 메시지는 대단히 불친절하다. 정확히 어떤 DOM이 어떻게 다른지를 콘솔에서 바로 볼 수 있으면 찾기가 쉬울 것 같은데, 현재로서는 개발자 도구에서 디버거를 사용해서 브레이크포인트를 걸고 찾는 수 외에는 뾰족한 방법이 없어 보인다. ![Hydration 에러 디버깅](https://d1085v6s0hknp1.cloudfront.net/boards/free_board/185c8140-382d-43a4-a65a-82c360f824e6_image.png) 위 사진처럼 Pause on uncaught exceptions, Pause on caught exceptions, DOM Mutation에 체크한 뒤 새로고침하고 스텝을 밟아가다보면, 어떤 값이 다른지를 찾을 수 있을 것이다. --- References - [Hydration Error: Minified React Error 해결하기](https://jaehan.blog/posts/nextjs/Hydration-Error-:-Minified-React-Error-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0)
0