개발/Flutter & Dart
Flutter 상태관리 완전 가이드 (Part 3/3)
까칠코더
2025. 11. 5. 20:58
반응형
10. 클린 아키텍처와 상태관리
대규모 Flutter 프로젝트에서는 Presentation / Domain / Data 계층 분리가 필수입니다.
lib/
├─ app/ # Router, Theme, DI
├─ core/ # 공용 유틸, Result, Error
├─ data/ # API, DTO, Repository 구현체
├─ domain/ # Entity, Repository Interface, UseCase
└─ features/
└─ todo/
├─ presentation/
├─ domain/
└─ data/
✅ 구조적 장점: 테스트 용이, 의존성 명확, 팀 협업 효율 ↑
11. Result 타입으로 상태 명시화
sealed class Result<T> {
const Result();
}
class Ok<T> extends Result<T> {
final T value;
const Ok(this.value);
}
class Err<T> extends Result<T> {
final Exception error;
const Err(this.error);
}
Result를 통해 성공/실패를 명확히 표현할 수 있습니다.
12. 로딩 / 에러 / 빈 상태 관리
Riverpod이나 BLoC에서 비동기 데이터를 다룰 때는 4가지 상태를 정의하세요.
상태의미
| Loading | 데이터 요청 중 |
| Success | 정상 데이터 |
| Empty | 데이터 없음 |
| Error | 예외 발생 |
Widget buildBody(AsyncValue<List<Todo>> todos) {
return todos.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('Error: $e'),
data: (items) => items.isEmpty
? const Text('No Data')
: ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => Text(items[i].title),
),
);
}
13. 성능 최적화
- ✅ select, Provider.family, BlocSelector로 최소 구독
- ✅ const 위젯 적극 사용
- ✅ ListView.builder + cacheExtent
- ✅ 이미지 캐싱(cached_network_image)
- ✅ debugPrintRebuildDirtyWidgets = true로 리빌드 추적
14. 테스트 전략
유닛 테스트
void main() {
test('UseCase returns expected result', () async {
final useCase = FetchUserUseCase(FakeUserRepository());
final user = await useCase('user1');
expect(user.name, '홍길동');
});
}
위젯 테스트
testWidgets('Counter increments', (tester) async {
await tester.pumpWidget(ProviderScope(child: const CounterPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
골든 테스트 (디자인 스냅샷)
testGoldens('Main page matches design', (tester) async {
await tester.pumpWidget(const MyHomePage());
await screenMatchesGolden(tester, 'home_page');
});
💡 테스트 자동화를 CI(GitHub Actions, Bitrise 등)에 연결하면 안정성 향상.
15. 라우팅과 상태 (go_router)
final authProvider = StreamProvider<User?>((ref) => authService.stream);
GoRouter router(WidgetRef ref) => GoRouter(
refreshListenable: GoRouterRefreshStream(ref.watch(authProvider.stream)),
redirect: (context, state) {
final user = ref.read(authProvider).valueOrNull;
final loggingIn = state.subloc == '/login';
if (user == null) return loggingIn ? null : '/login';
if (loggingIn) return '/';
return null;
},
routes: [...],
);
16. 실전 Todo 예제 (Riverpod)
final repoProvider = Provider<TodoRepository>((_) => RemoteTodoRepository());
final todosProvider = AsyncNotifierProvider<TodoController, List<Todo>>(
TodoController.new,
);
class TodoController extends AsyncNotifier<List<Todo>> {
@override
Future<List<Todo>> build() async {
return ref.read(repoProvider).fetch();
}
Future<void> add(String title) async {
final repo = ref.read(repoProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final added = await repo.add(title);
final prev = await future;
return [added, ...prev];
});
}
}
class TodoPage extends ConsumerWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: todos.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('$e'),
data: (items) => ListView.builder(
itemCount: items.length,
itemBuilder: (_, i) => Text(items[i].title),
),
),
);
}
}
17. 팀 협업 체크리스트
- ✅ freezed, json_serializable로 모델 불변화
- ✅ 공용 로딩/에러 위젯 통일
- ✅ Provider/BLoC의 DI 주입 위치 명확히
- ✅ flutter analyze + flutter test CI 자동화
- ✅ README에 상태관리 규칙 문서화
18. 요약 & 실천 가이드
단계추천 방법설명
| 1 | setState, ValueNotifier | 단순 UI |
| 2 | Provider | 공유 상태 |
| 3 | Riverpod, BLoC | 대규모 아키텍처 |
| 4 | GetX | MVP/프로토타입 |
| 5 | 테스트/아키텍처 | 장기 유지보수 대비 |
🚀 핵심 요약:
작게 시작하되, 구조를 일찍부터 나눠라.
Riverpod은 중·대형, BLoC은 복잡한 도메인, GetX는 빠른 개발에 이상적이다.
19. 결정 트리
작은 앱 → setState
공유 상태 → Provider
중대형 / 복잡 로직 → Riverpod or BLoC
빠른 MVP → GetX
디버깅 중요 → Redux
20. 필수 패키지
- 상태관리: flutter_riverpod, provider, bloc, get
- 모델/불변: freezed, json_serializable
- 로컬DB: isar, hive, shared_preferences
- 네트워크: dio, retrofit
- 라우팅: go_router, auto_route
- 테스트: flutter_test, mocktail, golden_toolkit
반응형