Loading | CoUI

Loading

작업 진행 중을 표시하는 로딩 인디케이터 컴포넌트

Loading#

비동기 작업이나 데이터 로딩 중임을 사용자에게 알리는 로딩 인디케이터 컴포넌트입니다. spinner, dots, bar, pulse 변형을 지원합니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 데이터를 불러오거나 저장하는 중에 사용자에게 진행 중임을 알릴 때
  • 콘텐츠 레이아웃을 예측할 수 없는 비동기 작업 중에 표시할 때
  • 버튼 클릭 후 처리가 완료될 때까지 기다리는 상태를 표시할 때
  • 페이지 전체 로딩 오버레이가 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • Skeleton: 콘텐츠 구조를 미리 알 수 있을 때 (더 나은 사용자 경험)
  • Progress: 작업 완료율을 퍼센트로 표시할 수 있을 때

기본 사용법 (Basic Usage)#

// 기본 스피너
Loading()

// 크기 지정
Loading(loadingStyle: CoreLoadingStyle(size: 32))

// 색상 지정
Loading(
  loadingStyle: CoreLoadingStyle(
    size: 24,
    color: CoreColor.token(CoreColors.primary),
  ),
)

// variant 지정
Loading(variant: CoreLoadingVariant.dots)

// 레이블 포함 패턴
Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    Loading(loadingStyle: CoreLoadingStyle(size: 32)),
    SizedBox(height: 8),
    Text('데이터를 불러오는 중...'),
  ],
)
// 기본 스피너
Loading()

// 크기 지정 (픽셀 단위)
Loading(loadingStyle: CoreLoadingStyle(size: 32))

// 색상 지정
Loading(
  loadingStyle: CoreLoadingStyle(
    size: 24,
    color: CoreColor.token(CoreColors.primary),
  ),
)

// variant 지정
Loading(variant: CoreLoadingVariant.dots)

// 레이블 포함 (flex 레이아웃으로 구성)
div(
  classes: 'flex flex-col items-center gap-2',
  [
    Loading(loadingStyle: CoreLoadingStyle(size: 32)),
    span(
      [Component.text('데이터를 불러오는 중...')],
      classes: 'text-sm text-muted-foreground',
    ),
  ],
)

Props / Parameters#

속성타입기본값설명
variant CoreLoadingVariant spinner 인디케이터 변형 스타일 (spinner, dots, bar, pulse)
componentSize CoreComponentSize? null 사이즈 토큰 (xs, sm, md, lg, xl).
loadingStyle CoreLoadingStyle? null 인스턴스별 chrome / dimensional 오버라이드 — color , trackColor , size , strokeWidth , duration 필드 포함.

변형 (Variants)#

Spinner#

원형으로 회전하는 기본 스피너입니다.

Loading(variant: CoreLoadingVariant.spinner)

Dots#

세 개의 점이 순서대로 강조되는 형태입니다.

Loading(variant: CoreLoadingVariant.dots)

Bar#

가로 바가 좌우로 움직이는 형태입니다.

Loading(variant: CoreLoadingVariant.bar)

Pulse#

요소가 맥박처럼 커졌다 작아지는 형태입니다.

Loading(variant: CoreLoadingVariant.pulse)

크기 (Sizes)#

크기componentSize픽셀
Extra SmallCoreComponentSize.xs12px
SmallCoreComponentSize.sm16px
MediumCoreComponentSize.md24px
LargeCoreComponentSize.lg32px
Extra LargeCoreComponentSize.xl48px

loadingStyle: CoreLoadingStyle(size: 32) 처럼 픽셀 값을 직접 지정할 수도 있습니다.

동작 스펙 (Behavior)#

인터랙션#

  • Loading은 표시 전용 컴포넌트입니다. 인터랙션이 없습니다.
  • 로딩 중에는 배경 컨텐츠 상호작용을 차단하는 오버레이와 함께 사용하는 경우가 많음

상태 전환#

  • 로딩 시작: 즉시 표시 (애니메이션 시작)
  • 로딩 완료: 부모에서 조건부 렌더링으로 제거

애니메이션#

  • Spinner: 360도 회전 1s linear 무한 반복
  • Dots: 각 점이 0.4s 간격으로 페이드인/아웃
  • Bar: 좌우 왕복 1.5s ease-in-out 무한 반복
  • Pulse: 크기 1.0 → 1.3 → 1.0, 1s ease-in-out 무한 반복

사용 가이드라인 (Usage Guidelines)#

✅ Do#

로딩 인디케이터로 진행 중인 작업 안내

Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    Loading(
      variant: CoreLoadingVariant.spinner,
      loadingStyle: CoreLoadingStyle(size: 40),
    ),
    Gap(size: CoreSpace.space8),
    Text('파일을 업로드하는 중... (2/5)'),
  ],
)

로딩 옆/아래에 텍스트로 어떤 작업이 진행 중인지 알려주면 사용자의 불안감을 줄일 수 있습니다.


❌ Don't#

레이아웃을 예측할 수 있는 경우 Loading 대신 Skeleton 사용

// ❌ 카드 목록 로딩에 단순 스피너 사용
if (isLoading) {
  return Center(child: Loading()); // 레이아웃 점프 발생
}
return CardList(items: items);

카드 목록처럼 구조가 예측 가능한 경우 Skeleton을 사용하면 레이아웃 점프 없이 더 나은 경험을 제공합니다.

✅ Do#

버튼 내 인라인 로딩으로 즉각적 피드백 제공

Button.primary(
  onPressed: isSubmitting ? null : handleSubmit,
  child: isSubmitting
    ? Row(mainAxisSize: MainAxisSize.min, children: [
        Loading(
          loadingStyle: CoreLoadingStyle(
            size: 16,
            color: CoreColor.token(CoreColors.onPrimary),
          ),
        ),
        Gap(size: CoreSpace.space8),
        Text('제출 중...'),
      ])
    : Text('제출하기'),
)

버튼 내 인라인 로딩은 사용자에게 클릭이 처리되고 있음을 즉각적으로 알려주고 중복 클릭을 방지합니다.


❌ Don't#

페이지 전체에 로딩 오버레이를 과도하게 사용 금지

// ❌ 작은 데이터 업데이트에 전체 페이지 오버레이
Stack(
  children: [
    MainContent(),
    if (isLoading) FullPageLoadingOverlay(), // 단순 항목 추가에 전체 블로킹
  ],
)

작은 업데이트에 전체 페이지를 블로킹하면 사용성이 크게 저하됩니다. 변경되는 영역에만 로딩을 표시하세요.

✅ Do#

로딩 시간이 길 때 진행률(Progress)을 함께 표시하세요.

// 진행률을 알 수 있을 때는 Progress 컴포넌트 사용
Progress(value: uploadProgress) // 0.0 ~ 1.0

진행률을 보여주면 사용자가 얼마나 기다려야 하는지 알 수 있어 이탈률을 낮출 수 있습니다.


❌ Don't#

짧은 작업(1초 미만)에 로딩 인디케이터를 표시하지 마세요.

// ❌ 매우 빠른 작업에도 로딩 스피너 표시
FutureBuilder(
  future: fastLocalOperation(),  // 50ms 작업
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Loading();  // 깜빡임 현상 발생
    }
    return content;
  },
)

너무 짧은 작업에 로딩을 보여주면 UI가 깜빡이는 플래시 효과가 발생합니다. 300ms 이상 걸리는 작업에만 표시하세요.

접근성 (Accessibility)#

키보드 인터랙션#

해당 없음. Loading은 인터랙티브 요소가 아닙니다.

스크린 리더#

  • Flutter: Semantics(label: '로딩 중', liveRegion: true) 적용
  • Web: role="status", aria-live="polite", aria-label="로딩 중" 자동 적용

터치 타겟#

해당 없음. 표시 전용 요소.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명LoadingLoading
Spinner 구현 CustomPainter 기반 자체 회전 CSS animation: spin
Pulse 구현 AnimationController + Transform.scale CSS @keyframes scale
색상 기본값 Theme.of(context).colorScheme.primary var(--coui-primary)
  • Skeleton: 콘텐츠 구조를 예측할 수 있을 때 더 나은 로딩 표시자
  • Progress: 진행률을 퍼센트로 표시할 때
  • EmptyState: 로딩 완료 후 데이터가 없을 때

조합 예제#

// Loading + 조건부 렌더링 + EmptyState 패턴
Widget buildContent() {
  return AnimatedSwitcher(
    duration: Duration(milliseconds: 200),
    child: isLoading
      ? Center(key: Key('loading'), child: Loading())
      : items.isEmpty
        ? EmptyState(
            key: Key('empty'),
            icon: Icon(Icons.search_off, size: 48),
            title: '결과가 없습니다',
          )
        : ListView.builder(
            key: Key('list'),
            itemCount: items.length,
            itemBuilder: (context, index) => ItemTile(item: items[index]),
          ),
  );
}