
๊ธฐ๋ณธ์ ์ธ 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๋ผ๋ '์คํ์ผ'์ ๋ถ์ฌ๋์ ์ํ์ด๋ค.
์ค์น
# 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
# ๋ฌด์จ๊ธฐ๋ฅ?
# ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ง ์์ 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 ๋ฌธ๋ฒ์ ๊ตฌํ ๋ธ๋ผ์ฐ์ ์ฉ์ผ๋ก ๋ณํ
- ์๋์ผ๋ก prefix ์ถ๊ฐ
- CSS ์์ถ
- Tailwind ์ฒ๋ฆฌ
Autoprefixer
# ์์ฑ ์ฝ๋
display: flex;
# Autoprefixer ์ ์ฉ ํ
display: -webkit-box;
display: -ms-flexbox;
display: flex;
Tailwind CSS โ PostCSS ์์์ ๋์
Autoprefixer โ PostCSS ํ๋ฌ๊ทธ์ธ
PostCSS โ CSS ๊ฐ๊ณต ์์ง
shadcn์ ๋ด ํ๋ก์ ํธ์ ํ์ผ์ ์ง์ ์์ ํ๊ธฐ ๋๋ฌธ์, ๊ฒฝ๋ก ๋ณ์นญ(Alias) ์ค์ ์ ๋ฐ๋์ ํด์ค์ผ ํ๋ค.
์ ๋ ๊ฒฝ๋ก ์ค์
๊ฒฝ๋ก๋ฅผ @/๋ก ์์ํ ์ ์๋๋ก compilerOptions ์์ ์ถ๊ฐํ๋ค.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Vite๊ฐ @ ๊ธฐํธ๋ฅผ ์ค์ ./src ํด๋๋ก ์ธ์ํ๊ฒ ์ค์ (์๋ฌ ๋ฐฉ์ง๋ฅผ ์ํด @types/node๋ฅผ ๋จผ์ ์ค์น: npm i -D @types/node)
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
์ด๋ค ํ์ผ์์ Tailwind ํด๋์ค๋ฅผ ์ฌ์ฉํ ์ง ๋ช ์
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
theme: { extend: {} },
plugins: [],
Tailwind๊ฐ ์์ฑํ ํด๋์ค๋ช
์ ์ค์ CSS๋ก ๋ณํํ๊ณ ๋ชจ๋ ๋ธ๋ผ์ฐ์ ์์ ๋์์ธ์ด ์ ์ง๋๋๋ก ํ๋ ์ค์ ๋ถ๋ถ ์ฒดํฌ๋ง !
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
ํ๋ก์ ํธ์ ๊ฐ์ฅ ์ต์์ ์ํธ๋ฆฌ CSS(๋๋ SCSS) ํ์ผ์ ๋ฃ์ด์ผ ํ๋ค.
src/index.css ๋๋ src/main.scss ํ์ผ๐ต๏ธ ์ค์ํ ๊ธฐ์ค:
main.tsx๋๋App.tsx์์import "./index.css"์ ๊ฐ์ด ์ง์ ์ ์ผ๋ก ๋ถ๋ฌ์ค๊ณ ์๋ ํ์ผ์ ์ ์ฉ
@tailwind base;
@tailwind components;
@tailwind utilities;
mt-4, text-red-500 ์ฒ๋ผ ์์ฃผ ๊ตฌ์ฒด์ ์ธ ์์ฑ๋ค์ด๋ค. ๊ฐ์ฅ ๋ง์ง๋ง์ ์์ผ ๋ด๊ฐ ํด๋์ค๋ก ์ค ์์ฑ์ด ๋ค๋ฅธ ์คํ์ผ์ ๋ฎ์ด์ฐ๊ธฐ(Override) ํ ์ ์๋ค.์๋ ๋ช
๋ น์ด๋ฅผ ์
๋ ฅํ๋ฉด shadcn์ด ํ์ํ ์ ํธ๋ฆฌํฐ ํ์ผ๋ค์ ์์ฑ
npx shadcn@latest init
Base color: Which color would you like to use as the base color? ๋ฅผ ๊ฒฐ์ ํ๋ค.
Slate, Gray, Zinc, Neutral, StoneSlate ๋๋ 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
{
"$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": {}
}
$schema: ์ด ํ์ผ์ด ์ง์ผ์ผ ํ ๊ท๊ฒฉ์ ์ ์, VS Code ๊ฐ์ ์๋ํฐ์์ ์๋ ์์ฑ์ด๋ ์คํ ๊ฒ์ฌ๋ฅผ ๋์์ค๋ค.style: ์ปดํฌ๋ํธ์ ๋์์ธ ์คํ์ผ (new-york ๋๋ default)new-york: ๋ ์์ ํฐํธ์ ์ธ๋ฐํ ํจ๋ฉ์ ๊ฐ์ง ํ๋์ ์ธ ์คํ์ผrsc (React Server Components): Next.js์ ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ ์ง ์ฌ๋ถtsx: ์์ฑ๋๋ ์ปดํฌ๋ํธ ํ์ผ์ ํ์ฅ์๋ฅผ .tsx๋ก ํ ์ง ๊ฒฐ์ TypeScript ์ฌ์ฉ์๋ผ๋ฉด ๋น์ฐํ truetailwind)config: tailwind.config.ts ํ์ผ์ ์์น CLI๊ฐ ํ
๋ง๋ฅผ ์
๋ฐ์ดํธํ ๋ ์ด ํ์ผ์ ์ฐธ์กฐcss: ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ ํ๋ก์ ํธ์ ๋ฉ์ธ CSS/SCSS ํ์ผ ๊ฒฝ๋กbaseColor: shadcn์ด ์์ฑํ๋ ๊ธฐ๋ณธ ์์ ํ๋ ํธ (neutral, slate ๋ฑ)cssVariables: ์์์ HSL ๊ฐ์ผ๋ก ์ง์ ๋ฐ์์ง, CSS ๋ณ์(--primary ๋ฑ)๋ก ๊ด๋ฆฌํ ์ง ๊ฒฐ์ prefix: Tailwind ํด๋์ค ์์ ๋ถ์ผ ์ ๋์ฌ (์: tw-) ๋ณดํต์ ๋น์๋ iconLibrary: ์ฌ์ฉํ ์์ด์ฝ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ธฐ๋ณธ๊ฐ์ lucidertl (Right-to-Left): ์๋์ด์ฒ๋ผ ์ค๋ฅธ์ชฝ์์ ์ผ์ชฝ์ผ๋ก ์ฝ๋ ์ธ์ด๋ฅผ ์ง์ํ ์ง ์ฌ๋ถaliases)์ปดํฌ๋ํธ๊ฐ ์ค์น๋ '์ฃผ์'๋ฅผ ์ ์ tsconfig์ paths ์ค์ ๊ณผ ์ผ์นํด์ผ ํ๋ค.
components: ์ง์ ๋ง๋ ์ปดํฌ๋ํธ๊ฐ ๋ค์ด๊ฐ ๋ฃจํธ ๊ฒฝ๋ก์
๋๋ค (@/components)ui: ํต์ฌ shadcn์์ ๋ด๋ ค๋ฐ๋ ์์ UI ์ปดํฌ๋ํธ(Button, Input ๋ฑ)๊ฐ ์ ์ฅ๋๋ ๊ณณ(@/components/ui)utils: cn ํจ์ ๊ฐ์ ์ ํธ๋ฆฌํฐ ํ์ผ์ ์์นlib, hooks: ๊ฐ๊ฐ ๊ณตํต ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ปค์คํ
ํ
์ด ์ ์ฅ๋ ์์นShadcn์ ๊ณต์ ์ฌ์ดํธ - ์ปดํฌ๋ํธ ๊ฒ์์ ํตํด ์ปดํฌ๋ํธ ์ถ๊ฐ ๋ช ๋ น์ด๋ฅผ ํ์ธํ๊ณ ์ถ๊ฐ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํ๋ค.
๋ฒํผ ์ปดํฌ๋ํธ
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
cn() ์ ํธ๋ฆฌํฐ ํจ์clsx์ tailwind-merge๋ฅผ ๊ฒฐํฉํ ํจ์.p-2์ p-4๊ฐ ๊ฐ์ด ๋ค์ด์์ ๋)์ ๋๋ํ๊ฒ ํด๊ฒฐํด ์ค๋ค.// ์์: className์ ์ธ๋ถ์์ ์ฃผ์
๋ฐ์ ํฉ์น ๋
<button className={cn("bg-blue-500", className)}>
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"
<a> ํ๊ทธ๋ ๋ค๋ฅธ ํ๊ทธ๋ก ๋ฐ๊พธ๊ณ ์ถ์ ๋ ์ฌ์ฉํ๋ ์ฌ๋กฏ (asChild ์์ฑ ๊ด๋ จ)2. ์คํ์ผ ์ ์(cva)
shadcn์ ๋์์ธ ์์ง์ด๋ค.
const buttonVariants = cva( "inline-flex items-center ...", // ๊ธฐ๋ณธ(๊ณตํต) ์คํ์ผ
{
variants: {
size: {
default: "ds-button-xs",
sm: "ds-button-sm",
// ... ์ต์
๋ค
}
},
defaultVariants: {
size: "default"
},
}
)
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 }
false์ผ ๋: ๊ธฐ๋ณธ <button> ํ๊ทธ๋ก ๋ ๋๋ง๋๋ค.true์ผ ๋: ์ ํ๊ทธ๋ฅผ ๋ง๋ค์ง ์๊ณ ์์(Child) ์์์๊ฒ ์์ ์ ์คํ์ผ๊ณผ ๊ธฐ๋ฅ์ ์ ๋ฌํ๋ค.<Button asChild><a href="...">๋งํฌ</a></Button>๋ผ๊ณ ์ฐ๋ฉด ๋ฒํผ ๋ชจ์์ ํ <a> ํ๊ทธ๊ฐ ํ์ํ๋ค. (HTML ํ์ค ์๋ฐ์ธ <button><a></a></button> ๊ตฌ์กฐ๋ฅผ ํผํ๊ธฐ ์ํจ)