2026. 6. 17.

Next.js Hydration failed: 서버 HTML과 첫 렌더를 원인별로 맞추세요

AI가 만든 Next.js 페이지에서 Hydration failed가 뜰 때 오류 스택에서 문제 컴포넌트를 찾고, 서버 HTML과 브라우저 첫 렌더가 달라지는 값을 원인별로 좁히는 순서입니다.

4 min read
Next.js Hydration failed: 서버 HTML과 첫 렌더를 원인별로 맞추세요 대표 이미지

AI가 만든 Next.js 페이지를 새로고침했더니 콘솔에 Hydration failed because the server rendered HTML didn't match the client가 뜹니다. 화면이 일부 보인다고 넘어가거나 페이지 전체에 "use client"를 붙이기 전에, 오류 문장과 컴포넌트 스택을 함께 복사하세요. 그다음 서버 HTML과 브라우저의 첫 렌더에서 달라질 수 있는 값을 찾으면 수정 범위를 빠르게 줄일 수 있습니다.

핵심은 단순합니다. React hydrateRoot 문서는 서버가 만든 HTML에 브라우저 동작을 연결할 때 브라우저의 첫 렌더도 그 HTML과 같기를 기대한다고 설명합니다. 따라서 먼저 문제 컴포넌트를 찾고, 그 안에서 시간·랜덤값·브라우저 API·잘못된 HTML 중첩·경로처럼 첫 렌더를 바꾸는 값을 표시해야 합니다. 값의 종류를 알아낸 뒤에만 useEffect, 작은 Client Component 경계, 안정적인 초기값 중 맞는 수정을 고릅니다.

개발자가 서버 HTML과 브라우저 첫 렌더의 차이를 비교하는 장면

오류 문장과 컴포넌트 스택을 한 묶음으로 찾습니다

브라우저 개발자 도구의 Console에서 첫 hydration 오류를 펼칩니다. 마지막 경고만 보지 말고 Hydration failed, Text content does not match server-rendered HTML 같은 시작 문장과 바로 아래 컴포넌트 이름·파일 위치를 함께 복사합니다. 같은 문제가 여러 줄로 이어져도 첫 불일치가 생긴 컴포넌트를 찾는 것이 우선입니다.

다음 세 항목을 메모하면 AI에게도 정확하게 다시 물을 수 있습니다.

  1. 첫 hydration 오류 문장
  2. 오류 스택에 나온 가장 가까운 사용자 컴포넌트와 파일 경로
  3. 새로고침, 페이지 이동, 특정 테마·로그인 상태 중 언제 재현되는지

React 공식 문서는 hydration에 전달한 트리가 서버에서 만들었던 결과와 같은 출력을 내야 한다고 설명합니다. 불일치는 개발 모드의 경고로 끝날 수도 있지만, 속성이나 이벤트 연결이 기대와 다를 수 있으므로 실제 원인을 수정 대상으로 봐야 합니다.

첫 렌더를 바꾸는 값을 다섯 갈래로 분류합니다

오류 위치를 찾았다면 해당 컴포넌트의 JSX와 초기 state만 봅니다. Next.js hydration 오류 문서가 분류한 원인과 대조하며 렌더 중 아래 값이 실행되는지 확인합니다.

시간과 랜덤값

new Date(), Date.now(), Math.random()은 서버가 HTML을 만들 때와 브라우저가 첫 렌더를 할 때 다른 값을 만들 수 있습니다. 현재 시각, 임시 ID, 무작위 문구가 JSX 안에서 바로 계산되는지 찾습니다. 필요한 값이 서버 데이터라면 한 번 정해 props로 전달하고, 브라우저에서만 필요한 표시라면 안정적인 초기 문구를 먼저 렌더한 뒤 마운트 후 바꿉니다.

브라우저 전용 API와 저장값

window, document, localStorage, sessionStorage, matchMedia는 서버 렌더에서 같은 방식으로 읽을 수 없습니다. 이미 window is not defined 단계에서 막힌다면 오류 줄에서 실행 위치를 찾는 순서를 먼저 적용하세요. hydration 단계까지 왔다면 브라우저 값을 조건부로 읽은 결과가 첫 JSX를 바꾸는지 확인합니다.

잘못된 HTML 중첩

<p> 안에 <div>를 넣거나, 인터랙티브 요소를 잘못 중첩하면 브라우저가 DOM을 고쳐 해석하면서 React가 기대한 구조와 달라질 수 있습니다. 오류 스택 주변의 태그를 단순화하고 HTML 중첩 규칙에 맞게 다시 배치합니다.

경로와 rewrite 결과

정적 생성된 원본 경로와 rewrite 뒤 브라우저 주소가 다르면 usePathname() 값도 첫 화면에서 어긋날 수 있습니다. Next.js usePathname 문서는 이때 경로에 의존하는 작은 표시만 분리하고, 서버에서는 안정적인 빈 값이나 기본값을 렌더한 뒤 마운트 후 실제 경로로 갱신하는 방식을 안내합니다.

외부에서 바뀐 HTML

브라우저 확장 기능이나 CDN의 HTML 변환이 서버 응답을 바꾸는 경우도 있습니다. 시크릿 창과 확장 기능을 끈 상태에서 재현 여부를 비교하고, 로컬 개발에서는 정상인데 공개 환경에서만 발생한다면 응답 변환 설정을 확인합니다.

원인에 맞는 최소 수정만 선택합니다

원인을 찾았다고 무조건 페이지 전체를 Client Component로 바꾸지는 않습니다. "use client"는 브라우저에서 실행되는 진입 경계를 선언합니다. Next.js use client 문서도 모든 Client Component 파일에 붙일 필요는 없다고 설명합니다. 버튼이나 테마 표시처럼 상호작용이 필요한 작은 컴포넌트만 경계 안으로 옮기세요. 경계를 고르는 일이 어렵다면 use client를 붙일 파일만 고르는 순서를 함께 볼 수 있습니다.

hydration 원인별로 안정적인 초기값과 작은 클라이언트 경계와 마운트 후 갱신을 고르는 체크리스트

수정 방향은 원인에 따라 달라집니다.

  • 시간·랜덤값: 서버와 브라우저가 공유할 고정값을 전달하거나 안정적인 초기 표시를 먼저 사용합니다.
  • 브라우저 API·저장값: 초기 렌더에는 공통값을 쓰고, useEffect에서 읽어 state를 갱신합니다.
  • 상호작용 컴포넌트: 필요한 작은 파일에만 "use client" 경계를 둡니다.
  • HTML 중첩: 태그 구조를 고칩니다. useEffect로 미룰 문제가 아닙니다.
  • rewrite·경로: 경로에 의존하는 UI를 작게 분리하고 서버 fallback을 유지합니다.

예를 들어 현재 시각을 바로 렌더하지 말고 서버와 브라우저가 처음에는 같은 문구를 그리게 만들 수 있습니다.

"use client";

import { useEffect, useState } from "react";

export default function LocalTimeBadge() {
  const [label, setLabel] = useState("시간 확인 중");

  useEffect(() => {
    setLabel(new Date().toLocaleTimeString("ko-KR"));
  }, []);

  return <span>{label}</span>;
}

이 코드는 모든 hydration 오류의 정답이 아닙니다. 서버와 브라우저의 첫 렌더는 시간 확인 중으로 같고, 브라우저 전용 값은 마운트 뒤에 갱신한다는 경계 예시입니다. 두 번 렌더하므로 느린 연결에서 표시가 바뀌는 느낌도 확인해야 합니다.

suppressHydrationWarning은 의도적인 한 요소에만 남깁니다

suppressHydrationWarning은 원인을 고치는 도구가 아니라 제한적인 탈출구입니다. React 문서는 한 단계 깊이에서만 작동하며 남용하지 말라고 안내합니다. 서버와 브라우저가 의도적으로 다른 시간 문자열처럼 피할 수 없는 단일 요소인지 먼저 판단하세요.

다음 중 하나라도 해당하면 경고를 숨기기보다 원인을 고칩니다.

  • 컴포넌트 전체 구조가 달라진다.
  • 버튼, 링크, 입력 상태가 첫 화면부터 달라진다.
  • window 존재 여부로 다른 JSX를 반환한다.
  • 어떤 값이 달라졌는지 아직 모른다.

차이를 설명할 수 없으면 suppressHydrationWarning을 붙이지 않습니다. 경고를 가리면 다음 수정 때 같은 문제가 다른 위치에서 다시 나타날 수 있습니다.

개발 새로고침과 production 실행을 모두 확인합니다

수정 뒤에는 개발 서버의 새로고침만 보지 않습니다. 오류가 났던 경로를 직접 열고, 다른 페이지에서 이동해 들어오고, 테마·로그인·저장값 조건을 바꿔 봅니다. 이어서 production build와 start 환경에서도 같은 경로를 열어 Console 경고가 사라졌는지 확인합니다.

AI에게 다시 요청할 때는 아래처럼 범위를 제한합니다.

Next.js hydration 오류가 나는 컴포넌트는 app/components/ThemeBadge.tsx입니다.
서버 첫 렌더와 브라우저 첫 렌더에서 달라지는 값 후보를 먼저 표시해 주세요.
페이지 전체를 Client Component로 바꾸지 말고,
안정적인 초기값, 작은 use client 경계, useEffect 이동 중 최소 수정을 제안해 주세요.
suppressHydrationWarning은 실제 차이가 의도적인 단일 요소일 때만 검토해 주세요.

오늘 남길 결과물은 큰 리팩터링이 아닙니다. 오류 문장, 문제 컴포넌트, 달라지는 값, 선택한 최소 수정, production 확인 결과를 한 줄씩 기록하세요. 버전별 동작 차이는 공식 문서와 최신 릴리스 노트를 확인하세요.

참고 출처

다음으로 읽을 기사

같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.

댓글 0

이 글을 읽은 독자들의 생각을 나눠보세요.

비밀번호(선택)

첫 번째 댓글을 남겨보세요

여러분의 생각이 다른 독자에게 도움이 됩니다.