본문 바로가기
Frontend2026년 4월 23일14분 읽기

shadcn/ui v3 + Radix Primitives — 2026 디자인 시스템 구축 가이드

YS
김영삼
조회 1
shadcn/ui v3 + Radix Primitives — 2026 디자인 시스템 구축 가이드

핵심 요약

shadcn/ui는 component "라이브러리"가 아니라 "코드 복사 패턴". Radix Primitives의 접근성 + Tailwind v4의 스타일링 + 자기 코드베이스로의 복사 = 가장 유연한 디자인 시스템 baseline.

  • shadcn/ui v3 (2026-02): React 19 호환, Tailwind v4 기본
  • Radix Primitives v2: 접근성·키보드·focus management
  • CVA(class-variance-authority): variant 시스템

1. 시작

pnpm dlx shadcn@latest init
# 프로젝트 분석 → tailwind.config·components.json 생성

pnpm dlx shadcn@latest add button input form dialog dropdown-menu

설치된 컴포넌트는 components/ui/ 에 코드 그대로 복사됨. 자기 프로젝트 코드처럼 자유 수정 가능.

2. 디자인 토큰 — Tailwind v4 통합

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

@theme {
  /* primitives */
  --color-blue-500: oklch(0.6 0.2 240);
  --color-blue-600: oklch(0.55 0.2 240);
  
  /* semantic */
  --color-background: var(--color-white);
  --color-foreground: var(--color-neutral-950);
  --color-primary: var(--color-blue-500);
  --color-primary-foreground: var(--color-white);
  --color-muted: var(--color-neutral-100);
  --color-border: var(--color-neutral-200);
  --color-ring: var(--color-blue-500);
  
  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
}

@media (prefers-color-scheme: dark) {
  @theme {
    --color-background: var(--color-neutral-950);
    --color-foreground: var(--color-neutral-50);
    --color-muted: var(--color-neutral-900);
    --color-border: var(--color-neutral-800);
  }
}

3. Button — variant 시스템

// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-red-500 text-white hover:bg-red-600',
        outline: 'border border-input bg-background hover:bg-muted',
        ghost: 'hover:bg-muted',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4',
        lg: 'h-12 px-6 text-lg',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: { variant: 'default', size: 'md' },
  }
)

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

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
  )
}

4. Form — react-hook-form + zod

// components/ui/form.tsx (shadcn 자동 설치)
// 사용 예시
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

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

function LoginForm() {
  const form = useForm<z.infer<typeof schema>>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', password: '' },
  })
  
  function onSubmit(values) { ... }
  
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>이메일</FormLabel>
              <FormControl><Input {...field} /></FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">로그인</Button>
      </form>
    </Form>
  )
}

5. Dialog — Radix 기반 접근성

import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from '@/components/ui/dialog'

<Dialog>
  <DialogTrigger asChild>
    <Button>새 항목</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>항목 추가</DialogTitle>
    </DialogHeader>
    <form>...</form>
  </DialogContent>
</Dialog>

Radix가 ESC·focus trap·scroll lock·ARIA 모두 처리. 직접 구현하면 30~50줄.

6. Theme Provider — 다크 모드 토글

'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'

export function ThemeProvider({ children }) {
  return (
    <NextThemesProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </NextThemesProvider>
  )
}

// 토글 버튼
import { useTheme } from 'next-themes'
function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  return (
    <Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} variant="ghost" size="icon">
      <Sun className="dark:hidden" />
      <Moon className="hidden dark:block" />
    </Button>
  )
}

7. 접근성 체크리스트

  • 모든 인터랙티브 요소 키보드 접근 가능
  • focus visible (focus-visible:ring 사용)
  • color contrast — 4.5:1 이상 (WCAG AA)
  • 모달·드롭다운 trap focus
  • aria-label·aria-describedby 적절히
  • 스크린리더 테스트 (VoiceOver·NVDA)

8. 자주 쓰는 컴포넌트 셋업

# Form 관련
pnpm dlx shadcn@latest add form input label select textarea checkbox radio-group switch

# Layout
pnpm dlx shadcn@latest add card sheet dialog popover dropdown-menu

# Feedback
pnpm dlx shadcn@latest add toast alert progress skeleton

# Data
pnpm dlx shadcn@latest add table data-table tabs accordion

9. CVA 컴포넌트 작성 패턴

shadcn에 없는 자체 컴포넌트도 CVA 패턴 따르기:

const cardVariants = cva(
  'rounded-lg border bg-card text-card-foreground shadow-sm',
  {
    variants: {
      padding: {
        none: '',
        sm: 'p-4',
        md: 'p-6',
        lg: 'p-8',
      },
      hoverable: {
        true: 'transition-shadow hover:shadow-md cursor-pointer',
        false: '',
      },
    },
    defaultVariants: { padding: 'md', hoverable: false },
  }
)

10. 디자인 시스템 진화

  1. Phase 1 (1주): shadcn 기본 컴포넌트 + Tailwind 토큰
  2. Phase 2 (2~4주): 자체 브랜드 색·폰트·spacing 적용
  3. Phase 3 (1~3개월): 자체 컴포넌트 추가 (DataTable·Pricing·Empty State)
  4. Phase 4 (지속): 컴포넌트 라이브러리 npm 패키지화 (모노레포 packages/ui)

자주 묻는 질문

shadcn vs MUI·Chakra?

shadcn: 코드 복사, 자유 수정, 가벼움. MUI: 풍부한 기능, 무거움. 빠른 프로덕트는 shadcn.

접근성을 직접 신경 써야 하나?

Radix Primitives가 90% 처리. 단 본인 작성 컴포넌트는 별도 점검.

디자이너와의 협업?Tailwind 토큰을 Figma Variables와 1:1 매핑. design system as code 패턴.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록