Develop

Next.js에 Tailwind CSS를 설치하고 다크모드를 설정하는 방법

Next.js App Router 프로젝트에 Tailwind CSS를 설치하고 class 전략으로 다크모드를 구현하는 방법을 정리했습니다. next-themes로 시스템 설정 연동과 토글 구현까지 다룹니다.

Next.jsTailwind CSS다크모드App Routerfrontend
Next.js 프로젝트에 Tailwind CSS와 다크모드가 적용된 화면 — 라이트/다크 토글 버튼과 배경색이 전환되는 모습
  • ·Tailwind CSS v4: PostCSS 플러그인 방식에서 Vite/Next.js 전용 플러그인 방식으로 변경
  • ·darkMode: 'class' — html 태그에 dark 클래스 추가/제거로 다크모드 전환
  • ·next-themes: SSR hydration mismatch 없이 Next.js에서 테마를 관리하는 라이브러리
  • ·dark: 접두사 — Tailwind에서 다크모드 전용 스타일을 지정하는 방법
Next.js 블로그에 다크모드를 붙이면서 Tailwind의 darkMode: 'class' 방식을 처음 써봤습니다. 처음엔 직접 localStorage로 테마를 관리하려다가 SSR 환경에서 hydration mismatch가 계속 나서 결국 next-themes를 쓰게 됐죠. ThemeProvider를 layout.tsx에 추가하는 것만으로 해결됐고, 그 이후로 다크모드 관련 Next.js 프로젝트에서는 next-themes를 쓰지 않을 이유가 없다고 생각하게 됐습니다.

1. 현대적인 UI 스타일링과 다크모드

Tailwind CSS v4 전용 플러그인 기반 Next.js 설치 절차

Next.js App Router 프로젝트에 Tailwind CSS를 붙일 때 가장 먼저 알아두어야 할 최신 트렌드는 v4 버전부터 PostCSS 설정 파일을 구구절절 생성하고 연동하던 기존 아키텍처가 완전히 역사의 뒤안길로 사라졌다는 점입니다. v4부터는 자체 컴파일러 컴파일 방식의 전용 플러그인을 도입하여 복잡한 설정 단계가 극도로 심플해졌죠. 프로젝트를 백지 상태에서 새로 파는 경우라면 npx create-next-app@latest 명령어 실행 단계에서 Tailwind 옵션을 엔터 키로 선택하면 자동으로 설정되지만, 이미 돌아가고 있는 기존 코드베이스에 추가할 때는 tailwindcss@tailwindcss/postcss 패키지를 설치한 뒤 postcss.config.mjs에 플러그인을 태워주기만 하면 세팅이 끝납니다. 이후 globals.css 파일 최상단에 @import 'tailwindcss'; 한 줄만 멋지게 박아넣는 것이 전부죠. 이렇게 심플하게 연결해 두면 컴포넌트 파일 내에서 곧장 bg-slate-900이나 text-white 같은 강력한 스타일 클래스 유틸리티를 자유자재로 조립해 스타일을 구성할 수 있습니다.

Tailwind CSS 설정 파일로 Next.js 디자인 토큰 정의하기

Tailwind CSS의 진가는 제공되는 기본 팔레트만 대충 가져다 쓰는 것이 아니라, 팀의 브랜드 아이덴티티에 맞춰 디자인 토큰을 커스터마이징할 때 비로소 발휘됩니다. 프로젝트 루트의 설정 파일 내 theme.extend 블록을 활용하면 기존의 기본 반응형 클래스 규격을 깨뜨리지 않고 브랜드 전용 색상값(colors: { brand: '#FF5733' })이나 공용 폰트 정의를 안전하게 덧씌울 수 있습니다. 특히 Next.js에서 기본 제공하는 구글 폰트 주입 팩인 next/font와 연동하여 커스텀 CSS 변수를 테마 설정에 매핑해 두면, 영문과 한글 폰트가 꼬이지 않고 기기 사양에 맞춰 일관성 있게 렌더링되도록 폰트 환경을 아름답게 조율할 수 있죠. 한 가지 실무 삽질 주의점은, 조건문에 따라 클래스 이름을 동적으로 조합(className={text-${status}-500})해서 소스코드를 짜두면 Tailwind 컴파일러가 빌드 타임에 해당 텍스트를 인지하지 못해 CSS 산출물에서 아예 지워버려 디자인이 깨진다는 점입니다. 동적 스타일을 다룰 때는 반드시 온전한 클래스 명칭 전체를 코드상에 남겨두거나 safelist 설정을 타이트하게 기재해야 스타일 소실 버그를 예방할 수 있습니다.

2. 테마 전환 메커니즘과 상태 관리

Tailwind CSS darkMode class 설정 방식의 웹 테마 연동 원리

Tailwind CSS 환경에서 사용자의 눈 건강을 지키기 위한 다크모드를 설계하는 작동 기지는 크게 미디어 쿼리 감지(media) 방식과 클래스 바인딩(class) 방식 두 분류로 갈립니다. 미디어 쿼리는 윈도우 OS나 macOS의 운영체제 다크 테마 설정을 조용히 관찰해 자동으로 전환하므로 세팅이 쉽지만, 사용자가 웹 화면 내부에서 별도 토글 스위치 버튼을 눌러 능동적으로 테마를 바꿀 수 없다는 폐쇄성이 단점이죠. 그래서 대다수의 웹앱 서비스들은 루트 <html> 태그 단에 dark라는 전용 CSS 클래스 이름이 부착되었을 때만 하위 요소들의 다크 테마가 연동 활성화되는 darkMode: 'class' 스펙을 채택합니다. 이 클래스 전략을 켜두면, 마크업 파일 안에서 bg-white dark:bg-zinc-950 형식으로 접두사 분기 코드를 적어두는 것만으로 html 노드의 클래스 추가/제거 상태에 맞물려 브라우저 배경 컬러가 즉시 자연스럽게 다크 테마로 전환되어 유동적인 테마 제어가 가능해집니다.

next-themes 모듈을 이용한 SSR 플래시 현상 방지 기법

만약 React의 로컬 상태나 단순 자바스크립트 스크립트만 가지고 다크모드를 손수 개발하게 되면, Next.js의 고유 렌더링 스펙인 서버 사이드 렌더링(SSR) 단계에서 악명 높은 '플래시(Flash) 현상'과 직면하게 됩니다. 서버에서는 사용자가 라이트 모드인지 다크 모드인지 알 수 없어 기본 HTML을 렌더링해 내려주는데, 브라우저가 화면을 일단 띄운 다음 뒤늦게 로컬스토리지의 다크 상태 값을 파싱하여 CSS 클래스를 꽂아 넣기 때문이죠. 이 시간차 때문에 페이지를 새로고침할 때마다 화면이 찰나의 순간 흰색으로 쩍 번쩍거리는 눈부심 테러(Hydration Mismatch)가 유발됩니다. 이를 원천 차단해 주는 고마운 모듈이 바로 next-themes 라이브러리입니다. next-themes는 브라우저가 첫 픽셀을 렌더링하기 직전에 아주 얇고 빠른 최우선 인라인 자바스크립트 코드를 주입하여 캐시된 다크 클래스를 html 노드에 강제로 박아넣음으로써 화면 깜박임을 완전히 종식시켜 줍니다. 루트 layout.tsxThemeProvider만 클라이언트 컴포넌트로 한 번 감싸두면 OS 기본 설정 검출과 깜박임 방지 혜택을 손쉽게 세팅할 수 있습니다.

3. 실무형 컴포넌트 및 CSS 변수 통합

다크모드 토글 버튼 컴포넌트 마운트 에러 제어하기

useTheme 훅을 사용하는 컴포넌트는 반드시 클라이언트 컴포넌트여야 합니다. 파일 상단에 'use client'를 선언하고 next-themes에서 useTheme을 import하죠. resolvedTheme으로 현재 적용된 테마를 읽고 setTheme으로 전환합니다. 토글 버튼은 resolvedTheme이 'dark'면 라이트 모드로 전환하는 아이콘을, 아니면 다크 모드로 전환하는 아이콘을 표시하면 됩니다. 주의할 점은 컴포넌트가 마운트되기 전에는 resolvedTheme이 undefined일 수 있다는 점입니다. 마운트 전에 아이콘을 렌더링하면 hydration mismatch가 발생할 수 있죠. useState와 useEffect를 조합해서 마운트 후에만 아이콘을 표시하도록 처리하거나, mounted 상태가 false일 때 빈 div나 스켈레톤을 반환하는 방식으로 해결합니다.

'use client'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'

export function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme()
  const [mounted, setMounted] = useState(false)

  useEffect(() => setMounted(true), [])

  if (!mounted) return <div className="w-9 h-9" />

  return (
    <button
      onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
      className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
    >
      {resolvedTheme === 'dark' ? '☀️' : '🌙'}
    </button>
  )
}

Tailwind CSS와 시맨틱 CSS 변수를 조합한 디자인 테마 관리

화면에 뿌려지는 글자, 테두리, 배경, 버튼 등 모든 디자인 요소마다 dark:bg-slate-900 dark:text-zinc-100과 같이 접두사를 하나하나 수동으로 적다 보면 코드 줄이 비대해지고 정렬이 꼬여서 유지보수가 지옥으로 변합니다. 이 디자인 복잡도를 지혜롭게 다스리는 최고급 설계 기법이 바로 '시맨틱 CSS 변수(Variables)' 체계와의 통합입니다. globals.css의 기본 루트 :root--background: #ffffff를 선언하고 하위 .dark 셀렉터 하단에 --background: #09090b를 정의해 둡니다. 그리고 Tailwind 테마 컬러 항목에서 이 CSS 변수를 그대로 참조 매핑하게 클래스 이름(bg-background)을 박아두면, 컴포넌트에는 그냥 bg-background 클래스 한 줄만 가볍게 적었음에도 html에 dark 클래스가 탈부착됨에 맞춰 자동으로 테마 색이 자연스럽게 연동됩니다. 이 방식은 shadcn/ui 등 메이저 디자인 시스템 패키지들이 뼈대로 삼고 있는 정석 아키텍처이며, 색상 디자인 교체가 필요할 때 globals.css 파일 한 곳의 숫자만 고치면 서비스 전반의 색상 톤이 알아서 정렬되므로 확장성 면에서 압도적으로 든든합니다.

자주 묻는 질문

다크모드 전환 시 화면이 잠깐 흰색으로 깜박이는 문제가 있습니다.+

렌더링 도중 클라이언트 사이드에서 테마 판별을 뒤늦게 수행해 일어나는 전형적인 Hydration 플래시 현상입니다. 테마 데이터를 HTML 렌더링 시작 전에 먼저 가로채 적용해 주는 next-themes 패키지를 사용해 연동 구조를 개선하셔야 합니다.

테마 토글 버튼을 추가했는데 resolvedTheme 값이 계속 undefined로 읽힙니다.+

리액트 컴포넌트가 브라우저에 마운트(Mount)되기 전 서버 렌더링 단계에서 상태를 읽으려 해서 생기는 버그입니다. useEffect 안에서 컴포넌트가 완전히 로드되었음을 알리는 플래시 변수(mounted)를 스위치하여 마운트가 확보된 이후에 테마 상태를 참조하게 안전 코드를 추가하셔야 합니다.

관련 글