개발/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
반응형