Next.js Hydration 오류 해결하는 법 2026 가이드

profile_image
작성자 프론트엔드 해결사 강하린
댓글 0건 조회 46회

화면은 뜨는데 콘솔이 빨개질 때 먼저 볼 것

Hydration 오류가 생기는 실제 이유

Next.js로 웹개발을 하다 보면 페이지는 정상처럼 보이는데 브라우저 콘솔에 Hydration failed, Text content does not match server-rendered HTML 같은 메시지가 뜨는 순간이 있습니다. 이 오류는 서버가 만든 HTML과 브라우저에서 React가 다시 연결하려는 화면 구조가 서로 다를 때 발생합니다. 즉, 코드는 실행되지만 React 입장에서는 “서버에서 본 화면과 클라이언트에서 본 화면이 다르다”고 판단한 상태입니다.

2026년 기준으로 Next.js 프로젝트는 App Router, Server Components, Client Components, 캐시, 동적 렌더링이 함께 쓰이는 경우가 많습니다. 그래서 예전처럼 단순히 useEffect로 옮기면 끝나는 문제가 아니라, 서버에서 계산해야 할 값과 브라우저에서만 계산해야 할 값을 정확히 나누는 감각이 중요합니다. 코딩 개념 자체가 낯설다면 코딩의 기본 정의를 먼저 확인해도 좋습니다.

  • 서버 HTML: 사용자가 페이지를 받기 전에 Next.js가 미리 만든 결과입니다.
  • 클라이언트 렌더링: 브라우저에서 JavaScript가 실행되며 React가 이벤트와 상태를 연결하는 과정입니다.
  • Hydration mismatch: 두 결과가 다르기 때문에 React가 경고하거나 일부 화면을 다시 그리는 상황입니다.
팁: Hydration 오류는 CSS 문제처럼 보일 수 있지만, 실제 원인은 대부분 시간, 랜덤값, 브라우저 전용 API, 조건부 렌더링, 외부 데이터의 불일치입니다.

가장 빠른 확인 순서

문제가 생겼을 때는 먼저 콘솔 메시지의 컴포넌트 스택을 확인하세요. Next.js 개발 서버는 어떤 태그나 컴포넌트에서 서버 출력과 클라이언트 출력이 달라졌는지 비교적 친절하게 보여줍니다. 오류 메시지를 무시하고 전체 레이아웃을 고치기 시작하면 원인과 상관없는 파일을 건드리게 됩니다.

  1. 오류가 처음 나타나는 URL을 고정합니다.
  2. 브라우저 새로고침 후 콘솔의 첫 번째 Hydration 메시지만 확인합니다.
  3. 표시된 컴포넌트에서 Date, Math.random, window, localStorage, navigator 사용 여부를 찾습니다.
  4. 서버 데이터와 클라이언트 데이터가 같은 포맷으로 출력되는지 비교합니다.

시간, 랜덤값, 사용자 환경 값부터 제거하세요

Date와 Math.random이 만드는 흔한 불일치

Hydration 오류의 대표 원인은 렌더링 중에 매번 달라지는 값입니다. 예를 들어 컴포넌트 본문에서 new Date().toLocaleString()을 바로 출력하면 서버가 만든 시간과 브라우저가 Hydration하는 시간이 달라질 수 있습니다. Math.random()으로 배너 문구, 추천 글, 색상, 키를 만들 때도 같은 문제가 생깁니다.

해결 원칙은 간단합니다. 서버와 클라이언트가 반드시 같은 값을 가져야 한다면 서버에서 값을 확정해 props로 넘기고, 브라우저에서만 보여도 되는 값이라면 마운트 이후 useEffect에서 계산합니다. 특히 한국어 날짜 포맷은 서버의 locale, timezone 설정에 따라 결과가 달라질 수 있으므로 2026년 현재 운영 환경에서는 UTC 저장, 화면 표시 시 명시적 타임존 변환 방식을 권장합니다.

  • 나쁜 예: 컴포넌트 렌더링 중 new Date()를 바로 문자열로 출력합니다.
  • 나쁜 예: Math.random()으로 li key를 생성합니다.
  • 좋은 예: 서버에서 생성한 createdAt을 ISO 문자열로 넘기고 클라이언트에서 동일하게 포맷합니다.
  • 좋은 예: 랜덤 추천은 서버 액션이나 API 응답에서 확정된 목록으로 제공합니다.

브라우저 전용 API는 렌더링 밖으로 빼기

window.innerWidth, localStorage, matchMedia, navigator.language는 서버에서 존재하지 않습니다. 이런 값을 렌더링 단계에서 조건문에 넣으면 서버는 A 화면을 만들고, 브라우저는 B 화면을 만들 가능성이 높습니다. 모바일 여부에 따라 메뉴 구조 자체를 바꾸는 코드가 대표적인 실패 사례입니다.

처음부터 반응형 레이아웃은 CSS 미디어쿼리로 처리하고, 꼭 JavaScript가 필요할 때만 마운트 이후 상태를 갱신하세요. 예를 들어 다크모드 토글은 서버에서 기본 테마를 쿠키로 읽고, 브라우저에서는 localStorage를 보조로 동기화하는 방식이 안정적입니다. 초보자라면 파이썬 입문 같은 기초 코딩 자습서로 변수와 실행 시점 개념을 익히면 React 오류를 이해하는 데도 도움이 됩니다.

전문가 조언: “서버에서도 실행될 수 있는가?”라는 질문에 답하지 못하는 코드는 Client Component 안에서도 렌더링 단계에 두지 않는 편이 안전합니다.

Server Component와 Client Component 경계를 점검하는 법

use client는 만능 해결책이 아닙니다

Next.js App Router에서 이벤트 핸들러, useState, useEffect를 쓰려면 파일 상단에 'use client'를 선언해야 합니다. 하지만 Hydration 오류가 난다고 모든 컴포넌트를 Client Component로 바꾸면 초기 JavaScript 용량이 커지고, 서버 렌더링의 장점도 줄어듭니다. 프로그래밍에서 중요한 것은 문제를 없애는 것이 아니라 원인을 좁혀 정확히 고치는 것입니다.

Server Component는 데이터 조회와 정적인 HTML 생성에 강하고, Client Component는 클릭, 입력, 드래그, 로컬 상태처럼 브라우저 상호작용에 강합니다. 오류가 나는 영역이 버튼 하나라면 전체 페이지를 클라이언트화하지 말고 버튼만 별도 컴포넌트로 분리하세요. 이 작은 분리가 성능, 유지보수, SEO 모두에 영향을 줍니다.

  • Server Component에 적합: 게시글 본문, 상품 목록, 가격표, 문서형 콘텐츠, SEO가 중요한 페이지
  • Client Component에 적합: 모달, 탭, 검색 자동완성, 장바구니 수량 변경, 드롭다운 메뉴
  • 분리 기준: 사용자 입력이나 브라우저 이벤트가 필요한 최소 영역만 클라이언트로 둡니다.

잘못된 조건부 렌더링 고치기

Hydration 오류는 “로그인했으면 이 버튼, 아니면 저 버튼”처럼 조건부 렌더링에서 자주 나타납니다. 서버에서는 쿠키를 기준으로 로그인 상태를 판단했는데, 클라이언트에서는 localStorage 토큰을 기준으로 판단하면 첫 화면 결과가 달라집니다. 같은 사용자라도 두 저장소의 동기화 타이밍이 다르면 오류가 반복됩니다.

인증 상태, 권한, 지역, 통화처럼 화면 구조를 바꾸는 값은 서버에서 먼저 확정하는 편이 좋습니다. 브라우저에서 뒤늦게 확인해야 한다면 초기에는 공통 스켈레톤을 렌더링하고, 마운트 후 실제 UI로 바꾸세요. 이때 버튼 위치와 영역 높이를 고정하면 레이아웃 점프도 함께 줄일 수 있습니다.

  1. 조건문에 들어가는 값이 서버에서도 동일하게 계산되는지 확인합니다.
  2. 쿠키, 헤더, URL 파라미터처럼 서버가 읽을 수 있는 값으로 우선 판단합니다.
  3. localStorage 값은 초기 렌더링 결과를 바꾸지 않게 useEffect 이후 반영합니다.
  4. 권한별 화면이 크게 다르면 페이지 단위로 라우팅을 분리합니다.

HTML 구조 오류와 외부 라이브러리 문제 해결

잘못된 태그 중첩은 생각보다 자주 터집니다

React 컴포넌트만 보면 문제가 없어 보이지만 브라우저가 HTML을 자동 보정하면서 서버 결과와 클라이언트 결과가 달라지는 경우가 있습니다. 대표적으로 p 태그 안에 div를 넣거나, table 내부 구조를 잘못 만들거나, button 안에 또 다른 button을 넣는 패턴입니다. 브라우저는 이런 HTML을 조용히 고쳐 렌더링하므로 개발자는 원인을 늦게 발견합니다.

블로그, 문서, 쇼핑몰 상세 페이지처럼 CMS에서 HTML을 받아 출력하는 프로젝트라면 더 주의해야 합니다. 사용자가 입력한 HTML이나 마크다운 변환 결과가 잘못된 구조를 만들 수 있기 때문입니다. 이런 경우 sanitize 과정과 HTML 파서를 함께 점검하고, dangerouslySetInnerHTML을 쓰는 컴포넌트 주변에서 오류가 시작되는지 확인하세요.

  • 점검 1: p 안에 div, section, table 같은 블록 요소가 들어가지 않았는지 봅니다.
  • 점검 2: ul 바로 아래에는 li가 오는지 확인합니다.
  • 점검 3: table, thead, tbody, tr, td 순서가 표준 구조인지 확인합니다.
  • 점검 4: 중첩 button, 중첩 a 태그가 없는지 검사합니다.

클라이언트 전용 라이브러리는 동적 로딩으로 분리

차트, 지도, 에디터, 애니메이션 라이브러리 중 일부는 내부에서 window나 document를 바로 참조합니다. 이런 라이브러리를 Server Component나 SSR 단계에서 불러오면 Hydration 오류뿐 아니라 빌드 오류까지 이어질 수 있습니다. 2026년에도 웹개발 현장에서는 지도 SDK, WYSIWYG 에디터, 결제 위젯에서 이 문제가 자주 발생합니다.

해결법은 해당 라이브러리를 Client Component로 감싸고, 필요하다면 next/dynamic에서 ssr: false 옵션으로 브라우저에서만 로드하는 것입니다. 다만 남용하면 검색엔진이 핵심 콘텐츠를 늦게 보거나 못 볼 수 있으므로, 차트의 제목과 요약 수치는 서버에서 텍스트로 먼저 제공하고 시각화만 클라이언트에서 붙이는 방식이 좋습니다. 관련 용어가 헷갈린다면 코딩 용어 설명처럼 기초 개념을 함께 참고하세요.

  1. 문제 라이브러리를 import하는 컴포넌트를 찾습니다.
  2. 해당 컴포넌트 상단에 'use client'가 필요한지 확인합니다.
  3. 서버에서 렌더링할 필요가 없는 위젯은 dynamic import로 분리합니다.
  4. SEO가 필요한 텍스트 정보는 서버에서 별도로 출력합니다.

디버깅 체크리스트와 재발 방지 루틴

오류를 빨리 좁히는 7단계

Hydration 오류는 한 번에 완벽히 고치려 하기보다 범위를 반씩 줄이는 방식이 빠릅니다. 페이지 전체에서 시작해 레이아웃, 섹션, 컴포넌트, 조건문 순서로 좁혀 가면 원인을 찾기 쉽습니다. 특히 공통 Header나 ThemeProvider에서 오류가 나면 모든 페이지에서 같은 메시지가 반복되므로 먼저 의심해야 합니다.

아래 체크리스트는 실제 프로젝트에서 반복적으로 쓰기 좋은 순서입니다. 팀 작업이라면 Pull Request 템플릿에 넣어도 좋습니다. 단순 튜토리얼 수준을 넘어 운영 서비스의 품질을 지키려면 Hydration 오류를 “개발 중 경고”로만 보지 말고, 사용자 경험과 성능을 해치는 신호로 다뤄야 합니다.

  1. 개발 서버에서 오류가 나는 URL과 브라우저를 기록합니다.
  2. 콘솔의 컴포넌트 스택에서 가장 안쪽 컴포넌트를 찾습니다.
  3. Date, Math.random, crypto.randomUUID, locale 포맷 코드를 검색합니다.
  4. window, document, localStorage, sessionStorage, navigator 사용 위치를 찾습니다.
  5. 조건부 렌더링 값이 서버와 클라이언트에서 같은 출처인지 확인합니다.
  6. 잘못된 HTML 중첩과 CMS 원문 HTML을 검사합니다.
  7. 외부 라이브러리를 Client Component 또는 dynamic import로 격리합니다.

팀에서 재발을 줄이는 코딩 규칙

개인 프로젝트에서는 임시 수정으로 넘어갈 수 있지만, 협업 프로젝트에서는 규칙이 필요합니다. 예를 들어 “렌더링 중 현재 시간 직접 출력 금지”, “랜덤 key 생성 금지”, “브라우저 API는 useEffect 또는 커스텀 훅에서만 사용” 같은 규칙을 문서화하면 리뷰 시간이 줄어듭니다. 크림코드 독자처럼 코딩 실력을 키우는 단계라면 이런 규칙을 습관으로 만드는 것이 중요합니다.

또한 ESLint 규칙과 코드 리뷰 체크 항목을 함께 운영하세요. 모든 문제를 자동화할 수는 없지만, 위험한 패턴을 팀원이 반복해서 발견하게 만드는 것만으로도 충분히 효과가 있습니다. 튜토리얼을 따라 만들 때도 “왜 이 코드는 서버에서 실행되면 안 되는가?”를 스스로 질문하면 실전 디버깅 속도가 달라집니다.

  • 규칙 1: 렌더링 함수 안에서 현재 시간, 랜덤값, 브라우저 전용 값을 직접 만들지 않습니다.
  • 규칙 2: UI 구조를 바꾸는 조건은 가능한 서버에서 확정합니다.
  • 규칙 3: 외부 위젯은 작은 Client Component로 감싸 영향 범위를 줄입니다.
  • 규칙 4: suppressHydrationWarning은 날짜처럼 불가피한 텍스트 차이에만 제한적으로 사용합니다.

자주 묻는 질문으로 보는 실전 판단 기준

suppressHydrationWarning을 써도 될까요?

suppressHydrationWarning은 Hydration 경고를 숨기는 도구이지 원인을 고치는 도구가 아닙니다. 서버와 클라이언트의 텍스트가 의도적으로 다를 수밖에 없는 아주 좁은 상황, 예를 들어 사용자의 로컬 시간대에 맞춰 표시되는 시간 텍스트 정도에만 신중히 쓰는 편이 좋습니다. 화면 구조가 달라지는 문제에 적용하면 실제 버그를 가릴 수 있습니다.

특히 가격, 재고, 로그인 상태, 권한 버튼처럼 사용자 행동에 직접 영향을 주는 정보에는 사용하지 마세요. 이런 값이 다르다면 경고를 숨길 것이 아니라 데이터 출처를 통일해야 합니다. 개발 단계에서 경고가 거슬린다는 이유로 무작정 적용하면 배포 후 특정 사용자에게만 버튼이 사라지는 난감한 문제가 생길 수 있습니다.

  • 사용 가능: 로컬 시간 표시처럼 의미상 허용되는 짧은 텍스트 차이
  • 사용 주의: 광고 문구, 추천 문구처럼 서버와 클라이언트 결과가 달라도 큰 문제가 없는 영역
  • 사용 금지에 가까움: 인증, 결제, 권한, 폼 기본값, 상품 가격, 재고 정보

개발 환경에서만 보이면 배포해도 괜찮을까요?

개발 환경에서만 보이는 것처럼 느껴져도 바로 배포하는 것은 위험합니다. 운영 빌드에서는 경고 표시 방식이 달라질 수 있지만, 서버 HTML과 클라이언트 결과가 달라지는 구조적 문제는 그대로 남아 있을 수 있습니다. 특히 모바일 브라우저, 느린 네트워크, 다른 타임존 사용자가 들어오면 개발자 PC에서 보이지 않던 문제가 드러납니다.

배포 전에는 npm run build와 실제 프로덕션 서버 실행 상태에서 다시 확인하세요. 가능하다면 Playwright 같은 E2E 테스트로 주요 페이지의 콘솔 오류를 수집하고, 로그인 전후 화면을 모두 검사하는 것이 좋습니다. 이 과정은 단순한 버그 수정이 아니라 프로그래밍 품질을 높이는 웹개발 루틴입니다.

  1. 로컬 개발 서버에서 원인 컴포넌트를 찾습니다.
  2. 프로덕션 빌드로 같은 URL을 확인합니다.
  3. 시크릿 모드, 모바일 뷰포트, 다른 locale 설정에서 재현합니다.
  4. 수정 후 콘솔 오류와 레이아웃 점프를 함께 확인합니다.

Next.js Hydration 오류 해결하는 법 2026 가이드

댓글목록

등록된 댓글이 없습니다.