Files
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

10 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-shadcn-ui shadcn/ui 10_Wiki/Topics verified self
shadcn
shadcn-ui
shadcn/ui
ui.shadcn.com
none A 0.95 applied
react
ui
components
tailwind
radix
design-system
2026-05-10 pending
language framework
TypeScript React 19/Next.js 15/Tailwind 4/Radix UI

shadcn/ui

매 한 줄

"매 install package 가 아니라 매 copy-paste component collection — 매 source code 매 own, 매 customize 자유". shadcn (Shaad Heuvel) 매 2023 출시, 매 React + Tailwind + Radix UI 위에 매 well-designed accessible primitive 를 매 CLI 로 가져다 쓰는 anti-library pattern. 2026 현재 매 React app 의 default UI starting kit — Vercel templates, AI SDK examples, Next.js docs 매 default.

매 핵심

매 differentiator

  • Not a dependency: 매 npm install shadcn/ui 가 아님. 매 npx shadcn add button → component code 매 your repo 에 copy.
  • Owned source: 매 매 component file 매 직접 modify. 매 lock-in 없음.
  • Built on Radix UI: accessibility primitives (focus management, ARIA, keyboard) 매 무료.
  • Tailwind: utility-first styling, theme via CSS variables.
  • Composable: 매 small primitive (Button, Input, Dialog) 매 large block 으로 조합.

매 stack

  • Foundation: Radix UI Primitives (unstyled accessible components).
  • Styling: Tailwind CSS v4 (2024+ JIT engine).
  • Variants: class-variance-authority (cva) — type-safe variant API.
  • Class merge: tailwind-merge + clsx (cn() helper).
  • Forms: react-hook-form + zod (resolver).
  • Charts: Recharts wrapper (shadcn/charts).
  • Icons: lucide-react.

매 CLI workflow

npx shadcn@latest init                  # one-time setup, creates components.json
npx shadcn@latest add button input dialog
npx shadcn@latest add https://ui.shadcn.com/r/styles/new-york/login-01.json

매 응용

  1. SaaS dashboard / admin panel.
  2. AI chat UI (shadcn/ui + Vercel AI SDK).
  3. Marketing site sections.
  4. Internal tools (Retool 대체).
  5. Design system foundation (매 fork 후 brand 적용).

💻 패턴

1. Initial setup (Next.js 15)

pnpm create next-app@latest my-app --ts --tailwind --app
cd my-app
pnpm dlx shadcn@latest init
# choose: New York style, Slate base, CSS variables
pnpm dlx shadcn@latest add button card input form dialog dropdown-menu

2. cn() utility (필수 helper)

// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

3. Button with cva variants

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 rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
);

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

export 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";

4. Form with react-hook-form + zod

"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export function LoginForm() {
  const form = useForm<z.infer<typeof schema>>({
    resolver: zodResolver(schema),
    defaultValues: { email: "", password: "" },
  });

  function onSubmit(values: z.infer<typeof schema>) {
    // server action
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField control={form.control} name="email" render={({ field }) => (
          <FormItem>
            <FormLabel>Email</FormLabel>
            <FormControl><Input type="email" {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <FormField control={form.control} name="password" render={({ field }) => (
          <FormItem>
            <FormLabel>Password</FormLabel>
            <FormControl><Input type="password" {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <Button type="submit" className="w-full">Sign in</Button>
      </form>
    </Form>
  );
}

5. Theming (CSS variables)

/* app/globals.css */
@import "tailwindcss";

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --radius: 0.5rem;
  }
  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;
  }
}

6. Data table (TanStack Table integration)

import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";

export function DataTable<T, V>({ columns, data }: { columns: ColumnDef<T, V>[]; data: T[] }) {
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
  return (
    <Table>
      <TableHeader>
        {table.getHeaderGroups().map((hg) => (
          <TableRow key={hg.id}>
            {hg.headers.map((h) => (
              <TableHead key={h.id}>{flexRender(h.column.columnDef.header, h.getContext())}</TableHead>
            ))}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody>
        {table.getRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((c) => (
              <TableCell key={c.id}>{flexRender(c.column.columnDef.cell, c.getContext())}</TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

7. AI chat UI (Vercel AI SDK + shadcn)

"use client";
import { useChat } from "@ai-sdk/react";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <div className="flex flex-col h-[600px] gap-2">
      <div className="flex-1 overflow-y-auto space-y-2 p-4">
        {messages.map((m) => (
          <Card key={m.id} className={cn("p-3", m.role === "user" && "bg-primary/10")}>
            <strong className="text-xs uppercase">{m.role}</strong>
            <p className="whitespace-pre-wrap">{m.content}</p>
          </Card>
        ))}
      </div>
      <form onSubmit={handleSubmit} className="flex gap-2 p-2">
        <Input value={input} onChange={handleInputChange} placeholder="Say something..." />
        <Button type="submit">Send</Button>
      </form>
    </div>
  );
}

매 결정 기준

상황 Approach
빠른 SaaS prototype shadcn/ui + Next.js + Vercel — 매 default 선택
강한 brand identity shadcn/ui fork → CSS variables 로 brand 적용
매 component lock-in 싫음 shadcn/ui (own source)
매 dependency 관리 single 파일 선호 Material UI / Mantine (전통 lib)
매 enterprise tabular admin shadcn/ui + TanStack Table + Recharts
매 native mobile (RN) NativeBase / Tamagui / shadcn-react-native (community)

기본값: Next.js 15 + shadcn/ui (New York style) + Tailwind 4 + lucide-react + react-hook-form + zod.

🔗 Graph

🤖 LLM 활용

언제: 매 modern React app 매 빠르게 launch — 매 LLM (Claude/GPT) 매 shadcn 패턴 매 well-trained, 매 generation quality 매 높음. 언제 X: 매 design-from-scratch creative work — 매 too opinionated. 매 매 non-React framework (Vue/Svelte) — 매 community port 있지만 매 first-class 아님.

안티패턴

  • shadcn 을 npm package 처럼 취급: 매 update CLI 매 npx shadcn diff 로 검토 후 매 manually merge.
  • cn() 무시: 매 className concat 매 직접 — 매 conflict 매 발생.
  • Tailwind v3 와 mix: 매 v4 와 v3 매 incompatible API. 매 lock 명시.
  • Radix unstyle 무시 직접 div: 매 accessibility 손실.
  • 모든 Button 을 custom variant 로: 매 base variant 4-5개 유지, 매 outlier 만 inline className.

🧪 검증 / 중복

  • Verified (ui.shadcn.com docs, Next.js learn, Vercel templates 2025-2026).
  • 신뢰도 A.

🕓 Changelog

날짜 변경
2026-05-08 Phase 1
2026-05-10 Manual cleanup — full setup + cva + form + AI chat patterns (2026 stack)