Flutter에서 상태 관리는 어떻게 하나요?

질문

Flutter 앱에서 상태 관리를 위한 다양한 방법과 그 특징은 무엇인가요?

답변

Flutter에서 상태 관리는 애플리케이션의 복잡성과 규모가 커질수록 중요해지는 핵심 개념입니다. 상태 관리는 앱 내의 데이터 흐름과 UI 업데이트를 체계적으로 처리하는 방법을 의미합니다. Flutter는 다양한 상태 관리 방법을 제공하며, 각각 특정 사용 사례와 앱 규모에 적합합니다.

1. 로컬 상태 관리

StatefulWidget

가장 기본적인 상태 관리 방법은 StatefulWidget을 사용하는 것입니다. 이는 위젯 내부에서 상태를 관리하는 방법으로, 작고 독립적인 상태에 적합합니다.

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('카운터: $_counter'),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: Text('증가'),
        ),
      ],
    );
  }
}

장점:

  • 간단하고 직관적
  • 외부 종속성 없음
  • 로컬 상태 관리에 적합

단점:

  • 깊은 위젯 트리에서 상태 공유가 어려움
  • 복잡한 상태 로직 처리에 적합하지 않음
  • 위젯 간 상태 공유가 어려움

2. 상위 위젯을 통한 상태 관리

InheritedWidget

InheritedWidget은 위젯 트리를 통해 데이터를 효율적으로 전달할 수 있는 방법입니다. Flutter의 많은 상태 관리 솔루션이 내부적으로 이를 사용합니다.

class CounterProvider extends InheritedWidget {
  final int counter;
  final Function increment;

  CounterProvider({
    required this.counter,
    required this.increment,
    required Widget child,
  }) : super(child: child);

  @override
  bool updateShouldNotify(CounterProvider oldWidget) {
    return counter != oldWidget.counter;
  }

  static CounterProvider of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!;
  }
}

class CounterApp extends StatefulWidget {
  @override
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CounterProvider(
      counter: _counter,
      increment: _incrementCounter,
      child: MyApp(),
    );
  }
}

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final provider = CounterProvider.of(context);
    return Text('카운터: ${provider.counter}');
  }
}

class CounterIncrement extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final provider = CounterProvider.of(context);
    return ElevatedButton(
      onPressed: () => provider.increment(),
      child: Text('증가'),
    );
  }
}

장점:

  • 위젯 트리를 통한 효율적인 데이터 전달
  • 불필요한 리빌드 방지

단점:

  • 직접 사용하기에는 코드가 복잡함
  • 복잡한 상태 로직에는 부족할 수 있음

Provider 패키지

ProviderInheritedWidget을 더 사용하기 쉽게 만든 라이브러리로, Flutter 팀에서도 권장하는 방법 중 하나입니다.

// pubspec.yaml
// dependencies:
//   provider: ^6.0.5

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // listen: true로 Counter 객체의 변경 사항을 구독
    final counter = Provider.of<Counter>(context);
    return Text('카운터: ${counter.count}');
  }
}

class CounterIncrement extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // listen: false로 UI 업데이트 없이 Counter 객체에 접근
    final counter = Provider.of<Counter>(context, listen: false);
    return ElevatedButton(
      onPressed: () => counter.increment(),
      child: Text('증가'),
    );
  }
}

장점:

  • 간결한 API
  • 종속성 주입 패턴 지원
  • 다양한 Provider 유형 제공 (ChangeNotifierProvider, FutureProvider, StreamProvider 등)
  • 테스트 용이성

단점:

  • 중대형 앱에서는 코드 구성이 복잡해질 수 있음
  • 확장성 제한

3. 상태 관리 라이브러리

Riverpod

Riverpod는 Provider의 개선된 버전으로, 컴파일 타임 안전성과 테스트 용이성을 제공합니다.

// pubspec.yaml
// dependencies:
//   flutter_riverpod: ^2.3.6

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 상태 제공자 정의
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
}

void main() {
  runApp(
    // 전체 앱을 ProviderScope로 감싸기
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 상태 읽기
    final count = ref.watch(counterProvider);

    return Column(
      children: [
        Text('카운터: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('증가'),
        ),
      ],
    );
  }
}

장점:

  • Provider보다 향상된 타입 안전성
  • 전역 접근 없이도 제공자에 접근 가능
  • 다양한 제공자 유형
  • 테스트 용이성

단점:

  • 학습 곡선이 있음
  • Provider보다 복잡한 API

Bloc / Flutter Bloc

Bloc(Business Logic Component) 패턴은 이벤트 기반 상태 관리를 위한 패턴으로, 복잡한 애플리케이션에 적합합니다.

// pubspec.yaml
// dependencies:
//   flutter_bloc: ^8.1.3

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// 이벤트 정의
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}

// Bloc 정의
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<IncrementEvent>((event, emit) {
      emit(state + 1);
    });
  }
}

void main() {
  runApp(
    BlocProvider(
      create: (context) => CounterBloc(),
      child: MyApp(),
    ),
  );
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Column(
            children: [
              Text('카운터: $count'),
              ElevatedButton(
                onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
                child: Text('증가'),
              ),
            ],
          );
        },
      ),
    );
  }
}

장점:

  • 명확한 아키텍처
  • 비즈니스 로직과 UI 완전 분리
  • 대규모 앱에 적합
  • 테스트 용이성
  • 강력한 디버깅 도구

단점:

  • 많은 보일러플레이트 코드
  • 간단한 앱에는 과도할 수 있음
  • 가파른 학습 곡선

GetX

GetX는 상태 관리, 라우팅, 종속성 관리를 위한 경량 라이브러리입니다.

// pubspec.yaml
// dependencies:
//   get: ^4.6.5

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class CounterController extends GetxController {
  var count = 0.obs;

  void increment() => count++;
}

void main() {
  runApp(GetMaterialApp(home: Home()));
}

class Home extends StatelessWidget {
  final CounterController controller = Get.put(CounterController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Obx(() => Text('카운터: ${controller.count}')),
            ElevatedButton(
              onPressed: () => controller.increment(),
              child: Text('증가'),
            ),
          ],
        ),
      ),
    );
  }
}

장점:

  • 간결한 문법
  • 상태 관리, 라우팅, 종속성 주입 통합
  • 빠른 개발 속도
  • 낮은 학습 곡선

단점:

  • 아키텍처 가이드라인 부족
  • 큰 프로젝트에서 구조화 어려움
  • 테스트 어려움

MobX

MobX는 반응형 프로그래밍 라이브러리로, 관찰 가능한 상태와 반응을 통해 상태를 관리합니다.

// pubspec.yaml
// dependencies:
//   flutter_mobx: ^2.0.6+5
//   mobx: ^2.2.0
// dev_dependencies:
//   build_runner: ^2.4.6
//   mobx_codegen: ^2.3.0

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';

// counter_store.dart
part 'counter_store.g.dart';

class CounterStore = _CounterStore with _$CounterStore;

abstract class _CounterStore with Store {
  @observable
  int count = 0;

  @action
  void increment() {
    count++;
  }
}

// main.dart
void main() {
  runApp(MyApp());
}

class CounterWidget extends StatelessWidget {
  final CounterStore store = CounterStore();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Observer(
          builder: (_) => Text('카운터: ${store.count}'),
        ),
        ElevatedButton(
          onPressed: () => store.increment(),
          child: Text('증가'),
        ),
      ],
    );
  }
}

장점:

  • 반응형 프로그래밍 모델
  • 자동 UI 업데이트
  • 계산된 값(computed) 지원
  • 코드 생성으로 보일러플레이트 감소

단점:

  • 코드 생성 설정 필요
  • 복잡한 상태 구조에서 이해하기 어려울 수 있음

Redux

Redux는 단일 방향 데이터 흐름과 예측 가능한 상태 컨테이너를 제공하는 라이브러리입니다.

// pubspec.yaml
// dependencies:
//   flutter_redux: ^0.10.0
//   redux: ^5.0.0

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

// 액션
enum Actions { Increment }

// 리듀서
int counterReducer(int state, dynamic action) {
  if (action == Actions.Increment) {
    return state + 1;
  }
  return state;
}

void main() {
  final store = Store<int>(counterReducer, initialState: 0);
  runApp(MyApp(store: store));
}

class MyApp extends StatelessWidget {
  final Store<int> store;

  MyApp({required this.store});

  @override
  Widget build(BuildContext context) {
    return StoreProvider<int>(
      store: store,
      child: MaterialApp(
        home: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            StoreConnector<int, String>(
              converter: (store) => store.state.toString(),
              builder: (context, count) {
                return Text('카운터: $count');
              },
            ),
            StoreConnector<int, VoidCallback>(
              converter: (store) {
                return () => store.dispatch(Actions.Increment);
              },
              builder: (context, callback) {
                return ElevatedButton(
                  onPressed: callback,
                  child: Text('증가'),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

장점:

  • 예측 가능한 상태 관리
  • 단일 방향 데이터 흐름
  • 시간 여행 디버깅
  • 미들웨어 지원

단점:

  • 많은 보일러플레이트 코드
  • 간단한 앱에는 과도할 수 있음
  • 가파른 학습 곡선

4. 기타 접근법

ValueNotifier와 ValueListenableBuilder

Flutter의 내장 기능을 사용한 간단한 상태 관리 방법입니다.

class CounterWidget extends StatelessWidget {
  final ValueNotifier<int> counter = ValueNotifier<int>(0);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ValueListenableBuilder<int>(
          valueListenable: counter,
          builder: (context, value, child) {
            return Text('카운터: $value');
          },
        ),
        ElevatedButton(
          onPressed: () => counter.value++,
          child: Text('증가'),
        ),
      ],
    );
  }
}

장점:

  • 외부 종속성 없음
  • 간단한 상태에 적합
  • 낮은 오버헤드

단점:

  • 복잡한 상태 관리에 부적합
  • 전역 상태 관리가 어려움

Streams와 StreamBuilder

Dart의 Stream을 활용한 반응형 상태 관리 방법입니다.

class CounterBloc {
  final _counterController = StreamController<int>();
  int _counter = 0;

  Stream<int> get counterStream => _counterController.stream;

  void increment() {
    _counter++;
    _counterController.sink.add(_counter);
  }

  void dispose() {
    _counterController.close();
  }
}

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  final CounterBloc _bloc = CounterBloc();

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        StreamBuilder<int>(
          stream: _bloc.counterStream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text('카운터: ${snapshot.data}');
          },
        ),
        ElevatedButton(
          onPressed: () => _bloc.increment(),
          child: Text('증가'),
        ),
      ],
    );
  }
}

장점:

  • 반응형 프로그래밍 모델
  • 비동기 이벤트 처리에 강함
  • 외부 패키지 불필요

단점:

  • StreamController 관리 필요
  • 복잡한 상태에서 코드가 복잡해질 수 있음

상태 관리 선택 가이드

상태 관리 방법을 선택할 때 고려해야 할 사항:

  1. 앱 복잡성

    • 간단한 앱: StatefulWidget, Provider, GetX
    • 중간 복잡도: Provider, Riverpod, MobX
    • 복잡한 앱: Bloc, Redux, Riverpod
  2. 팀 경험

    • 학습 곡선이 낮은 것: Provider, GetX
    • 체계적인 아키텍처 필요: Bloc, Redux, Riverpod
  3. 상태 종류

    • 로컬 상태: StatefulWidget, ValueNotifier
    • 공유 상태: Provider, Riverpod, Bloc
    • 전역 상태: Provider, Riverpod, Redux, GetX
  4. 비동기 작업

    • 비동기 작업이 많은 경우: Bloc, Riverpod, Streams
  5. 테스트 용이성

    • 테스트 중심 개발: Bloc, Redux, Riverpod

멀티 상태 관리 접근법

실제 대규모 앱에서는 한 가지 상태 관리 방법만 사용하는 것이 아니라, 여러 접근법을 함께 사용하는 경우가 많습니다:

  • 로컬 UI 상태는 StatefulWidget으로
  • 화면 간 공유되는 상태는 ProviderRiverpod으로
  • 복잡한 비즈니스 로직은 Bloc 패턴으로

요약

Flutter의 상태 관리는 앱의 요구사항과 규모에 따라 다양한 방법을 선택할 수 있습니다:

  1. 로컬 상태 관리:

    • StatefulWidget
    • ValueNotifierValueListenableBuilder
  2. 공유 상태 관리:

    • InheritedWidget
    • Provider
  3. 라이브러리 기반 상태 관리:

    • Riverpod
    • Bloc / Flutter Bloc
    • GetX
    • MobX
    • Redux
  4. 기타 접근법:

    • Stream 및 StreamBuilder
    • RxDart

적절한 상태 관리 방법을 선택하는 것은 앱의 복잡성, 팀의 경험, 테스트 요구사항 등에 따라 달라질 수 있습니다. 작은 앱에서는 단순한 방법을 사용하고, 앱이 성장함에 따라 더 구조화된 접근법으로 전환하는 것이 일반적입니다.

results matching ""

    No results matching ""