Design Tokens 통합

디자인 토큰을 Storybook에서 시각화하고 문서화합니다.

← Storybook 가이드로 돌아가기

🎨 Design Tokens란?

디자인 시스템의 시각적 속성(색상, 타이포그래피, 간격 등)을 재사용 가능한 변수로 정의한 것입니다.

일관성

모든 플랫폼 동일

확장성

중앙 관리

협업

디자인-개발 동기화

🏗️ Token 구조 정의

// tokens/colors.ts
export const colors = {
  // Primary Palette
  primary: {
    50: '#EFF6FF',
    100: '#DBEAFE',
    200: '#BFDBFE',
    300: '#93C5FD',
    400: '#60A5FA',
    500: '#3B82F6', // Main
    600: '#2563EB',
    700: '#1D4ED8',
    800: '#1E40AF',
    900: '#1E3A8A',
  },
  
  // Secondary Palette
  secondary: {
    50: '#F5F3FF',
    500: '#8B5CF6',
    900: '#4C1D95',
  },
  
  // Semantic Colors
  semantic: {
    success: '#22C55E',
    warning: '#F59E0B',
    error: '#EF4444',
    info: '#3B82F6',
  },
  
  // Neutral Colors
  neutral: {
    white: '#FFFFFF',
    black: '#000000',
    gray: {
      50: '#F9FAFB',
      100: '#F3F4F6',
      200: '#E5E7EB',
      300: '#D1D5DB',
      400: '#9CA3AF',
      500: '#6B7280',
      600: '#4B5563',
      700: '#374151',
      800: '#1F2937',
      900: '#111827',
    },
  },
};

// tokens/typography.ts
export const typography = {
  fontFamily: {
    sans: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    mono: '"Fira Code", Consolas, Monaco, "Courier New", monospace',
  },
  
  fontSize: {
    xs: '0.75rem',    // 12px
    sm: '0.875rem',   // 14px
    base: '1rem',     // 16px
    lg: '1.125rem',   // 18px
    xl: '1.25rem',    // 20px
    '2xl': '1.5rem',  // 24px
    '3xl': '1.875rem', // 30px
    '4xl': '2.25rem', // 36px
  },
  
  fontWeight: {
    light: 300,
    normal: 400,
    medium: 500,
    semibold: 600,
    bold: 700,
  },
  
  lineHeight: {
    tight: 1.25,
    normal: 1.5,
    relaxed: 1.75,
  },
};

// tokens/spacing.ts
export const spacing = {
  0: '0',
  1: '0.25rem',  // 4px
  2: '0.5rem',   // 8px
  3: '0.75rem',  // 12px
  4: '1rem',     // 16px
  5: '1.25rem',  // 20px
  6: '1.5rem',   // 24px
  8: '2rem',     // 32px
  10: '2.5rem',  // 40px
  12: '3rem',    // 48px
  16: '4rem',    // 64px
};

// tokens/borderRadius.ts
export const borderRadius = {
  none: '0',
  sm: '0.125rem',   // 2px
  base: '0.25rem',  // 4px
  md: '0.375rem',   // 6px
  lg: '0.5rem',     // 8px
  xl: '0.75rem',    // 12px
  '2xl': '1rem',    // 16px
  full: '9999px',
};

// tokens/shadows.ts
export const shadows = {
  sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
  base: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
  md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
  lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
  xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
};

📚 Storybook에서 Tokens 문서화

// tokens.stories.mdx
import { Meta, ColorPalette, ColorItem, Typeset } from '@storybook/blocks';
import { colors, typography, spacing } from '../tokens';

<Meta title="Design System/Tokens" />

# Design Tokens

디자인 시스템의 핵심 토큰들입니다.

## Colors

### Primary Colors
<ColorPalette>
  <ColorItem
    title="Primary"
    subtitle="브랜드 메인 컬러"
    colors={{
      50: colors.primary[50],
      100: colors.primary[100],
      200: colors.primary[200],
      300: colors.primary[300],
      400: colors.primary[400],
      500: colors.primary[500],
      600: colors.primary[600],
      700: colors.primary[700],
      800: colors.primary[800],
      900: colors.primary[900],
    }}
  />
</ColorPalette>

### Semantic Colors
<ColorPalette>
  <ColorItem
    title="Success"
    subtitle="성공 상태"
    colors={{ Success: colors.semantic.success }}
  />
  <ColorItem
    title="Warning"
    subtitle="경고 상태"
    colors={{ Warning: colors.semantic.warning }}
  />
  <ColorItem
    title="Error"
    subtitle="오류 상태"
    colors={{ Error: colors.semantic.error }}
  />
  <ColorItem
    title="Info"
    subtitle="정보 상태"
    colors={{ Info: colors.semantic.info }}
  />
</ColorPalette>

## Typography

<Typeset
  fontFamily={typography.fontFamily.sans}
  fontSizes={[
    Number(typography.fontSize.xs.replace('rem', '')) * 16,
    Number(typography.fontSize.sm.replace('rem', '')) * 16,
    Number(typography.fontSize.base.replace('rem', '')) * 16,
    Number(typography.fontSize.lg.replace('rem', '')) * 16,
    Number(typography.fontSize.xl.replace('rem', '')) * 16,
  ]}
  fontWeight={typography.fontWeight.normal}
  sampleText="The quick brown fox jumps over the lazy dog"
/>

## Spacing Scale

| Token | Value | Example |
|-------|-------|---------|
| spacing.1 | 4px | □ |
| spacing.2 | 8px | □□ |
| spacing.4 | 16px | □□□□ |
| spacing.8 | 32px | □□□□□□□□ |

## Border Radius

| Token | Value | Preview |
|-------|-------|---------|
| none | 0 | ▪️ |
| sm | 2px | ▫️ |
| base | 4px | ◽ |
| lg | 8px | ◻️ |
| full | 9999px | ⚪ |

🎨 Token Visualizer 컴포넌트

// ColorGrid.tsx
interface ColorGridProps {
  colors: Record<string, string>;
  title: string;
}

export const ColorGrid: React.FC<ColorGridProps> = ({ 
  colors, 
  title 
}) => (
  <div className="mb-6">
    <h3 className="font-bold mb-3">{title}</h3>
    <div className="grid grid-cols-5 gap-2">
      {Object.entries(colors).map(([key, value]) => (
        <div key={key} className="text-center">
          <div
            className="w-full h-16 rounded-lg shadow-sm mb-2"
            style={{ backgroundColor: value }}
          />
          <div className="text-xs font-mono">
            <div className="font-semibold">{key}</div>
            <div className="text-gray-600">{value}</div>
          </div>
        </div>
      ))}
    </div>
  </div>
);

// TokenTable.tsx
interface TokenTableProps {
  tokens: Record<string, any>;
  category: string;
}

export const TokenTable: React.FC<TokenTableProps> = ({ 
  tokens, 
  category 
}) => (
  <div className="overflow-x-auto">
    <table className="w-full border-collapse">
      <thead>
        <tr className="bg-gray-100">
          <th className="p-2 text-left">Token</th>
          <th className="p-2 text-left">Value</th>
          <th className="p-2 text-left">Usage</th>
        </tr>
      </thead>
      <tbody>
        {Object.entries(tokens).map(([key, value]) => (
          <tr key={key} className="border-t">
            <td className="p-2 font-mono text-sm">
              {category}.{key}
            </td>
            <td className="p-2 font-mono text-sm text-gray-600">
              {String(value)}
            </td>
            <td className="p-2 text-sm">
              Example usage description
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  </div>
);

📖 Token Story 예제

// tokens.stories.tsx
import type { Meta } from '@storybook/react';
import { ColorGrid, TokenTable } from './components';
import { colors, spacing, typography } from './tokens';

const meta: Meta = {
  title: 'Design System/Tokens',
  parameters: {
    layout: 'fullscreen',
  },
};

export default meta;

export const Colors = () => (
  <div className="p-8">
    <h1 className="text-3xl font-bold mb-8">
      Color Tokens
    </h1>
    
    <ColorGrid 
      title="Primary" 
      colors={colors.primary} 
    />
    
    <ColorGrid 
      title="Semantic" 
      colors={colors.semantic} 
    />
  </div>
);

export const Spacing = () => (
  <div className="p-8">
    <h1 className="text-3xl font-bold mb-8">
      Spacing Tokens
    </h1>
    
    <TokenTable 
      tokens={spacing} 
      category="spacing" 
    />
  </div>
);

export const Typography = () => (
  <div className="p-8">
    <h1 className="text-3xl font-bold mb-8">
      Typography Tokens
    </h1>
    
    <TokenTable 
      tokens={typography.fontSize} 
      category="typography.fontSize" 
    />
  </div>
);

🔄 Figma → Storybook 자동 동기화

1. Style Dictionary 설정

// style-dictionary.config.js
module.exports = {
  source: ['tokens/**/*.json'],
  platforms: {
    ts: {
      transformGroup: 'js',
      buildPath: 'src/tokens/',
      files: [{
        destination: 'index.ts',
        format: 'javascript/es6',
      }]
    },
    css: {
      transformGroup: 'css',
      buildPath: 'src/styles/',
      files: [{
        destination: 'variables.css',
        format: 'css/variables',
      }]
    }
  }
};

2. Figma Tokens Plugin

// tokens/colors.json
{
  "color": {
    "primary": {
      "500": {
        "value": "#3B82F6",
        "type": "color",
        "description": "Main brand color"
      }
    }
  }
}

// 빌드 스크립트
{
  "scripts": {
    "build:tokens": "style-dictionary build",
    "sync:figma": "figma-tokens sync"
  }
}

🎯 Token 사용 예제

// Button.tsx
import { colors, spacing, borderRadius } from '@/tokens';

export const Button = styled.button`
  background-color: ${colors.primary[500]};
  padding: ${spacing[3]} ${spacing[6]};
  border-radius: ${borderRadius.lg};
  
  &:hover {
    background-color: ${colors.primary[600]};
  }
  
  &:disabled {
    background-color: ${colors.neutral.gray[300]};
  }
`;

// CSS Variables 사용
const Button = () => (
  <button style={{
    backgroundColor: 'var(--color-primary-500)',
    padding: 'var(--spacing-3) var(--spacing-6)',
    borderRadius: 'var(--border-radius-lg)',
  }}>
    Click me
  </button>
);

// Tailwind 통합
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: colors,
      spacing: spacing,
      borderRadius: borderRadius,
    }
  }
};

📱 다크모드 Token

// tokens/themes.ts
export const lightTheme = {
  background: {
    primary: colors.neutral.white,
    secondary: colors.neutral.gray[50],
    tertiary: colors.neutral.gray[100],
  },
  text: {
    primary: colors.neutral.gray[900],
    secondary: colors.neutral.gray[600],
  },
};

export const darkTheme = {
  background: {
    primary: colors.neutral.gray[900],
    secondary: colors.neutral.gray[800],
    tertiary: colors.neutral.gray[700],
  },
  text: {
    primary: colors.neutral.white,
    secondary: colors.neutral.gray[300],
  },
};

// Storybook에서 테마 전환
export const parameters = {
  backgrounds: {
    values: [
      { name: 'Light', value: lightTheme.background.primary },
      { name: 'Dark', value: darkTheme.background.primary },
    ],
  },
};

✅ Best Practices

  • ✓ 의미 있는 토큰 이름
  • ✓ 계층 구조로 조직화
  • ✓ Semantic 토큰 활용
  • ✓ 문서에 사용 예제
  • ✓ 접근성 기준 준수
  • ✓ 버전 관리

🔧 도구 추천

  • • Style Dictionary
  • • Figma Tokens Plugin
  • • Theo (Salesforce)
  • • Design Tokens W3C
  • • Token Studio

📊 Token 카테고리

  • • Colors
  • • Typography
  • • Spacing
  • • Border Radius
  • • Shadows
  • • Z-Index
  • • Breakpoints
  • • Animations

🚀 Token 관리 워크플로우

1. 정의

Figma에서 디자인 토큰 정의

2. 추출

Tokens Plugin으로 JSON 추출

3. 변환

Style Dictionary로 빌드

4. 문서화

Storybook에 시각화

💡 실전 팁

Semantic vs Primitive

Primitive 토큰(blue-500)보다 Semantic 토큰(primary-color)을 사용하면 테마 변경이 쉬움

Token 네이밍

일관된 네이밍 규칙 사용: category-property-variant-state

문서화 자동화

CI/CD에서 토큰 변경 시 자동으로 Storybook 재빌드

버전 관리

Breaking change 시 Major 버전 업데이트 (Semver)