스타일 · Shadcn UI · 철학 · 설치 · 구성

Shadcn UI · 철학, 설치, 구성

Shadcn UI? 는 버튼·다이얼로그·셀렉트 같은 컴포넌트?설치하는 라이브러리가 아니라, 그 소스 코드를 내 프로젝트로 복사해 내가 소유하는 방식입니다. 여기서는 그 철학, 설치(init), 구성(components.json)을 차근차근 봅니다.

밀키트 vs 레시피 카드의 차이예요. 보통 UI 라이브러리는 "완성된 밀키트"라 정해진 맛만 나오지만, Shadcn은 "레시피와 재료"를 통째로 건네줍니다. 받은 레시피는 내 노트에 옮겨 적은 내 것이라, 간을 바꾸든 재료를 빼든 마음대로 고쳐 쓸 수 있어요. 대신 그 노트(코드)는 이제 내가 관리합니다.

1. Shadcn UI란? — "복사해서 소유하는" 방식

전통적인 UI 라이브러리는 npm install 로 설치하면, 컴포넌트 코드가 node_modules 폴더 깊숙이 숨어 들어갑니다. 내가 직접 못 보고, 못 고치죠. 색이나 동작을 바꾸려면 라이브러리가 미리 열어둔 테마 옵션 범위 안에서만 가능합니다. 버전을 올리면 스타일이 통째로 바뀌기도 합니다(버전·스타일 종속).

Shadcn UI 는 정반대입니다. 컴포넌트를 추가하면 그 React? + TypeScript 코드 파일(.tsx)이 내 레포 안으로 복사됩니다. 그 순간부터 그건 라이브러리 코드가 아니라 내가 쓴 코드예요. 줄을 열어 클래스를 바꾸고, 동작을 더하고, 통째로 다시 써도 됩니다. 공식 문서의 표현 그대로 "이것은 컴포넌트 라이브러리가 아니라, 당신의 컴포넌트 라이브러리를 만드는 방법"입니다.

구분전통 UI 라이브러리 (설치형)Shadcn UI (복사형)
가져오는 법npm installimportCLI로 코드를 내 레포에 복사
코드 위치node_modules 안 (안 보임)src/components/ui/ (내 코드)
수정 자유테마 옵션 범위 안에서만코드를 직접 고쳐 무엇이든
버전 종속업그레이드 시 스타일이 바뀔 수 있음내 코드라 내가 멈춰둔 그대로
관리 책임라이브러리가 관리내가 관리 (자유의 대가)
적합빠르게 정해진 부품을 채울 때디자인을 손에 쥐고 싶을 때
중요 — 설치하는 "그 라이브러리"가 아니다

package.json 의존성 목록에서 "shadcn-ui" 같은 항목을 찾을 수 없습니다. 설치되는 건 도구(CLI)일 뿐이고, 컴포넌트는 그 도구가 내 파일로 복사해 줍니다. 버전(현재 3.7.0)은 라이브러리가 아니라 이 CLI 도구의 버전입니다.

2. 네 개의 기둥

복사된 컴포넌트 한 개를 열어 보면, 사실 아래 네 가지 도구가 손을 맞잡고 있습니다. 각자 역할이 또렷이 나뉘어 있어요.

기둥역할비유
헤드리스 프리미티브
(Radix UI / Base UI)
동작과 접근성 (headless). 모양 없이 "여닫기·키보드·포커스·스크린리더" 같은 알맹이 동작만 담당. shadcn 기본은 Radix UI, 이 프로젝트는 후속인 Base UI(@base-ui/react) 사용옷 없는 마네킹의 관절·뼈대
Tailwind?스타일. 유틸리티 클래스(flex, px-4, rounded-md)로 겉모습을 입힘마네킹에 입히는
cvaclass-variance-authority. variant="outline" 같은 변형 규칙을 한곳에 깔끔히 정의옷의 색·사이즈 카탈로그
cnclsx + tailwind-merge. 여러 클래스를 합치고 충돌을 정리(같은 종류는 뒤엣것이 이김)옷을 겹쳐 입을 때 코디 정리

cva 로 변형을 정의하는 모습 — 버튼의 variantsize 를 한곳에 모읍니다.

import { cva } from "class-variance-authority"

const buttonVariants = cva(
  "inline-flex items-center rounded-md text-sm font-medium", // 공통
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        outline: "border border-input bg-background",
      },
      size: { sm: "h-8 px-3", lg: "h-10 px-6" },
    },
    defaultVariants: { variant: "default", size: "sm" },
  }
)

그리고 cn 은 "기본 클래스 + 내가 덧붙인 클래스"를 충돌 없이 합쳐 줍니다. 이 프로젝트의 실제 cn 정의는 이렇게 짧아요(src/lib/utils.ts).

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))   // 합치고(clsx) → 충돌 정리(twMerge)
}

// 예: 같은 padding 이 충돌하면 뒤엣것이 이김
cn("px-2 py-1", "px-4")   // → "py-1 px-4"

3. 설치 / 초기화 — init 과 components.json

프로젝트에서 단 한 번, 초기화를 합니다. 이때 의존성을 깔고, 위에서 본 cn 유틸을 만들고, CSS 변수를 설정하고, 무엇보다 components.json 이라는 설정 파일을 만듭니다.

# 프로젝트 한 번만 초기화 (CLI 3.7.0)
npx shadcn@latest init

# → components.json 생성 + cn 유틸 추가 + CSS 변수 설정

components.json 은 "앞으로 컴포넌트를 복사할 때 어떤 규칙으로 어디에 둘지"를 적어둔 메모지입니다. 이 프로젝트의 실제 설정값을 한 줄씩 보면서 의미를 익혀 봅시다.

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "base-vega",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/app/global.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}
항목이 프로젝트 값의미 (쉽게)
$schemaui.shadcn.com/schema.json이 파일의 형식 검사용 주소. 오타나면 에디터가 알려줌
stylebase-vega컴포넌트 스타일. base- 는 헤드리스 기반이 Base UI임을, vega 는 디자인 톤을 뜻함(전통 기본은 radix-=Radix UI). 초기화 후엔 바꾸기 어려움
rscfalseReact Server Components 안 씀. 그래서 "use client" 자동 추가 안 함
tsxtrueTypeScript(.tsx)로 복사. false.jsx
tailwind.config(빈 값)비워둠 = Tailwind v4 방식(설정을 CSS에서 함)
tailwind.csssrc/app/global.cssTailwind를 불러오는 CSS 파일 위치
tailwind.baseColorneutral기본 색 팔레트(무채색 계열). 초기화 후엔 바꾸기 어려움
tailwind.cssVariablestrue색을 CSS 변수(예: --primary)로 관리 → 다크모드·테마 쉬움
tailwind.prefix(빈 값)클래스 앞에 붙일 접두사 없음(예: tw- 안 씀)
iconLibrarylucide아이콘은 lucide 사용 → lucide-react 패키지
aliases.components@/components일반 컴포넌트 import 경로
aliases.ui@/components/ui복사된 shadcn 컴포넌트가 들어갈 폴더
aliases.utils@/lib/utilscn 같은 유틸 위치
aliases.lib@/lib공용 라이브러리 폴더
aliases.hooks@/hooks커스텀 훅 폴더
aliases가 하는 일

@/ 는 "src 폴더부터"를 뜻하는 별명입니다. CLI는 이 별명들을 보고 복사한 파일을 어디에 둘지, 또 import 문을 어떻게 쓸지 정합니다. 그래서 ../../components/ui/button 대신 늘 @/components/ui/button 처럼 깔끔하게 쓸 수 있어요.

4. 컴포넌트 추가 — add

초기화가 끝나면, 필요한 컴포넌트를 하나씩 골라 추가합니다. 그때마다 코드 파일이 내 폴더로 복사됩니다.

# 버튼 컴포넌트를 내 코드로 복사
npx shadcn@latest add button

# → src/components/ui/button.tsx 파일이 생김 (내 코드!)
#   (aliases.ui = "@/components/ui" 설정을 따라 이 위치로 들어감)

이제 그 버튼을 import 해서 씁니다. variant 는 위에서 본 cva 가 정의한 변형이에요.

import { Button } from "@/components/ui/button"

function Toolbar() {
  return (
    <div className="flex gap-2">
      <Button>저장</Button>
      <Button variant="outline">취소</Button>
    </div>
  )
}

마음에 안 드는 부분이 있으면? button.tsx 를 열어 직접 고치면 됩니다 — 그게 곧 내 디자인이 됩니다.

5. 아이콘 — lucide-react

설정의 iconLibrary: "lucide" 에 따라, 아이콘은 lucide-react 에서 컴포넌트처럼 가져다 씁니다. 각 아이콘이 하나의 컴포넌트? 라서, 크기·색을 props 와 클래스로 조절해요.

import { Search, Plus } from "lucide-react"
import { Button } from "@/components/ui/button"

function SearchBar() {
  return (
    <Button>
      <Search className="size-4" />   {/* 아이콘도 컴포넌트 */}
      검색
    </Button>
  )
}

6. 폴더 구조 — 복사본은 어디에?

설정의 aliases 대로, 복사된 컴포넌트와 유틸은 정해진 자리에 모입니다.

src/
├─ components/
│  └─ ui/              ← aliases.ui (@/components/ui)
│     ├─ button.tsx        복사된 shadcn 컴포넌트들
│     ├─ dialog.tsx        (모두 내 코드, 수정 가능)
│     └─ select.tsx
├─ lib/
│  └─ utils.ts         ← aliases.utils (@/lib/utils): cn 정의
└─ app/
   └─ global.css       ← tailwind.css: Tailwind 불러옴

핵심만 기억하면 됩니다. 컴포넌트는 @/components/ui 에, 이들을 받쳐주는 cn@/lib/utils 에 있습니다.

7. 버튼 모양 미리보기 (라이브)

복사되는 버튼이 대략 어떤 모습인지, Tailwind? 클래스로 흉내 낸 미리보기입니다. (실제 컴포넌트는 cva·cn 로 더 정교하게 만들어집니다.)

한눈에 — 구성 요약

요소역할 한 줄이 프로젝트
방식코드를 복사해 내가 소유CLI 3.7.0
헤드리스 프리미티브동작·접근성(headless)Base UI (@base-ui/react, base-vega)
Tailwind스타일(유틸 클래스)global.css
cva변형(variant) 정의button 등에서
cn클래스 병합·충돌 정리@/lib/utils
설정복사 규칙components.json (base-vega/neutral)
아이콘아이콘 컴포넌트lucide-react
위치복사본 폴더@/components/ui
이 프로젝트와의 관계

이 프로젝트의 실제 components.json 은 style base-vega, baseColor neutral, 아이콘 lucide, tsx: true, rsc: false, cssVariables true 로 설정돼 있고, 복사된 컴포넌트는 @/components/ui, cn@/lib/utils 에 있습니다. 프론트엔드 환경 전반은 프론트엔드 환경(env-frontend) 에서 함께 보세요.

다음 단계