Next.js의 서버 사이드 렌더링은 속도가 빠르다는 장점이 있다. 하지만 다크 모드와 같이 user preference를 가져와야 하는 기능의 경우 적용이 까다로울 수 있다. Next.js로 만든 우리 블로그에 다크모드 적용하고 토글 기능 추가하는 방법을 소개한다...!!!
next-themes
다크 모드를 적용할 때 사용자의 시스템 설정을 가져와 브라우저의 localStorage에 저장하는데, 서버 컴포넌트에서 이것을 읽을 수 없기 때문에 문제가 발생한다. 사이트가 로드되는 동안 적용되었던 theme과 로드되고 나서 theme이 달라 화면이 순간적으로 깜빡이는 현상이 생길 수 있다. Next.js에서는 이러한 문제를 피할 수 있도록 'next-themes' 패키지를 제공한다. 터미널에 다음을 입력해 next themes 패키지를 설치한다.
npm i next-themes
next-themes에서 제공하는 ThemeProvider를 앱 전체에 적용하기 위해 다음과 같은 과정을 따른다.
app 폴더에 다음과 같은 파일을 추가한다. 브라우저 시스템 설정을 이용하는 클라이언트 컴포넌트이므로 파일 맨 앞에 'use client'를 추가해 클라이언트에서 실행되도록 명시한다.
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }){
return <ThemeProvider attribute="class" defaultTheme='system' enableSystem>{children}</ThemeProvider>
}
이 컴포넌트를 다음과 같이 루트 layout.tsx의 <body>
밑에 추가한다.
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<div className="text-gray-800 dark:text-slate-100 dark:bg-gray-900"><Providers>
<Header />
<main>{children}</main>
</Providers></div>
</body>
</html>
);
}
suppressHydrationWarning
위 코드의 <html>
태그 안에 suppressHydrationWarning
prop이 추가되어 있는 것을 볼 수 있다. React에서 hydration 과정 중에 서버에서 렌더링해 클라이언트에 보낸 HTML과 클라이언트에서 렌더링한 결과에 차이가 있으면 hydration warning이 발생한다. 이를 오버라이드해야 하는 경우에 suppressHydrationWarning
을 사용한다. 이 prop을 true로 설정하면 이 prop이 추가된 태그 내부의 차이에 대해서 hydration warning을 띄우지 않게 된다.
TailwindCSS config 파일 수정
tailwindCSS에서 제공하는 다크모드를 이용하려면 다음 예시와 같이 tailwind.config.ts 파일을 수정하여야 한다.
import type { Config } from "tailwindcss";
const config: Config = {
// ...
darkMode: "class",
// ...
다크 모드를 적용하고자 하는 html 요소의 tailwind class에 다음과 같이 dark:
를 붙여서 사용한다.
className="container mb-r p-4 hover:bg-gradient-to-r from-gray-100 to-gray-200 hover:dark:bg-gradient-to-r dark:from-slate-900 dark:to-slate-800"
useTheme
app/components에 토글 기능을 수행하는 컴포넌트를 추가한다.
'use client'
import React from "react";
import { useTheme } from "next-themes";
import { FiSun, FiMoon } from "react-icons/fi"
import Image from "next/image"
export default function ThemeSwitch() {
const [mounted, setMounted] = React.useState(false);
const { setTheme, resolvedTheme } = useTheme();
React.useEffect(() => setMounted(true), []);
if (!mounted)
return (
<Image
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMCIgaGVpZ2h0PSIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxyZWN0IHdpZHRoPSIwIiBoZWlnaHQ9IjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0ibm9uZSIvPgo8L3N2Zz4="
width={36}
height={36}
sizes="36x36"
alt="Loading Light/Dark Toggle"
priority={false}
title="Loading Light/Dark Toggle"
/>
);
if (resolvedTheme === "dark") {
return <FiSun onClick={() => setTheme("light")}/>;
}
if (resolvedTheme === "light") {
return <FiMoon onClick={() => setTheme("dark")} />;
}
}
컴포넌트가 클라이언트 사이드에서 mount되면 mounted를 true로 바꾸도록 한다. 그 전까지는 아무것도 띄우지 않도록 토글 스위치와 크기가 같은 투명 이미지를 보여준다.
결론
이렇게 Next.js에서 제공하는 next-themes 패키지를 이용해 블로그에 다크모드 기능을 추가해보았다. 서버 사이드 렌더링과 클라이언트 사이드 렌더링을 구분하고 각 과정이 어떻게 이루어지는지 이해하기 위해 적용해보기 좋은 예시인 것 같다.