Next.js 16과 Tailwind CSS에서 다크/라이트 테마 깜빡임(FOUC) 없이 구현하기
Next.js 16과 Tailwind CSS에서 다크/라이트 테마 깜빡임(FOUC) 없이 구현하기
웹사이트를 개발할 때 다크 모드(Dark Mode)는 이제 선택이 아닌 필수가 되었습니다. 특히 개발 관련 글이나 긴 코드를 읽는 개발자 블로그에서는 눈의 피로를 덜어주는 어두운 테마에 대한 선호도가 매우 높습니다.
하지만 서버 사이드 렌더링(SSR)을 기본으로 제공하는 Next.js 환경에서 다크 모드를 잘못 구현하면, **페이지를 새로고침할 때마다 하얀 화면이 순간적으로 번쩍거리는 현상(FOUC: Flash of Unstyled Content)**이 발생합니다.
이번 글에서는 외부 무거운 서드파티 라이브러리 없이, 순수 React Context와 인라인 부트스트랩 스크립트, 그리고 Tailwind CSS를 조합하여 새로고침 깜빡임이 전혀 없는 100% 깔끔한 다크/라이트 테마 시스템을 구축한 과정을 공유합니다.
1. 다크 모드 구현 시 마주치는 깜빡임(FOUC)의 원인
Next.js의 App Router는 기본적으로 서버에서 HTML을 먼저 생성하여 브라우저로 내려보냅니다.
- 서버 사이드 렌더링 단계: 서버는 방문자의 로컬 스토리지(
localStorage)에 접근할 수 없기 때문에, 기본값(보통 라이트 모드)으로 HTML을 렌더링합니다. - 브라우저 로드 단계: 브라우저가 화면을 라이트 모드로 먼저 그립니다.
- 클라이언트 하이드레이션 단계: React 코드가 실행되면서
useEffect안에서localStorage의 테마를 확인하고, 다크 모드 클래스(.dark)를<html>태그에 추가합니다.
결과적으로, 브라우저는 **[라이트 모드 화면 출력] -> [0.1초 후 다크 모드로 변경]**의 과정을 거치게 되며, 이 순간 유저의 눈에는 불쾌한 화면 번쩍임이 고스란히 노출됩니다.
2. 해결 전략: 렌더링 블로킹 인라인 부트스트랩 스크립트
이 문제를 해결하는 가장 확실한 방법은, 브라우저가 첫 픽셀을 화면에 그리기(Paint) 전에 <html> 요소에 .dark 클래스를 확정 짓는 것입니다.
브라우저는 HTML 문서의 <head> 안에 위치한 동기식(Inline) <script> 태그를 만나면, 해당 스크립트의 실행이 끝날 때까지 렌더링 파이프라인을 잠시 멈춥니다(Render-blocking). 이 특성을 역이용하면 깜빡임을 완전히 없앨 수 있습니다.
인라인 스크립트 코드 (src/app/layout.tsx)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `(function(){try{
var t = localStorage.getItem('theme');
var d = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (t === 'dark' || (!t && d) || (!t)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}catch(e){}})()`,
}}
/>
</head>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
localStorage에 저장된 테마 값이 있다면 최우선으로 반영합니다.- 저장된 값이 없다면 OS 시스템 설정(
prefers-color-scheme: dark)을 감지합니다. <html>태그에suppressHydrationWarning을 추가하여, 서버 렌더링된 클래스와 클라이언트 첫 렌더 클래스 간의 불일치 경고를 방지합니다.
3. 전역 테마 상태 관리 (ThemeProvider)
클라이언트 런타임에서는 React 컴포넌트들이 언제든 테마를 변경하고 현재 상태를 구독할 수 있어야 합니다.
// src/components/theme-provider.tsx
"use client";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light";
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>("dark");
useEffect(() => {
const isDark = document.documentElement.classList.contains("dark");
setThemeState(isDark ? "dark" : "light");
}, []);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem("theme", newTheme);
if (newTheme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
4. 부드러운 회전 인터랙션 토글 버튼 (ThemeToggle)
테마 전환 버튼은 시각적인 피드백이 가장 중요한 UI 요소 중 하나입니다. 해(Sun)와 달(Moon) 아이콘이 360도 회전하며 교차 페이드아웃되는 인터랙션을 구현했습니다.
// src/components/theme-toggle.tsx
"use client";
import { useTheme } from "./theme-provider";
import { Sun, Moon } from "lucide-react";
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
aria-label="테마 전환"
className="relative flex size-9 items-center justify-center rounded-lg border border-border/80 bg-background/80 hover:bg-accent transition-colors"
>
<Sun className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0 text-amber-500" />
<Moon className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 text-indigo-400" />
</button>
);
}
5. 도입 후 얻은 결론과 장점
- 완벽한 제로 플리커(Zero Flicker): 네트워크 속도가 느리거나 저사양 기기에서도 새로고침 시 화면 깜빡임이 완전히 사라졌습니다.
- 번들 사이즈 최적화:
next-themes같은 무거운 외부 패키지에 의존하지 않고 단 40줄의 순수 코드로 구현하여 번들 오버헤드를 0으로 유지했습니다. - 시스템 테마 완벽 연동: 사용자가 OS 다크 모드를 켜거나 껐을 때의 기본값 또한 매끄럽게 처리되었습니다.
사용자 경험(UX)은 눈에 띄는 화려한 기능뿐만 아니라, 이러한 디테일한 깜빡임 방지에서 결정된다는 점을 다시 한번 체감할 수 있었습니다.
클릭할 때마다 작가에게 따뜻한 응원이 전달됩니다 ❤️
함께 읽으면 좋은 다른 개발일지
'개발' 및 추천 아카이브 글 모음