크로스 플랫폼 | CoUI
LogoCoUI

크로스 플랫폼

Flutter와 Web 동시 개발 가이드

크로스 플랫폼#

CoUI는 Flutter와 Jaspr Web에서 동일한 컴포넌트 세트를 제공합니다.

아키텍처#

coui_core (공유 계약)
├── coui_flutter (Flutter 구현)
└── coui_web (Jaspr Web 구현)

공유 계약 (coui_core)#

플랫폼 간 공통 인터페이스를 정의합니다:

// 예: CoreButtonContract (widget 타입만 <W> generic)
abstract interface class CoreButtonContract<W> {
  bool? get enabled;
  CoreVoidCallback? get onPressed;
  CoreButtonVariant get variant;
  CoreButtonStyle? get buttonStyle;
}

API 비교#

생성자 패턴#

// Named parameters, Widget tree — 이름·순서·기본값이 Web과 동일
Button(
  variant: CoreButtonVariant.primary,
  size: CoreComponentSize.md,
  onPressed: () {},
  child: Text('Click'),
)
// Named parameters, Component tree — Flutter와 글자 그대로 동일
Button(
  variant: CoreButtonVariant.primary,
  size: CoreComponentSize.md,
  onPressed: () {},
  child: Text('Click'),
)

테마 적용#

// CoUIApp(Flutter 의 앱 루트) + CoreComponentTheme 슬롯
CoUIApp(
  theme: ThemeData.fromCore(
    CoreThemePresets.light,
    coreComponentTheme: const CoreComponentTheme(
      button: CoreButtonTheme(
        style: CoreButtonStyle(
          borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
        ),
      ),
    ),
  ),
  home: MyHomePage(),
)
// CoUIWeb(coui_web 의 Theme provider, Flutter CoUIApp 대응) + 동일한
// CoreComponentTheme 슬롯
CoUIWeb(
  theme: ThemeData.fromCore(
    CoreThemePresets.light,
    coreComponentTheme: const CoreComponentTheme(
      button: CoreButtonTheme(
        style: CoreButtonStyle(
          borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
        ),
      ),
    ),
  ),
  child: MyApp(),
)

컴포넌트 매핑#

전체 140+ 컴포넌트가 양쪽 플랫폼에서 동일하게 제공됩니다:

카테고리FlutterWeb
Form (Input, Select, etc.)20+20+
Display (Avatar, Badge, etc.)25+25+
Navigation (Menu, Tabs, etc.)10+10+
Overlay (Dialog, Toast, etc.)10+10+
Layout (Accordion, Grid, etc.)15+15+
Control (Button, FAB, etc.)10+10+
Chart (Tracker)11
Locale (Localizations)11

디자인 토큰#

CoUI는 coui_core에 정의된 디자인 토큰을 Web/Flutter 양쪽에서 공유합니다. 소비자용 맵은 디자인 토큰 을 보세요.

Border Radius 토큰#

field/selector/box 같은 글로벌 시맨틱 radius 토큰은 의도적으로 없다 — 컴포넌트가 자기 CoreXxxStyle.defaultBorderRadius에서 CoreRadius의 숫자 primitive(radius0 ~ radius64, radius9999)를 직접 고른다. 각 컴포넌트가 실제로 고른 값:

컴포넌트 CoreXxxStyle.defaultBorderRadius Web (Tailwind) Flutter
TextField CoreRadius.radius4 rounded-${CoreRadius.scale.radius4} BorderRadius.circular(CoreRadius.radius4)
Button CoreRadius.radius8 rounded-${CoreRadius.scale.radius8} BorderRadius.circular(CoreRadius.radius8)
Card CoreRadius.radius16 rounded-${CoreRadius.scale.radius16} BorderRadius.circular(CoreRadius.radius16)
Badge CoreRadius.radius9999 rounded-${CoreRadius.scale.radius9999} BorderRadius.circular(CoreRadius.radius9999)

Size 토큰#

시맨틱 크기는 CoreComponentSize(xs/sm/md/lg/xl) 하나로 통일되고, 실제 픽셀 값은 각 컴포넌트의 CoreXxxStyle.defaultsBySize map이 정한다 — 전역 "field size" 상수는 없다.

컴포넌트size: md일 때 높이Web (Tailwind)Flutter
TextField CoreSize.size40 = 40px h-10 CoreTextFieldStyle.defaultsBySize[.md]!.height

애니메이션 토큰#

인터랙션 애니메이션은 CoreDuration을 단일 소스로 사용합니다.

컴포넌트는 시맨틱 alias tier 를 참조합니다. raw 스케일(CoreDuration.ms150 등)은 그대로 남아 있지만, alias 를 거쳐야 reduced-motion 같은 전역 모션 오버라이드가 그 값에 닿습니다.

alias용도
CoreDuration.instant100ms즉각 반응 — 체크 표시, 토글
CoreDuration.fast150ms작은 요소 — 툴팁 등장, 버튼 hover, 입력 포커스
CoreDuration.normal200ms대부분의 전환 기본값 — 드롭다운, 카드 hover, 아코디언
CoreDuration.moderate300ms중간 요소 — 모달 진입, 패널 슬라이드
CoreDuration.slow500ms페이지 레벨 전환, 복합 choreography

표시 시간(CoreDuration.ms1000 이상)은 "얼마나 오래 머무르나"이지 "얼마나 빨리 움직이나"가 아니라 alias 를 갖지 않습니다.

여러 요소가 순차로 들어올 때는 항목 간격이 아니라 시퀀스 총합을 정합니다 — CoreDuration.slow(500ms)를 넘지 않고, 그 총합이 Core 의 duration 필드에서 와야 "동작 줄이기" 설정이 닿습니다. 형태는 둘 다 유효합니다: 고정 간격(30–50ms)으로 차례로 들어오거나, 총합 / 개수로 간격을 파생해 다 같이 도착하거나. 항목 수가 런타임에 정해지므로 절대 간격 하나로 못 박지 않습니다.

순환하는 표시(스피너·로딩 점·shimmer)는 들어오는 게 아니라 도는 것이라 이 상한의 대상이 아니지만, 양 플랫폼이 같은 Core 값을 읽어야 하는 것은 같습니다.

// Duration(milliseconds:)가 CoreDuration 토큰을 경유
const kDefaultDuration = Duration(milliseconds: CoreDuration.fast);      // 150ms
const kToggleDuration = Duration(milliseconds: CoreDuration.instant);    // 100ms

// 컴포넌트에서 사용
AnimatedContainer(
  duration: Duration(milliseconds: CoreDuration.normal),  // 200ms
  curve: Curves.easeInOut,
  child: card,
)
// Tailwind duration 클래스로 매핑 (컴포넌트 resolver 가 CoreDuration 토큰에서 파생)
Button(
  variant: CoreButtonVariant.primary,
  // base: 'transition-colors duration-150'
  onPressed: () {},
  child: Text('Primary'),
)

// onTap 이 있는 카드는 interactive 로 간주되어 hover shadow 가 자동 적용
Card(
  onTap: () {},
  // base: 'transition-shadow duration-200'
  child: Text('Card'),
)

컴포넌트별 애니메이션 매핑#

컴포넌트인터랙션DurationWebFlutter
Button hover 색상 fast (150ms) transition-colors duration-150 WidgetState 기반
Card hover shadow (onTap 있을 때) normal (200ms) transition-shadow duration-200 hoverElevation + AnimatedContainer
Toggle thumb slide instant (100ms) transition-transform duration-100 AnimatedPositioned
Accordion expand/collapse normal (200ms) transition-all duration-200 SizeTransition
TextField focus ring fast (150ms) transition-colors duration-150 AnimatedContainer

Spring 프리셋#

Flutter에서 물리 기반 애니메이션이 필요할 때 CoreSpringConfig를 사용합니다:

프리셋dampingstiffness용도
gentle0.8200부드러운 전환
standard0.7300기본
bouncy0.5400탄성 효과
snappy0.9500빠른 반응
stiff1.0600즉각 반응

통일된 컴포넌트 API#

Web과 Flutter는 동일한 named properties API를 사용합니다.

Layout#

슬롯 이름이 같고 Text 도 양 플랫폼에서 같은 이름이라, 이 예제는 Flutter 와 Web 에서 글자 그대로 동일합니다 — 탭을 나눌 것이 없습니다.

Card(
  header: Text('Card Title').titleMedium.semibold,
  body: Text('Description').bodyMedium.onSurfaceVariant,
  footer: Button(
    variant: CoreButtonVariant.primary,
    onPressed: () {},
    child: Text('Action'),
  ),
)

Form#

Select<String>(
  items: [
    CoreSelectItem(value: 'apple', label: 'Apple'),
    CoreSelectItem(value: 'banana', label: 'Banana'),
  ],
  placeholder: 'Select a fruit',
  onChanged: (v) => print(v),
)
Select<String>(
  items: [
    CoreSelectItem(value: 'apple', label: 'Apple'),
    CoreSelectItem(value: 'banana', label: 'Banana'),
  ],
  placeholder: 'Select a fruit',
  onChanged: (v) => print(v),
)

Display#

데이터 모델(CoreTimelineItem)은 coui_core 에 살기 때문에 양 플랫폼이 같은 타입을 그대로 씁니다 — 여기서도 코드가 동일합니다.

Timeline(
  items: [
    CoreTimelineItem(title: 'Order placed', timestamp: '10:00 AM'),
    CoreTimelineItem(title: 'Shipped', timestamp: '2:00 PM'),
  ],
)

개발 팁#

  1. API 먼저 설계: coui_core에 계약을 정의하고 양쪽 구현
  2. Named properties 우선: child 대신 title, content, footer 등 명시적 파라미터
  3. String 입력 지원: 가능하면 String을 받아 내부에서 Text/Component.text로 변환
  4. CoreDuration 사용: 하드코딩 duration 대신 Duration(milliseconds: CoreDuration.*) 참조
  5. 테스트 분리: Flutter widget test + Web unit test 별도 작성
  6. DCM 통일: 양쪽 패키지에 동일한 DCM 규칙 적용
  7. Barrel export: 양쪽 coui_flutter.dart / coui_web.dart 동기화 유지