frontend

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를 쓰지 않을 이유가 없다고 생각하게 됐다.

Next.js에 Tailwind CSS 설치하기

Next.js App Router 프로젝트에 Tailwind CSS를 설치하는 방법

Tailwind CSS를 Next.js 프로젝트에 설치하는 방법은 버전에 따라 다르다. Tailwind CSS v4부터는 PostCSS 방식이 아닌 전용 플러그인 방식으로 바뀌었다. npm create next-app@latest로 Next.js 프로젝트를 새로 만들 때 Tailwind CSS 옵션을 선택하면 자동으로 설정된다. 기존 프로젝트에 추가한다면 npm install tailwindcss @tailwindcss/postcss postcss로 설치하고, postcss.config.mjs에 @tailwindcss/postcss 플러그인을 추가한다. CSS 파일에 @import 'tailwindcss'를 한 줄 추가하면 끝이다. v3 이하에서는 tailwind.config.js에 content 경로를 지정하고 globals.css에 @tailwind base, @tailwind components, @tailwind utilities 세 줄을 추가하는 방식이었다. v4에서는 이 설정들이 대부분 자동화됐다. 설치 후 JSX에서 className에 Tailwind 클래스를 쓰면 바로 스타일이 적용된다. bg-white, text-gray-900, p-4, rounded-lg 같은 유틸리티 클래스를 조합해서 스타일을 구성한다. 처음 Tailwind를 쓸 때 익숙하지 않아서 어색하지만 한 번 손에 익으면 CSS 파일 없이 컴포넌트 안에서 스타일을 완결할 수 있다는 게 큰 장점이다.

Tailwind CSS 설정 파일로 Next.js 프로젝트 커스터마이징하는 방법

Tailwind CSS는 tailwind.config.ts 파일로 기본 디자인 토큰을 커스터마이징할 수 있다. theme.extend 안에서 기존 값을 유지하면서 새 값을 추가한다. colors 항목에 프로젝트 고유 색상 팔레트를 정의하면 text-brand-primary 같은 커스텀 클래스를 쓸 수 있다. fontFamily에 Google Fonts나 로컬 폰트를 등록하고 font-sans처럼 참조하는 방식도 많이 쓴다. Next.js 프로젝트에서 next/font로 불러온 폰트 변수를 Tailwind 설정에 연결하면 폰트 관리가 깔끔해진다. screens 항목에서 반응형 브레이크포인트를 조정할 수 있다. 기본 sm, md, lg, xl, 2xl 외에 프로젝트에 맞는 커스텀 브레이크포인트를 추가하는 것도 가능하다. content 설정은 Tailwind가 사용된 클래스를 스캔할 파일 경로를 지정한다. 이 경로 밖에 있는 파일에서 쓴 클래스는 빌드 결과에 포함되지 않는다. 동적으로 생성된 클래스 이름은 Tailwind가 스캔할 수 없어서 safelist에 추가하거나 완전한 클래스 이름을 코드에 포함시키는 방식으로 해결한다.

Tailwind CSS로 Next.js 다크모드 구현하기

Tailwind CSS darkMode class 설정으로 Next.js 다크모드를 구현하는 방법

Tailwind CSS에서 다크모드를 구현하는 방식은 두 가지다. media 방식은 OS의 다크모드 설정을 자동으로 따라간다. class 방식은 html 태그에 dark 클래스가 있을 때 다크 스타일을 적용한다. 사용자가 직접 토글할 수 있는 다크모드를 만들려면 class 방식이 적합하다. tailwind.config.ts에 darkMode: 'class'를 추가하면 된다. 이후 Tailwind 클래스 앞에 dark: 접두사를 붙이면 html에 dark 클래스가 있을 때만 적용되는 스타일이 된다. 예를 들어 bg-white dark:bg-gray-900는 라이트 모드에서는 흰 배경, 다크 모드에서는 어두운 배경이 된다. dark: 접두사는 hover:, sm:, focus: 같은 다른 수정자와 조합할 수도 있다. hover:dark:bg-gray-700처럼 다크 모드에서 hover 시 색상을 지정하는 방식도 된다. html 태그에 dark 클래스를 추가하고 제거하는 로직이 실제 테마 전환의 핵심이다. JavaScript에서 document.documentElement.classList.add('dark')를 호출하면 다크모드가 활성화되고, remove('dark')로 해제된다. 이 상태를 localStorage에 저장해두면 페이지를 새로 고침해도 유지된다.

next-themes로 Next.js 다크모드 토글과 시스템 설정 연동을 구현하는 방법

Next.js App Router에서 다크모드를 직접 구현하면 SSR 환경에서 hydration mismatch 문제가 생기기 쉽다. 서버에서 렌더링할 때는 테마를 알 수 없어서 기본값(라이트 모드)으로 HTML을 만들고, 클라이언트에서 localStorage를 읽어 다크 모드로 바꾸면 그 사이에 화면이 깜박이는 플래시 현상이 발생한다. next-themes는 이 문제를 해결하는 라이브러리다. npm install next-themes로 설치하고 app/layout.tsx에 ThemeProvider를 추가한다. ThemeProvider는 클라이언트 컴포넌트여야 하기 때문에 별도 파일로 분리한다. attribute prop에 'class'를 설정하면 Tailwind의 class 방식과 연동된다. defaultTheme으로 기본 테마를 지정하고, enableSystem을 true로 설정하면 OS 다크모드 설정을 자동으로 따른다. useTheme 훅으로 현재 테마 상태와 setTheme 함수를 가져와서 토글 버튼 컴포넌트에서 쓸 수 있다. resolvedTheme을 사용하면 system 설정일 때 실제로 적용된 테마(light 또는 dark)를 알 수 있어서 토글 버튼 아이콘 전환에 유용하다.

Tailwind CSS 다크모드 실용 패턴

Next.js Tailwind CSS 다크모드 토글 버튼 컴포넌트를 만드는 방법

useTheme 훅을 사용하는 컴포넌트는 반드시 클라이언트 컴포넌트여야 한다. 파일 상단에 'use client'를 선언하고 next-themes에서 useTheme을 import한다. resolvedTheme으로 현재 적용된 테마를 읽고 setTheme으로 전환한다. 토글 버튼은 resolvedTheme이 'dark'면 라이트 모드로 전환하는 아이콘을, 아니면 다크 모드로 전환하는 아이콘을 표시하면 된다. 주의할 점은 컴포넌트가 마운트되기 전에는 resolvedTheme이 undefined일 수 있다는 점이다. 마운트 전에 아이콘을 렌더링하면 hydration mismatch가 발생할 수 있다. useState와 useEffect를 조합해서 마운트 후에만 아이콘을 표시하도록 처리하거나, mounted 상태가 false일 때 빈 div나 스켈레톤을 반환하는 방식으로 해결한다. 다크모드 토글 버튼은 Header 컴포넌트 같은 공통 레이아웃 영역에 넣어두면 모든 페이지에서 쓸 수 있다. 아이콘은 lucide-react나 heroicons 같은 라이브러리를 쓰거나 직접 SVG를 작성해도 된다.

'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 다크모드에서 Next.js CSS 변수와 함께 쓰는 방법

Tailwind의 dark: 접두사 방식은 간단하지만, 색상이 많아지면 모든 클래스에 dark: 변형을 추가해야 해서 코드가 길어진다. CSS 변수를 활용하면 이 반복을 줄일 수 있다. globals.css에서 :root 셀렉터에 라이트 모드 색상 변수를 정의하고, .dark 셀렉터에 다크 모드 색상 변수를 정의한다. tailwind.config.ts에서 colors 항목에 var(--변수명) 형태로 CSS 변수를 참조하면 bg-background, text-foreground 같은 시맨틱 클래스를 만들 수 있다. 이 클래스들은 테마에 따라 자동으로 색상이 바뀌기 때문에 컴포넌트에서 dark: 접두사를 쓸 필요가 없어진다. shadcn/ui 라이브러리가 이 패턴을 기반으로 구축되어 있다. shadcn/ui를 설치하면 이 CSS 변수 체계와 Tailwind 설정이 자동으로 구성되어서, 직접 설정하는 번거로움 없이 바로 쓸 수 있다. 프로젝트 초기에 디자인 토큰 체계를 CSS 변수 기반으로 잡아두면 나중에 테마 색상을 바꿀 때 변수 값만 수정하면 되어서 유지 관리가 편해진다.

자주 묻는 질문

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

next-themes를 사용하면 이 flashing 문제가 해결됩니다. next-themes는 HTML이 렌더링되기 전에 인라인 스크립트로 테마를 먼저 적용해서 화면 깜박임을 방지합니다.

Tailwind CSS v3와 v4의 설치 방법이 다른가요?+

네, 다릅니다. v3는 tailwind.config.js와 PostCSS 설정이 필요하고, globals.css에 @tailwind 지시어 세 줄을 추가합니다. v4는 @tailwindcss/postcss 플러그인을 쓰고 CSS에 @import 'tailwindcss' 한 줄만 추가하면 됩니다. Next.js create-next-app에서 Tailwind 옵션을 선택하면 자동으로 올바른 버전으로 설정됩니다.

dark: 클래스가 적용되지 않는 이유가 무엇인가요?+

tailwind.config.ts에 darkMode: 'class'가 설정되어 있는지 확인하세요. 그다음 html 태그에 실제로 dark 클래스가 추가되는지 브라우저 개발자 도구에서 확인합니다. next-themes를 쓴다면 ThemeProvider의 attribute 값이 'class'인지 확인하세요.

시스템 다크모드 설정을 따르면서 사용자가 직접 바꿀 수도 있게 하려면 어떻게 하나요?+

next-themes의 ThemeProvider에 enableSystem: true와 defaultTheme: 'system'을 설정하면 됩니다. 사용자가 직접 토글하면 그 값이 localStorage에 저장되어 이후 시스템 설정과 무관하게 사용자 선택이 우선됩니다.

관련 글