Loading#
비동기 작업이나 데이터 로딩 중임을 사용자에게 알리는 로딩 인디케이터 컴포넌트입니다. spinner, dots, bar, pulse 변형을 지원합니다.
Live Preview#
class LoadingDefaultExample extends StatelessComponent {
const LoadingDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const CoLoading();
}
}
class LoadingDefaultExample extends StatelessWidget {
const LoadingDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const CoLoading();
}
}
class LoadingDotsExample extends StatelessComponent {
const LoadingDotsExample({super.key});
@override
Component build(BuildContext context) {
return const CoLoading(variant: CoreLoadingVariant.dots);
}
}
class LoadingDotsExample extends StatelessWidget {
const LoadingDotsExample({super.key});
@override
Widget build(BuildContext context) {
return const CoLoading(variant: CoreLoadingVariant.dots);
}
}
class LoadingBarExample extends StatelessComponent {
const LoadingBarExample({super.key});
@override
Component build(BuildContext context) {
return const CoLoading(variant: CoreLoadingVariant.bar);
}
}
class LoadingBarExample extends StatelessWidget {
const LoadingBarExample({super.key});
@override
Widget build(BuildContext context) {
return const CoLoading(variant: CoreLoadingVariant.bar);
}
}
class LoadingPulseExample extends StatelessComponent {
const LoadingPulseExample({super.key});
@override
Component build(BuildContext context) {
return const CoLoading(variant: CoreLoadingVariant.pulse);
}
}
class LoadingPulseExample extends StatelessWidget {
const LoadingPulseExample({super.key});
@override
Widget build(BuildContext context) {
return const CoLoading(variant: CoreLoadingVariant.pulse);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 데이터를 불러오거나 저장하는 중에 사용자에게 진행 중임을 알릴 때
- 콘텐츠 레이아웃을 예측할 수 없는 비동기 작업 중에 표시할 때
- 버튼 클릭 후 처리가 완료될 때까지 기다리는 상태를 표시할 때
- 페이지 전체 로딩 오버레이가 필요할 때
대신 다른 컴포넌트를 사용하세요:
Skeleton: 콘텐츠 구조를 미리 알 수 있을 때 (더 나은 사용자 경험)Progress: 작업 완료율을 퍼센트로 표시할 수 있을 때
기본 사용법 (Basic Usage)#
// 기본 스피너
CoLoading()
// 크기 지정
CoLoading(size: 32)
// 색상 지정
CoLoading(
size: 24,
color: Colors.blue,
)
// variant 지정
CoLoading(variant: CoreLoadingVariant.dots)
// 레이블 포함 패턴
Column(
mainAxisSize: MainAxisSize.min,
children: [
CoLoading(size: 32),
SizedBox(height: 8),
Text('데이터를 불러오는 중...'),
],
)
// 기본 스피너
CoLoading()
// 크기 지정 (픽셀 단위)
CoLoading(size: 32)
// 색상 지정
CoLoading(
size: 24,
color: Colors.blue,
)
// variant 지정
CoLoading(variant: CoreLoadingVariant.dots)
// 레이블 포함 (flex 레이아웃으로 구성)
div(
classes: 'flex flex-col items-center gap-2',
[
CoLoading(size: 32),
span(
[Component.text('데이터를 불러오는 중...')],
classes: 'text-sm text-muted-foreground',
),
],
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
variant |
CoreLoadingVariant |
spinner |
인디케이터 변형 스타일 (spinner, dots, bar, pulse) |
size |
double? |
null |
인디케이터 크기 (픽셀). componentSize보다 우선. |
componentSize |
CoreComponentSize? |
null |
사이즈 토큰 (sm, md, lg). |
color |
Color? |
null |
인디케이터 색상 (기본값: 테마 primary) |
변형 (Variants)#
Spinner#
원형으로 회전하는 기본 스피너입니다.
CoLoading(variant: CoreLoadingVariant.spinner)
Dots#
세 개의 점이 순서대로 강조되는 형태입니다.
CoLoading(variant: CoreLoadingVariant.dots)
Bar#
가로 바가 좌우로 움직이는 형태입니다.
CoLoading(variant: CoreLoadingVariant.bar)
Pulse#
요소가 맥박처럼 커졌다 작아지는 형태입니다.
CoLoading(variant: CoreLoadingVariant.pulse)
크기 (Sizes)#
| 크기 | componentSize | 픽셀 |
|---|---|---|
| Small | CoreComponentSize.sm | 16px |
| Medium | CoreComponentSize.md | 24px |
| Large | CoreComponentSize.lg | 40px |
size 파라미터로 픽셀 값을 직접 지정할 수도 있습니다.
동작 스펙 (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: [
CoLoading(variant: CoreLoadingVariant.spinner, size: 40),
SizedBox(height: 8),
Text('파일을 업로드하는 중... (2/5)'),
],
)
로딩 옆/아래에 텍스트로 어떤 작업이 진행 중인지 알려주면 사용자의 불안감을 줄일 수 있습니다.
❌ Don't#
레이아웃을 예측할 수 있는 경우 Loading 대신 Skeleton 사용
// ❌ 카드 목록 로딩에 단순 스피너 사용
if (isLoading) {
return Center(child: CoLoading()); // 레이아웃 점프 발생
}
return CardList(items: items);
카드 목록처럼 구조가 예측 가능한 경우 Skeleton을 사용하면 레이아웃 점프 없이 더 나은 경험을 제공합니다.
✅ Do#
버튼 내 인라인 로딩으로 즉각적 피드백 제공
CoButton.primary(
onPressed: isSubmitting ? null : handleSubmit,
child: isSubmitting
? Row(mainAxisSize: MainAxisSize.min, children: [
CoLoading(size: 16, color: Colors.white),
SizedBox(width: 8),
Text('제출 중...'),
])
: Text('제출하기'),
)
버튼 내 인라인 로딩은 사용자에게 클릭이 처리되고 있음을 즉각적으로 알려주고 중복 클릭을 방지합니다.
❌ Don't#
페이지 전체에 로딩 오버레이를 과도하게 사용 금지
// ❌ 작은 데이터 업데이트에 전체 페이지 오버레이
Stack(
children: [
MainContent(),
if (isLoading) FullPageLoadingOverlay(), // 단순 항목 추가에 전체 블로킹
],
)
작은 업데이트에 전체 페이지를 블로킹하면 사용성이 크게 저하됩니다. 변경되는 영역에만 로딩을 표시하세요.
✅ Do#
로딩 시간이 길 때 진행률(Progress)을 함께 표시하세요.
// 진행률을 알 수 있을 때는 Progress 컴포넌트 사용
CoProgress(value: uploadProgress) // 0.0 ~ 1.0
진행률을 보여주면 사용자가 얼마나 기다려야 하는지 알 수 있어 이탈률을 낮출 수 있습니다.
❌ Don't#
짧은 작업(1초 미만)에 로딩 인디케이터를 표시하지 마세요.
// ❌ 매우 빠른 작업에도 로딩 스피너 표시
FutureBuilder(
future: fastLocalOperation(), // 50ms 작업
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CoLoading(); // 깜빡임 현상 발생
}
return content;
},
)
너무 짧은 작업에 로딩을 보여주면 UI가 깜빡이는 플래시 효과가 발생합니다. 300ms 이상 걸리는 작업에만 표시하세요.
접근성 (Accessibility)#
키보드 인터랙션#
해당 없음. Loading은 인터랙티브 요소가 아닙니다.
스크린 리더#
- Flutter:
Semantics(label: '로딩 중', liveRegion: true)적용 -
Web:
role="status",aria-live="polite",aria-label="로딩 중"자동 적용
터치 타겟#
해당 없음. 표시 전용 요소.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | CoLoading | CoLoading |
| Spinner 구현 | CustomPainter 기반 자체 회전 |
CSS animation: spin |
| Pulse 구현 | AnimationController + Transform.scale |
CSS @keyframes scale |
| 색상 기본값 | Theme.of(context).colorScheme.primary |
var(--coui-primary) |
관련 컴포넌트 (Related Components)#
- Skeleton: 콘텐츠 구조를 예측할 수 있을 때 더 나은 로딩 표시자
- Progress: 진행률을 퍼센트로 표시할 때
- EmptyState: 로딩 완료 후 데이터가 없을 때
조합 예제#
// Loading + 조건부 렌더링 + EmptyState 패턴
Widget buildContent() {
return AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: isLoading
? Center(key: Key('loading'), child: CoLoading())
: items.isEmpty
? CoEmptyState(
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]),
),
);
}