
웹사이트를 만들 때는 글자, 버튼, 입력창, 메뉴 같은 UI(사용자 인터페이스) 요소들이 필요하다.
예를 들어, 로그인 페이지를 만든다고 하면 “아이디 입력창 + 비밀번호 입력창 + 로그인 버튼” 같은 기본 UI가 꼭 들어가야 한다.
보통 개발자들은 이런 UI를 매번 처음부터 만들지 않고, 이미 만들어져 있는 컴포넌트(부품)를 가져다 쓴다. 이런 걸 모아둔 게 바로 UI 라이브러리다.
shadcn/ui는 그중 하나인데, 조금 특별하다.
다른 UI 라이브러리는 보통 설치만 하면 바로 쓸 수 있게 제공되는데,shadcn/ui는 “코드를 직접 프로젝트 안에 복사해 쓰는 방식”을 쓴다.이게 무슨 뜻일까?
- 그냥 가져다 쓰는 게 아니라 → 내 코드 안으로 들어온다.
- 그래서 필요에 따라 내 마음대로 수정할 수 있다.
- 예를 들어 버튼 색, 크기, 애니메이션을 바꾸는 게 훨씬 자유롭다.
👉 정리하면,
shadcn/ui는 버튼, 모달, 드롭다운 같은 UI를 빠르게 가져오고, 내 입맛대로 고쳐 쓸 수 있는 “UI 부품 상자”라고 생각하면 된다.
참고자료
UI 라이브러리는 이미 많다. 그런데 왜 굳이
shadcn/ui를 써야 할까?
핵심은 자유도와 일관성에 있다.
일반적인 UI 라이브러리는 “정해진 디자인”에 맞춰야 해서, 내가 원하는 대로 수정하려면 제약이 많다.
하지만shadcn/ui는 코드를 직접 들고 오기 때문에 내 프로젝트에 맞게 커스터마이징하기가 훨씬 쉽다.
또한,shadcn/ui는 단순히 코드만 모아둔 게 아니라 Tailwind CSS 디자인 시스템을 기반으로 작성되어 있다.
즉, 가져온 순간부터 내 프로젝트 스타일과 잘 어울리고, 통일성 있는 UI를 빠르게 만들 수 있다.
정리하면,shadcn/ui의 장점은 이렇다:
- 내 코드처럼 수정 가능 → 자유로운 커스터마이징
- Tailwind 기반 → 스타일 일관성 확보
- 빠른 개발 속도 → 자주 쓰는 UI를 바로 가져다 쓸 수 있음
👉 그래서
shadcn/ui는 “빠르고, 자유롭고, 일관된 UI 개발”을 하고 싶은 사람에게 딱 맞는 선택이다.
- 설치가 아니라 “가져오기”
shadcn/ui는 라이브러리를 링크해 쓰는 게 아니라, 컴포넌트 소스를 내 프로젝트에 복사해 쓴다.
- 업데이트는 직접 관리
자동 업그레이드가 없다. 바뀐 템플릿이 필요하면shadcn add …로 새 소스를 받아 수동 머지한다.
- Tailwind 필수
스타일이 Tailwind 기반이다. v4(config-less)든 v3(설정 파일)든 전역 CSS 셋업이 먼저다.
- Next.js 친화적
문서/템플릿이 Next.js(App Router) 기준. 다른 환경도 가능하지만 Next.js가 제일 편하다.
➕ shadcn에서 버튼 커스텀화 — “기본 코드”와 “여기에 추가됨”만으로 끝내기
핵심:
src/components/ui/button.tsx안의buttonVariants(cva) 를 확장해variant/size/state를 정의하고,
사용처에서는variant,size,data-attr(예:selected)로 제어한다.
중요:pill은 형식(스타일) 예시일 뿐이고, 이름은 완전 자유다. 팀 컨벤션에 맞는 작명을 쓰면 된다.1) 기본 코드
// src/components/ui/button.tsx import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( [ "inline-flex items-center justify-center gap-2", "rounded-md text-sm font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", "disabled:pointer-events-none disabled:opacity-50", "[&_svg:not([class*='size-'])]:size-4", ].join(" "), { variants: { variant: { default: "bg-primary text-primary-foreground hover:opacity-90", outline: "border bg-transparent hover:bg-muted", ghost: "bg-transparent hover:bg-muted", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4", sm: "h-8 px-3 text-xs", lg: "h-11 px-6 text-base", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default" }, } ); export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; } export function Button({ className, variant, size, asChild, ...props }: ButtonProps) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size }), className)} {...props} /> ); }2) 버튼 커스텀화 (
variant/size/state)2-1) 새로운 스타일(variant)
예시 이름일 뿐이므로,pill,tile,cta,chip,brand등 원하는 이름으로 다 가능하다.// buttonVariants 내부 variants.variant 에 "여기에 추가됨" variant: { // ...기존... pill: "rounded-full bg-primary/90 text-white hover:bg-primary", pillChoice: "rounded-full bg-muted text-foreground hover:bg-muted/80 " + "data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground", tile: "rounded-xl border bg-card text-card-foreground hover:shadow", },
2-2) 새로운 크기(size)
// buttonVariants 내부 variants.size 에 "여기에 추가됨" size: { // ...기존... pill: "h-9 px-5 text-sm", },
2-3) 선택/토글 상태 키(selected)
// buttonVariants 내부 variants & defaultVariants 에 "여기에 추가됨" variants: { // ...기존 variant, size... selected: { true: "", false: "" }, }, defaultVariants: { // ...기존... selected: false, },
2-4) 컴포넌트 Prop & data-attr 연결
// 타입 확장 — "여기에 추가됨" export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; selected?: boolean; // 여기에 추가됨 } // 렌더 연결 — "여기에 추가됨" export function Button({ className, variant, size, asChild, selected, ...props }: ButtonProps) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" data-selected={selected} // 여기에 추가됨 className={cn(buttonVariants({ variant, size, selected }), className)} // 여기에 추가됨 {...props} /> ); }
2-5) 사용 예시
import { Button } from "@/components/ui/button"; export default function Demo() { return ( <div className="flex gap-2"> <Button variant="tile" size="lg">타일</Button> <Button variant="pillChoice" size="pill" selected>오늘</Button> <Button variant="pillChoice" size="pill">이번 주</Button> <Button variant="pill" size="pill">저장</Button> </div> ); }운영 팁
shadcn add button재실행 시 로컬 수정 덮어쓰기 주의 → 먼저 커밋하고 diff 머지.- 반복되는 페이지 커스텀은 즉시
variant/size로 승격해 일관성 유지.- 네이밍은 자유:
pill같은 예시 대신 팀 컨벤션(역할/톤/도메인)에 맞춰 작명한다.- 사이트 전체 톤 변경은 전역 색 토큰(globals.css / @theme) 조정으로 해결.
shadcn/ui 설치하기🧩 어떤 프로젝트 환경에서?
- 권장:
Next.js(앱 라우터)+React 18++Tailwind CSS- 런타임:
Node 18+- 패키지 매니저:
pnpm(권장) /npm/yarn- 비고:
Tailwind CSS가 없어도 초기화 과정에서 설정을 도와준다. (미리 깔려있어도 됨)
shadcn/ui는Next.js + TypeScript프로젝트 설치 이후에 바로 세팅하는게 제일 좋은 순서이다.
⬇️ 설치 명령어
pnpm dlx shadcn@latest init
- 한 번 실행해서 기본 설정을 만든 뒤, 필요한 컴포넌트를 골라서 추가하는 방식이다.
- 참고: 이 단계에서는 대부분 설정만 바뀐다.
components/ 폴더는 보통 아직 없다.
❓ 설치 명령어 실행 시 나오는 질문 (KO/EN)
프로젝트 상태에 따라 일부 항목은 자동 감지되어 생략되거나 경로 질문이 추가될 수 있다.
1. 어떤 스타일을 쓸까요? / Which style would you like to use?
- 예:
Default,New York(디자인 프리셋)
- 기본 색상을 무엇으로 할까요? / Which color would you like to use as the base color?
- 예:
slate,gray,zinc,neutral,stone등
- 색상용 CSS 변수를 설정할까요? / Configure CSS variables for colors?
- 라이트/다크 모드 토큰을 전역에 추가
- 전역 CSS 경로는 어디인가요? (덮어씁니다)
/ Where is your global CSS file? (this file will be overwritten)
- 예:
src/app/globals.css또는styles/globals.css
- 컴포넌트를 어디에 설치할까요? / Where should components be installed?
- 예:
components→ 내부에components/ui생성
- 유틸 파일 경로는 어디로 할까요? / Where should the utils file be located?
- 예:
lib/utils.ts(cn등 유틸)
- 임포트 별칭을 설정할까요? / Configure the import alias?
- 예:
@/*,@/components,@/lib등
- 필요한 의존성을 지금 설치할까요? / Install and configure required dependencies now?
Tailwind/애니메이션/유틸 패키지 설치
📦 설치가 끝나면: 생성·수정되는 것
✅ 폴더
components/
components/ui/: 컴포넌트 소스가 복사될 위치
lib/
lib/utils.ts:cn등 유틸 함수
✅ 파일
components.json
baseColor, 전역css경로,aliases등 초기화 답변이 기록됨
tailwind.config.*(갱신 또는 생성 V4버전일 경우tailwind.config.*이 없을 수도 있음)
- 프리셋/플러그인/테마 설정 반영
- 전역 CSS (예:
src/app/globals.css)
- 라이트/다크 CSS 변수 토큰 주입
tsconfig.json/jsconfig.json
@/*등 임포트 별칭 반영
package.json
- 의존성 추가/갱신
✅ 코드(내용)
- 전역 CSS에 컬러 토큰(
:root,.dark)과 기본 스타일 스니펫 추가tailwind.config에 애니메이션/프리셋/경로 등 반영- 별칭 기반 임포트 사용 가능:
@/components/ui/...,@/lib/utils
✅ 설치가 끝나면: 생성·수정되는 것 (정리)
항상 생성/수정(=
init결과)
components.json:baseColor, 전역 CSS 경로, 별칭 등 질문 답변이 기록된다.tailwind.config.*업데이트: 프리셋/플러그인/경로 반영.- 전역 CSS(예:
src/app/globals.css) 갱신: 라이트/다크 CSS 변수 토큰 주입.lib/utils.ts:cn등 유틸 생성(또는 갱신).tsconfig.json/jsconfig.json:@/*등 경로 별칭 반영.package.json: 필요한 의존성 추가/갱신.중요: 이 시점엔
components/폴더가 없을 수 있다. 정상이다.
첫 컴포넌트를 추가할 때 생성(=
add결과)
components/및components/ui/…: 실제 컴포넌트 소스 파일이 여기 생긴다.- (필요 시) Radix/아이콘 등 추가 의존성이 함께 설치된다.
🧪 간단 테스트 (동작 확인)
- 컴포넌트 하나 추가
pnpm dlx shadcn@latest add button.
2. 페이지에서 사용// 예: src/app/page.tsx import { Button } from "@/components/ui/button"; --- export default function Page() { return <Button variant="outline">확인</Button>; }
- 로컬 실행
pnpm dev
- 브라우저에서 페이지를 열었을 때
Button이 정상 렌더링되면 성공이다.
🧾
components.json핵심 필드
init에서 답한 값이 기록되고, 이후add로 가져오는 컴포넌트의 규칙이 된다.{ "aliases": { "components": "@/components", "utils": "@/lib/utils" }, "tailwind": { "css": "src/app/globals.css", "baseColor": "slate", "cssVariables": true } }
aliases.components: 컴포넌트 임포트 기준 경로 (@/components→ 실제src/components)aliases.utils: 유틸 경로 (@/lib/utils)tailwind.css: 전역 CSS 경로tailwind.baseColor: 초기 팔레트 베이스 색tailwind.cssVariables: 라이트/다크 컬러 토큰 활성화
📂 폴더/파일 트리 (상태별)
init직후project/ ├─ src/ │ └─ app/ │ └─ globals.css # 전역 CSS(컬러 토큰 주입) ├─ lib/ │ └─ utils.ts # cn 유틸 ├─ components.json # 초기화 답변 기록 ├─ tailwind.config.(js|ts)? # v3/설정 파일 방식이면 존재, v4 config-less면 없을 수 있음 ├─ tsconfig.json # @/* 등 별칭 └─ package.json # 의존성.
첫 컴포넌트
add후project/ └─ src/ ├─ app/... ├─ lib/utils.ts └─ components/ └─ ui/ └─ button.tsx # 예: add button 시 생성
components/ui/*는add시점에 생긴다.init만으로는 안 생겨도 정상이다.
🎨 Tailwind 설정에 따른 전역 CSS (두 가지 케이스)
0) 개념 정리
Tailwind와
shadcn/ui를 함께 쓰다 보면 전역에 여러 색 토큰이 생긴다.
이걸 이해하려면 두 개의 개념만 기억하면 된다.
- 원시 팔레트 (Primitive)
→ 피그마나 디자인 시스템에서 정한 “색 그 자체”
예:--brand-500,--gray-100,--blue-400
- 의미 토큰 (Semantic)
→ 컴포넌트가 쓰는 “역할 이름”
예:--primary,--background,--border보통은 원시 팔레트를 유지하면서 의미 토큰을 거기에 매핑한다.
즉, 색의 원본은 그대로 두고, 각 컴포넌트가 이해할 수 있는 이름만 붙이는 방식이다.
1) shadcn이 전역에 “추가하는 것”(정확히)
🧩 코어 (필수)
대부분의 컴포넌트가 참조하는 핵심 색상이다.
--background / --foreground --primary / --primary-foreground --muted / --muted-foreground --border, --input, --ring --destructive / --destructive-foreground🎨 편의 (선택)
일부 패턴에서만 쓰이는 보조 색상이다.
--card / --card-foreground --popover / --popover-foreground --secondary / --secondary-foreground --accent / --accent-foreground⚙️ Tailwind 유틸 매핑
- v3 (설정파일 방식) →
theme.extend.colors.{background, primary, …} = hsl(var(--...))- v4 (config-less) →
@theme inline { --color-background: var(--background); … }
정리 요약
코어 토큰은 꼭 유지하고, 편의 토큰은 필요할 때만 추가하면 된다.
(토큰 정의 + 매핑 둘 다 생략 가능)
2) 일반적으로는 이렇게 동작한다
보통
shadcn init을 실행하면globals.css에
기본 팔레트가 자동 추가되고, 그 색을 shadcn의 토큰으로 주입하는 구조가 만들어진다.:root { --background: 0 0% 100%; --foreground: 222 84% 5%; --primary: 221 83% 53%; --muted: 210 40% 96%; --border: 214 32% 92%; /* … 다른 토큰들 */ } @theme inline { --color-background: var(--background); --color-primary: var(--primary); /* … */ }즉,
“shadcn이 기본 색 구조를 만들어두고,
Tailwind가 그걸 유틸(bg-background,text-foreground)로 변환해 쓰는”
전형적인 패턴이다.
✅ 케이스 A —
tailwind.config.(js|ts)있음 (v3 스타일 또는 v4에서도 설정 파일 사용하는 경우)
tailwind.config.ts(예시)import type { Config } from "tailwindcss"; . export default { content: [ "./src/app/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}", "./src/lib/**/*.{ts,tsx}" ], theme: { extend: { colors: { background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: "hsl(var(--primary))", muted: "hsl(var(--muted))", border: "hsl(var(--border))" } } }, plugins: [require("tailwindcss-animate")] } satisfies Config;
globals.css(예시)@tailwind base; @tailwind components; @tailwind utilities; . /* 1) 디자인 토큰 */ @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --muted: 210 40% 96.1%; --border: 214.3 31.8% 91.4%; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --primary: 217.2 91.2% 59.8%; --muted: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%; } } . /* 2) 기본 바탕 적용 */ @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } }이 방식은 색 이름(
bg-background,text-foreground)을 config의theme.extend.colors에서hsl(var(--...))로 매핑해 쓴다.
🅱️ 케이스 B - Tailwind v4 Config-less 방식
꼭
shadcn이 추가해준 기본 팔레트를 그대로 쓸 필요는 없다.
프로젝트 브랜드 팔레트가 이미 명확하다면,
globals.css안의@theme inline에 직접 커스텀 토큰을 정의할 수도 있다.
즉,
“원시 팔레트 + 의미 토큰”으로 분리하지 않고,
한 번에 inline에서 색을 정의하는 구조다.
(config 파일도 필요 없다)
✅ 예시 —
globals.css한 곳에서 커스텀 토큰 정의하기/* 0) Tailwind 활성화 */ @import "tailwindcss"; /* 1) 커스텀 토큰을 직접 정의 (inline에서 바로 의미 토큰화) */ @theme inline { /* radius scale */ --radius-sm: 0.4rem; --radius-md: 0.6rem; --radius-lg: 0.8rem; --radius-xl: 1rem; --radius: var(--radius-md); /* core palette (브랜드 색 직접 입력) */ --background: #fafafa; --foreground: #111827; --primary: #06c; --primary-foreground: #ffffff; --muted: #f5f5f5; --muted-foreground: #525252; --border: #e5e5e5; --input: #d4d4d4; --ring: #60a5fa; --destructive: #dc2626; --destructive-foreground: #ffffff; /* 필요한 편의 토큰만 선택적으로 추가 */ --card: var(--background); --card-foreground: var(--foreground); } /* 2) Base Reset */ @layer base { :root { color-scheme: light; } /* 다크모드 미사용 */ html { font-family: Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-size: 62.5%; } body { background: var(--background); color: var(--foreground); line-height: 1.6; min-height: 100dvh; } * { @apply border-border; } } /* 3) 팀 공통 타이포그래피 유틸 (선택) */ @layer utilities { .t-14-b { font-size: 1.4rem; font-weight: 700; } /* … */ }.
💬 이렇게 구성했을 때의 장점
구분 일반적인 shadcn 방식 커스텀 inline 방식 팔레트 관리 shadcn 기본 팔레트 자동 주입 프로젝트 브랜드 색 직접 정의 파일 구조 설정파일(config) 또는 별도 팔레트 필요 globals.css한 곳에서 관리유지보수 shadcn 업데이트에 영향 받음 내가 정의한 토큰만 관리 다크모드 기본 포함 미사용 (Light 고정 가능) 확장성 토큰이 많고 복잡 필수 토큰만 정의해 단순함 💡 정리하자면
대부분의 프로젝트는 shadcn이 만들어준 팔레트를 그대로 사용하지만,
Tailwind v4 환경에서는globals.css한 곳에서 직접 커스텀 토큰을 정의하는 방식도 충분히 가능하다.이 방법은
- 의미 없는 기본 토큰을 제거하고,
- 프로젝트의 실제 색상을 한눈에 볼 수 있으며,
- 유지보수가 훨씬 단순해진다는 장점이 있다.
즉, “shadcn의 구조를 그대로 쓰기보다 내 디자인 시스템을 중심으로 재정의한다”는 관점이다.
🧰
lib/utils.ts의cn유틸조건부 클래스를 합칠 때 중복을 깔끔하게 정리한다.
// lib/utils.ts import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; . export function cn(...inputs: unknown[]) { return twMerge(clsx(inputs)); }사용 예시
import { cn } from "@/lib/utils"; . export function Chip({ active }: { active?: boolean }) { return ( <span className={cn( "inline-flex items-center rounded-md px-2 py-1 text-sm", active ? "bg-primary text-white" : "bg-muted text-foreground/70" )} Label </span> ); }.
🔗 경로 별칭 동작 확인
tsconfig.json에서@/* → src/*매핑을 맞춘다.{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }
components.json의aliases.components가"@/components"라면 실제 생성 위치는src/components다.
🌓 (선택) 테마 Provider 배선
다크 모드를 쓸 계획이면 루트 레이아웃에서 한 번만 감싼다.
// src/app/layout.tsx import { ThemeProvider } from "next-themes"; import "./globals.css"; . export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko" suppressHydrationWarning> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider> </body> </html> ); }👉 한 줄 요약:
init은 규칙과 토큰을 깐다.
Tailwind는 “설정 파일 방식”과 “v4 config-less” 두 갈래로 전역 CSS가 달라진다.
이후add가 실제 컴포넌트를 복사한다.
1) 필요한 컴포넌트 고르기 → 추가
# 예: 버튼만 pnpm dlx shadcn@latest add button -- # 예: 버튼 + 입력 + 다이얼로그 pnpm dlx shadcn@latest add button input dialog
- 템플릿 소스가
components/ui/*에 복사된다.- 필요한 의존성(예:
@radix-ui/react-*,class-variance-authority,tailwind-merge,clsx)이 없는 경우에만 자동 추가된다.- 동일 파일이 있으면 덮어쓸지 확인한다.
2) 생성 위치 빠른 체크
src/ ├─ components/ │ └─ ui/ │ ├─ button.tsx │ ├─ input.tsx │ └─ dialog.tsx # 예시 └─ lib/utils.ts
components.json의aliases.components가"@/components"이면 실제 폴더는 보통src/components다.
보이지 않으면tsconfig.json의@/* → src/*매핑과components.json의 별칭을 맞춘다.
3) 바로 써보기
A.
Button// src/app/page.tsx import { Button } from "@/components/ui/button"; . export default function Page() { return ( <div className="p-6 space-x-2"> <Button>기본</Button> <Button variant="outline">아웃라인</Button> <Button size="sm">작게</Button> </div> ); }B.
Input+labelimport { Input } from "@/components/ui/input"; . export default function FormMini() { return ( <form className="p-6 space-y-2"> <label htmlFor="email" className="text-sm font-medium">이메일</label> <Input id="email" type="email" placeholder="you@example.com" /> </form> ); }C.
Dialog"use client"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; . export default function DemoDialog() { const [open, setOpen] = useState(false); return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button variant="outline">열기</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>알림</DialogTitle> <DialogDescription>다이얼로그가 열렸다.</DialogDescription> </DialogHeader> </DialogContent> </Dialog> ); }상호작용이나 상태가 있는 파일은 상단에
use client를 선언한다.
4) UI 커스터마이징
className으로 국소 덮어쓰기대부분 컴포넌트는 내부에서
cn(내 클래스, className)순으로 병합한다.
겹치면 내가 넘긴className이 우선한다. 색·크기·여백처럼 미세 조정에 적합하다.<Button className="h-11 px-6 bg-emerald-600 hover:bg-emerald-700"> 저장 </Button> . <DialogContent className="max-w-lg rounded-2xl p-8">...</DialogContent>더 큰 톤 변경이 필요하면
전역 HSL 토큰을, 재사용 표준화가 필요하면cva의variant·size를 고려하면 된다.
5) 접근성/UX 포인트
label↔input을 연결한다. 스크린리더가 필드명을 읽고 라벨 클릭으로 포커스가 이동한다.<label htmlFor="email" className="text-sm font-medium">이메일</label> <Input id="email" type="email" placeholder="you@example.com" />오류 문구가 있으면
aria-describedby="email-error"로 연결한다.
Dialog/Sheet/Popover는Esc로 닫힘과 포커스 트랩이 동작한다. 제목/설명은DialogTitle,DialogDescription으로 전달한다.<Dialog> <DialogTrigger asChild><Button>열기</Button></DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>알림</DialogTitle> <DialogDescription>설명을 여기에.</DialogDescription> </DialogHeader> </DialogContent> </Dialog>.
- 포커스 링은 유지한다. 제거했다면 대체 포커스 스타일을 제공한다.
<Button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ring-offset-background"> 저장 </Button>.
6) 업데이트/삭제 — 왜, 언제, 어떻게
왜
복사된 템플릿 코드는 자동으로 갱신되지 않는다.
버그·접근성 개선이나Tailwind/Next 변화에 맞추려면 업데이트가 필요하다.
쓰지 않는 컴포넌트·패키지를 정리하면 번들 크기와 유지보수 부담이 줄어든다.
언제
메이저 업그레이드 전후, 접근성/렌더 이슈 발견 시, 전역 토큰 변경 후 일부 톤이 어긋날 때, 템플릿 신규 구조를 통일하고 싶을 때가 적기다.
어떻게
업데이트(재-
add)# 변경사항 커밋 후 pnpm dlx shadcn@latest add button # 다르면 덮어쓸지 묻는다. 커스터마이징이 크면 임시 파일로 받아 수동 머지한다..
삭제
1.components/ui/x.tsx를 제거한다.
2. 전역의import와 JSX 사용처를 정리한다.
3. 필요하면 의존성도 정리한다.pnpm why @radix-ui/react-dialog pnpm remove @radix-ui/react-dialog.
7) 흔한 오류 빠른 진단
- 경로 에러:
@/* → src/*(tsconfig.json)와components.json의 별칭이 실제 경로와 일치하는지 확인한다.
- 스타일 미적용:
- v4(config-less): 전역 CSS에
@import "tailwindcss"와@theme가 있는지 확인한다.- v3/설정 파일:
@tailwind base/components/utilities와theme.extend.colors매핑을 확인한다.
- 상호작용 미동작:
use client선언 누락을 확인한다.
- 의존성 누락:
add를 재실행하거나package.json변경을 확인한다.
✅ 1) 준비 점검:
Next.js/Tailwind/ 경로 별칭1-1. 런타임·패키지 버전
node -v # >= 18 pnpm -v jq -r '.dependencies.next,.dependencies.react,.devDependencies.typescript' package.json
- 권장:
next@14+,react@18+,typescript사용
1-2. App Router 구조
src/ └─ app/ ├─ layout.tsx └─ page.tsx
layout.tsx에서 전역 CSS를 불러온다:// src/app/layout.tsx import "./globals.css";.
1-3. Tailwind 모드 결정 + 전역 CSS 경로 확정 (중요)
전역 CSS는
src/app/globals.css로 통일한다. 파일이 없다면 지금 만든다.
아래 두 모드 중 하나를 택해 내용만 넣어준다.
A) v4 config-less (설정 파일 없음)
- 루트에
tailwind.config.*없어도 된다.- 전역 CSS는 한 줄이면 된다:
/* src/app/globals.css */ @import "tailwindcss";
- 토큰/테마는 5단계에서
@theme등으로 추가한다.
B) 설정 파일 방식 (v3 또는 v4-with-config)
- 루트에
tailwind.config.(js|ts)존재.- 전역 CSS는 지시문을 사용:
/* src/app/globals.css */ @tailwind base; @tailwind components; @tailwind utilities;.
1-4. 경로 별칭 정렬 (
@/* → src/*)// tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }이후
shadcn의components.json에서"aliases.components": "@/components"로 잡으면 실제 물리 경로는src/components가 된다.
1-5. 빠른 시동 점검
pnpm dev # 에러 없이 서버가 뜨는지 확인.
⚙️ 2)
shadcn init실행2-1. 명령 실행
pnpm dlx shadcn@latest init
- 초기 세팅만 잡는 단계다. 이 시점엔
components/ui/*폴더가 없어도 정상이다(다음 단계add에서 생성).
2-2. “왜 Base color만 떴지?”
shadcnCLI가 프로젝트 상태를 자동 감지해서 비어 있는 항목만 질문한다.
Next.js + TS를 처음부터 세팅 직후엔 질문이 여러 개 나오지만, 중간 도입이면 이미 있는 설정을 스킵해 “Base color만” 묻는 경우가 흔하다.
2-3. 이번 시나리오(질문: Base color만) — 선택값 & 수정 포인트
- 내가 고른 Base color:
zinc향후 바꾸고 싶다면
components.json에서 수정할 수 있다{ "aliases": { "components": "@/components", "utils": "@/lib/utils" }, "tailwind": { "css": "src/app/globals.css", "baseColor": "zinc", // ← 여기 값을 바꾸면 됨 (예: "slate") "cssVariables": true } }
- 참고:
baseColor를 바꿔도 이미 주입된globals.css의 토큰 값은 자동 변경되지 않는다.
전역 색 토큰을 함께 바꾸려면globals.css의 HSL 변수(5단계에서 다룸)를 수동 조정하거나, 필요 시 템플릿을 다시 들여와 반영하는 식으로 맞춘다.
🗂️ 3) 생성물 확인
3-1. 현재 디렉터리 상태(요약)
project/ ├─ src/ │ ├─ app/ │ │ └─ globals.css # 전역 CSS (v4 config-less) │ └─ lib/ │ └─ utils.ts # cn 유틸 ├─ components.json # shadcn 설정 └─ (없음) src/components/ui # ← 컴포넌트 소스는 아직 없음 (다음 단계 add).
3-2.
components.json{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "src/app/globals.css", "baseColor": "zinc", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "registries": {} }핵심 포인트
"style": "new-york": 프리셋(스타일 톤)."tailwind.baseColor": "zinc": 베이스 컬러. 필요하면 여기서 값만 바꿔도 됨."tailwind.config": "": v4 config-less 모드(설정 파일 없음)."tailwind.css": "src/app/globals.css": 전역 CSS 경로."aliases":@/components,@/lib,@/components/ui등을 바로 임포트할 수 있음."iconLibrary": "lucide": 아이콘 기본 라이브러리.
3-3.
lib/utils.ts(cn유틸)// src/lib/utils.ts import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; . export function cn(...inputs: unknown[]) { return twMerge(clsx(inputs)); }
className병합/중복 해소에 사용한다.
3-4.
globals.css— 왜 많았고, 뭐가 추가된 거가요? 그리고 무엇을 남겼나왜 많았나
shadcn init는 바로 써볼 수 있게 테마 토큰(라이트/다크), 베이스 스타일, Tailwind v4용@theme매핑을 한꺼번에 넣는다.
- v4에서는
@import "tailwindcss"+@theme/@theme inline만으로bg-background,text-foreground같은 유틸 클래스가 생성되도록 설계되어 있다.
이번에 실제로 어떤 것들이 추가/변경됐나 (핵심)
아래 내용은 기본적으로
shadcn init직후globals.css에 반영되는 전형적인 항목들이다.
(프로젝트/버전에 따라 표현이 조금 달라질 수 있음. 그래도 “무엇을, 왜 넣는지”는 같음)
1. Tailwind v4 활성화
@import "tailwindcss";한 줄이 들어간다.
→ Tailwind 유틸리티들이 빌드에 포함되도록 엔진을 켠다.
- 색 토큰(라이트/다크) 주입
- 라이트 모드 기본값:
:root { --background, --foreground, --primary, … }가 추가된다.- 다크 모드 덮어쓰기:
.dark { --background, --foreground, --primary, … }가 함께 들어간다.
→ 컴포넌트들이 공통적으로 참조할 의미 토큰(semantic) 세트를 만들어 준다.
- Tailwind 유틸 매핑용
@theme inline
@theme inline { --color-background: var(--background); … }가 들어간다.
→bg-background,text-foreground,border-border,ring같은 유틸 클래스가 생성되도록
의미 토큰 → Tailwind 색 이름표(--color-*)로 연결한다.
- 베이스 레이어 기본값
@layer base블록에서 다음이 설정된다
body { background: var(--background); color: var(--foreground); }- 포커스 링:
:focus-visible { outline: 2px solid var(--ring); … }- 텍스트 선택 색:
::selection { background: var(--primary); color: var(--primary-foreground); }- (종종) 전역 보더 색:
* { @apply border-border; }- (종종) 폰트 스무딩 / 숫자표기 / 모션 감소 등 접근성 기본값
- 반경/애니메이션 등 공통 토큰
--radius계열(예:--radius-sm/md/lg)과tailwindcss-animate기반 애니메이션 프리셋이 포함되기도 한다.
→ shadcn 컴포넌트의 모서리/포커스/모션 일관성을 위한 공통 설정.
- (가끔) 데모/예시용 토큰
- 템플릿 미리보기를 위해 차트/사이드바 같은 예시 토큰이 따라오는 경우가 있다.
→ 실제 프로젝트에선 불필요하면 삭제해도 무방하다.
기본 주입 예시(요약)
@import "tailwindcss"; /* 라이트/다크 의미 토큰 */ :root { --background: 0 0% 100%; --foreground: 222 84% 5%; --primary: 221 83% 53%; --muted: 210 40% 96%; --border: 214 32% 92%; --ring: 217 100% 50%; /* ... */ } .dark { --background: 222 84% 5%; --foreground: 210 40% 98%; --primary: 217 92% 60%; /* ... */ } /* Tailwind 유틸 매핑 */ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-primary: var(--primary); --color-primary-foreground: #fff; --color-muted: var(--muted); --color-border: var(--border); --color-ring: var(--ring); /* ... 필요시 card/secondary/accent 등 편의 토큰 */ } /* 베이스 적용 */ @layer base { body { background: var(--color-background); color: var(--color-foreground); } :focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; } ::selection { background: var(--color-primary); color: var(--color-primary-foreground); } /* ... */ }이게 “init 직후의 기본 상태”다.
이후 우리는 이 기본값을 우리 프로젝트에 맞게 슬림화/정돈(다크 제거, 불용 토큰 삭제, 의미 토큰 최소 세트 유지, 커스텀 용도 토큰 추가 등)했다.
나는 이렇게 커스텀화했다
- 색상은
@theme inline의 의미 토큰이 단일 소스가 되도록 정리했다.
→ 팔레트 값은 바로 의미 토큰에 입력(별도의 원시 팔레트/다크 세트 미유지).
- 필수 코어 토큰만 남기고, 편의 토큰은 전부 제거(필요해질 때만 추가).
- 우리 문서에 있던 “사용처 주석”을 실제 토큰으로 승격해, 팀이 어디에 무엇을 써야 하는지 이름만 봐도 이해되게 했다.
- 베이스 레이어는 모두
--color-*로 통일해 Tailwind 유틸(bg-background,text-foreground,border-border,ring)과 일관되게 동작하도록 했다.
- 다크 모드를 쓰지 않으므로 관련 토큰/스타일은 전부 제거해 복잡도를 낮췄다.
수정된 globla.css (수정 코드만)
/* ------------------------------------------------- 0) Tailwind & 외부 의존성 ------------------------------------------------- */ @import "tailwindcss"; /* Pretendard: pnpm add @fontsource/pretendard */ @import "@fontsource/pretendard/400.css"; @import "@fontsource/pretendard/500.css"; @import "@fontsource/pretendard/600.css"; @import "@fontsource/pretendard/700.css"; /* 선택: 애니메이션 유틸 */ @import "tw-animate-css"; /* (주의) 다크모드 미사용: dark 변형자/변수 정의 없음 */ /* ------------------------------------------------- 1) Design Tokens — 브랜드/테마 (Light only) - @theme: 원본 브랜드/팔레트 - @theme inline: Tailwind/shadcn이 쓰는 이름으로 매핑 ------------------------------------------------- */ @theme { /* === Font stacks === */ ... /* === Breakpoints === */ ... /* === Colors === */ --color-black: #000000; /* 메인 텍스트 */ --color-white: #ffffff; /* === Brand Colors === */ --primary-blue: #06c; --primary-gray: #111827; --primary-red: #dc2626; /* === Gray Scale === */ --color-gray-900: #111827; /* 랜딩/로그인 메인 텍스트, 제목, CTA 텍스트, 로그인버튼 배경, 링크 */ --color-gray-800: #1f1f1f; /* 모든 버튼 :hover */ --color-gray-700: #2e2e2e; /* 인풋 라벨 텍스트 */ --color-gray-600: #525252; /* 보조 텍스트, Nav 텍스트, 설명문, 푸터 텍스트, 안내문구 */ --color-gray-500: #737373; /* 플레이스홀더, 비활성 텍스트, 간편 로그인 제목 */ --color-gray-400: #a3a3a3; /* 서브 경계선, 보조 아이콘, 비활성 버튼 배경 */ --color-gray-300: #d4d4d4; /* 버튼 테두리, 입력 필드 테두리 */ --color-gray-200: #e5e5e5; /* 카드/모달 테두리, 구분선 */ --color-gray-100: #f5f5f5; /* 버튼 배경 */ --color-gray-50: #fafafa; /* 푸터/페이지 기본 배경 */ /* === Error / Danger Scale === */ --color-danger-600: #dc2626; /* 에러/경고 상태, 경고 버튼 */ /* === Blue Scale === */ --color-blue-600: #2563eb; /* 진한 블루: 모달 헤더, 주요 CTA 강조 */ --color-blue-500: #3b82f6; /* 기본 블루: 일반 버튼, 포인트 */ --color-blue-400: #60a5fa; /* 밝은 블루: hover 보조 강조 */ --color-blue-100: #3b82f61a; /* 연한 블루 배경 (10% 투명도 느낌) */ /* === Radius & Shadow === */ --radius-2xl: 1rem; --shadow-soft: 0 6px 16px rgba(0, 0, 0, 0.08); } /* === Tailwind/shadcn 매핑 (Light) ============================ - bg-background → var(--color-background) - text-foreground → var(--color-foreground) - border-border → var(--color-border) - ring, input, muted 등 컴포넌트 공통 토큰 - 버튼/플레이스홀더/비활성/링크/호버 등 "주석 용도"에 맞춘 커스텀 토큰 추가 ============================================================ */ @theme inline { /* radius scale (shadcn 컴포넌트 대응) */ --radius-sm: calc(var(--radius-2xl) - 8px); --radius-md: calc(var(--radius-2xl) - 4px); --radius-lg: var(--radius-2xl); --radius-xl: calc(var(--radius-2xl) + 4px); --radius: var(--radius-lg); /* === shadcn 코어 토큰 (필수) === */ --color-background: var(--color-gray-50); /* 페이지/푸터 배경 */ --color-foreground: var(--color-gray-900); /* 메인 텍스트/제목/CTA 텍스트 */ --color-primary: var(--primary-blue); /* 브랜드 프라이머리 (CTA, 액션) */ --color-primary-foreground: #ffffff; /* 프라이머리 위 텍스트 */ --color-muted: var(--color-gray-100); /* 버튼/보조 영역 배경 */ --color-muted-foreground: var(--color-gray-600); /* 보조 텍스트/Nav/설명문 */ --color-border: var(--color-gray-200); /* 카드/모달 테두리, 구분선 */ --color-input: var(--color-gray-300); /* 입력 필드 테두리 */ --color-ring: var(--color-blue-400); /* 포커스 링 */ --color-destructive: var(--color-danger-600); /* 위험/에러 */ --color-destructive-foreground: #ffffff; /* === 주석 기반 "사용처 특화" 커스텀 토큰 === */ --color-button-solid: var(--color-gray-900); /* 로그인 버튼 배경(고정 다크) */ --color-button-solid-foreground: #ffffff; /* 로그인 버튼 텍스트 */ --color-button-hover: var(--color-gray-800); /* 모든 버튼 :hover(다크계열) */ --color-placeholder: var(--color-gray-500); /* 인풋 플레이스홀더 */ --color-label: var(--color-gray-700); /* 인풋 라벨 텍스트 */ --color-disabled-foreground: var(--color-gray-500); /* 비활성 텍스트 */ --color-disabled-background: var(--color-gray-400); /* 비활성 버튼 배경 */ --color-subtle-border: var(--color-gray-400); /* 서브 경계선 */ --color-link: var(--color-gray-900); /* 텍스트 링크 (비밀번호/회원가입) */ /* === 블루 계열 특화 (선택적으로 사용) === */ --color-cta: var(--color-blue-600); /* 주요 CTA 강조 */ --color-cta-foreground: #ffffff; --color-button: var(--color-blue-500); /* 일반 버튼 */ --color-button-foreground: #ffffff; --color-hover-accent: var(--color-blue-400); /* 보조 hover 강조 */ --color-subtle-accent-bg: var(--color-blue-100); /* 연한 블루 배경 */ } /* ------------------------------------------------- 2) Base Reset & a11y ------------------------------------------------- */ @layer base { :root { color-scheme: light; } /* 라이트 고정 */ html { font-size: 62.5%; /* 1rem = 10px */ font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; font-feature-settings: "liga", "kern"; font-variant-numeric: tabular-nums; font-synthesis-weight: none; } html:lang(ko) { font-family: var(--font-sans-ko); } body { background: var(--color-background); color: var(--color-foreground); line-height: 1.6; letter-spacing: -0.01em; min-height: 100dvh; } /* 페이지 공통 컨테이너 (선택) */ .container { max-width: 1200px; margin-inline: auto; padding-inline: 1.6rem; } /* 접근성: 키보드 포커스 링 */ :focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; } @media (forced-colors: active) { :focus-visible { outline: 2px solid CanvasText; } } /* 텍스트 선택 */ ::selection { background: var(--color-primary); color: var(--color-primary-foreground); } /* 과도한 모션 방지 */ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } } /* 모든 요소에 기본 border 색/outline 색만 지정 (폭은 건드리지 않음) */ @layer base { * { @apply border-border; outline-color: oklch(from var(--color-ring) l c h / 0.5); } } /* ------------------------------------------------- 3) 팀 공통 타이포그래피 유틸 ------------------------------------------------- */ @layer utilities { /* 폰트 유틸 ... */.
이렇게 줄여도
add시 괜찮나?
- 문제 없음. 런타임 에러는 없고, 카드/팝오버/세컨더리/액센트류도 폴백 덕분에 기본 스타일이 안전하게 나온다.
- 더 섬세한 톤이 필요해지면 토큰 값만 조정하면 된다(컴포넌트 소스 수정 불필요).
5-1. 컴포넌트 추가
# 하나씩 pnpm dlx shadcn@latest add button pnpm dlx shadcn@latest add input # 여러 개 한 번에 pnpm dlx shadcn@latest add button input
- 실행하면 소스가 프로젝트에 복사되고, 필요한 Radix / 유틸 의존성이 자동 추가됨.
5-2. 생성물 확인
src/ ├─ components/ │ └─ ui/ │ ├─ button.tsx │ └─ input.tsx └─ lib/utils.ts # cn 유틸(이미 있음)
- 안 보이면:
components.json의aliases.ui(예:"@/components/ui")와tsconfig.json의@/* → src/*매핑 확인.
button.tsx — 구조 이해
0) imports
import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils";
- Slot:
<Button asChild><Link/></Button>처럼 다른 태그에 버튼 스타일 이식- cva/VariantProps: variant/size 규칙 선언 + 타입 안전
- cn: 클래스 병합 유틸
1) cva로 “규칙 세트” 선언
const buttonVariants = cva( "inline-flex items-center ... focus-visible:ring-[3px] ...", { variants: { variant: { default: "...", outline: "...", ghost: "...", /* ... */ }, size: { default: "h-9 px-4", sm: "h-8 px-3", lg: "h-10 px-6", icon: "size-9" } }, defaultVariants: { variant: "default", size: "default" } } );
- 베이스: 공통 레이아웃/타이포, 아이콘 규칙(
&[_svg]), 포커스/에러 a11y 포함- variants: 시각 스타일(
variant) & 크기(size) 이름으로 선택- defaultVariants: 옵션 생략 시 기본값
2) 베이스가 하는 일 (대표 라인만)
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium", "disabled:pointer-events-none disabled:opacity-50", "[&_svg:not([class*='size-'])]:size-4", // 아이콘 기본 16px "focus-visible:ring-ring/50 focus-visible:ring-[3px]", // 키보드 포커스 "aria-invalid:border-destructive" // 에러 상태
- 접근성/상태/아이콘 처리를 베이스에서 표준화
3) 실제 컴포넌트
function Button({ className, variant, size, asChild=false, ...props }: Props) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn( buttonVariants({ variant, size }), className )} {...props} /> ); }
asChild: 다른 태그로 렌더하되 버튼 스타일 유지
cn 병합: cva 결과 + 추가 클래스 충돌 없이 결합
커스텀 포인트(버튼)팀 공통 스타일 넣기:
variants.variant에 키 추가/정리크기 체계:
variants.size확장기본 규격 변경: 베이스 문자열 수정
input.tsx — 구조 이해
0) imports
import * as React from "react"; import { cn } from "@/lib/utils";
- cn: 공통 클래스 병합
1) 베이스(폼 규격) + 상태 클래스
function Input({ className, type, ...props }) { return ( <input type={type} data-slot="input" className={cn( "h-9 w-full rounded-md border bg-transparent px-3 py-1 text-base md:text-sm", "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:border-destructive aria-invalid:ring-destructive/20", "disabled:pointer-events-none disabled:opacity-50", "file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium", className )} {...props} /> ); }
베이스: 크기/라운드/보더/배경/폰트 사이즈
placeholder/selection: 플레이스홀더 컬러, 선택 배경/텍스트 색
focus-visible: 키보드 포커스용 보더/링
aria-invalid: 에러 상태 보더/링(접근성 API)
file:: 파일 입력 기본 버튼 최소 보정
커스텀 포인트(인풋)기본/포커스/에러 보더 정책:
border·focus-visible:*·aria-invalid:*라운드/높이/패딩: 베이스 문자열
라벨 컬러: 인풋 외부 요소 → 사용처에서 지정
5-3. 로컬 서버 렌더 테스트
page.tsx에button.tsx와input.tsx를 불러와서 에러가 안나는지 테스트 해보았다.// src/app/test-shad/page.tsx import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; . export default function Home() { const compact = false; return ( <main className={cn("p-8 space-y-8", compact && "p-6 space-y-6")}> <section className="space-y-2"> <label htmlFor="email" className="text-sm font-medium">이메일</label> <Input id="email" type="email" placeholder="you@example.com" /> </section> <section className="space-x-2"> <Button>버튼 테스트</Button> <Button variant="outline">아웃라인</Button> </section> </main> ); }
로컬 서버를 실행해서
인풋 + 버튼 2개(기본/아웃라인)이 정상적으로 렌더되는 것을 볼 수 있다.
5-4. 커스텀 테스트
커스텀마이즈
페이지마다
className을 덧붙이는 대신, 공통 규격을 소스 파일에 직접 심었다.
그래서button.tsx와input.tsx를 필수만 남기고 슬림하게 손봤다.
왜 “소스 직접 수정”을 택했나
- 일관성: 어디서 쓰든 같은 모양·동작 보장
- 유지보수: 한 곳만 고치면 전역 반영
- 속도: 사용처 코드는
<Button />,<Input />처럼 짧고 단순해짐
어디를 어떻게 건드렸나 (핵심만)
Button (
src/components/ui/button.tsx)
shadcn 기본
cva구조는 유지, 불필요한 variant/size는 정리팀에서 반복해 쓸 공동 스타일을 단일
variant로 고정즉,
variants블록에서 규격(라운드/높이/간격/상태)을 기본값으로 확정
Input (src/components/ui/input.tsx)
기본 유틸을 우리 베이스 규격으로 교체
기본/포커스/에러 등 상태 규칙을 소스에 일괄 정의
라벨 색은 인풋 바깥 요소라 사용처에서만 지정하도록 분리
포인트: “페이지 커스텀”이 아니라 “컴포넌트 기본값”을 바꿨다. → 공통 UX 확보
➕ 앞으로 스타일이 늘어나면 (운영 가이드)
- 공통으로 반복 →
button.tsx의variants에 새variant추가
(예:soft,danger,ghost)
- 크기 체계 필요 →
size에sm / md / lg추가하고defaultVariants.size로 기본값 지정
- 도메인 전용 버튼 → 원본을 감싼 래퍼 컴포넌트로 분리 (예:
FilterButton)
- 사이트 전반 톤 변경 → 전역 토큰(globals.css)만 교체 (코드 수정 없이 전역 반영)
- 일회성 실험 → 사용처에서
className임시 적용(반복되면 즉시variant/래퍼로 승격)