Extra attributes from the server 에러

다크모드를 추가해둔 사이트에서 Extra attributes from the server: data-theme,style 에러가 발생했다
서버에서 렌더링된 html 파일과 다른 data-theme, style 속성이 있어 발생하는 에러다
suppressHydrationWarning
<html suppressHydrationWarning>
...
</html>
suppressHydrationWarning으로 hydration mismatch로 발생하는 에러를 무시하도록 설정할 수 있지만, 다른 원인으로 hydration mismatch가 일어났을 때도 경고를 무시할 수 있으므로 다른 방법을 찾아보고싶었다
enableColorScheme
<ThemeProvider
attribute='data-theme'
defaultTheme='dark'
enableColorScheme={false}
>
{children}
</ThemeProvider>
ThemeProvider에서 enableColorScheme을 fasle로 설정하면 next-themes가 동적으로 인라인 스타일을 추가하지 않는다
이 설정으로 hydration mismatch는 해결됐다
cookie 활용하기

지난번 다크모드 토글 버튼의 깜빡임을 개선하기 위해 마운트 전까지 skeleton ui를 렌더링하는 방식을 선택했었다.
개발을 하는 입장에서는 첫 렌더링은 서버에서 한 후, 클라이언트에서 사용자의 다크모드 선택 여부를 알 수 있으니 그 사이 로딩이 있다고 생각이 들면서도 사용자 입장에서 그 ui가 로딩되는 건 어색하게 느껴져 skeleton ui를 없애고싶었다.

서버에서 판단한 버튼의 텍스트가 클라이언트에서 렌더링될 때 다른 이유를 파악하기 위해 next-themes의 useTheme에서 theme을 확인했는데, 마운트 전까지는 undefined임을 발견했다
<button>
{theme === 'dark' ? '라이트모드' : '다크모드'}
</button>
theme이 dark인지 판단하여 버튼의 텍스트를 결정하고 있었기때문에 기본적으로 '다크모드'라고 렌더링되면서 깜빡임이 발생한 것이었다
서버에서부터 사용자가 마지막에 설정한 테마로 렌더링된 html을 받아올 수 있다면 hydration mismatch와 함께 내가 없애고싶었던 skeleton ui를 없앨 수 있겠다 싶었고, 클라이언트에 저장된 값을 서버로 어떻게 보낼 수 있을지 고민했다
이 사이트에 접속하면 페이지 로드를 위해 요청을 보내므로 그 때 전송된 쿠키를 활용해 theme을 바로 설정하는 방식으로 구현해보기로 했다
쿠키 사용을 위해 js-cookie 라이브러리를 사용했다
// layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const theme = cookies().get('theme')?.value || 'dark';
return (
<html
lang='ko'
data-theme={theme}
>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<NextThemesProvider>{children}</NextThemesProvider>
</body>
</html>
);
}
최상단 레이아웃에서 쿠키값을 받아와 data-theme을 설정한다
'use client';
import { useEffect } from 'react';
import Cookies from 'js-cookie';
import { ThemeProvider } from 'next-themes';
function NextThemesProvider({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
useEffect(() => {
const cookieTheme = Cookies.get('theme');
const localTheme = localStorage.getItem('theme');
if (!cookieTheme && localTheme) {
Cookies.set('theme', localTheme, { expires: 365 });
}
}, []);
return (
<ThemeProvider
attribute='data-theme'
defaultTheme='dark'
enableColorScheme={false}
>
{children}
</ThemeProvider>
);
}
export default NextThemesProvider;
cookie 만료 등의 이유로 localStorage와 cookie에 설정된 theme이 다른 경우 동기화시켜준다
import { cookies } from 'next/headers';
import ThemeToggleButton from './ThemeToggleButton';
function ThemeResolver() {
const theme = cookies().get('theme')?.value || 'dark';
return <ThemeToggleButton initialTheme={theme} />;
}
export default ThemeResolver;
서버컴포넌트에서 쿠키에 저장된 theme을 확인해 클라이언트 컴포넌트인 ThemeToggleButton으로 전달한다
'use client';
import { useTheme } from 'next-themes';
import Cookies from 'js-cookie';
type ThemeToggleProps = { initialTheme: string };
export default function ThemeToggle({ initialTheme }: ThemeToggleProps) {
const { theme, setTheme } = useTheme();
const toggleTheme = (): void => {
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
Cookies.set('theme', newTheme, { expires: 365 });
};
return (
<button
onClick={toggleTheme}
className={`w-[120px] h-[40px] flex items-center justify-center rounded-md transition ${
(theme ?? initialTheme) === 'dark'
? 'bg-gray-800 text-white hover:bg-gray-700'
: 'bg-gray-200 text-black hover:bg-gray-300'
}`}
>
{(theme ?? initialTheme) === 'dark' ? '라이트모드' : '다크모드'}
</button>
);
}
useTheme의 theme을 렌더링 조건에 활용하되 undefined인 경우에는 props로 받은 initialTheme을 사용하게 했다
버튼 이벤트에서도 쿠키에 저장된 theme을 업데이트 해준다

테마 즉각 반영 + 깜빡임 없음 + 스켈레톤 UI 없음
원했던 조건이 모두 반영됐다!
'개인 프로젝트 > 포트폴리오' 카테고리의 다른 글
Next.js로 포트폴리오 만들기 1 - next-themes로 다크모드 구현하기 (0) | 2025.03.04 |
---|