스페이스바AI
블로그문서강의가격
스페이스바AI

AI를 제대로 활용하는 실전 가이드

(주)스페이스바 | 대표: 김정우

서비스

  • 블로그
  • 문서
  • 강의
  • 가격

법적 고지

  • 이용약관
  • 개인정보처리방침

© 2025 (주)스페이스바. All rights reserved.

모든 글 보기
Web Development

shadcn/ui 완벽 가이드: 복사 가능한 컴포넌트로 UI 빠르게 구축하기

shadcn/ui의 철학, 설치 방법, 주요 컴포넌트 사용법, 커스터마이징, Tailwind CSS와의 통합을 상세히 알아봅니다.

Spacebar AI
2025년 12월 7일
15분
#shadcn/ui
#React
#Tailwind CSS
#UI
#컴포넌트
#Next.js
shadcn/ui 완벽 가이드: 복사 가능한 컴포넌트로 UI 빠르게 구축하기

shadcn/ui 완벽 가이드

shadcn/ui란?

shadcn/ui는 "컴포넌트 라이브러리가 아닙니다." - 복사/붙여넣기할 수 있는 재사용 가능한 컴포넌트 컬렉션입니다. npm 패키지가 아니라 소스 코드를 직접 프로젝트에 추가합니다.

핵심 철학

  • 소유권: 코드가 완전히 당신의 것
  • 커스터마이징: 제한 없이 수정 가능
  • 의존성 최소화: 불필요한 코드 없음
  • Radix UI 기반: 접근성과 품질 보장

설치

Next.js 프로젝트 초기화

# 새 프로젝트 생성
npx create-next-app@latest my-app --typescript --tailwind --eslint

# shadcn/ui 초기화
cd my-app
npx shadcn@latest init

설정 옵션

Would you like to use TypeScript? yes
Which style would you like to use? Default
Which color would you like to use as base color? Slate
Where is your global CSS file? app/globals.css
Would you like to use CSS variables for colors? yes
Are you using a custom tailwind prefix? (Leave blank)
Where is your tailwind.config.js located? tailwind.config.ts
Configure the import alias for components? @/components
Configure the import alias for utils? @/lib/utils
Are you using React Server Components? yes

컴포넌트 추가

CLI로 추가

# 단일 컴포넌트
npx shadcn@latest add button

# 여러 컴포넌트
npx shadcn@latest add button card dialog

# 모든 컴포넌트
npx shadcn@latest add --all

수동 추가

직접 소스 코드를 복사하여 components/ui/ 폴더에 추가할 수 있습니다.

주요 컴포넌트

Button

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

export default function Demo() {
  return (
    <div className="flex gap-4">
      <Button>Default</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>

      <Button size="sm">Small</Button>
      <Button size="lg">Large</Button>

      <Button disabled>Disabled</Button>
      <Button>
        <Mail className="mr-2 h-4 w-4" /> Login with Email
      </Button>
    </div>
  )
}

Card

import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

export default function Demo() {
  return (
    <Card className="w-[350px]">
      <CardHeader>
        <CardTitle>계정 생성</CardTitle>
        <CardDescription>
          새 계정을 만들어 서비스를 이용하세요.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form>
          <Input placeholder="이름" />
          <Input placeholder="이메일" type="email" />
        </form>
      </CardContent>
      <CardFooter className="flex justify-between">
        <Button variant="outline">취소</Button>
        <Button>생성</Button>
      </CardFooter>
    </Card>
  )
}

Dialog

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  DialogFooter,
} from "@/components/ui/dialog"

export default function Demo() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">프로필 편집</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>프로필 편집</DialogTitle>
          <DialogDescription>
            프로필 정보를 수정하세요.
          </DialogDescription>
        </DialogHeader>
        <div className="grid gap-4 py-4">
          <Input placeholder="이름" />
          <Input placeholder="사용자명" />
        </div>
        <DialogFooter>
          <Button type="submit">저장</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Form (with React Hook Form + Zod)

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"

const formSchema = z.object({
  username: z.string().min(2, "최소 2자 이상 입력하세요"),
  email: z.string().email("유효한 이메일을 입력하세요"),
})

export default function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>사용자명</FormLabel>
              <FormControl>
                <Input placeholder="홍길동" {...field} />
              </FormControl>
              <FormDescription>
                공개적으로 표시되는 이름입니다.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">제출</Button>
      </form>
    </Form>
  )
}

Data Table

"use client"

import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table"

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
}

export function DataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  return (
    <Table>
      <TableHeader>
        {table.getHeaderGroups().map((headerGroup) => (
          <TableRow key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <TableHead key={header.id}>
                {flexRender(
                  header.column.columnDef.header,
                  header.getContext()
                )}
              </TableHead>
            ))}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody>
        {table.getRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

커스터마이징

테마 설정

/* globals.css */
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;
    /* ... */
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 11.2%;
    /* ... */
  }
}

컴포넌트 수정

// components/ui/button.tsx를 직접 수정
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        // 새로운 variant 추가
        gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
      },
    },
  }
)

다크모드 설정

// components/theme-provider.tsx
"use client"

import { ThemeProvider as NextThemesProvider } from "next-themes"

export function ThemeProvider({ children, ...props }) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

결론

shadcn/ui는 기존 컴포넌트 라이브러리의 제약을 벗어나 완전한 소유권과 커스터마이징의 자유를 제공합니다. Radix UI 기반의 접근성과 Tailwind CSS의 유틸리티를 결합하여 빠르고 아름다운 UI를 구축할 수 있습니다.