티스토리 뷰
실제 경험과 인사이트를 AI와 함께 정리한 글
Next.js + Tailwind CSS v4 + shadcn/ui 테마 시스템 구축 가이드
pak2251 2025. 12. 22. 14:29
작성일: 2025년 12월 22일
카테고리: Frontend, Next.js, Tailwind CSS
키워드: Next.js, Tailwind CSS v4, shadcn/ui, next-themes, Dark Mode, CSS Variables
요약
shadcn/ui의 zinc 테마를 next-themes로 확장할 때 테마 전환이 동작하지 않는 문제가 발생한다. 근본 원인은 text-zinc-400 같은 하드코딩된 Tailwind 색상 클래스가 CSS 변수를 참조하지 않기 때문이다. 이 글에서는 올바른 테마 시스템 설정 방법과 자주 하는 실수, 그리고 색상 사용 가이드를 정리한다.
문제 상황: 왜 테마가 깨지는가?
전형적인 시나리오
shadcn/ui로 Next.js 프로젝트 초기화 (zinc 테마 선택)next-themes로 다크/라이트 모드 전환 기능 추가- 문제 발생: 테마 전환이 안 되거나, 일부 컴포넌트만 변경됨
flowchart LR
subgraph 잘못된 방식
A[text-zinc-400] --> B[항상 동일한 색상]
end
subgraph 올바른 방식
C[text-muted-foreground] --> D{현재 테마?}
D -->|다크| E[밝은 회색]
D -->|라이트| F[어두운 회색]
end
style A stroke:#dc2626,stroke-width:2px
style B stroke:#dc2626,stroke-width:2px
style C stroke:#16a34a,stroke-width:2px
style D stroke:#16a34a,stroke-width:2px
style E stroke:#16a34a,stroke-width:2px
style F stroke:#16a34a,stroke-width:2px
근본 원인
| 방식 | 예시 | 테마 전환 시 |
|---|---|---|
| 하드코딩 | text-zinc-400 |
변경 안 됨 |
| CSS 변수 | text-muted-foreground |
자동 변경 |
zinc-400은 Tailwind의 고정 색상이고, muted-foreground는 CSS 변수를 참조한다.
핵심 원칙: CSS 변수 vs Tailwind 색상
Tailwind 색상 팔레트 (하드코딩)
/* 항상 동일한 색상값 */
.text-zinc-400 { color: #a1a1aa; }
.bg-zinc-950 { color: #09090b; }
CSS 변수 (테마 반응형)
/* 다크 모드 */
.dark {
--foreground: oklch(0.985 0 0); /* 거의 흰색 */
--muted-foreground: oklch(0.708 0 0); /* 밝은 회색 */
--background: oklch(0.145 0 0); /* 거의 검정 */
}
/* 라이트 모드 */
:root {
--foreground: oklch(0.145 0 0); /* 거의 검정 */
--muted-foreground: oklch(0.556 0 0); /* 어두운 회색 */
--background: oklch(1 0 0); /* 흰색 */
}
핵심 인사이트: shadcn/ui가 zinc 테마로 초기화했다고 해서 zinc-* 클래스를 사용하라는 의미가 아니다. CSS 변수가 zinc 색상 계열로 설정되어 있고, 코드에서는 text-foreground 같은 시맨틱 클래스를 사용해야 한다.
올바른 설정 방법
1. 프로젝트 초기화
# Next.js + Tailwind CSS v4 프로젝트 생성
pnpm create next-app@latest my-app --typescript --tailwind --eslint
# shadcn/ui 초기화 (zinc 테마 선택)
pnpm dlx shadcn@latest init
2. next-themes 설정
pnpm add next-themes
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
3. CSS 변수 확인 (Tailwind v4)
/* app/globals.css - Tailwind v4 방식 */
@import "tailwindcss";
@layer base {
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--muted: oklch(0.967 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
/* ... 기타 변수들 */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0.15);
/* ... 기타 변수들 */
}
}
자주 하는 실수와 해결책
실수 1: 배경색 하드코딩
// 잘못된 방식 - 항상 검은색 배경
<div className="bg-zinc-950 text-zinc-50">
// 올바른 방식 - 테마에 따라 변경
<div className="bg-background text-foreground">
실수 2: 보조 텍스트 색상
// 잘못된 방식
<p className="text-zinc-400">보조 텍스트</p>
<span className="text-zinc-500">설명 텍스트</span>
// 올바른 방식
<p className="text-muted-foreground">보조 텍스트</p>
<span className="text-muted-foreground">설명 텍스트</span>
실수 3: 테두리 색상
// 잘못된 방식
<div className="border border-zinc-700">
// 올바른 방식
<div className="border border-border">
실수 4: 입력 필드 스타일
// 잘못된 방식
<input className="bg-zinc-900 border-zinc-700 text-white placeholder:text-zinc-600" />
// 올바른 방식
<input className="bg-muted/30 border-border text-foreground placeholder:text-muted-foreground/50" />
실수 5: 역할별 색상 설정
// 잘못된 방식 - 기본 역할에 zinc 하드코딩
const ROLE_CONFIG = {
admin: { color: 'text-blue-400', bgColor: 'bg-blue-500/10 border-blue-500/20' },
member: { color: 'text-zinc-400', bgColor: 'bg-zinc-500/10 border-zinc-500/20' },
};
// 올바른 방식 - 기본 역할은 테마 변수 사용
const ROLE_CONFIG = {
admin: { color: 'text-blue-400', bgColor: 'bg-blue-500/10 border-blue-500/20' },
member: { color: 'text-muted-foreground', bgColor: 'bg-muted/30 border-border' },
};
색상 사용 가이드
사용해야 하는 CSS 변수 클래스
| 용도 | 클래스 | 설명 |
|---|---|---|
| 메인 배경 | bg-background |
페이지/카드 배경 |
| 메인 텍스트 | text-foreground |
제목, 본문 |
| 보조 텍스트 | text-muted-foreground |
설명, 힌트 |
| 테두리 | border-border |
카드, 입력 필드 테두리 |
| 비활성 배경 | bg-muted |
비활성 상태, 스켈레톤 |
| 카드 배경 | bg-card |
카드 컴포넌트 |
| 입력 배경 | bg-muted/30 |
입력 필드 배경 |
하드코딩해도 되는 경우
특정 브랜드 색상이나 상태를 나타내는 경우에만 하드코딩한다:
| 용도 | 예시 | 이유 |
|---|---|---|
| 강조 색상 | text-amber-400 |
브랜드 액센트 |
| 상태 표시 | text-red-400, text-green-400 |
에러/성공 |
| 역할 색상 | text-blue-400 (관리자) |
시각적 구분 |
변환 치트시트
text-zinc-50 → text-foreground
text-zinc-100 → text-foreground/90
text-zinc-300 → text-foreground/80
text-zinc-400 → text-muted-foreground
text-zinc-500 → text-muted-foreground
text-zinc-600 → text-muted-foreground/70
bg-zinc-950 → bg-background
bg-zinc-900 → bg-card 또는 bg-muted
bg-zinc-800 → bg-muted
border-zinc-700 → border-border
border-zinc-800 → border-border/50
placeholder:text-zinc-500 → placeholder:text-muted-foreground/50
placeholder:text-zinc-600 → placeholder:text-muted-foreground/50체크리스트
새 프로젝트 시작 시
-
next-themes설치 및 설정 -
suppressHydrationWarning추가 - CSS 변수 정의 확인 (라이트/다크)
컴포넌트 작성 시
-
zinc-*대신 시맨틱 클래스 사용 - 배경:
bg-background,bg-card,bg-muted - 텍스트:
text-foreground,text-muted-foreground - 테두리:
border-border
코드 리뷰 시
# zinc 하드코딩 검색
grep -r "zinc-" --include="*.tsx" --include="*.ts" src/
# 의도적 사용이 아닌 경우 수정 필요
수정하면 안 되는 경우
- 색상 팔레트 UI: 실제 색상을 보여주는 경우 (테마 설정 페이지)
- 다크 모드 전용 컴포넌트: 의도적으로 항상 다크 스타일인 경우
- 비주얼 에디터: 특수한 디자인 시스템을 가진 경우
핵심 정리
| 개념 | 설명 |
|---|---|
| CSS 변수 | :root와 .dark에 정의된 색상값. 테마 전환 시 자동 변경 |
| 시맨틱 클래스 | text-foreground 등 CSS 변수를 참조하는 Tailwind 클래스 |
| 하드코딩 | text-zinc-400 등 고정 색상. 테마 전환에 반응 안 함 |
| OKLCH | Tailwind v4의 기본 색상 공간. 더 정확한 색상 보간 지원 |
기억할 것: shadcn/ui의 "zinc 테마"는 CSS 변수의 기본값이 zinc 계열이라는 의미다. 코드에서는 항상 text-foreground, bg-background 같은 시맨틱 클래스를 사용해야 한다.
참고 자료
공식 문서
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| Google Antigravity 업데이트 주의: 같은 폴더에서 AI 도구 동시 사용 시 작업 손실 위험 (0) | 2025.12.22 |
|---|---|
| HTTP Cookie Deep Dive: 웹 상태 관리의 핵심 (0) | 2025.12.22 |
| GORM 실무 트러블슈팅: 운영 환경에서 만난 함정들 (0) | 2025.12.22 |
| GORM 기반 엔터프라이즈 Go API Server 아키텍처 (0) | 2025.12.22 |
| GORM 소개: Go 개발자를 위한 ORM 완벽 가이드 (0) | 2025.12.22 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- AI agent
- AI Development
- Developer Tools
- api gateway
- Go
- Rag
- 개발 도구
- Kubernetes
- Tax Analysis
- PYTHON
- authentication
- AI
- LLM
- Tailwind CSS
- troubleshooting
- claude code
- knowledge graph
- Claude
- Next.js
- authorization
- SHACL
- architecture
- react
- Ontology
- workflow
- ai 개발 도구
- security
- frontend
- LangChain
- backend
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
글 보관함