nextjs 13 버전에서 next-themes를 이용해 다크 모드를 구현하던 중 아래와 같은 에러가 발생했다.
현재 providers와 ThemeButton 컴포넌트의 코드는 다음과 같다.
// Providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
const Providers = ({ children }: { children: React.ReactNode }) => {
return <ThemeProvider attribute="class">{children}</ThemeProvider>
}
export default Providers
// ThemeButton.tsx
'use client'
import { useTheme } from 'next-themes'
const ThemeButton = () => {
const { theme, setTheme } = useTheme()
return (
<button
className="fixed bottom-2 right-2 rounded-md border p-2 text-xs"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
테마 버튼
</button>
)
}
export default ThemeButton
server에서 첫 렌더링 시 useTheme의 theme는 서버에서 알 수 없는 상태이기 때문에 undefined의 상태이고 ThemeProvider가 렌더링 되면 theme의 상태가 존재하므로 server와 client의 구조가 달라지게 되어 hydration 에러가 발생하는 것이었다.
Next.js는 클라이언트에게 웹 페이지를 보내기 전에 Server side에서 Pre-Rendering을 한다.
그리고 Pre-Rendering으로 인해 생성된 HTML 문서를 클라이언트에게 전송한다. 이 때의 문서는 단지 웹 화면만 보여주는 HTML일 뿐이고, 자바스크립트 요소들이 등록되지 않은 상태이다.
이후에 리액트가 번들링 된 자바스크립트 코드들을 클라이언트에게 전송한다.
이 자바스크립트 코드들이 이전에 보내진 HTML DOM 요소 위에 한번 더 렌더링을 하면서 매칭이 된다. 이 과정을 hydration이라 한다.
suppressHydrationWarning의 값을 true로 설정하면 hydration 에러가 더 이상 발생하지 않는다는 글을 보았는데, 이와 같은 경우 hydration 에러를 무시하는 것과 다름이 없음으로 다른 방법을 찾아보았다.
Providers 컴포넌트에 현재 마운트 상태를 확인하는 상태 isMount를 false로 초기화하고 isMount가 false 일 때는 null을 리턴하고, true 일 때는 ThemeProvider를 리턴하도록 작성하면 해당 에러가 사라지는 것을 확인할 수 있다.
'use client'
import { useEffect, useState } from 'react'
import { ThemeProvider } from 'next-themes'
const Providers = ({ children }: { children: React.ReactNode }) => {
const [isMount, setMount] = useState(false)
useEffect(() => {
setMount(true)
}, [])
if (!isMount) {
return null
}
return <ThemeProvider attribute="class">{children}</ThemeProvider>
}
export default Providers