테마 전환

shadcn/ui 설치와 CSS 변수로 테마 설정하는 방법

Easton editorial illustration: design-system assembly tray

처음 shadcn/ui를 사용했을 때는 ‘npm 패키지가 아니다’라는 방식이 낯설었습니다. 코드를 프로젝트에 복사한다고요? 너무 원시적인 방법처럼 들렸습니다.

하지만 몇 번 사용해 보니 바로 그 점이 shadcn/ui의 강점이라는 사실을 알게 됐습니다. 모든 컴포넌트의 소스 코드를 직접 소유하므로 원하는 대로 수정할 수 있고, 버전 충돌이나 컴포넌트 라이브러리의 디자인 제약도 걱정할 필요가 없습니다.

여기서는 shadcn/ui 설치와 설정, 테마 맞춤 설정을 살펴봅니다. 특히 CSS 변수로 브랜드 디자인 시스템을 구현하는 방법에 집중합니다. 기본 설정은 5분 안에 마치고, 한 시간 정도 더 투자하면 원하는 모습으로 테마를 조정할 수 있습니다.


1. 빠른 설치: 두 가지 방법

방법 1: CLI로 한 번에 초기화하기(권장)

새 프로젝트에서는 다음 명령을 바로 실행합니다.

npx shadcn@latest init

실행하면 여러 가지를 묻습니다. TypeScript와 JavaScript 중 무엇을 사용할지, 어떤 스타일과 기본 테마를 선택할지 정하면 됩니다. 전체 과정이 대화형이므로 안내에 따라 선택하면 됩니다.

설치가 끝나면 프로젝트에 다음 파일이 추가됩니다.

  • components.json - 설정 파일
  • lib/utils.ts - 유틸리티 함수
  • components/ui/ - 컴포넌트 디렉터리

컴포넌트를 추가하는 방법도 간단합니다. 버튼이 필요하다면 다음 명령을 실행합니다.

npx shadcn@latest add button

컴포넌트 코드가 components/ui/button.tsx에 자동으로 복사되므로 바로 import해서 사용할 수 있습니다.

주의할 점이 있습니다. 프로젝트 개발이 어느 정도 진행됐다면 tailwind.config.js와 globals.css에 이미 많은 설정이 들어 있을 수 있습니다. shadcn의 init 명령은 이 파일을 덮어쓸 수 있으므로 프로젝트를 시작할 때 설치하는 편이 좋습니다.

어떤 블로거의 조언이 꽤 정확했습니다. shadcn/ui를 프로젝트의 ‘첫 번째 의존성 묶음’으로 생각하고 나중으로 미루지 말라는 이야기였습니다. 실제로 겪어 보면 실감하게 됩니다.

방법 2: 수동 설치(기존 프로젝트에 적합)

프로젝트 구조가 이미 자리 잡아 CLI가 설정을 덮어쓸 위험이 크다면 수동으로 설치합니다.

단계별로 진행해 보겠습니다.

1단계: Tailwind CSS 설치 확인

shadcn 컴포넌트는 모두 Tailwind로 작성돼 있습니다. 아직 설치하지 않았다면 먼저 Tailwind를 설치하세요. 공식 문서의 안내가 잘 정리돼 있습니다.

2단계: 의존성 설치

npm install class-variance-authority clsx tailwind-merge
npm install lucide-react

class-variance-authority(줄여서 CVA)는 유용한 도구입니다. 뒤에서 컴포넌트 변형을 만들 때 사용합니다.

3단계: 경로 별칭 설정

tsconfig.json에 다음 설정을 추가합니다.

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

이제 ../../../를 길게 쓰지 않고 @/components/ui/button처럼 컴포넌트를 가져올 수 있습니다.

4단계: components.json 생성

프로젝트 루트에 다음 파일을 만듭니다.

{
  "style": "new-york",
  "rsc": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

cssVariables: true가 중요합니다. Tailwind utility class 대신 CSS 변수로 테마를 구성한다는 의미입니다.

5단계: 스타일 추가

globals.css에 shadcn의 기본 스타일을 추가합니다. 테마를 설명하는 다음 절에서 자세히 다룹니다.


2. 테마 시스템 이해하기: CSS 변수 활용법

shadcn/ui의 테마 시스템은 간단한 규칙을 따릅니다. 각 색상에는 backgroundforeground 변수가 한 쌍으로 존재합니다.

무슨 뜻일까요? 다음 예를 보겠습니다.

:root {
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
}

--primary는 버튼의 배경색이고, --primary-foreground는 버튼 위의 글자색입니다. 이렇게 짝을 지으면 변수 하나만 바꿔도 관련된 모든 컴포넌트의 색상이 함께 변경됩니다.

CSS 변수 목록

shadcn/ui는 기본적으로 다음 변수를 정의합니다.

변수용도
--background페이지 배경색
--foreground페이지 글자색
--card카드 배경
--card-foreground카드 글자색
--popover팝오버 배경
--popover-foreground팝오버 글자색
--primary기본 색상(버튼, 링크)
--primary-foreground기본 색상 위의 글자색
--secondary보조 색상
--secondary-foreground보조 색상 위의 글자색
--muted차분한 배경색
--muted-foreground차분한 글자색
--accent강조 색상
--accent-foreground강조 색상 위의 글자색
--destructive위험한 작업(삭제 버튼)
--destructive-foreground위험한 작업 색상 위의 글자색
--border테두리
--input입력 필드
--ring포커스 링

변수가 많아 보이지만 background/foreground 짝만 이해하면 쉽게 기억할 수 있습니다.

HSL 형식의 비밀

shadcn의 색상 값이 표준 HSL 형식과 다르다는 점을 눈치챘을 수도 있습니다.

/* ❌ 표준 HSL */
--primary: hsl(222.2, 47.4%, 11.2%);

/* ✅ shadcn 형식 */
--primary: 222.2 47.4% 11.2%;

왜 이런 ‘괄호 없는’ 형식으로 작성할까요?

Tailwind가 투명도 수정자를 지원하기 때문입니다. 예를 들어 bg-primary/50은 기본 색상에 50% 투명도를 적용합니다. 변수가 완전한 hsl() 형식이면 이 기능을 사용할 수 없습니다.

값만 작성하면 Tailwind가 hsl()과 투명도를 자동으로 더합니다. 영리한 설계입니다.


3. 브랜드 테마 맞춤 설정하기

방법 1: CSS 변수 직접 수정

가장 간단한 방법은 globals.css를 열고 :root 부분의 색상 값을 바꾸는 것입니다.

예를 들어 기본 색상을 파란색에서 보라색으로 변경하려면 다음과 같이 작성합니다.

:root {
  --primary: 270 60% 60%;
  --primary-foreground: 0 0% 100%;
}

.dark {
  --primary: 270 60% 70%;
  --primary-foreground: 0 0% 0%;
}

저장하면 bg-primary를 사용하는 모든 버튼과 링크가 보라색으로 바뀝니다.

방법 2: OKLCH 색 공간 사용(Tailwind v4)

Tailwind v4를 사용한다면 OKLCH 색 공간을 고려할 수 있습니다. HSL보다 사람의 색상 인식에 가깝고, 더 균일한 색조 단계를 만들 수 있습니다.

:root {
  --primary: oklch(0.6 0.2 270);
  --primary-foreground: oklch(0.98 0 0);
}

여기서 oklch(0.6 0.2 270)의 세 매개변수는 다음과 같습니다.

  • 0.6 - 밝기(0~1)
  • 0.2 - 채도(약 0~0.4)
  • 270 - 색상 각도(0~360)

방법 3: 온라인 도구로 생성

직접 색상을 조합하기 번거롭다면 온라인 도구를 사용할 수 있습니다.

Shadcn Theme Generator를 추천합니다.

기본 색상을 선택하면 도구가 라이트와 다크 테마를 포함한 전체 CSS 변수 세트를 자동으로 생성합니다. 결과를 복사해 globals.css에 붙여 넣으면 됩니다.


4. 다크 모드 설정

next-themes로 테마 전환 구현

shadcn/ui 자체에는 테마 전환 기능이 없지만 next-themes 라이브러리로 구현할 수 있습니다.

먼저 설치합니다.

npm install next-themes

그다음 layout.tsx에 다음과 같이 설정합니다.

import { ThemeProvider } from "next-themes"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

핵심은 다음과 같습니다.

  • 하이드레이션 경고를 피하려면 suppressHydrationWarning을 반드시 추가합니다.
  • attribute="class"는 class 이름으로 테마를 전환한다는 뜻입니다.
  • defaultTheme="system"은 시스템 설정을 기본값으로 사용한다는 뜻입니다.
  • enableSystem은 시스템 테마 감지를 활성화합니다.

테마 전환 버튼 만들기

useTheme hook으로 현재 테마와 테마 설정 함수를 가져옵니다.

import { useTheme } from "next-themes"
import { Moon, Sun } from "lucide-react"

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <button
      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
      className="p-2 rounded-md hover:bg-accent"
    >
      {theme === "dark" ? <Sun size={20} /> : <Moon size={20} />}
    </button>
  )
}

다크 모드를 기본값으로 설정

사이트를 기본적으로 다크 모드로 표시하려면 두 가지 방법이 있습니다.

방법 1: dark class 하드코딩

<html lang="ko" className="dark">

이렇게 작성하면 테마가 다크 모드로 고정돼 전환할 수 없습니다.

방법 2: 기본 테마 설정

<ThemeProvider
  attribute="class"
  defaultTheme="dark"  // 다크 모드를 기본값으로 설정
  enableSystem={false} // 시스템 감지 비활성화
>

이 방법은 사용자가 수동으로 전환할 수 있지만 초기 상태는 다크 모드입니다.


5. 고급 맞춤 설정: 컴포넌트 변형

CVA로 사용자 정의 변형 만들기

버튼에 ‘위험’, ‘성공’, ‘그라데이션’ 같은 여러 스타일을 추가해야 할 때가 있습니다. CVA를 사용하면 이런 변형을 쉽게 정의할 수 있습니다.

import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

컴포넌트에서는 다음과 같이 사용합니다.

<button className={buttonVariants({ variant: "destructive", size: "lg" })}>
  삭제
</button>

shadcn 컴포넌트 소스 코드를 직접 수정하지 않기

이는 중요한 모범 사례입니다.

shadcn 컴포넌트 코드는 프로젝트에 있으므로 원하는 대로 수정할 수 있습니다. 하지만 원본 파일은 직접 수정하지 않는 편이 좋습니다. 대신 래퍼 컴포넌트를 만드세요.

왜일까요? shadcn은 컴포넌트를 자주 업데이트합니다. 원본 파일을 수정하면 업데이트할 때마다 수동으로 병합해야 하므로 번거롭습니다.

더 나은 방법은 다음과 같습니다.

// components/brand-button.tsx
import { Button } from "@/components/ui/button"
import { cva } from "class-variance-authority"

const brandButtonVariants = cva("...", {
  variants: {
    brand: {
      primary: "bg-brand-primary text-white",
      secondary: "bg-brand-secondary text-black",
    },
  },
})

export function BrandButton({ brand, ...props }) {
  return <Button className={brandButtonVariants({ brand })} {...props} />
}

원본 Button 컴포넌트는 그대로 두고 BrandButton을 별도로 만들었습니다. 이렇게 하면 나중에 shadcn을 업데이트해도 맞춤 설정에 영향을 주지 않습니다.


6. 자주 발생하는 문제와 해결 방법

문제 1: 설치 후 스타일이 적용되지 않음

다음 항목을 확인하세요.

  1. globals.csslayout.tsx에서 import했나요?
  2. Tailwind의 content 설정에 components/**/*가 포함돼 있나요?
  3. components.json의 경로 설정이 올바른가요?

문제 2: 테마 전환 시 화면 깜빡임

대개 하이드레이션 불일치가 원인입니다. 다음을 확인하세요.

  1. <html> 태그에 suppressHydrationWarning을 추가합니다.
  2. ThemeProvider가 전체 앱을 감싸야 합니다.
  3. 서버 렌더링 중에는 theme 값을 읽지 않습니다(undefined가 됩니다).

문제 3: CSS 변수가 적용되지 않음

가능한 원인은 다음과 같습니다.

  1. 변수 이름을 잘못 작성했습니다(--primaryForeground가 아니라 --primary-foreground입니다).
  2. 대응하는 .dark 스타일이 없습니다.
  3. 변수 값 형식이 잘못됐습니다(괄호 없는 HSL 또는 OKLCH를 사용해야 합니다).

문제 4: 컴포넌트 스타일 충돌

프로젝트에 이미 스타일 시스템이 있다면 shadcn의 스타일과 충돌할 수 있습니다. 해결 방법은 다음과 같습니다.

  1. shadcn 컴포넌트에 namespace를 추가합니다(예: shadcn-button).
  2. Tailwind의 layer 우선순위를 조정합니다.
  3. CVA로 자체 변형을 만들고 기본 스타일에 의존하지 않습니다.

7. 정리

shadcn/ui의 설치와 설정은 어렵지 않습니다. 핵심은 ‘의존성 패키지 대신 코드를 복사한다’는 설계 철학을 이해하는 것입니다. 이 방식은 모든 부분을 제어할 수 있다는 장점이 있지만, 프로젝트마다 컴포넌트 코드 세트를 직접 관리해야 한다는 단점도 있습니다.

테마 맞춤 설정에서는 CSS 변수 시스템이 깔끔하게 설계돼 있습니다. 변수 몇 개만 바꾸면 전체 애플리케이션의 색상 구성이 함께 변경됩니다. next-themes와 결합하면 라이트 모드와 다크 모드 전환도 몇 줄의 코드로 구현할 수 있습니다.

마지막으로 몇 가지를 권합니다.

  1. 새 프로젝트에서는 CLI 초기화를 우선 사용하세요. 수동 설정 작업을 줄일 수 있습니다.
  2. primary, secondary 같은 의미 기반 색상 변수를 사용하세요. 구체적인 색상 이름은 피하는 편이 좋습니다.
  3. 라이트 모드와 다크 모드에서 대비를 모두 테스트하세요. 읽기 쉬운지 확인해야 합니다.
  4. 원본 소스 코드를 수정하는 대신 래퍼 컴포넌트를 만드세요. 향후 업데이트가 쉬워집니다.

다음에 테마가 있는 UI를 빠르게 구성해야 한다면 shadcn/ui를 사용해 보세요. 복사하고 붙여 넣는 방식의 편리함은 직접 써 보면 알 수 있습니다.



참고 자료

shadcn/ui 설치와 테마 맞춤 설정

shadcn/ui를 처음부터 설치하고 테마 시스템을 구성해 브랜드 디자인을 구현합니다

⏱️ Estimated time: 30 min

  1. 1

    Step 1: CLI 빠른 초기화

    새 프로젝트에서 설치 명령을 실행합니다.

    • npx shadcn@latest init
    • TypeScript, New York 스타일, 기본 테마 선택
    • CLI가 설정을 마칠 때까지 대기
  2. 2

    Step 2: 브랜드 기본 색상 변경

    globals.css의 CSS 변수를 편집합니다.

    • app/globals.css 열기
    • :root 아래의 --primary 변수 찾기
    • 브랜드 색상으로 변경(HSL 또는 OKLCH 형식)
    • 대비를 확보하도록 --primary-foreground도 함께 변경
  3. 3

    Step 3: 다크 모드 설정

    next-themes를 설치하고 설정합니다.

    • npm install next-themes
    • layout.tsx에 ThemeProvider 추가
    • 하이드레이션 경고를 방지하도록 suppressHydrationWarning 설정
    • 테마 전환 컴포넌트 생성
  4. 4

    Step 4: 컴포넌트 변형 만들기

    CVA로 사용자 정의 스타일을 정의합니다.

    • class-variance-authority 설치
    • variants와 defaultVariants 정의
    • 컴포넌트에서 buttonVariants() 적용
    • 원본 shadcn 컴포넌트 유지

FAQ

shadcn/ui는 기존 UI 컴포넌트 라이브러리와 무엇이 다른가요?
shadcn/ui는 npm 패키지가 아니라 컴포넌트 소스 코드를 프로젝트에 복사합니다. 완전히 제어하고 맞춤 설정할 수 있으며 버전 충돌을 걱정하지 않아도 된다는 장점이 있지만, 각 프로젝트에서 컴포넌트 코드를 직접 관리해야 한다는 단점도 있습니다.
새 프로젝트를 초기화할 때 shadcn/ui를 설치하는 것이 좋은 이유는 무엇인가요?
shadcn의 init 명령이 tailwind.config.js와 globals.css를 덮어쓸 수 있기 때문입니다. 프로젝트 개발이 이미 진행됐다면 이 파일의 기존 설정이 사라질 수 있으므로 가능한 한 일찍 설치하는 편이 좋습니다.
CSS 변수는 왜 표준 HSL 대신 괄호 없는 값 형식으로 작성하나요?
222.2 47.4% 11.2% 같은 괄호 없는 값 형식은 Tailwind의 투명도 수정자를 지원합니다. 예를 들어 bg-primary/50은 기본 색상에 50% 투명도를 적용합니다. 완전한 hsl() 형식에서는 이 기능을 사용할 수 없습니다.
브랜드 기본 색상은 어떻게 변경하나요?
globals.css를 열고 :root 아래의 --primary와 --primary-foreground 변수를 브랜드 색상 값으로 바꿉니다. 변경하면 bg-primary를 사용하는 모든 컴포넌트가 자동으로 업데이트됩니다.
다크 모드 전환 시 화면이 깜빡이는 이유는 무엇인가요?
대개 하이드레이션 불일치가 원인입니다. html 태그에 suppressHydrationWarning 속성을 추가하고 ThemeProvider가 전체 앱을 올바르게 감싸는지 확인하세요. 서버 렌더링 중에는 theme 값을 읽지 않아야 합니다.
shadcn 컴포넌트 소스 코드를 직접 수정해야 하나요?
권장하지 않습니다. shadcn 컴포넌트는 자주 업데이트되므로 원본 파일을 수정하면 업데이트할 때 수동으로 병합해야 합니다. 원본 컴포넌트를 유지하고 래퍼 컴포넌트를 만드는 편이 낫습니다.

3분 읽기 · 게시일: 2026년 3월 26일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog