frontend/react

Next.js hydration error #418 원인과 해결 방법 정리

mirabo01 2026. 3. 23. 16:34

Next.js 프로젝트를 운영하다 보면 배포 후 브라우저 콘솔에서 아래와 같은 오류를 보게 되는 경우가 있습니다.

Uncaught Error: Minified React error #418

개발 환경에서는 잘 보이지 않다가, 운영 환경이나 특정 서버 배포 환경에서만 나타나는 경우도 있어서 더 당황스럽습니다. 특히 로컬에서는 정상인데 배포 서버에서만 발생하면 원인을 찾기가 더 어렵습니다.

이 글에서는 Next.js에서 hydration error #418이 왜 발생하는지, 어떤 상황에서 자주 나타나는지, 그리고 실무에서 어떻게 점검하고 해결하면 되는지를 정리해보겠습니다.


hydration error #418은 무엇을 의미할까

이 오류는 보통 서버에서 렌더링된 HTML과 클라이언트에서 React가 다시 그린 결과가 서로 다를 때 발생합니다.

즉, 서버는 A라는 화면 구조를 내려줬는데 브라우저에서 React가 다시 붙으면서 B라는 구조가 나오면, React는 “초기 렌더링 결과가 일치하지 않는다”고 판단합니다. 이를 hydration mismatch라고 부릅니다.

운영 빌드에서는 React가 축약된 에러 번호 형태로 보여주기 때문에, 콘솔에는 #418처럼 짧게 표시되는 경우가 많습니다.


왜 Next.js에서 자주 발생할까

Next.js는 서버 렌더링(SSR)과 클라이언트 렌더링이 함께 동작하기 때문에, 같은 컴포넌트라도 서버에서 한 번, 브라우저에서 다시 한 번 렌더링됩니다.

이 과정에서 서버와 클라이언트가 서로 다른 값을 사용하면 hydration 오류가 생길 수 있습니다.

대표적으로 이런 경우가 많습니다.

  • 렌더링 중 window, document, localStorage 같은 브라우저 전용 객체를 직접 참조한 경우
  • Date.now(), new Date(), Math.random()처럼 렌더링마다 값이 달라지는 값을 바로 출력한 경우
  • 서버와 클라이언트에서 조건 분기가 다르게 동작한 경우
  • API 응답 전후로 초기 UI 구조가 달라진 경우
  • 외부 라이브러리가 SSR을 제대로 지원하지 않는 경우

가장 흔한 원인 1: 브라우저 객체를 바로 사용하는 경우

아래처럼 컴포넌트 렌더링 중에 브라우저 객체를 직접 사용하면 문제가 생기기 쉽습니다.

const theme = localStorage.getItem('theme');
return <div>{theme}</div>;

이 코드는 브라우저에서는 동작할 수 있지만, 서버에서는 localStorage가 존재하지 않습니다. 그래서 서버 렌더링 결과와 브라우저 렌더링 결과가 달라질 수 있습니다.

해결 방법

브라우저 전용 값은 렌더링 단계가 아니라 useEffect 안에서 처리하는 것이 안전합니다.

import { useEffect, useState } from 'react';

export default function ThemeBox() {
  const [theme, setTheme] = useState('');

  useEffect(() => {
    const savedTheme = localStorage.getItem('theme') || 'light';
    setTheme(savedTheme);
  }, []);

  return <div>{theme}</div>;
}

이렇게 하면 서버에서는 빈 값으로 시작하고, 브라우저에서 마운트된 뒤 실제 값을 읽어오게 됩니다.


가장 흔한 원인 2: 렌더링 시점마다 값이 바뀌는 경우

다음처럼 Math.random()이나 현재 시간을 바로 렌더링하면 서버와 클라이언트 결과가 다를 수 있습니다.

export default function RandomBox() {
  return <div>{Math.random()}</div>;
}

서버가 만든 숫자와 브라우저에서 다시 계산한 숫자가 같을 가능성은 사실상 없기 때문에 hydration mismatch가 발생할 수 있습니다.

해결 방법

이런 값은 렌더링 단계에서 직접 만들지 말고, 마운트 후 상태로 설정하거나 서버에서 미리 확정한 값을 props로 내려주는 편이 좋습니다.

import { useEffect, useState } from 'react';

export default function RandomBox() {
  const [value, setValue] = useState('');

  useEffect(() => {
    setValue(String(Math.random()));
  }, []);

  return <div>{value}</div>;
}

가장 흔한 원인 3: 조건 분기가 서버와 클라이언트에서 다른 경우

아래와 같은 패턴도 자주 문제를 만듭니다.

return (
  <div>
    {typeof window !== 'undefined' ? 'client' : 'server'}
  </div>
);

이 코드는 의도는 이해되지만, 서버에서는 server, 브라우저에서는 client가 렌더링되므로 초기 결과가 달라질 수 있습니다.

해결 방법

초기 렌더링에서는 동일한 UI를 보여주고, 마운트 이후에만 클라이언트 전용 상태를 반영하는 편이 낫습니다.

import { useEffect, useState } from 'react';

export default function ClientOnlyText() {
  const [mounted, setMounted] = useState(false);

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

  if (!mounted) return null;

  return <div>client</div>;
}

가장 흔한 원인 4: SSR 미지원 라이브러리 사용

일부 차트 라이브러리, 에디터, 브라우저 전용 UI 라이브러리는 서버 렌더링 환경과 맞지 않아 hydration 문제를 일으킬 수 있습니다.

이런 경우에는 해당 컴포넌트를 클라이언트 전용으로 분리하는 방법이 자주 사용됩니다.

import dynamic from 'next/dynamic';

const ClientOnlyEditor = dynamic(() => import('./Editor'), {
  ssr: false,
});

export default function Page() {
  return <ClientOnlyEditor />;
}

이 방식은 SSR 이점을 일부 포기하는 대신, 브라우저 전용 라이브러리로 인한 mismatch를 줄이는 데 효과적입니다.


운영 서버에서만 발생하는 이유는 무엇일까

로컬에서는 안 보이는데 배포 서버에서만 보이는 경우도 많습니다. 이런 차이는 보통 아래 이유에서 발생합니다.

  • 운영 빌드에서 최적화가 더 강하게 적용됨
  • 서버 환경 변수나 API 응답이 로컬과 다름
  • 배포 환경의 시간대, 지역, 데이터 상태가 다름
  • 개발 모드에서는 눈에 띄지 않던 mismatch가 운영에서 드러남

특히 서버 시간, 사용자 언어 설정, 쿠키, 로그인 상태, 환경 변수 차이 때문에 같은 코드라도 결과가 달라질 수 있습니다.


실무에서 점검할 순서

hydration error가 뜨면 아래 순서대로 보면 비교적 빠르게 범위를 좁힐 수 있습니다.

  1. 에러가 나는 페이지와 컴포넌트를 먼저 좁힌다
  2. 렌더링 중 window, document, localStorage 사용 여부 확인
  3. Date.now(), Math.random(), new Date() 직접 출력 여부 확인
  4. 서버와 클라이언트에서 다른 조건 분기가 있는지 확인
  5. 최근 추가한 외부 라이브러리가 SSR 호환인지 확인
  6. 필요하면 문제 컴포넌트를 dynamic(..., { ssr: false })로 분리해 재현 여부 확인

즉, 처음부터 전체 프로젝트를 다 뒤지기보다 “렌더링 시점 차이”를 만드는 코드가 어디 있는지부터 찾는 게 핵심입니다.


이런 식으로 고치면 안 되는 경우도 있다

hydration error를 피하려고 모든 컴포넌트를 무조건 ssr: false로 바꾸는 경우가 있는데, 이건 보통 좋은 해결은 아닙니다.

왜냐하면 이 방식은 문제를 숨길 수는 있어도, Next.js의 SSR 장점을 크게 줄일 수 있기 때문입니다.

가장 좋은 순서는 보통 이렇습니다.

  • 먼저 mismatch 원인을 찾는다
  • 브라우저 전용 로직을 useEffect로 이동한다
  • 정말 SSR이 어려운 컴포넌트만 클라이언트 전용으로 분리한다

한 줄 정리

Next.js hydration error #418은 대부분 서버에서 만든 HTML과 브라우저에서 다시 렌더링한 결과가 다를 때 발생합니다.

  • 브라우저 전용 객체를 렌더링 중 바로 사용했는지
  • 시간·랜덤 값처럼 매번 달라지는 값을 직접 출력했는지
  • 조건 분기가 서버와 클라이언트에서 다르게 동작하는지
  • SSR 미지원 라이브러리를 그대로 사용했는지

이 네 가지를 먼저 보면 대부분 원인을 좁힐 수 있습니다.

실무에서는 “왜 로컬에서는 멀쩡한데 운영에서만 깨질까”가 가장 답답한데, 결국 핵심은 서버와 클라이언트가 같은 첫 화면을 그리고 있는지를 확인하는 것입니다.