SSR — 서버가 먼저 그린다 · 서버는 3분 전을 그렸다 · 이론
하이드레이션은 다시 그리지 않는다
한 줄 요약
하이드레이션은 서버가 그린 DOM 에 이벤트 처리기를 붙이는 일이지 다시 그리는
일이 아니다. 그래서 두 쪽이 다른 것을 그렸을 때 화면은 조용히 틀린 채로 남는다.
왜 이게 필요했나
서버가 만든 HTML 은 이미 브라우저 화면에 있다. 클라이언트가 같은 컴포넌트를
한 번 더 돌려 보는 이유는 화면을 만들기 위해서가 아니라, **어느 DOM 노드에 어떤
처리기를 붙일지 알아내기 위해서**다. 그래서 두 결과가 같다는 전제가 깨지면
처리기가 엉뚱한 노드에 붙는다.
React 문서는 이 지점을 분명히 적는다 — 피할 수 없이 다른 값(예: 타임스탬프)에는suppressHydrationWarning 을 쓸 수 있지만 그건 한 겹만 통하는 탈출구이고,
React 는 어긋난 텍스트를 고쳐 주지 않는다
([hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot)).
경고를 지운다고 값이 맞아지는 것이 아니라는 뜻이다.
원인은 거의 언제나 넷 중 하나다.
| 원인 | 왜 갈리나 |
|---|---|
| 시각 | 서버가 그린 시각과 브라우저가 그린 시각이 다르다 |
| 난수·증가 id | 두 곳에서 따로 만들면 같을 리가 없다 |
| 로캘·시간대 | 서버는 UTC, 브라우저는 사용자 시간대 |
| 브라우저만 아는 값 | localStorage, 화면 폭, 쿠키 |
어떻게 동작하나
값을 만드는 자리를 하나로 모은다
시각도 난수도 서버에서 한 번 만들어 상태에 담아 넘기면 두 쪽이 같은 값을
쓴다. ssr-core 에서 초기 상태를 태그에 심은 것이 바로 이 일을 위한 것이다.
로캘과 시간대도 마찬가지다 — 포맷을 Intl.DateTimeFormat 에 맡기되
시간대와 로캘을 옵션으로 명시하면 두 쪽 결과가 같아진다
([Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)).
아무것도 안 적으면 실행 환경의 기본값을 따르는데, 그 기본값이 서버와 브라우저에서
다르다는 것이 문제의 전부다.
증가 카운터로 만드는 id 는 특히 고약하다. React 가 useId 를 따로 둔 이유를
문서가 설명한다 — 클라이언트 컴포넌트가 하이드레이션되는 순서가 서버 HTML 이
나온 순서와 같다고 보장할 수 없어서 전역 카운터로는 맞출 수가 없다
([useId](https://react.dev/reference/react/useId)).
정말 다를 수밖에 없는 값은 뒤로 미룬다
브라우저만 아는 값은 서버가 알 방법이 없다. 이럴 때 옳은 방법은 **서버와
클라이언트의 첫 렌더를 같게 만들고**, 그다음에 브라우저 값으로 바꾸는 것이다.
첫 렌더가 같으므로 불일치가 없고, 바뀌는 것은 하이드레이션이 끝난 뒤다.
불일치는 가둬야 한다
같은 불일치라도 어디서 났는지에 따라 값이 완전히 다르다. 경계 안쪽의
불일치는 그 경계 하위만 다시 그리면 되지만, 경계 밖의 불일치는 페이지 전체를
다시 그리게 만든다. 첫 화면을 빨리 보여 주려고 SSR 을 넣었는데, 경계 밖 불일치
하나가 그 이득을 통째로 날린다.
그래서 비교기를 만들 때 지켜야 할 규칙이 둘 있다.
- 모양이 어긋나면 그 자리에서 멈춘다. 태그가 다르거나 자식 수가 다르면 그
- 억제는 한 겹만. 시각 한 곳을 봐주자고 그 아래 트리까지 눈을 감으면 그
아래는 애초에 짝이 아니다. 계속 내려가면 항목 하나가 밀린 목록에서 불일치
수백 개가 쏟아지고, 진짜 원인 하나가 그 더미에 묻힌다.
안의 진짜 불일치가 영영 안 보인다. 위에서 본 React 의 규칙과 같다.
현장에서 만나는 모습
가장 흔한 제보는 "새로고침하면 잠깐 다른 값이 보였다가 바뀐다" 가 아니라
"값이 그냥 틀리다" 이다. 하이드레이션이 텍스트를 고쳐 주지 않으니, 서버가
그린 옛 값이 그대로 남아 있다가 다음 상태 변경에서야 갱신된다. 그래서 버그
신고가 "가끔 숫자가 안 맞아요" 로 들어오고, 재현 절차에 새로고침이 빠져 있다.
두 번째로 흔한 것은 개발 환경에서만 경고가 나는 경우다. 경고를 없애려고suppressHydrationWarning 을 위쪽 노드에 넓게 붙이는 팀이 있는데, 그러면 경고만
사라지고 틀린 값은 그대로 남는다. 게다가 한 겹만 통하므로 **아래쪽 불일치는
여전히 경고를 낸다** — 넓게 붙일수록 신호만 나빠진다.
마지막은 시간대다. 서버가 UTC 로 그린 날짜를 브라우저가 로컬 시간대로 그리면
자정 근처의 하루가 통째로 밀린다. QA 가 낮에만 하면 절대 안 잡힌다.
고치는 순서도 정해져 있다. 먼저 경고를 지우지 말고 원인을 나눈다 — 시각인지,
id 인지, 로캘인지, 브라우저만 아는 값인지. 앞의 셋은 값을 만드는 자리를 서버
하나로 모아 없앨 수 있고, 마지막 하나만 첫 렌더를 같게 두었다가 뒤로 미루는
처리가 필요하다. 이 구분 없이 억제부터 붙이면 고칠 수 있었던 셋까지 함께
덮인다.
다음 실습에서 할 것
서버 트리와 클라이언트 트리를 받아 어긋난 자리를 경로와 함께 찾아내는 비교기를
만든다. 속성 순서는 무시하고, 모양이 어긋나면 멈추고, 억제는 한 겹만 허용하고,
불일치마다 가장 가까운 경계를 붙인다. 마지막에는 그 결과로 **무엇을 다시 그려야
하는지**를 계산한다.