Next.js를 사용해서 페이지 전체에 라이트모드/다크모드를 구현하려고 하던 중, 콘솔 창에 경고가 발생했다. 기능은 동작하지만, 경고를 살펴보고자 했다.
모드를 변경하는 토클은 next-themes을 사용하여 구현했고,
const { setTheme } = useTheme();
// ...
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
// ...
RootLayout이라는 상위 레이아웃에서 다음과 같이 ThemeProvider로 감싸서 모드를 적용하고자 했다.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
그러자 다음과 같은 에러가 발생했다.
Warning: Extra attributes from the server: class, style at html at RootLayout (Server) at RedirectErrorBoundary
Next.js는 SSR(Server-Side Rendering) 즉, 서버 사이드 렌더링 방식을 지원한다. 이는 서버 사이드(서버 측)에서 페이지를 미리 렌더링을 하고, 이 HTML 파일을 클라이언트로 전달하는 방식이다.
이 경고는 서버 사이드에서 받은 HTML 요소에 예상치 못한 추가 속성이 포함되어 있을 때 발생한다고 한다.
즉, 서버 사이드에서 렌더링 한 HTML과 클라이언트 사이드에서 렌더링한 HTML이 다를 때 이러한 경고가 발생하게 되는 것이다.
특히 여기선 "class"와 "style" 속성이 추가로 포함된 것 같다고 알려주고 있는데, 서버에서 처음 렌더링 될 때의 theme의 상태와, ThemeProvider가 렌더링 된 후의 theme의 상태가 다르기 때문에 발생한 것이다.
문제를 해결하기 위해 검색해본 결과, 다음과 같은 속성을 적용하는 해결 방법을 찾아볼 수 있었다.
suppressHydrationWarning: Next.js에서 서버 사이드 렌더링과 클라이언트 사이드 하이드레이션(hydration) 간의 미묘한 차이로 인해 발생할 수 있는 경고를 억제하는 데 사용되는 속성이다.
찾아본 방법 대로 RootLayout의 html 태그에 해당 속성을 적용하여 경고 메시지를 억제할 수 있었다.
<html lang="en" suppressHydrationWarning={true}>
그렇다면 하이드레이션이란 무엇일까?
서버 사이드 렌더링된 HTML과 클라이언트 사이드에서 React가 관리하는 DOM을 결합하는 과정이다.
하이드레이션의 과정은 다음과 같다.
- 서버 사이드 렌더링(SSR): 서버에서 HTML 생성하고, 클라이언트에 전송하는 과정
- 클라이언트 사이드 하이드레이션: 클라이언트 측에서 React가 이 HTML을 받아서 DOM을 분석하고 이를 기반으로 Virtual DOM을 생성하고, 이벤트 핸들러 추가 등 작업 수행하는 과정
하이드레이션은 완전한 HTML을 서버에서 미리 생성하기 때문에 SEO에 친화적이고, 초기 로딩 속도를 줄여, 사용자에게 빠르게 웹페이지를 제공할 수 있다는 장점이 있다.
하지만 하이드레이션과 관련된 문제가 발생할 수 있다고 한다.
하이드레이션 과정에서 서버에서 렌더링된 HTML과 클라이언트에서 렌더링된 HTML이 일치하지 않으면 경고 메시지가 발생할 수 있는데, 이를 불일치 경고라고 한다.
이러한 불일치 경고를 해결하는 다른 방법도 있었다.
경고를 없애기 위해 다음과 같이 ThemeProvider를 리턴하기 전에 useEffect를 사용하여 클라이언트 측에서 렌더링되었는지 확인하는 과정을 추가하는 방법이다.
useEffect 훅은 클라이언트 측에서 컴포넌트가 마운트된 후에 실행되므로, 이를 사용하여 클라이언트 사이드 렌더링 여부를 확인하여 마운트가 된 이후에만 리턴하도록 해준다.
마운트 단계: 컴포넌트 생성, 초기화, 렌더링, DOM 삽입, 마운트 완료(컴포넌트가 DOM에 삽입된 후 'componentDidMount'나 'useEffect' 훅이 호출된다.)
클라이언트 측에서 렌더링이 완료된 이후에만 ThemeProvider를 반환하기 때문에, 서버 측 렌더링과 클라이언트 측에서 렌더링한 HTML 파일이 같게 되고(theme이 undefined), 렌더링 이후에만 theme의 상태가 존재하게 되기 때문에 경고가 발생하지 않게 된다.
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [isMount, setIsMount] = React.useState(false);
React.useEffect(() => {
setIsMount(true);
}, []);
if (!isMount) {
return null;
}
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
참고로 이 방법은 하드 리프레시(hard refresh)를 할 때마다 흰색 화면이 깜빡이는 현상(white flash)을 발생시킬 수 있다고 한다.
결국, 문제를 해결하기 위해서는 서버와 클라이언트 간의 불일치를 해결하는 것이 근본적인 방법인데, 이러한 해결 방법들은 불일치 경고를 억제하기만을 위한 방법이기 때문에 근본적인 해결 방법이라고 보기는 어려운 것 같다.