Featuring Design System

Design Philosophy

Featuring Design System의 설계 원칙과 디자인 철학

핵심 철학

Featuring Design System은 소비자(consumer) 우선의 설계 철학을 따릅니다. 디자인 시스템은 제약이 아니라 도구여야 합니다. 소비자가 시스템과 싸우지 않고, 자연스럽게 확장하고 커스터마이징할 수 있어야 합니다.

이 철학은 세 가지 핵심 가치로 요약됩니다:

  1. 소비자 CSS가 항상 이긴다 — CSS Cascade Layers를 통해 !important 없이 오버라이드 가능
  2. 접근성은 기본값이다 — disabled, focus, hover 등 인터랙션 상태가 WAI-ARIA 패턴을 준수
  3. 조합이 설정을 이긴다 — Compound Component 패턴으로 유연성과 타입 안전성을 동시에 확보

설계 원칙

1. Token-First Design

디자인의 모든 결정은 토큰으로 시작합니다. 색상, 간격, 타이포그래피, 그림자 — 모든 시각적 속성이 체계적인 토큰 시스템 위에 구축됩니다.

<HStack $css={{ gap: '$spacing-300' }}>
{/* 토큰을 사용한 일관된 스타일링 */}
<Box $css={{
  padding: '$spacing-400',
  bgColor: '$background-1',
  color: '$text-1',
  borderRadius: '$radius-200',
  border: '1px solid',
  borderColor: '$border-default',
}}>Token-based</Box>
<Box $css={{
  padding: '$spacing-400',
  bgColor: '$primary-10',
  color: '$primary-70',
  borderRadius: '$radius-200',
}}>Primary</Box>
<Box $css={{
  padding: '$spacing-400',
  bgColor: '$support-error-1',
  color: '$support-error-3',
  borderRadius: '$radius-200',
}}>Error</Box>
</HStack>

토큰을 사용하면:

  • 일관성: 모든 화면에서 동일한 시각적 언어를 사용합니다
  • 유지보수: 값을 한 곳에서 변경하면 전체에 반영됩니다
  • 브랜드 전환: CSS 파일 교체만으로 Featuring ↔ DataEffect 테마를 전환할 수 있습니다

2. Consumer CSS Always Wins

CSS Cascade Layers를 사용하여 디자인 시스템의 모든 CSS를 @layer 안에 배치합니다. 소비자 CSS는 레이어 밖에 있으므로 항상 디자인 시스템 스타일보다 우선합니다.

@layer ft.reset, ft.normalize, ft.components;

/* 이 순서대로 우선순위가 결정됩니다:
   ft.reset      → 가장 낮은 우선순위
   ft.normalize   → 브라우저 정규화
   ft.components  → 컴포넌트 스타일
   (unlayered)    → 소비자 CSS — 항상 이김 */

이것이 의미하는 것:

  • 소비자는 !important 없이 어떤 컴포넌트 스타일이든 오버라이드할 수 있습니다
  • $css prop으로 토큰 기반 스타일링을 하되, 필요하면 일반 CSS로 자유롭게 확장 가능합니다
  • 디자인 시스템이 소비자의 스타일링을 방해하지 않습니다

3. Zero-Runtime CSS

Vanilla Extract를 기반으로 모든 CSS가 빌드 타임에 생성됩니다. 런타임에 JavaScript로 스타일을 계산하지 않으므로:

  • 성능: 스타일 계산으로 인한 JavaScript 번들 증가가 없습니다
  • 예측 가능성: 생성된 CSS는 정적이며, FOUC(Flash of Unstyled Content)가 발생하지 않습니다
  • 디버깅: 브라우저 DevTools에서 일반 CSS로 확인할 수 있습니다

4. Accessible by Default

인터랙티브 컴포넌트의 상태 관리는 WAI-ARIA 패턴을 준수합니다. 특히 disabled 상태에서의 접근성을 두 가지 레벨로 구분합니다:

HTML disabled — 요소를 완전히 비활성화합니다. 포커스, 클릭, 키보드 이벤트 모두 차단됩니다.

aria-disabled="true" + data-disabled — 시각적으로는 비활성화되지만, 포커스는 가능합니다. 툴팁 표시, 스크린 리더 접근 등을 위해 사용합니다.

// 완전 비활성화 — 포커스 불가
<Button.Root disabled>저장</Button.Root>

// 포커스 가능한 비활성화 — 툴팁, 접근성 유지
<Button.Root disabled focusableWhenDisabled>저장</Button.Root>

이 패턴은 rainbow-sprinkles의 조건부 셀렉터에 반영되어 있습니다:

  • hover / active 조건은 &:not([data-disabled]) 가드를 포함하여, 비활성화 상태에서 hover/active 스타일이 적용되지 않습니다
  • disabled 조건은 &:is(:disabled, [data-disabled])로 양쪽 모두를 처리합니다
  • focus 조건은 &:focus-visible로, focusableWhenDisabled 상태에서도 포커스 링이 표시됩니다

왜 비활성화 상태에서 hover를 차단할까? disabled 버튼에 hover 효과가 적용되면 사용자에게 "클릭 가능하다"는 잘못된 신호를 줍니다. Material UI, Radix, Chakra UI 등 주요 디자인 시스템도 동일한 패턴을 따릅니다.

5. Composition over Configuration

하나의 거대한 컴포넌트보다 작고 조합 가능한 컴포넌트를 선호합니다.

<VStack $css={{ gap: '$spacing-400' }}>
{/* Composition: 순서를 자유롭게 변경 가능 */}
<HStack $css={{ gap: '$spacing-300' }}>
  <Button.Root variant="primary">
    <Button.Icon><IconAddOutline /></Button.Icon>
    <Button.Text>Icon + Text</Button.Text>
  </Button.Root>
  <Button.Root variant="secondary">
    <Button.Text>Text + Icon</Button.Text>
    <Button.Icon><IconCaretDownFilled /></Button.Icon>
  </Button.Root>
  <Button.Root variant="tertiary">
    <Button.Icon><IconAddOutline /></Button.Icon>
    <Button.Text>Both</Button.Text>
    <Button.Icon><IconCloseOutline /></Button.Icon>
  </Button.Root>
</HStack>
</VStack>

Compound Component 패턴을 통해:

  • 렌더링 순서를 자유롭게 변경할 수 있습니다
  • 각 하위 요소에 개별적으로 스타일을 적용할 수 있습니다
  • Context를 통해 부모 상태(size, loading, disabled)가 하위 컴포넌트에 자동 전달됩니다
  • 타입 안전성을 유지하면서 유연한 API를 제공합니다

6. Polymorphic Rendering

모든 레이아웃 컴포넌트는 render prop을 통해 렌더링할 HTML 요소를 변경할 수 있습니다.

<VStack $css={{ gap: '$spacing-300' }}>
{/* div 대신 section으로 렌더링 */}
<Box render={<section />} $css={{
  padding: '$spacing-400',
  bgColor: '$background-2',
  borderRadius: '$radius-200',
}}>
  &lt;section&gt;으로 렌더링
</Box>

{/* h1으로 렌더링 */}
<Typo render={<h2 />} variant="$heading-3">
  &lt;h2&gt;으로 렌더링된 Typo
</Typo>

{/* span으로 렌더링 */}
<Box render={<span />} $css={{
  padding: '$spacing-200',
  bgColor: '$primary-10',
  borderRadius: '$radius-100',
  display: 'inline-block',
}}>
  &lt;span&gt;으로 렌더링
</Box>
</VStack>

시맨틱 HTML을 유지하면서도 디자인 시스템의 스타일링 기능을 활용할 수 있습니다.

7. Responsive-First

모든 레이아웃 속성은 반응형을 기본으로 지원합니다. Mobile-first 접근 방식으로, 작은 화면에서 시작하여 큰 화면으로 확장합니다.

<Box $css={{
padding: { mobile: '$spacing-300', desktop: '$spacing-600' },
bgColor: '$primary-10',
borderRadius: '$radius-200',
textAlign: 'center',
}}>
모바일: spacing-300 / 데스크톱: spacing-600
</Box>

4단계 브레이크포인트를 제공합니다:

이름최소 너비용도
mobile0px기본값 (모바일 우선)
tablet768px태블릿 및 소형 노트북
desktop1024px데스크톱
wide1440px와이드 모니터

8. $css — 단일 스타일링 표면

$css prop은 디자인 시스템의 유일한 스타일링 인터페이스입니다. 토큰 값($spacing-400)과 임의 값(16px)을 모두 받아들이며, 반응형 조건과 인터랙션 조건을 단일 객체로 표현합니다.

<Box $css={{
  // 토큰 값
  padding: '$spacing-400',
  bgColor: '$background-1',

  // 임의 값도 허용
  maxWidth: '600px',

  // 반응형 — 레이아웃 속성
  gap: { mobile: '$spacing-200', desktop: '$spacing-400' },

  // 인터랙션 — 색상 속성
  backgroundColor: { default: '$background-1', hover: '$background-2' },
}} />

이 설계의 핵심:

  • 학습 비용 최소화: 하나의 prop만 익히면 모든 스타일링이 가능합니다
  • 토큰 가이드: $ 접두사로 시작하는 값은 디자인 토큰이라는 시각적 힌트를 제공합니다
  • 타입 안전성: 토큰 이름과 CSS 속성 모두 자동완성됩니다
  • 임의 값 허용: 토큰에 없는 값도 자유롭게 사용할 수 있어 디자인 시스템이 제약이 되지 않습니다

토큰 아키텍처 (3 Layers)

토큰 시스템은 세 가지 레이어로 구성됩니다:

Global Tokens — 브랜드 독립적인 원시 값

--global-colors-red-50: #e97259
--global-spacing-400: 1rem
--global-radius-200: 8px

Semantic Tokens — UI 맥락에 매핑된 토큰. 글로벌 토큰을 참조합니다.

--semantic-color-text-1: var(--global-colors-gray-90)
--semantic-color-background-1: var(--global-colors-white)
--semantic-color-support-error-1: var(--global-colors-red-50)

Color Sets — 브랜드별 Primary 컬러 팔레트

/* Featuring */
--global-colors-primary-60: #5e51ff

/* DataEffect */
--global-colors-primary-60: #0065ff

이 구조를 통해 시맨틱 토큰이 글로벌 토큰을 참조하고, Primary 색상만 브랜드별로 교체되므로 일관성과 유연성을 동시에 확보합니다. CSS 파일 하나를 교체하는 것만으로 전체 브랜드 테마가 전환됩니다.


기술 스택

기술역할
Vanilla ExtractZero-runtime CSS-in-JS. 빌드 타임 CSS 생성
Rainbow Sprinkles토큰 기반 atomic CSS. $css prop 제공
Base UIuseRender, mergeProps — 폴리모픽 렌더링과 prop 병합
CSS Cascade Layers소비자 CSS 우선순위 보장
TypeScript모든 토큰과 API에 타입 안전성 제공
React 18+UI 렌더링 라이브러리

컴포넌트 패밀리

디자인 시스템은 세 가지 컴포넌트 패밀리로 구성됩니다.

Layout Primitives

Box, Flex, HStack, VStack, Center, Grid, TypouseRenderComponent 훅을 사용하며 $css prop, render prop, 상태 콜백 className/style을 지원합니다.

<VStack $css={{ gap: '$spacing-400', padding: '$spacing-400' }}>
<Typo variant="$heading-3" render={<h2 />}>제목</Typo>
<HStack $css={{ gap: '$spacing-200' }}>
  <Box $css={{
    flex: '1',
    padding: '$spacing-400',
    bgColor: '$primary-10',
    borderRadius: '$radius-100',
    textAlign: 'center',
  }}>Left</Box>
  <Box $css={{
    flex: '1',
    padding: '$spacing-400',
    bgColor: '$primary-20',
    borderRadius: '$radius-100',
    textAlign: 'center',
  }}>Right</Box>
</HStack>
</VStack>

Compound Components (신규)

Button, Tag — Namespace export 패턴(Button.Root, Button.Icon, Button.Text)을 사용합니다. Context를 통해 size, loading, disabled 등 상태를 하위 컴포넌트에 공유하며, 각 하위 요소의 렌더링 순서와 스타일을 자유롭게 제어할 수 있습니다.

<HStack $css={{ gap: '$spacing-300', alignItems: 'center' }}>
<Button.Root variant="primary" size="md">
  <Button.Icon><IconAddOutline /></Button.Icon>
  <Button.Text>추가</Button.Text>
</Button.Root>
<Tag.Root tagType="primary" size="md">
  <Tag.Text>Active</Tag.Text>
  <Tag.RemoveButton />
</Tag.Root>
</HStack>

Legacy (Core*) Components

CoreButton, CoreTextInput, CoreSelect, CoreModal, CoreTag 등 — forwardRef 기반의 UI 컴포넌트. Recipe 기반 변형(variant)과 Vanilla Extract 스타일을 사용합니다.

<HStack $css={{ gap: '$spacing-300' }}>
<CoreButton buttonType="primary" size="md" text="Primary" />
<CoreButton buttonType="secondary" size="md" text="Secondary" />
<CoreButton buttonType="tertiary" size="md" text="Tertiary" />
</HStack>

마이그레이션 방향: 새로운 컴포넌트는 모두 Compound Component 패턴으로 개발됩니다. 기존 Core* 컴포넌트는 하위 호환성을 유지하며 점진적으로 대체됩니다. 새 컴포넌트(Button, Tag)가 Core* 대응물(CoreButton, CoreTag)보다 더 유연한 API를 제공합니다.