목차
화면은 멀쩡히 뜨는데 개발자 도구 콘솔에 빨간 글씨로 "Hydration failed" 같은 오류가 찍혀 있을 때가 있어요. 내 컴퓨터에서는 안 나는데, 누군가의 폰이나 다른 언어 설정의 브라우저에서만 나기도 해요. 이 글은 그 오류가 왜 생기는지, 원인을 어떻게 좁히는지, 어떻게 고치고 어떻게 미리 막는지를 순서대로 정리해요.
혹시 이런 고민 있으신가요?
- 콘솔에 하이드레이션 오류가 떴는데, 어느 컴포넌트가 문제인지 메시지만 봐서는 모르겠어요.
- 제 컴퓨터에서는 멀쩡한데, 다른 사람 화면에서만 깜빡이거나 오류가 난다고 해요.
- 날짜나 난수 때문이라는 건 아는데, 그런 게 없는 화면에서도 오류가 나요.
이 글을 읽으면
- 하이드레이션이 무엇이고, 서버 화면과 브라우저 화면이 달라지면 왜 문제인지 설명할 수 있어요.
- 오류가 나는 자리를 찾아서 원인을 네 가지 갈래 중 하나로 가를 수 있어요.
- 고친 뒤에 같은 종류의 오류가 다시 생기지 않게 확인하는 습관을 만들 수 있어요.
1단계. 하이드레이션이 무엇인지 알고 오류 메시지 읽기
Next.js 같은 서버 렌더링 프레임워크는 페이지를 두 번 그려요. 먼저 서버에서 완성된 HTML 을 만들어 보내요. 그러면 브라우저가 자바스크립트를 받기 전에도 글자가 보여요. 그다음 브라우저에서 같은 화면을 한 번 더 계산해서, 서버가 보낸 HTML 에 클릭 같은 동작을 붙여요. 이 붙이는 과정을 하이드레이션이라고 불러요.
비유하면 이래요. 서버는 가구 조립 설명서대로 미리 조립한 가구를 보내 주고, 브라우저는 같은 설명서를 보며 "내가 조립해도 이 모양이겠네" 하고 확인한 다음 서랍에 손잡이를 달아요. 설명서를 읽는 사람마다 결과가 달라지면, 이미 도착한 가구와 맞지 않아요.
그래서 규칙은 하나예요. 서버가 만든 HTML 과 브라우저의 첫 번째 렌더 결과는 똑같아야 해요. 다르면 React 는 서버 HTML 을 버리고 화면을 처음부터 다시 그리거나, 어긋난 채로 두기도 해요. 깜빡임, 잘못된 순서, 눌러도 반응 없는 버튼이 그 증상이에요.
오류 메시지는 대개 이런 모양이에요.
Hydration failed because the server rendered HTML didn't match the client.... <div className="relative">+ <span aria-hidden ...>읽을 곳은 두 군데예요. 어떤 요소에서 달라졌는지, 그리고 비교 표시에서 한쪽에만 있는 줄이 어디인지예요. 위 예시처럼 한쪽에만 요소가 있으면 "한쪽이 그리고 한쪽은 안 그렸다", 같은 요소인데 글자가 다르면 "계산 결과가 다르다"예요.
TIP
개발 모드의 오류 화면은 서버와 브라우저의 차이를 줄 단위로 보여 줘요. 원인을 추측하기 전에 이 비교부터 끝까지 읽으세요. 어느 한쪽에만 있는 줄이 곧 범위예요.
2단계. 원인을 네 갈래로 나누기
원인은 결국 "서버가 모르는 것을 첫 렌더에서 썼다"로 모여요. 서버는 브라우저의 설정도, 사용자의 시간대도, 화면 크기도 몰라요. 그래서 이런 값을 첫 렌더에서 읽으면 서버와 브라우저가 서로 다른 답을 내요. 저는 아래 네 갈래로 가르고 시작해요.
| 갈래 | 대표 예 | 찾는 단서 |
|---|---|---|
| 시간과 난수 | 현재 시각, 무작위 값, 고유 번호를 직접 만들기 | 코드에 현재 시각이나 난수 호출이 보여요 |
| 브라우저에만 있는 값 | 창 너비, 로컬 저장소, 쿠키 | 서버에서는 없는 값을 첫 렌더에서 읽어요 |
| 환경 설정 | 동작 줄이기, 다크 모드, 언어 | 사용자 설정에 따라 갈라지는 분기가 있어요 |
| 환경 차이 | 정렬 순서, 날짜와 숫자 표기 | 서버와 브라우저의 기본 언어 설정이 달라요 |
앞의 둘은 검색하면 설명이 많아서 금방 찾아요. 어려운 쪽은 뒤의 둘이에요. 코드에 눈에 띄는 난수도 시간도 없는데, 사용자의 설정이나 기기 환경에 따라 결과가 달라지기 때문이에요. 이 글의 나머지는 저희가 실제로 만난 뒤의 두 경우를 예로 들어요.
서버가 모르는 값을 첫 렌더에서 썼는지로 갈라요
3단계. 설정에 따라 갈라지는 분기 고치기, 동작 줄이기 예
첫 번째 사례는 "동작 줄이기" 설정이에요. 휴대폰과 컴퓨터에는 화면이 움직이는 것이 불편한 사람을 위해 애니메이션을 줄이는 접근성 설정이 있어요. 브라우저는 이 값을 알려 주고, 많은 애니메이션 라이브러리가 읽을 수 있게 훅을 제공해요.
저희 랜딩 화면에는 추천 요금제 카드 둘레에서 은은하게 깜빡이는 링이 있어요. 처음에는 이렇게 짰어요.
function GlowPulse({ children }) { const reduce = useReducedMotion() return ( <div className="relative"> {!reduce && <motion.span aria-hidden className="absolute inset-0 ring-[10px]" />} {children} </div> )}겉으로는 문제가 없어 보여요. 동작 줄이기를 켠 사람에게는 링을 아예 그리지 않으니 친절한 코드처럼도 보여요. 하지만 서버는 사용자의 설정을 몰라요. 서버는 "링을 그린다"로 HTML 을 만들어 보내고, 동작 줄이기를 켠 브라우저는 첫 렌더에서 "링을 안 그린다"로 계산해요. 설정을 켠 기기에서만 서버 HTML 과 첫 렌더가 달라지고, 개발자의 기기에서는 설정이 꺼져 있으니 오류가 나지 않아요. 그래서 "내 컴퓨터에서는 멀쩡해요"가 되는 거예요.
고치는 방법은 분기를 자바스크립트에서 CSS 로 옮기는 거예요. 링은 항상 그리고, 설정이 켜진 사용자에게만 CSS 로 숨겨요.
<motion.span aria-hidden className="absolute inset-0 ring-[10px] motion-reduce:hidden"/>motion-reduce: 는 Tailwind CSS 가 제공하는 접두어로, 브라우저의 동작 줄이기 설정이 켜진 경우에만 스타일을 적용해요. 서버와 브라우저의 HTML 은 항상 같고, 보이고 안 보이고는 CSS 가 사용자 기기에서 정해요. 숨겨진 요소는 화면에 안 그려지니 접근성 면에서도 같은 효과예요.
TIP
사용자 설정에 따른 분기는 "자바스크립트로 렌더를 바꾸기"보다 "항상 렌더하고 CSS 로 숨기기"가 안전해요. 다크 모드, 화면 크기, 동작 줄이기, 인쇄 모드가 모두 CSS 미디어 쿼리로 처리돼요.
CSS 로 못 옮기는 경우도 있어요. 설정에 따라 완전히 다른 컴포넌트를 써야 할 때예요. 그럴 때는 첫 렌더에는 서버와 같은 기본값을 쓰고, 브라우저에서 마운트된 뒤에 값을 읽어 한 번 더 그리세요. 잠깐 기본 모양이 보이는 대가를 치르는 방식이에요.
4단계. 환경 차이로 달라지는 결과 고치기, 정렬 순서 예
두 번째 사례는 더 눈에 띄지 않아요. 내부 운영 화면의 지출 목록에서 거래처 이름 순으로 정렬하는 코드가 있었어요.
rows.sort( (a, b) => a.date.localeCompare(b.date) || a.vendor.localeCompare(b.vendor),)localeCompare 는 문자열을 "사람이 읽는 순서"로 비교하는 함수예요. 이때 어느 언어의 규칙을 따를지를 두 번째 인자로 정하고, 안 적으면 실행 환경의 기본 언어를 따라가요. 서버는 서버의 기본 언어로, 한국어 브라우저는 한국어 규칙으로 정렬해요. 한글과 영문이 섞인 거래처 이름을 두 환경이 서로 다른 순서로 세우면, 서버가 그린 행 순서와 브라우저가 계산한 행 순서가 달라져요. 한글 거래처만 있었다면 안 드러났을 거예요. 영문이 섞이자 드러났어요.
고치는 방법은 단순해요. 두 환경이 같은 규칙을 쓰도록 언어를 직접 적어요.
rows.sort( (a, b) => a.date.localeCompare(b.date) || a.vendor.localeCompare(b.vendor, "ko"),)날짜, 숫자, 통화 표기도 같은 종류예요. 아래 표로 기억해 두세요.
| 쓰는 곳 | 안 적으면 | 고정하는 법 |
|---|---|---|
| 문자열 정렬 | 실행 환경 기본 언어 | localeCompare(b, "ko") |
| 날짜 표기 | 실행 환경 기본 언어와 시간대 | 날짜 포맷 함수에 언어와 시간대를 둘 다 적기 |
| 숫자와 통화 | 실행 환경 기본 언어 | 숫자 포맷 함수에 언어를 적기 |
참고
서버 컴퓨터의 시간대와 사용자의 시간대가 다른 건 매우 흔해요. 날짜를 보여 줄 때 시간대를 고정할지, 브라우저에서 마운트된 뒤에 사용자 시간대로 바꿀지는 화면의 목적에 따라 골라야 해요. 정답이 하나는 아니에요.
5단계. 고친 뒤 확인하고, 같은 일이 다시 안 생기게 하기
고쳤다면 이번에는 "내 기기에서만 괜찮은 것"이 아닌지 확인해야 해요. 이 오류는 기기마다 달라서, 한 번 통과했다고 안심할 수 없어요. 저는 아래 순서로 확인해요.
- 원인으로 꼽은 설정을 직접 바꿔 보세요. 운영체제의 동작 줄이기를 켜고, 브라우저 언어를 바꾸고, 시간대를 바꿔서 페이지를 새로 열어요.
- 콘솔에 하이드레이션 경고가 하나도 없는지 보세요. 개발 모드에서는 처음 한 번만 뜨니 새로고침으로 확인하세요.
- 같은 패턴이 다른 컴포넌트에도 있는지 검색하세요. 훅으로 설정을 읽어 렌더를 분기하는 코드와, 언어 인자 없는 정렬과 날짜 포맷을 찾아요.
한 번 겪은 오류는 팀의 규칙으로 남기는 게 가장 싸요. 저희는 아래 두 줄을 코드 리뷰 기준으로 삼아요.
사용자 설정에 따라 요소를 그릴지 말지는 CSS 로 정한다.정렬과 날짜, 숫자 표기에는 언어를 직접 적는다.TIP
규칙을 자동으로 검사하는 린트 규칙이 있으면 더 좋지만, 처음에는 위 두 줄을 리뷰 체크리스트에 적는 것만으로도 충분해요. 같은 실수가 두 번 나올 때 린트 규칙을 만드세요.
자주 묻는 질문
Q1. 하이드레이션 오류를 억제하는 옵션을 쓰면 안 되나요?
React 에는 특정 요소의 불일치 경고를 끄는 옵션이 있어요. 시각 표시처럼 서버와 브라우저가 다를 수밖에 없는 한 요소에는 쓸 만해요. 하지만 경고를 끄는 것이지 화면을 맞추는 게 아니라서, 원인을 고치는 대신 쓰면 어긋난 화면이 그대로 남아요. 먼저 원인을 찾고, 정말 불가피할 때만 쓰세요.
Q2. 다크 모드도 같은 문제가 생기나요?
설정을 자바스크립트로 읽어 렌더를 바꾸면 똑같이 생겨요. 색만 다르다면 CSS 변수와 미디어 쿼리로 처리하는 편이 안전해요. 사용자가 고른 테마를 저장소에서 읽는 경우에는 첫 화면이 잠깐 다른 테마로 보일 수 있어서, 마운트된 뒤에 적용하거나 서버가 쿠키로 테마를 알게 하는 방법을 써요.
Q3. 개발 모드에서만 나고 배포에서는 안 나는데 무시해도 되나요?
배포 빌드에서는 경고 문구만 줄어들 뿐 불일치 자체는 그대로일 수 있어요. 화면이 깜빡이거나 순서가 바뀌는 증상이 사용자에게 가요. 개발 모드의 경고가 가장 친절한 단서이니 그때 고치세요.
정리
- 하이드레이션은 서버가 보낸 HTML 과 브라우저의 첫 렌더가 같다고 믿고 동작을 붙이는 과정이에요.
- 오류의 뿌리는 "서버가 모르는 값을 첫 렌더에서 썼다"예요. 시간, 브라우저 값, 사용자 설정, 환경 기본값 네 갈래로 가르세요.
- 설정에 따른 분기는 항상 그리고 CSS 로 숨기는 쪽이 안전해요.
- 정렬과 날짜, 숫자에는 언어를 직접 적어서 서버와 브라우저가 같은 규칙을 쓰게 하세요.
- 고친 뒤에는 설정과 언어를 바꿔 가며 확인하고, 같은 패턴을 검색해서 한꺼번에 고치세요.
내 기기에서만 괜찮은 코드는 아직 확인이 끝난 코드가 아니에요. 서버가 모르는 값이 첫 렌더에 들어갔는지, 이 질문 하나를 습관으로 만들면 대부분의 하이드레이션 오류는 코드를 쓰는 순간 잡혀요.