Design Philosophy
Featuring Design System의 설계 원칙과 디자인 철학
핵심 철학
소비자(consumer) 우선의 설계 철학을 따릅니다. 디자인 시스템은 제약이 아니라 도구. 소비자가 시스템과 싸우지 않고 자연스럽게 확장할 수 있어야 합니다.
세 가지 핵심 가치:
- 소비자 CSS가 항상 이긴다 — CSS Cascade Layers를 통해
!important없이 오버라이드 가능 - 접근성은 기본값이다 — disabled, focus, hover 등 인터랙션 상태가 WAI-ARIA 패턴을 준수
- 조합이 설정을 이긴다 — 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를 @layer 안에 배치합니다. 소비자 CSS는 레이어 밖에 있으므로 항상 우선합니다.
@layer ft-reset, ft-normalize, ft-components, ft-utilities;
/* 이 순서대로 우선순위가 결정됩니다:
ft-reset → 가장 낮은 우선순위
ft-normalize → 브라우저 정규화
ft-components → 컴포넌트 스타일
ft-utilities → $css prop atomic CSS
(unlayered) → 소비자 CSS — 항상 이김 */이 구조 덕분에:
!important없이 어떤 컴포넌트 스타일이든 오버라이드 가능$cssprop으로 토큰 기반 스타일링을 하되, 필요하면 일반 CSS로 확장- 디자인 시스템이 소비자의 스타일링을 방해하지 않음
3. Zero-Runtime CSS
Vanilla Extract 기반으로 모든 CSS가 빌드 타임에 생성됩니다. 런타임에 JS가 스타일을 계산하는 일은 없습니다.
- 성능 — 스타일 계산으로 인한 JS 번들 증가 없음
- 예측 가능성 — CSS는 정적. FOUC 없음
- 디버깅 — 브라우저 DevTools에서 일반 CSS로 확인
4. Accessible by Default
인터랙티브 컴포넌트의 상태 관리는 WAI-ARIA 패턴을 준수합니다. disabled 상태는 두 레벨로 구분:
| 방식 | 동작 | 용도 |
|---|---|---|
disabled (HTML) | 포커스·클릭·키보드 이벤트 모두 차단 | 완전 비활성화 |
aria-disabled + 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]) | HTML disabled + aria-disabled 모두 처리 |
focus | &:focus-visible | focusableWhenDisabled에서도 포커스 링 표시 |
disabled 요소에 hover 효과가 남아 있으면 사용자는 클릭 가능하다고 오해합니다. 그래서 비활성 상태에서는 hover/active 스타일을 차단합니다.
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 패턴:
- JSX 순서만 바꾸면 렌더링 순서가 바뀜
- 하위 요소에 개별 스타일 적용 가능
- 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', }}> <section>으로 렌더링 </Box> {/* h1으로 렌더링 */} <Typo render={<h2 />} variant="$heading-3"> <h2>으로 렌더링된 Typo </Typo> {/* span으로 렌더링 */} <Box render={<span />} $css={{ padding: '$spacing-200', bgColor: '$primary-10', borderRadius: '$radius-100', display: 'inline-block', }}> <span>으로 렌더링 </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단계 브레이크포인트:
| 이름 | 최소 너비 | 용도 |
|---|---|---|
mobile | 0px | 기본값 (모바일 우선) |
tablet | 768px | 태블릿 및 소형 노트북 |
desktop | 1024px | 데스크톱 |
wide | 1440px | 와이드 모니터 |
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 — 브랜드가 정의한 원시 값
컬러 팔레트 14종 + Gray, 간격, 반지름, 그림자. Color Set(브랜드별 Primary 팔레트)도 --global-colors-primary-*로 여기에 들어갑니다.
--global-colors-red-50: #e97259
--global-colors-primary-60: #5e51ff /* Color Set — 브랜드별로 다른 값 */
--global-spacing-400: 1rem
--global-radius-200: 8pxSemantic Tokens — UI 맥락에 매핑한 토큰
글로벌 토큰을 참조합니다.
--semantic-color-text-1: var(--global-colors-gray-90)
--semantic-color-background-1: var(--global-colors-white)
--semantic-color-support-info-1: var(--global-colors-primary-10)Component Tokens — 컴포넌트별 디자인 토큰
내부에서 createThemeContract로 각 컴포넌트가 자체 토큰을 정의합니다. variant별 색상, 상태별 스타일이 여기에 들어갑니다.
/* Button — variant별 색상 토큰 */
button.colors.primary.default: var(--global-colors-primary-60)
button.colors.primary.hover: var(--global-colors-primary-70)
/* Tag — variant별 색상 토큰 */
tag.colors.red.default: var(--global-colors-red-10)
tag.colors.red.hover: var(--global-colors-red-20)Global은 원시 값, Semantic은 UI 용도, Component는 컴포넌트별 디자인을 담당합니다. 브랜드 전환은 Global의 Primary 팔레트(Color Set)만 교체하면 됩니다.
기술 스택
| 기술 | 역할 |
|---|---|
| Vanilla Extract | Zero-runtime CSS-in-JS. 빌드 타임 CSS 생성 |
| Rainbow Sprinkles | 토큰 기반 atomic CSS. $css prop 제공 |
| Base UI | useRender, mergeProps — 폴리모픽 렌더링과 prop 병합 |
| CSS Cascade Layers | 소비자 CSS 우선순위 보장 |
| TypeScript | 모든 토큰과 API에 타입 안전성 제공 |
| React 18+ | UI 렌더링 라이브러리 |
컴포넌트 패밀리
디자인 시스템은 세 가지 컴포넌트 패밀리로 구성됩니다.
Layout Primitives
Box, Flex, HStack, VStack, Center, Grid, Typo — useRenderComponent 훅을 사용하며 $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, Avatar, Badge, Calendar, Checkbox, Dropdown, Menu, Modal, Drawer, Popover, Pagination, Radio, SectionMessage, SegmentedControl, Select, Switch, Tabs, TextInput, TextArea, Toast, Tooltip, Field, Label, Link 등이 Namespace export 패턴(Button.Root, Button.Icon, Button.Text)을 사용합니다. Context로 size, 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를 제공합니다.