Hydration 경고 문제와 해결 시도

verdantgreeny·2025년 3월 18일

본캠프

목록 보기
55/56

문제 발생

suppressHydrationWarning={true}하면 위의 경고는 숨겨짐yarn dev로 개발 서버를 실행할 때만 뜨는 에러

처음에는 개발 모드에서 Warning: Extra attributes from the server: cz-shortcut-listen 경고가 발생했다. 이 문제는 suppressHydrationWarning={true}를 사용하면 경고가 무시되어진다고 한다. 물론 이 방법은 경고를 숨기는 것일 뿐, 근본적인 문제를 해결한 것은 아니었다.

그러다 Tailwind CSS를 사용하면서 yarn dev로 개발 서버를 실행할 때만 Warning: Extra attributes from the server: class, style 에러가 발생했다.

yarn build && yarn start로 했을 때는 문제가 없었지만, 개발 모드에서만 이 에러가 발생했다. 이로 인해 개발 중에 콘솔에 계속 경고가 떠서 이를 해결해 보고자 했다.

원인 추론

이 문제는 서버 사이드 렌더링(SSR)클라이언트 사이드 렌더링(CSR) 간의 불일치로 인해 발생하는 것이라고 한다. 개발 모드에서는 React가 더 엄격하게 검사를 하기 때문에, 서버와 클라이언트에서 생성된 HTML이 조금이라도 다르면 경고를 표시한다.

특히, 다음과 같은 원인을 나름대로 추론해보았다.

  • 다크 모드 설정: 다크 모드를 사용하면서 서버와 클라이언트에서 다크 모드 상태가 달라서 class나 style 속성이 다르게 적용되었을 수 있다.

  • CSS 변수 사용: var(--color)와 같은 CSS 변수를 사용하면서 서버와 클라이언트에서 변수 값이 다르게 계산되었을 수 있다.

  • Tailwind CSS 버전 문제: Tailwind CSS v4를 사용하면서 설정이 제대로 적용되지 않았을 수 있다.

해결 시도

이 문제를 해결하기 위해 다음과 같은 방법을 시도했다.

1. 다크 모드 상태 동기화

next-themes 라이브러리를 사용하여 서버와 클라이언트에서 다크 모드 상태를 동기화하려고 했다. 하지만 여전히 경고가 발생했다.

2. CSS 변수 대신 Tailwind CSS 클래스 사용

var(--color) 대신 Tailwind CSS의 색상 시스템을 사용하도록 코드를 수정했다. 예를 들어, bg-[var(--gray-3)] 대신 bg-gray-3를 사용하려 했다. 하지만 문제가 해결되지 않았고, 스타일도 적용이 되지 않았다.

3. Tailwind CSS 설정 검토

tailwind.config.js에서 content, theme, darkMode 등을 다시 확인하고 수정했다. 하지만 여전히 경고가 발생했다.

4. Tailwind CSS v3로 다운그레이드 시도

실은 처음에 next를 최신 버전으로 깔았다가 다운그레이드 한 적이 있는데, 이때, Tailwind도 최신으로 깔린 듯 했다. 그리하여 Tailwind CSS v4가 불안정하다고 판단하고, v3로 다운그레이드를 시도했다. package.json에서 tailwindcss 버전을 ^3.4.1로 변경하고, yarn install로 재설치했다. 하지만 다운그레이드 후 기존 CSS가 완전히 엉망이 되어버렸다. 스타일이 제대로 적용되지 않았고, 마감날짜가 얼마 남지 않았기 때문에 결국에는 다시 Tailwind CSS v4로 돌아가야 했다.( -> ++ 3/19(수) : 이부분은 튜터님과 함께 하여 다운그레이드 하였는데 실제 hydration 경고와는 관련은 없었다. )

시도 결과

위의 방법들을 시도했지만, 문제를 해결하지 못했다. 개발 모드에서만 발생하는 이 문제는 여전히 해결되지 않은 상태다(밑에 있는 문제해결도 실제 배포할 때는 적용하지 않았다). ( -> ++ 3/19(수) : 해결 )

문제 임시 해결

해결 방안

결국, mounted 상태를 사용하여 클라이언트가 완전히 로드될 때까지 렌더링을 지연시키는 방법으로 문제를 임시로 해결(?)해 봤다. 이 방법은 서버와 클라이언트 간의 테마 불일치 문제를 해결하여 Hydration 경고가 뜨지 않게 만들어 준다.

"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
import { ReactNode, useEffect, useState } from "react";

const queryClient = new QueryClient();

export function Providers({ children }: { children: ReactNode }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted)
    return (
      <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--gray-cool)]">
        <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[var(--gold)] mb-4"></div>
        <p className="text-[var(--gray-1)]">Loading...</p>
      </div>
    );

  return (
    <ThemeProvider attribute="class" defaultTheme="dark">
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </ThemeProvider>
  );
}

해결 원리 순서

1. mounted 상태 사용

mounted 상태는 클라이언트에서 컴포넌트가 마운트되었는지 여부를 나타낸다.

useEffect는 클라이언트에서만 실행되므로, mounted가 true로 설정되면 클라이언트가 완전히 로드된 상태임을 의미한다.

if (!mounted) return null;을 통해 클라이언트가 완전히 로드되기 전에는 아무것도 렌더링하지 않는다.

다만 아무것도 렌더링 하지 않을 동안은 흰화면이 떠서 나의 경우는 아래와 같이 로딩처리를 해주었다. 그리고 이부분이 솔직히 아쉽게 느껴졌다. 로딩이 필요하기 때문에 UX 측면에서도 좋지 않고 되려 성능이 저하 된 듯 싶었다. 특히 이렇게 클라이언트가 로드 될 때까지 기다리게 되면 SEO 측면에서도 좋지 않다고 생각이 됐다 (-> ++ 3/19(수) : SEO에는 문제 없는 듯 하다).

  if (!mounted)
    return (
      <div className="flex flex-col items-center justify-center min-h-screen bg-[var(--gray-cool)]">
        <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[var(--gold)] mb-4"></div>
        <p className="text-[var(--gray-1)]">Loading...</p>
      </div>
    );
if (!mounted) return null;리턴에 로딩처리

2. 테마 동기화됨

클라이언트가 완전히 로드된 후에만 ThemeProvider가 렌더링되므로, 서버와 클라이언트의 테마가 일치하게 된다.

이로 인해 data-theme 및 style 속성의 불일치가 발생하지 않는다.

3. Hydration 경고 안나옴

React는 서버와 클라이언트의 초기 렌더링이 일치하지 않으면 경고를 표시한다.

mounted 상태를 사용해 클라이언트가 완전히 로드될 때까지 렌더링을 지연시킴으로써, 서버와 클라이언트의 초기 상태가 일치하게 된다.

따라서 Hydration 경고가 발생하지 않는다.

최종 결과

mounted 상태를 사용하여 클라이언트가 완전히 로드될 때까지 렌더링을 지연시킴으로써, Hydration 경고가 사라졌다.

근데 솔직히 말해 mounted 상태를 사용한 방법은 Hydration 경고를 해결하긴 했지만, 사용자 경험과 코드의 완성도 측면에서 완벽하지는 않다고 느낀다. 특히, 로딩 화면의 필요성과 한계때문에 임시적인 해결책이라고 생각한다.

시간이 된다면 더 나은 해결책을 찾아야 할 거 같다.

그래서 이 방법을 일단은 배포할 때는 빼두었다.

++ 3/19(수) 변경사항

Hydration 관련 경고가 뜨는 문제는 suppressHydrationWarning 속성을 추가해서 일단 보이지 않게 처리했다. 이 속성이 한 depth에서만 유효하다고 해서 혹시 몰라 <html> 태그에도 적용했더니 경고 메시지가 사라졌다.

튜터님께 여쭤보니, 이 방법이 그나마 가장 적절하다고 하셨다. mounted 상태를 활용하는 방법도 있지만, 그렇게 하면 결국 모든 곳에서 CSR을 사용하게 되어 Next.js를 쓰는 의미가 사라진다는 이유였다.

이 문제는 서버에서 첫 렌더링할 때 useTheme의 theme 값을 알 수 없기 때문에 undefined 상태가 되고, 이후 ThemeProvider가 렌더링되면서 theme 값이 설정되면서 서버와 클라이언트의 상태가 달라져 Hydration 에러가 발생하는 구조였다. 따라서 불가피하게 suppressHydrationWarning 속성을 추가해 경고를 숨기는 것이 더 나은 해결책이라고 했다. 게다가 공식 문서에서도 이 방법을 추천하고 있었다.

  <html lang="ko" suppressHydrationWarning>
      <body suppressHydrationWarning className="flex flex-col min-h-screen ">

0개의 댓글