Shadcn: Shadcn TailwindCSS ์„ธํŒ…

NuyHesยท2026๋…„ 2์›” 24์ผ

ํŠœํ† ๋ฆฌ์–ผ

๋ชฉ๋ก ๋ณด๊ธฐ
34/34
post-thumbnail

Shadcn์ด๋ž€?

๐ŸŒshadcn/ui

๊ธฐ๋ณธ์ ์ธ UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ Shadcn์˜ ํฐ ์ฐจ์ด๋Š” ์ฝ”๋“œ๊ฐ€ ์–ด๋””์— ์ €์žฅ๋˜๋Š”๊ฐ€? ์ด๋‹ค.

์ฐจ์ด์ 

์ผ๋ฐ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(MUI, AntDesign ๋“ฑ)

  • node_modules ์•ˆ์— ๋“ค์–ด์žˆ์Œ
  • ์‚ฌ์šฉ์ž๋Š” ๊ทธ์ € import { Button } from 'mui'๋กœ ๋ถˆ๋Ÿฌ ์“ธ ๋ฟ ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ ๋กœ์ง์ด๋‚˜ ์Šคํƒ€์ผ์„ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ๋ณต์žกํ•œ ๋ฐฉ๋ฒ•์„ ๊ฑฐ์ณ์•ผํ•œ๋‹ค.

Shadcn

  • npx shadcn add button์„ ์ž…๋ ฅํ•˜๋ฉด ๋ฒ„ํŠผ ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ๊ฐ€ ๋‚ด ํ”„๋กœ์ ํŠธ์˜ ์˜ˆ) src/components/ui/button.tsx์— ํŒŒ์ผ๋กœ ๋ณต์‚ฌ๊ฐ€ ๋˜์–ด ๋“ค์–ด์˜จ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ๋ณต์‚ฌ๊ฐ€ ๋œ ํŒŒ์ผ์€ ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ ์†Œ์œ ๊ถŒ์ด ๋œ๋‹ค. ์ง์ ‘ ์ปค์Šคํ…€ ๋ฐ ์กฐ์ž‘์ด ์šฉ์ดํ•˜๋‹ค.

๊ธฐ๋Šฅ๊ณผ ๋””์ž์ธ์˜ ๋ถ„๋ฆฌ

"๊ทธ๋Ÿผ ์ฝ”๋“œ๋ฅผ ์ผ์ผ์ด ๋‹ค ์งœ์ฃผ๋Š” ๊ฑด๊ฐ€?" ์‹ถ๊ฒ ์ง€๋งŒ shadcn์€ ๋ฐ”๋‹ฅ๋ถ€ํ„ฐ ๋งŒ๋“œ๋Š” ๊ฑด ์•„๋‹ˆ๋‹ค. Radix UI๋ผ๋Š” '๋ผˆ๋Œ€(Headless UI)' ์œ„์— Tailwind CSS๋ผ๋Š” '์Šคํƒ€์ผ'์„ ๋ถ™์—ฌ๋†“์€ ์ƒํƒœ์ด๋‹ค.

  • Radix UI (๋ผˆ๋Œ€): ์ ‘๊ทผ์„ฑ(Accessibility), ํ‚ค๋ณด๋“œ ์กฐ์ž‘, ํŒ์—… ์—ด๊ณ  ๋‹ซ๊ธฐ ๊ฐ™์€ ๋ณต์žกํ•œ ๊ธฐ๋Šฅ ๋‹ด๋‹น (๋ˆˆ์— ์•ˆ ๋ณด์ž„).
  • Tailwind CSS (์‚ด): ๋””์ž์ธ๊ณผ ์Šคํƒ€์ผ๋ง ๋‹ด๋‹น.
  • shadcn (์กฐํ•ฉ๊ธฐ): ์ด ๋‘˜์„ ์กฐํ•ฉํ•ด์„œ "๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ ํŒŒ์ผ" ํ˜•ํƒœ๋กœ ์šฐ๋ฆฌ์—๊ฒŒ ์ „๋‹ฌํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ๋ฐ Tailwind CSS ์„ค์น˜

์„ค์น˜

# Tailwind CSS ๋ฐ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜
npm install -D tailwindcss postcss autoperfixer

tailwind.config.js ์ƒ์„ฑ

# tailwind.config.js ์ƒ์„ฑ
npx tailwindcss init -p 

๋˜๋Š” 

npx tailwindcss init

๐Ÿ•ต๏ธ ๋ช…๋ น์–ด -p๋ผ๋Š” ์˜ต์…˜์€?

  • npx tailwindcss init: tailwind.config.js๋งŒ ์ƒ์„ฑ
  • npx tailwindcss init -p: tailwind.config.js์™€ ํ•จ๊ป˜ postcss.config.js๋ฅผ ์„ธํŠธ๋กœ ์ƒ์„ฑ

Q: PostCSS๋ž‘ Autoprefixer๋Š” ๊ผญ ์„ค์น˜ํ•ด์•ผ ๋˜๋‚˜?
A: Tailwind๊ฐ€ ์ž‘์„ฑํ•œ ํด๋ž˜์Šค๋ช…์„ ์‹ค์ œ CSS๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋””์ž์ธ์ด ์œ ์ง€๋˜๋„๋ก ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์—ฌ์ฃผ๋Š” ํ•ต์‹ฌ ์ผ๊พผ๋“ค์ด๋‹ค. init -p ๋ช…๋ น์–ด๋กœ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•˜๋Š” ๊ฑธ ๊ถŒ์žฅ


๐Ÿ•ต๏ธ TailwindCSS PostCSS Autoprefixer ๋ž€?

Tailwind CSS

  • ์œ ํ‹ธ๋ฆฌํ‹ฐ ํผ์ŠคํŠธ(utility-first) CSS ํ”„๋ ˆ์ž„์›Œํฌ
# ๋ฌด์Šจ๊ธฐ๋Šฅ?
# ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด์ง„ ์ž‘์€ CSS ํด๋ž˜์Šค๋“ค์„ ์กฐํ•ฉํ•ด์„œ UI๋ฅผ ๋งŒ๋“ ๋‹ค
# ์ง์ ‘ CSS ๊ธธ๊ฒŒ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ  ํด๋ž˜์Šค๋งŒ์œผ๋กœ ์Šคํƒ€์ผ์„ ๊ตฌ์„ฑ

<div class="bg-blue-500 text-white p-4 rounded-lg">
  ๋ฒ„ํŠผ
</div>

- `bg-blue-500` โ†’ ๋ฐฐ๊ฒฝ ํŒŒ๋ž‘
- `text-white` โ†’ ๊ธ€์ž ํฐ์ƒ‰
- `p-4` โ†’ ํŒจ๋”ฉ
- `rounded-lg` โ†’ ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ

PostCSS

  • CSS๋ฅผ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ๋„๊ตฌ(ํ”Œ๋žซํผ)
# ๋ฌด์Šจ๊ธฐ๋Šฅ?
# CSS์— ์—ฌ๋Ÿฌ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์ ์šฉํ•ด ๊ธฐ๋Šฅ์„ ํ™•์žฅ
# ์ž์ฒด์ ์œผ๋กœ ์Šคํƒ€์ผ์„ ์ œ๊ณตํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด CSS๋ฅผ ๊ฐ€๊ณตํ•˜๋Š” ์—ญํ• 

- ์ตœ์‹  CSS ๋ฌธ๋ฒ•์„ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์šฉ์œผ๋กœ ๋ณ€ํ™˜
- ์ž๋™์œผ๋กœ prefix ์ถ”๊ฐ€
- CSS ์••์ถ•
- Tailwind ์ฒ˜๋ฆฌ

Autoprefixer

  • PostCSS ํ”Œ๋Ÿฌ๊ทธ์ธ ์ค‘ ํ•˜๋‚˜ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด CSS์— ์ž๋™์œผ๋กœ vendor prefix๋ฅผ ๋ถ™์—ฌ์ค€๋‹ค.
# ์ž‘์„ฑ ์ฝ”๋“œ
display: flex;

# Autoprefixer ์ ์šฉ ํ›„
display: -webkit-box;
display: -ms-flexbox;
display: flex;

Tailwind CSS โ†’ PostCSS ์œ„์—์„œ ๋™์ž‘
Autoprefixer โ†’ PostCSS ํ”Œ๋Ÿฌ๊ทธ์ธ
PostCSS โ†’ CSS ๊ฐ€๊ณต ์—”์ง„


ํ•„์ˆ˜ ํ™˜๊ฒฝ ์„ค์ •

shadcn์€ ๋‚ด ํ”„๋กœ์ ํŠธ์˜ ํŒŒ์ผ์„ ์ง์ ‘ ์ˆ˜์ •ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๊ฒฝ๋กœ ๋ณ„์นญ(Alias) ์„ค์ •์„ ๋ฐ˜๋“œ์‹œ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

์ ˆ๋Œ€ ๊ฒฝ๋กœ ์„ค์ •

1. tsconfig.json ์ˆ˜์ •

๊ฒฝ๋กœ๋ฅผ @/๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋„๋ก compilerOptions ์•ˆ์— ์ถ”๊ฐ€ํ•œ๋‹ค.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
2. vite.config.ts ์ˆ˜์ •

Vite๊ฐ€ @ ๊ธฐํ˜ธ๋ฅผ ์‹ค์ œ ./src ํด๋”๋กœ ์ธ์‹ํ•˜๊ฒŒ ์„ค์ • (์—๋Ÿฌ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด @types/node๋ฅผ ๋จผ์ € ์„ค์น˜: npm i -D @types/node)

export default defineConfig({
	plugins: [react()], 
	resolve: { 
		alias: { 
			"@": path.resolve(__dirname, "./src"), 
		}, 
	}, 
})
3. tailwind.config.js ์ˆ˜์ •

์–ด๋–ค ํŒŒ์ผ์—์„œ Tailwind ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๋ช…์‹œ

/** @type {import('tailwindcss').Config} */ 
export default { 
	content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 
	theme: { extend: {} }, 
	plugins: [],
4. postcss.config.js

Tailwind๊ฐ€ ์ž‘์„ฑํ•œ ํด๋ž˜์Šค๋ช…์„ ์‹ค์ œ CSS๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋””์ž์ธ์ด ์œ ์ง€๋˜๋„๋ก ํ•˜๋Š” ์„ค์ • ๋ถ€๋ถ„ ์ฒดํฌ๋งŒ !

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
5. Tailwind CSS์˜ ์—”์ง„ ์ผœ๊ธฐ

ํ”„๋กœ์ ํŠธ์˜ ๊ฐ€์žฅ ์ตœ์ƒ์œ„ ์—”ํŠธ๋ฆฌ CSS(๋˜๋Š” SCSS) ํŒŒ์ผ์— ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.

  • Vite ํ”„๋กœ์ ํŠธ ๊ธฐ์ค€: ๋ณดํ†ต src/index.css ๋˜๋Š” src/main.scss ํŒŒ์ผ

๐Ÿ•ต๏ธ ์ค‘์š”ํ•œ ๊ธฐ์ค€: main.tsx ๋˜๋Š” App.tsx์—์„œ import "./index.css"์™€ ๊ฐ™์ด ์ง์ ‘์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์žˆ๋Š” ํŒŒ์ผ์— ์ ์šฉ

@tailwind base;
@tailwind components;
@tailwind utilities;
  • base: ๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ์ œ๊ฐ๊ฐ์ธ ๊ธฐ๋ณธ ์—ฌ๋ฐฑ ๋“ฑ์„ ์ดˆ๊ธฐํ™” ์ด๊ฒŒ ์ œ์ผ ๋ฐ‘๋ฐ”๋‹ฅ์— ๊น”๋ ค์•ผ ํ•จ
  • components: ๋ฒ„ํŠผ์ด๋‚˜ ์นด๋“œ ๊ฐ™์€ ๋ฉ์–ด๋ฆฌ ์Šคํƒ€์ผ์ด ๋“ค์–ด๊ฐ
  • utilities: mt-4, text-red-500 ์ฒ˜๋Ÿผ ์•„์ฃผ ๊ตฌ์ฒด์ ์ธ ์†์„ฑ๋“ค์ด๋‹ค. ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์™€์•ผ ๋‚ด๊ฐ€ ํด๋ž˜์Šค๋กœ ์ค€ ์†์„ฑ์ด ๋‹ค๋ฅธ ์Šคํƒ€์ผ์„ ๋ฎ์–ด์“ฐ๊ธฐ(Override) ํ•  ์ˆ˜ ์žˆ๋‹ค.
6. shadcn/ui ์ดˆ๊ธฐํ™” (Init)

์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด shadcn์ด ํ•„์š”ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํŒŒ์ผ๋“ค์„ ์ƒ์„ฑ

npx shadcn@latest init

Base color: Which color would you like to use as the base color? ๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค.

  • ์„ ํƒ์ง€: Slate, Gray, Zinc, Neutral, Stone
  • ์„ค๋ช…: ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ๋ณธ ๋ฌด์ฑ„์ƒ‰ ํ†ค์„ ๊ฒฐ์ •
  • ์ถ”์ฒœ: Slate ๋˜๋Š” Zinc.
โœ” Writing components.json.
โœ” Checking registry.
โœ” Updating tailwind.config.ts
โœ” Updating CSS variables in src/shared/styles/index.scss
โœ” Installing dependencies.
โœ” Created 1 file:
  - src/lib/utils.ts

Success! Project initialization completed.
You may now add components.

src/lib/utils.ts ๊ฒฝ๋กœ์— ์ค‘๋ณต Tailwind ํด๋ž˜์Šค๋Š” ์ •๋ฆฌํ•ด์ฃผ๋Š” ํ—ฌํผ ์œ ํ‹ธ ํ•จ์ˆ˜๊ฐ€ ์ƒ์„ฑ๋˜๋Š”๋ฐ ๊ทธ๋Œ€๋กœ ๋‘๊ฑฐ๋‚˜ ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ ํด๋”๊ตฌ์กฐ์— ์˜ฎ๊ธฐ๊ณ  components.json์—์„œ ๊ฒฝ๋กœ ์ˆ˜์ •์„ ํ•œ๋‹ค.

index.scss์— ์ž๋™์œผ๋กœ CSS ๋ณ€์ˆ˜๊ฐ€ ์ƒ์„ฑ๋˜๋Š”๋ฐ ๋‚˜๋Š” ๋”ฐ๋กœ ๊ด€๋ฆฌ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด index.scss๊ฐ€ ์•„๋‹Œ ๋”ฐ๋กœ tailwind.css๋กœ ์˜ฎ๊ฒผ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด๋•Œ lucide-react(์•„์ด์ฝ˜), tailwind-merge, clsx(ํด๋ž˜์Šค ํ•ฉ์„ฑ ์œ ํ‹ธ) ๊ฐ™์€ ๊ธฐ๋ณธ ์˜์กด์„ฑ๋“ค์ด ํ•œ๊บผ๋ฒˆ์— ์„ค์น˜๋œ๋‹ค.


components.json ์ƒ์„ธ ์†์„ฑ

# components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/shared/styles/tailwind.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "rtl": false,
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "registries": {}
}
1. ๊ธฐ๋ณธ ์„ค์ • (Core)
  • $schema: ์ด ํŒŒ์ผ์ด ์ง€์ผœ์•ผ ํ•  ๊ทœ๊ฒฉ์„ ์ •์˜, VS Code ๊ฐ™์€ ์—๋””ํ„ฐ์—์„œ ์ž๋™ ์™„์„ฑ์ด๋‚˜ ์˜คํƒ€ ๊ฒ€์‚ฌ๋ฅผ ๋„์™€์ค€๋‹ค.
  • style: ์ปดํฌ๋„ŒํŠธ์˜ ๋””์ž์ธ ์Šคํƒ€์ผ (new-york ๋˜๋Š” default)
    • new-york: ๋” ์ž‘์€ ํฐํŠธ์™€ ์„ธ๋ฐ€ํ•œ ํŒจ๋”ฉ์„ ๊ฐ€์ง„ ํ˜„๋Œ€์ ์ธ ์Šคํƒ€์ผ
  • rsc (React Server Components): Next.js์˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ์—ฌ๋ถ€
  • tsx: ์ƒ์„ฑ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์˜ ํ™•์žฅ์ž๋ฅผ .tsx๋กœ ํ• ์ง€ ๊ฒฐ์ • TypeScript ์‚ฌ์šฉ์ž๋ผ๋ฉด ๋‹น์—ฐํžˆ true
2. ํ…Œ์ผ์œˆ๋“œ ์„ค์ • (tailwind)
  • config: tailwind.config.ts ํŒŒ์ผ์˜ ์œ„์น˜ CLI๊ฐ€ ํ…Œ๋งˆ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์ด ํŒŒ์ผ์„ ์ฐธ์กฐ
  • css: ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„ ํ”„๋กœ์ ํŠธ์˜ ๋ฉ”์ธ CSS/SCSS ํŒŒ์ผ ๊ฒฝ๋กœ
  • baseColor: shadcn์ด ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋ณธ ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ (neutral, slate ๋“ฑ)
  • cssVariables: ์ƒ‰์ƒ์„ HSL ๊ฐ’์œผ๋กœ ์ง์ ‘ ๋ฐ•์„์ง€, CSS ๋ณ€์ˆ˜(--primary ๋“ฑ)๋กœ ๊ด€๋ฆฌํ• ์ง€ ๊ฒฐ์ •
  • prefix: Tailwind ํด๋ž˜์Šค ์•ž์— ๋ถ™์ผ ์ ‘๋‘์‚ฌ (์˜ˆ: tw-) ๋ณดํ†ต์€ ๋น„์›Œ๋‘ 
3. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฐ ๊ธฐํƒ€
  • iconLibrary: ์‚ฌ์šฉํ•  ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ณธ๊ฐ’์€ lucide
  • rtl (Right-to-Left): ์•„๋ž์–ด์ฒ˜๋Ÿผ ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ์ฝ๋Š” ์–ธ์–ด๋ฅผ ์ง€์›ํ• ์ง€ ์—ฌ๋ถ€
4. ๊ฒฝ๋กœ ๋ณ„์นญ (aliases)

์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„ค์น˜๋  '์ฃผ์†Œ'๋ฅผ ์ •์˜ tsconfig์˜ paths ์„ค์ •๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•œ๋‹ค.

  • components: ์ง์ ‘ ๋งŒ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋“ค์–ด๊ฐˆ ๋ฃจํŠธ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค (@/components)
  • ui: ํ•ต์‹ฌ shadcn์—์„œ ๋‚ด๋ ค๋ฐ›๋Š” ์ˆœ์ˆ˜ UI ์ปดํฌ๋„ŒํŠธ(Button, Input ๋“ฑ)๊ฐ€ ์ €์žฅ๋˜๋Š” ๊ณณ(@/components/ui)
  • utils: cn ํ•จ์ˆ˜ ๊ฐ™์€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํŒŒ์ผ์˜ ์œ„์น˜
  • lib, hooks: ๊ฐ๊ฐ ๊ณตํ†ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ปค์Šคํ…€ ํ›…์ด ์ €์žฅ๋  ์œ„์น˜

์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ ๋ฐ ์‚ฌ์šฉ

1. ์ปดํฌ๋„ŒํŠธ ์„ค์น˜ ๋ช…๋ น

๐ŸŒComponents - shadcn/ui

Shadcn์˜ ๊ณต์‹ ์‚ฌ์ดํŠธ - ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ƒ‰์„ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ ๋ช…๋ น์–ด๋ฅผ ํ™•์ธํ•˜๊ณ  ์ถ”๊ฐ€ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•œ๋‹ค.

๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ

๐ŸŒButton - shadcn/ui

npx shadcn@latest add button

๐Ÿ•ต๏ธ ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ฒ˜์Œ ์ถ”๊ฐ€ํ•  ๋•Œ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•œ Radix UiํŒจํ‚ค์ง€๋ฅผ Shadcn์ด ๊ฐ์ง€ํ•ด์„œ ์„ค์น˜ํ•ด์ค€๋‹ค.

์ถ”๊ฐ€ ์˜ˆ์‹œ

jaiden-linux@DESKTOP-TPMO2QL:~/projects/test$ npx shadcn@latest add button
โœ” Checking registry.
โœ” Installing dependencies.
โœ” Created 1 file:
  - src/shared/ui/button.tsx
2. cn() ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
  • ์—ญํ• : clsx์™€ tailwind-merge๋ฅผ ๊ฒฐํ•ฉํ•œ ํ•จ์ˆ˜.
  • ์žฅ์ : ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง์„ ํŽธํ•˜๊ฒŒ ํ•˜๊ณ , Tailwind ํด๋ž˜์Šค ๊ฐ„์˜ ์ถฉ๋Œ(์˜ˆ: p-2์™€ p-4๊ฐ€ ๊ฐ™์ด ๋“ค์–ด์™”์„ ๋•Œ)์„ ๋˜‘๋˜‘ํ•˜๊ฒŒ ํ•ด๊ฒฐํ•ด ์ค€๋‹ค.
// ์˜ˆ์‹œ: className์„ ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›์•„ ํ•ฉ์น  ๋•Œ
<button className={cn("bg-blue-500", className)}>
3. Shadcn ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ (Button ์˜ˆ์‹œ)
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/shared/lib/utils/styles.util"

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

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

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ˜ธ์ถœ

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/shared/lib/utils/styles.util"
  • Slot (Radix UI): ์ด ๋ฒ„ํŠผ์„ <a> ํƒœ๊ทธ๋‚˜ ๋‹ค๋ฅธ ํƒœ๊ทธ๋กœ ๋ฐ”๊พธ๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์Šฌ๋กฏ (asChild ์†์„ฑ ๊ด€๋ จ)
  • cva (Class Variance Authority): CSS ํด๋ž˜์Šค๋ฅผ ์กฐ๊ฑด๋ถ€๋กœ ๊ด€๋ฆฌํ•ด์ฃผ๋Š” ํ•ต์‹ฌ ๋„๊ตฌ "๋นจ๊ฐ„์ƒ‰ ๋ฒ„ํŠผ", "ํฐ ๋ฒ„ํŠผ" ๊ฐ™์€ ์Šคํƒ€์ผ ์˜ต์…˜์„ ์—ฌ๊ธฐ์„œ ์ •์˜ํ•œ๋‹ค.
  • cn: ์ดˆ๊ธฐ ์„ธํŒ…์— ์ƒ์„ฑ๋˜๋Š” ๋จธ์ง€ ์œ ํ‹ธ ํ•จ์ˆ˜ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํด๋ž˜์Šค๋ฅผ ์ถฉ๋Œ ์—†์ด ํ•ฉ์ณ์ฃผ๋Š” ์—ญํ• 

2. ์Šคํƒ€์ผ ์ •์˜(cva)

shadcn์˜ ๋””์ž์ธ ์—”์ง„์ด๋‹ค.

const buttonVariants = cva( "inline-flex items-center ...", // ๊ธฐ๋ณธ(๊ณตํ†ต) ์Šคํƒ€์ผ 
	{ 
	variants: { 
		size: { 
			default: "ds-button-xs", 
			sm: "ds-button-sm", 
			// ... ์˜ต์…˜๋“ค 
		}
	}, 
	defaultVariants: { 
			size: "default"
		}, 
	} 
)
  • variants: size๋‚˜ variant(์ƒ‰์ƒ) ๋“ฑ์„ ์ •์˜ํ•œ๋‹ค. ์˜ต์…˜์„ ๋ฏธ๋ฆฌ ์ •ํ•ด๋‘๋Š” ๊ณณ

๐Ÿ•ต๏ธ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ <Button size="lg">๋ผ๊ณ  ์“ฐ๋ฉด ds-button-lg ํด๋ž˜์Šค๊ฐ€ ์ž๋™์œผ๋กœ ๋ถ™๋Š”๋‹ค.


3. ํƒ€์ž… ์ •์˜

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}
  • ์ด ๋ฒ„ํŠผ์ด ์–ด๋–ค Props๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š”์ง€ ์ •์˜
  • React.ButtonHTMLAttributes๋ฅผ ์ƒ์†๋ฐ›๊ธฐ ๋•Œ๋ฌธ์— onClick, type="submit", disabled ๊ฐ™์€ ํ‘œ์ค€ ๋ฒ„ํŠผ ์†์„ฑ์„ ๊ทธ๋Œ€๋กœ ๋‹ค ์“ธ ์ˆ˜ ์žˆ๋‹ค.

4. ์ปดํฌ๋„ŒํŠธ

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }
  • forwardRef: ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ด ๋ฒ„ํŠผ์˜ DOM์— ์ง์ ‘ ์ ‘๊ทผ(Focus ๋“ฑ)ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ œ์ž‘ ์‹œ ํ•„์ˆ˜์ ์ธ ํŒจํ„ด
  • asChild: * false์ผ ๋•Œ: ๊ธฐ๋ณธ <button> ํƒœ๊ทธ๋กœ ๋ Œ๋”๋ง๋œ๋‹ค.
    • true์ผ ๋•Œ: ์ƒˆ ํƒœ๊ทธ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ์ž์‹(Child) ์š”์†Œ์—๊ฒŒ ์ž์‹ ์˜ ์Šคํƒ€์ผ๊ณผ ๊ธฐ๋Šฅ์„ ์ „๋‹ฌํ•œ๋‹ค.
    • ์˜ˆ: <Button asChild><a href="...">๋งํฌ</a></Button>๋ผ๊ณ  ์“ฐ๋ฉด ๋ฒ„ํŠผ ๋ชจ์–‘์„ ํ•œ <a> ํƒœ๊ทธ๊ฐ€ ํƒ„์ƒํ•œ๋‹ค. (HTML ํ‘œ์ค€ ์œ„๋ฐ˜์ธ <button><a></a></button> ๊ตฌ์กฐ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•จ)

0๊ฐœ์˜ ๋Œ“๊ธ€