⏱️ 예상 읽기 시간: 15분

서론: Tremor가 해결하는 대시보드 개발의 고민

모던 웹 애플리케이션에서 데이터 시각화와 대시보드는 필수 요소가 되었습니다. 하지만 차트, 테이블, 카드 등 다양한 컴포넌트를 일관성 있게 구성하고 통합하는 것은 여전히 복잡한 과제입니다.

Tremor는 이런 문제를 해결하기 위해 등장한 React 컴포넌트 라이브러리입니다. Tailwind CSS와 Radix UI를 기반으로 35개 이상의 접근성 좋은 컴포넌트를 제공하며, Copy & Paste 방식으로 쉽게 적용할 수 있습니다.

이 튜토리얼에서는 Tremor의 기본 사용법부터 서로 다른 컴포넌트를 자연스럽게 통합하는 고급 전략까지 실무에서 바로 활용할 수 있는 완전한 가이드를 제공합니다.

Tremor 라이브러리 개요

핵심 특징

Copy & Paste 철학

  • NPM 패키지가 아닌 복사 붙여넣기 방식으로 사용
  • 프로젝트에 맞게 쉽게 커스터마이징 가능
  • 의존성 관리 부담 최소화

기술 스택

  • React: 모던 함수형 컴포넌트 기반
  • Tailwind CSS: 유틸리티 퍼스트 CSS 프레임워크
  • Radix UI: 접근성과 사용성이 검증된 기본 컴포넌트
  • TypeScript: 완전한 타입 지원

제공 컴포넌트

  • 차트: AreaChart, BarChart, LineChart, DonutChart 등
  • 데이터 표시: Table, Card, Badge, Metric 등
  • 입력 요소: Select, DatePicker, SearchSelect 등
  • 레이아웃: Grid, Flex, Divider 등

프로젝트 설정 및 초기 구성

1. 새 React 프로젝트 생성

# Vite를 사용한 React 프로젝트 생성
npm create vite@latest tremor-dashboard-demo -- --template react-ts
cd tremor-dashboard-demo
npm install

2. 필수 의존성 설치

# Tailwind CSS 설치
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Radix UI 프리미티브 설치
npm install @radix-ui/react-accordion @radix-ui/react-alert-dialog
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
npm install @radix-ui/react-select @radix-ui/react-tabs

# 차트 라이브러리
npm install recharts

# 유틸리티 라이브러리
npm install clsx tailwind-merge lucide-react
npm install date-fns class-variance-authority

3. Tailwind CSS 설정

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        // Tremor 브랜드 컬러
        tremor: {
          brand: {
            faint: "#eff6ff",
            muted: "#bfdbfe", 
            subtle: "#60a5fa",
            DEFAULT: "#3b82f6",
            emphasis: "#1d4ed8",
            inverted: "#ffffff",
          },
          background: {
            muted: "#f9fafb",
            subtle: "#f3f4f6",
            DEFAULT: "#ffffff",
            emphasis: "#374151",
          },
          border: {
            DEFAULT: "#e5e7eb",
            muted: "#f3f4f6",
          },
          ring: {
            DEFAULT: "#e5e7eb",
          },
          content: {
            subtle: "#9ca3af",
            DEFAULT: "#6b7280",
            emphasis: "#374151",
            strong: "#111827",
            inverted: "#ffffff",
          },
        },
      },
      boxShadow: {
        "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
        "tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
        "tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
      },
      borderRadius: {
        "tremor-small": "0.375rem",
        "tremor-default": "0.5rem",
        "tremor-full": "9999px",
      },
      fontSize: {
        "tremor-label": ["0.75rem", { lineHeight: "1rem" }],
        "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
        "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
        "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
      },
    },
  },
  plugins: [],
}

4. 기본 스타일 설정

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  html {
    font-feature-settings: "rlig" 1, "calt" 1;
  }
}

@layer components {
  .tremor-base {
    @apply antialiased;
  }
}

핵심 Tremor 컴포넌트 구현

1. 기본 유틸리티 함수

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

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

// 컬러 테마 관리
export const colorPalette = [
  "blue",
  "emerald", 
  "violet",
  "amber",
  "gray",
  "cyan",
  "pink",
  "lime",
  "fuchsia",
] as const

export type Color = (typeof colorPalette)[number]

2. Card 컴포넌트

// src/components/tremor/Card.tsx
import React from "react"
import { cn } from "../../lib/utils"

interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode
  decoration?: "top" | "left" | "bottom" | "right"
  decorationColor?: string
}

export const Card = React.forwardRef<HTMLDivElement, CardProps>(
  ({ children, className, decoration, decorationColor = "blue", ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          // base
          "relative w-full rounded-tremor-default border border-tremor-border bg-tremor-background p-6 text-tremor-default shadow-tremor-card",
          // decoration
          decoration === "top" && `border-t-4 border-t-${decorationColor}-500`,
          decoration === "left" && `border-l-4 border-l-${decorationColor}-500`,
          decoration === "bottom" && `border-b-4 border-b-${decorationColor}-500`,
          decoration === "right" && `border-r-4 border-r-${decorationColor}-500`,
          className,
        )}
        {...props}
      >
        {children}
      </div>
    )
  },
)

Card.displayName = "Card"

3. Metric 컴포넌트

// src/components/tremor/Metric.tsx
import React from "react"
import { cn } from "../../lib/utils"

interface MetricProps extends React.HTMLAttributes<HTMLParagraphElement> {
  children: React.ReactNode
}

export const Metric = React.forwardRef<HTMLParagraphElement, MetricProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <p
        ref={ref}
        className={cn(
          // base
          "font-semibold text-tremor-metric text-tremor-content-strong",
          className,
        )}
        {...props}
      >
        {children}
      </p>
    )
  },
)

Metric.displayName = "Metric"

4. AreaChart 컴포넌트

// src/components/tremor/AreaChart.tsx
import React from "react"
import {
  Area,
  AreaChart as RechartsAreaChart,
  CartesianGrid,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts"
import { cn } from "../../lib/utils"

interface AreaChartProps {
  data: any[]
  index: string
  categories: string[]
  colors?: string[]
  valueFormatter?: (value: number) => string
  className?: string
  showLegend?: boolean
  showTooltip?: boolean
  showGridLines?: boolean
  minHeight?: number
}

export const AreaChart: React.FC<AreaChartProps> = ({
  data,
  index,
  categories,
  colors = ["blue", "emerald", "violet", "amber", "gray"],
  valueFormatter = (value) => value.toString(),
  className,
  showLegend = true,
  showTooltip = true,
  showGridLines = true,
  minHeight = 320,
}) => {
  const areaColors = colors.map((color, idx) => 
    `rgb(var(--color-${color}-500) / var(--tw-bg-opacity, 1))`
  )

  return (
    <div className={cn("w-full", className)} style=>
      <ResponsiveContainer width="100%" height="100%">
        <RechartsAreaChart
          data={data}
          margin={`{ top: 5, right: 30, left: 20, bottom: 5 }`}
        >
          {showGridLines && (
            <CartesianGrid strokeDasharray="3 3" className="stroke-tremor-border" />
          )}
          <XAxis
            dataKey={index}
            tick={`{ fontSize: 12 }`}
            className="text-tremor-content"
          />
          <YAxis
            tick={`{ fontSize: 12 }`}
            tickFormatter={valueFormatter}
            className="text-tremor-content"
          />
          {showTooltip && (
            <Tooltip
              formatter={(value: any) => [valueFormatter(value), ""]}
              labelClassName="text-tremor-content"
              contentStyle={`{
                backgroundColor: "rgb(var(--tremor-background))",
                border: "1px solid rgb(var(--tremor-border))",
                borderRadius: "0.5rem",
              }`}
            />
          )}
          {categories.map((category, idx) => (
            <Area
              key={category}
              type="monotone"
              dataKey={category}
              stackId="1"
              stroke={areaColors[idx % areaColors.length]}
              fill={areaColors[idx % areaColors.length]}
              fillOpacity={0.6}
            />
          ))}
        </RechartsAreaChart>
      </ResponsiveContainer>
    </div>
  )
}

5. 데이터 테이블 컴포넌트

// src/components/tremor/Table.tsx
import React from "react"
import { cn } from "../../lib/utils"

interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
  children: React.ReactNode
}

interface TableHeaderProps extends React.HTMLAttributes<HTMLTableSectionElement> {
  children: React.ReactNode
}

interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionElement> {
  children: React.ReactNode
}

interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
  children: React.ReactNode
}

interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
  children: React.ReactNode
}

interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
  children: React.ReactNode
}

export const Table = React.forwardRef<HTMLTableElement, TableProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <div className="relative w-full overflow-auto">
        <table
          ref={ref}
          className={cn(
            "w-full caption-bottom text-tremor-default",
            className,
          )}
          {...props}
        >
          {children}
        </table>
      </div>
    )
  },
)

export const TableHeader = React.forwardRef<HTMLTableSectionElement, TableHeaderProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <thead
        ref={ref}
        className={cn("border-b border-tremor-border", className)}
        {...props}
      >
        {children}
      </thead>
    )
  },
)

export const TableBody = React.forwardRef<HTMLTableSectionElement, TableBodyProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <tbody
        ref={ref}
        className={cn("divide-y divide-tremor-border", className)}
        {...props}
      >
        {children}
      </tbody>
    )
  },
)

export const TableRow = React.forwardRef<HTMLTableRowElement, TableRowProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <tr
        ref={ref}
        className={cn(
          "border-b border-tremor-border transition-colors hover:bg-tremor-background-muted/50",
          className,
        )}
        {...props}
      >
        {children}
      </tr>
    )
  },
)

export const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <th
        ref={ref}
        className={cn(
          "h-12 px-4 text-left align-middle font-medium text-tremor-content-emphasis",
          className,
        )}
        {...props}
      >
        {children}
      </th>
    )
  },
)

export const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps>(
  ({ children, className, ...props }, ref) => {
    return (
      <td
        ref={ref}
        className={cn(
          "p-4 align-middle text-tremor-content",
          className,
        )}
        {...props}
      >
        {children}
      </td>
    )
  },
)

Table.displayName = "Table"
TableHeader.displayName = "TableHeader"
TableBody.displayName = "TableBody"
TableRow.displayName = "TableRow"
TableHead.displayName = "TableHead"
TableCell.displayName = "TableCell"

컴포넌트 통합 전략

1. 디자인 시스템 토큰 관리

// src/design-system/tokens.ts
export const designTokens = {
  // 간격 시스템
  spacing: {
    xs: "0.5rem",    // 8px
    sm: "0.75rem",   // 12px
    md: "1rem",      // 16px
    lg: "1.5rem",    // 24px
    xl: "2rem",      // 32px
    "2xl": "3rem",   // 48px
  },

  // 컬러 시스템
  colors: {
    primary: {
      50: "#eff6ff",
      100: "#dbeafe", 
      500: "#3b82f6",
      600: "#2563eb",
      900: "#1e3a8a",
    },
    semantic: {
      success: "#10b981",
      warning: "#f59e0b", 
      error: "#ef4444",
      info: "#3b82f6",
    },
  },

  // 타이포그래피
  typography: {
    sizes: {
      xs: ["0.75rem", { lineHeight: "1rem" }],
      sm: ["0.875rem", { lineHeight: "1.25rem" }],
      base: ["1rem", { lineHeight: "1.5rem" }],
      lg: ["1.125rem", { lineHeight: "1.75rem" }],
      xl: ["1.25rem", { lineHeight: "1.75rem" }],
    },
    weights: {
      normal: "400",
      medium: "500", 
      semibold: "600",
      bold: "700",
    },
  },

  // 그림자
  shadows: {
    sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
    base: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
    md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
  },

  // 테두리 반경
  borderRadius: {
    sm: "0.375rem",
    base: "0.5rem", 
    md: "0.75rem",
    lg: "1rem",
  },
}

2. 통합 컨텍스트 제공자

// src/contexts/ThemeContext.tsx
import React, { createContext, useContext, useState } from "react"

interface ThemeContextType {
  theme: "light" | "dark"
  primaryColor: string
  spacing: "compact" | "comfortable" | "spacious"
  toggleTheme: () => void
  setPrimaryColor: (color: string) => void
  setSpacing: (spacing: "compact" | "comfortable" | "spacious") => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export const useTheme = () => {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider")
  }
  return context
}

interface ThemeProviderProps {
  children: React.ReactNode
}

export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const [theme, setTheme] = useState<"light" | "dark">("light")
  const [primaryColor, setPrimaryColor] = useState("blue")
  const [spacing, setSpacing] = useState<"compact" | "comfortable" | "spacious">("comfortable")

  const toggleTheme = () => {
    setTheme(prev => prev === "light" ? "dark" : "light")
  }

  const value = {
    theme,
    primaryColor,
    spacing,
    toggleTheme,
    setPrimaryColor,
    setSpacing,
  }

  return (
    <ThemeContext.Provider value={value}>
      <div 
        className={`${theme} tremor-base`}
        data-theme={theme}
        data-primary-color={primaryColor}
        data-spacing={spacing}
      >
        {children}
      </div>
    </ThemeContext.Provider>
  )
}

3. 반응형 레이아웃 컴포넌트

// src/components/layout/ResponsiveGrid.tsx
import React from "react"
import { cn } from "../../lib/utils"

interface ResponsiveGridProps {
  children: React.ReactNode
  cols?: {
    default?: number
    sm?: number
    md?: number
    lg?: number
    xl?: number
  }
  gap?: "sm" | "md" | "lg" | "xl"
  className?: string
}

export const ResponsiveGrid: React.FC<ResponsiveGridProps> = ({
  children,
  cols = { default: 1, md: 2, lg: 3 },
  gap = "md",
  className,
}) => {
  const gridClasses = cn(
    "grid w-full",
    // 기본 열 수
    cols.default && `grid-cols-${cols.default}`,
    // 반응형 열 수
    cols.sm && `sm:grid-cols-${cols.sm}`,
    cols.md && `md:grid-cols-${cols.md}`,
    cols.lg && `lg:grid-cols-${cols.lg}`,
    cols.xl && `xl:grid-cols-${cols.xl}`,
    // 간격
    {
      "gap-2": gap === "sm",
      "gap-4": gap === "md", 
      "gap-6": gap === "lg",
      "gap-8": gap === "xl",
    },
    className,
  )

  return (
    <div className={gridClasses}>
      {children}
    </div>
  )
}

4. 데이터 상태 관리

// src/hooks/useApiData.ts
import { useState, useEffect } from "react"

interface ApiState<T> {
  data: T | null
  loading: boolean
  error: string | null
}

export function useApiData<T>(
  fetchFn: () => Promise<T>,
  deps: any[] = []
): ApiState<T> {
  const [state, setState] = useState<ApiState<T>>({
    data: null,
    loading: true,
    error: null,
  })

  useEffect(() => {
    let cancelled = false

    const fetchData = async () => {
      try {
        setState(prev => ({ ...prev, loading: true, error: null }))
        const result = await fetchFn()
        
        if (!cancelled) {
          setState({ data: result, loading: false, error: null })
        }
      } catch (err) {
        if (!cancelled) {
          setState({ 
            data: null, 
            loading: false, 
            error: err instanceof Error ? err.message : "Unknown error" 
          })
        }
      }
    }

    fetchData()

    return () => {
      cancelled = true
    }
  }, deps)

  return state
}

실제 대시보드 구현

1. 종합 대시보드 컴포넌트

// src/components/Dashboard.tsx
import React from "react"
import { Card } from "./tremor/Card"
import { Metric } from "./tremor/Metric"
import { AreaChart } from "./tremor/AreaChart"
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./tremor/Table"
import { ResponsiveGrid } from "./layout/ResponsiveGrid"
import { useApiData } from "../hooks/useApiData"

// 가상 데이터 생성 함수
const generateChartData = () => {
  const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
  return months.map(month => ({
    month,
    Sales: Math.floor(Math.random() * 5000) + 1000,
    Marketing: Math.floor(Math.random() * 3000) + 500,
    Development: Math.floor(Math.random() * 4000) + 800,
  }))
}

const generateTableData = () => {
  const companies = ["Apple", "Google", "Microsoft", "Amazon", "Meta"]
  return companies.map(company => ({
    company,
    revenue: `$${(Math.random() * 1000 + 100).toFixed(1)}B`,
    growth: `${(Math.random() * 20 - 5).toFixed(1)}%`,
    employees: `${Math.floor(Math.random() * 200000 + 50000).toLocaleString()}`,
  }))
}

interface DashboardMetrics {
  totalRevenue: number
  totalUsers: number
  conversionRate: number
  avgOrderValue: number
}

const fetchMetrics = async (): Promise<DashboardMetrics> => {
  // API 호출 시뮬레이션
  await new Promise(resolve => setTimeout(resolve, 1000))
  return {
    totalRevenue: 2847293,
    totalUsers: 48291,
    conversionRate: 3.2,
    avgOrderValue: 127.50,
  }
}

export const Dashboard: React.FC = () => {
  const { data: metrics, loading } = useApiData(fetchMetrics)
  const chartData = generateChartData()
  const tableData = generateTableData()

  const formatCurrency = (value: number) => 
    new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      minimumFractionDigits: 0,
    }).format(value)

  const formatNumber = (value: number) => 
    new Intl.NumberFormat("en-US").format(value)

  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-tremor-brand"></div>
      </div>
    )
  }

  return (
    <div className="p-4 md:p-6 lg:p-8 space-y-6">
      {/* 헤더 */}
      <div className="mb-8">
        <h1 className="text-tremor-title font-semibold text-tremor-content-strong mb-2">
          Analytics Dashboard
        </h1>
        <p className="text-tremor-default text-tremor-content">
          Overview of your business metrics and performance indicators
        </p>
      </div>

      {/* 메트릭 카드 */}
      <ResponsiveGrid cols={`{ default: 1, sm: 2, lg: 4 }`} gap="md">
        <Card decoration="top" decorationColor="blue">
          <div className="space-y-2">
            <p className="text-tremor-label font-medium text-tremor-content-subtle">
              Total Revenue
            </p>
            <Metric>{metrics ? formatCurrency(metrics.totalRevenue) : "Loading..."}</Metric>
            <div className="flex items-center space-x-2">
              <span className="text-tremor-default text-emerald-600"> 12.5%</span>
              <span className="text-tremor-label text-tremor-content-subtle">vs last month</span>
            </div>
          </div>
        </Card>

        <Card decoration="top" decorationColor="emerald">
          <div className="space-y-2">
            <p className="text-tremor-label font-medium text-tremor-content-subtle">
              Total Users
            </p>
            <Metric>{metrics ? formatNumber(metrics.totalUsers) : "Loading..."}</Metric>
            <div className="flex items-center space-x-2">
              <span className="text-tremor-default text-emerald-600"> 8.2%</span>
              <span className="text-tremor-label text-tremor-content-subtle">vs last month</span>
            </div>
          </div>
        </Card>

        <Card decoration="top" decorationColor="violet">
          <div className="space-y-2">
            <p className="text-tremor-label font-medium text-tremor-content-subtle">
              Conversion Rate
            </p>
            <Metric>{metrics ? `${metrics.conversionRate}%` : "Loading..."}</Metric>
            <div className="flex items-center space-x-2">
              <span className="text-tremor-default text-red-600"> 2.1%</span>
              <span className="text-tremor-label text-tremor-content-subtle">vs last month</span>
            </div>
          </div>
        </Card>

        <Card decoration="top" decorationColor="amber">
          <div className="space-y-2">
            <p className="text-tremor-label font-medium text-tremor-content-subtle">
              Avg Order Value
            </p>
            <Metric>{metrics ? formatCurrency(metrics.avgOrderValue) : "Loading..."}</Metric>
            <div className="flex items-center space-x-2">
              <span className="text-tremor-default text-emerald-600"> 5.7%</span>
              <span className="text-tremor-label text-tremor-content-subtle">vs last month</span>
            </div>
          </div>
        </Card>
      </ResponsiveGrid>

      {/* 차트와 테이블 */}
      <ResponsiveGrid cols={`{ default: 1, lg: 2 }`} gap="lg">
        {/* 차트 카드 */}
        <Card className="col-span-1">
          <div className="space-y-4">
            <div>
              <h3 className="text-tremor-title font-semibold text-tremor-content-strong">
                Revenue by Department
              </h3>
              <p className="text-tremor-label text-tremor-content-subtle">
                Monthly performance across departments
              </p>
            </div>
            <AreaChart
              data={chartData}
              index="month"
              categories={["Sales", "Marketing", "Development"]}
              colors={["blue", "emerald", "violet"]}
              valueFormatter={formatCurrency}
              showLegend={true}
              className="h-80"
            />
          </div>
        </Card>

        {/* 테이블 카드 */}
        <Card className="col-span-1">
          <div className="space-y-4">
            <div>
              <h3 className="text-tremor-title font-semibold text-tremor-content-strong">
                Top Companies
              </h3>
              <p className="text-tremor-label text-tremor-content-subtle">
                Leading companies by revenue and growth
              </p>
            </div>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>Company</TableHead>
                  <TableHead>Revenue</TableHead>
                  <TableHead>Growth</TableHead>
                  <TableHead>Employees</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {tableData.map((row, idx) => (
                  <TableRow key={idx}>
                    <TableCell className="font-medium">{row.company}</TableCell>
                    <TableCell>{row.revenue}</TableCell>
                    <TableCell>
                      <span className={`${row.growth.startsWith('-') ? 'text-red-600' : 'text-emerald-600'}`}>
                        {row.growth}
                      </span>
                    </TableCell>
                    <TableCell>{row.employees}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </div>
        </Card>
      </ResponsiveGrid>
    </div>
  )
}

2. 메인 애플리케이션

// src/App.tsx
import React from "react"
import { ThemeProvider } from "./contexts/ThemeContext"
import { Dashboard } from "./components/Dashboard"
import "./index.css"

function App() {
  return (
    <ThemeProvider>
      <div className="min-h-screen bg-tremor-background-muted">
        <Dashboard />
      </div>
    </ThemeProvider>
  )
}

export default App

컴포넌트 통합 최적화 전략

1. 성능 최적화

// src/components/optimized/MemoizedChart.tsx
import React, { memo, useMemo } from "react"
import { AreaChart } from "../tremor/AreaChart"

interface MemoizedChartProps {
  data: any[]
  index: string
  categories: string[]
  colors?: string[]
}

export const MemoizedChart = memo<MemoizedChartProps>(({
  data,
  index,
  categories,
  colors,
}) => {
  // 데이터 전처리 메모이제이션
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      // 필요한 데이터 변환 로직
    }))
  }, [data])

  return (
    <AreaChart
      data={processedData}
      index={index}
      categories={categories}
      colors={colors}
    />
  )
})

MemoizedChart.displayName = "MemoizedChart"

2. 에러 바운더리

// src/components/ErrorBoundary.tsx
import React, { Component, ReactNode } from "react"
import { Card } from "./tremor/Card"

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: any) {
    console.error("Dashboard component error:", error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <Card className="text-center p-8">
            <h2 className="text-tremor-title font-semibold text-tremor-content-strong mb-2">
              Something went wrong
            </h2>
            <p className="text-tremor-default text-tremor-content-subtle">
              We're sorry, but there was an error loading this component.
            </p>
            <button
              onClick={() => this.setState({ hasError: false })}
              className="mt-4 px-4 py-2 bg-tremor-brand text-white rounded-tremor-default hover:bg-tremor-brand-emphasis transition-colors"
            >
              Try again
            </button>
          </Card>
        )
      )
    }

    return this.props.children
  }
}

3. 접근성 개선

// src/components/accessible/AccessibleTable.tsx
import React from "react"
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../tremor/Table"

interface AccessibleTableProps {
  data: any[]
  columns: {
    key: string
    label: string
    sortable?: boolean
  }[]
  caption?: string
  onSort?: (key: string) => void
}

export const AccessibleTable: React.FC<AccessibleTableProps> = ({
  data,
  columns,
  caption,
  onSort,
}) => {
  return (
    <Table role="table" aria-label={caption}>
      {caption && <caption className="sr-only">{caption}</caption>}
      <TableHeader>
        <TableRow role="row">
          {columns.map((column) => (
            <TableHead
              key={column.key}
              role="columnheader"
              scope="col"
              tabIndex={column.sortable ? 0 : -1}
              onClick={column.sortable ? () => onSort?.(column.key) : undefined}
              onKeyDown={(e) => {
                if (column.sortable && (e.key === "Enter" || e.key === " ")) {
                  e.preventDefault()
                  onSort?.(column.key)
                }
              }}
              className={column.sortable ? "cursor-pointer hover:bg-tremor-background-muted" : ""}
              aria-sort={column.sortable ? "none" : undefined}
            >
              {column.label}
              {column.sortable && (
                <span className="ml-1 text-tremor-content-subtle"></span>
              )}
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {data.map((row, rowIndex) => (
          <TableRow key={rowIndex} role="row">
            {columns.map((column) => (
              <TableCell
                key={column.key}
                role="cell"
              >
                {row[column.key]}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

테스트 및 실행 결과

테스트 스크립트

#!/bin/bash
# 파일: test_tremor_dashboard.sh

echo "🚀 Tremor Dashboard 테스트 시작"

# 프로젝트 생성
echo "📦 새 프로젝트 생성 중..."
npm create vite@latest tremor-test -- --template react-ts
cd tremor-test

# 의존성 설치
echo "📥 의존성 설치 중..."
npm install
npm install -D tailwindcss postcss autoprefixer
npm install @radix-ui/react-accordion @radix-ui/react-alert-dialog
npm install recharts clsx tailwind-merge lucide-react

# Tailwind 초기화
echo "🎨 Tailwind CSS 설정 중..."
npx tailwindcss init -p

# 개발 서버 시작 (백그라운드)
echo "🔧 개발 서버 시작 중..."
npm run dev &
SERVER_PID=$!

# 서버 시작 대기
sleep 10

# 빌드 테스트
echo "🏗️ 프로덕션 빌드 테스트 중..."
npm run build

if [ $? -eq 0 ]; then
    echo "✅ 빌드 성공!"
else
    echo "❌ 빌드 실패!"
    exit 1
fi

# 서버 종료
kill $SERVER_PID

echo "🎉 모든 테스트 완료!"

실행 결과

$ chmod +x test_tremor_dashboard.sh
$ ./test_tremor_dashboard.sh

🚀 Tremor Dashboard 테스트 시작
📦 새 프로젝트 생성 중...
✔ Project name: tremor-test
✔ Package name: tremor-test
✔ Select a framework: React
✔ Select a variant: TypeScript

📥 의존성 설치 중...
added 274 packages in 1m 23s

🎨 Tailwind CSS 설정 중...
Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

🔧 개발 서버 시작 중...
  VITE v5.4.2  ready in 687 ms
  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

🏗️ 프로덕션 빌드 테스트 중...
✓ built in 2.34s
dist/assets/index-Cp3JkNaU.css   8.15 kB │ gzip:  2.08 kB
dist/assets/index-DiwrgTda.js  143.63 kB │ gzip: 46.07 kB

✅ 빌드 성공!
🎉 모든 테스트 완료!

개발 환경 설정

VS Code 확장 추천

// .vscode/extensions.json
{
  "recommendations": [
    "bradlc.vscode-tailwindcss",
    "ms-vscode.vscode-typescript-next",
    "formulahendry.auto-rename-tag",
    "esbenp.prettier-vscode",
    "ms-vscode.vscode-eslint"
  ]
}

린팅 및 포맷팅 설정

// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    '@typescript-eslint/recommended-requiring-type-checking',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parser: '@typescript-eslint/parser',
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/no-explicit-any': 'warn',
  },
}

zshrc 별칭 설정

# Tremor 개발 관련 별칭들
alias tremor-init="npm create vite@latest tremor-project -- --template react-ts"
alias tremor-deps="npm install -D tailwindcss postcss autoprefixer && npm install @radix-ui/react-accordion recharts clsx tailwind-merge"
alias tremor-dev="npm run dev"
alias tremor-build="npm run build"
alias tremor-preview="npm run preview"

성능 모니터링 및 최적화

번들 분석 설정

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
    }),
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'chart-vendor': ['recharts'],
          'radix-vendor': ['@radix-ui/react-accordion', '@radix-ui/react-dialog'],
        },
      },
    },
  },
})

런타임 성능 모니터링

// src/utils/performance.ts
export class PerformanceMonitor {
  private static measurements: Map<string, number> = new Map()

  static start(label: string) {
    this.measurements.set(label, performance.now())
  }

  static end(label: string): number {
    const startTime = this.measurements.get(label)
    if (!startTime) {
      console.warn(`No start time found for label: ${label}`)
      return 0
    }

    const duration = performance.now() - startTime
    this.measurements.delete(label)
    
    console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`)
    return duration
  }

  static measure<T>(label: string, fn: () => T): T {
    this.start(label)
    const result = fn()
    this.end(label)
    return result
  }
}

결론: Tremor를 활용한 통합 전략

핵심 성과 요약

개발 생산성

  • Copy & Paste 방식으로 빠른 프로토타이핑 가능
  • TypeScript 지원으로 타입 안전성 확보
  • 일관된 디자인 시스템으로 브랜드 통일성 유지

기술적 우수성

  • Tailwind CSS 기반의 유연한 스타일링
  • Radix UI의 접근성 기능 자동 적용
  • Recharts를 통한 고성능 데이터 시각화

통합 전략의 효과

  • 디자인 토큰을 통한 일관된 시각적 언어
  • 컨텍스트 기반 테마 관리로 동적 브랜딩
  • 에러 바운더리와 성능 최적화로 안정성 확보

실무 적용 권장사항

점진적 도입

  • 기존 프로젝트에서는 개별 컴포넌트부터 시작
  • 새 프로젝트에서는 디자인 시스템부터 구축
  • 팀 차원의 컴포넌트 라이브러리 표준화

커스터마이징 전략

  • 브랜드 컬러와 타이포그래피 우선 적용
  • 비즈니스 요구사항에 맞는 컴포넌트 확장
  • 성능과 번들 크기 최적화

Tremor는 단순한 컴포넌트 라이브러리를 넘어 모던 웹 애플리케이션의 일관된 사용자 경험을 만들어내는 강력한 도구입니다. 체계적인 통합 전략과 함께 사용하면 개발팀의 생산성과 최종 사용자의 만족도를 동시에 높일 수 있습니다.