next-themes 를 이용하여 Dark Mode 적용하기

오늘은 TODO List 에 작성한 목록 중 하나인 시스템 모드에 따라 다크모드/라이트모드 전환 을 적용해보려고 합니다.

Frontend
Next.js, Theming, UI/UX

My Blog Mode

오늘은 TODO List 에 작성한 목록 중 하나인 시스템 모드에 따라 다크모드/라이트모드 전환 을 적용해보려고 합니다.

현재 제 블로그는 다크/라이트 모드 토글 기능 및 Tailwind CSS class 방식 구현이 된 상태이나, localStorage 저장 (새로고침 시 초기상태로 복원) 및 시스템 설정 감지가 없는 상태입니다. (OS 설정 무시)

이에 따라 next-themes 를 이용하여 사용자의 OS에 설정한 다크모드를 자동으로 감지하여 보여주는 방식으로 진행하려고 합니다.

next-themes는 Next.js 전용으로 최적화된 테마 관리 라이브러리로, 시스템 설정 감지, 다크/라이트 모드 전환, 페이지 로딩 시 깜빡임 방지 등을 자동으로 처리합니다.

🤔 잠깐, next-themes 라이브러리, 정말 필요할까?

next-themes를 사용하면 좋은 경우:

  1. 시스템 설정 감지가 필요한 경우
    • 사용자가 OS에 설정한 다크모드를 자동으로 감지
    • 처음 방문 시 사용자 시스템 설정 존중
  2. UX 개선이 필요한 경우
    • 새로고침해도 사용자가 선택한 테마 유지
    • 탭 간 테마 동기화 (여러 탭 열었을 때 모두 같은 테마)
    • 플래시 없음 (페이지 로드 시 깜빡임 방지)
  3. 엔터프라이즈 애플리케이션
    • 복잡한 테마 전환 로직
    • 다중 테마 지원
    • SSR/SSG 호환성

직접 구현해도 되는 경우:

  • 간단한 다크모드만 필요
  • 시스템 설정은 무시해도 괜찮음
  • 새로고침 후 초기값으로 돌아가도 괜찮음

next-themes 적용하기

1. 설치하기

npm install next-themes

혹은

yarn add next-themes

2. 레이아웃에 ThemeProvider 설정

// app/layout.tsx
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import "./globals.css";
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" enableSystem>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

저는 tailwind 를 통해 class로 이미 구성하고 있었기에 attribute="class" 를 작성하였습니다.

기본 값은 <html data-theme="dark"> 로 구성되게 합니다.

[data-theme='dark'] {
  --background: black;
  --foreground: white;
}

이런식으로 CSS-in-JS 라이브러리 사용하거나 여러 테마를 지원할 때에는 기본값으로 작성해주시면 됩니다.

3. Header.tsx 수정하기 (다크/라이트모드 토글기능)

// 기존 Header 코드
const Header = () => {
  const [isDark, setIsDark] = useState(false);
  const [isVisible, setIsVisible] = useState(true);
  const [lastScrollY, setLastScrollY] = useState(0);
 
  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  }, [isDark]);
  
  ...
  return (
	  ...
	  <button
	      className="cursor-pointer rounded-full px-4 py-[10px] hover:bg-slate-100 dark:hover:bg-[#11161b]"
	      aria-label={isDark ? "라이트 모드로 변경" : "다크 모드로 변경"}
	      title={isDark ? "라이트 모드" : "다크 모드"}
	      onClick={() => setIsDark((prev) => !prev)}
	    >
	      {isDark ? (
	        <LightModeIcon className="size-[24px]" />
	      ) : (
	        <DarkModeIcon className="size-[24px]" />
	      )}
     </button>
     ...
    );

기존에는 useStateuseEffect 를 사용하여 직접 dark 클래스를 추가/제거 해주었습니다.

next-theme 에서 useTheme 훅을 지원해주기 때문에 useState , useEffect 를 제거해주고 다음과 같이 작성해줍니다.

const { resolvedTheme, setTheme } = useTheme();
 
{resolvedTheme === 'dark' ? (
  <LightModeIcon className="size-[24px]" />
) : (
  <DarkModeIcon className="size-[24px]" />
)}

Hydration 방지

const [mounted, setMounted] = useState(false);
 
useEffect(() => {
  setMounted(true);
}, []);
 
// 서버와 클라이언트 렌더링이 일치할 때까지 기다리기
if (!mounted) {
  return null;
} 

이제는 사용자의 시스템 설정에 맞추어 홈페이지가 자동으로 다크모드 / 라이트모드 토글이 가능해졌습니다.

저는 원래 다크모드 / 라이트모드 같은 테마 전환에 대한 니즈가 있었어서 프로젝트 초반부터 dark 모드를 염두 해두었었기에 쉽게 적용 할 수 있었던것 같아요.

다크모드를 진행하면서 어떻게 해야 사용자에게 위화감 없이 보여주게 될까 라는 생각도 듭니다.
아직 UX에 대한 지식이 부족한터라 다른 사이트들을 많이 참고하며 공부를 해보아야겠어요.


https://github.com/pacocoursey/next-themes

관련 글