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

질문

Flutter에서 사용할 수 있는 다양한 상태 관리 방법과 각각의 장단점을 설명해주세요.

답변

Flutter에서 상태 관리는 앱의 복잡성이 증가함에 따라 매우 중요한 요소가 됩니다. 효과적인 상태 관리는 코드의 유지보수성을 높이고, 버그를 줄이며, 확장 가능한 애플리케이션을 구축하는 데 도움이 됩니다. Flutter에서는 다양한 상태 관리 방법이 있으며, 각각 고유한 장단점을 가지고 있습니다.

1. setState

가장 기본적인 상태 관리 방법으로, StatefulWidget에서 제공하는 내장 메커니즘입니다.

예제 코드:

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('카운터 예제')),
      body: Center(
        child: Text('카운터: $_counter', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • 간단하고 이해하기 쉬움
  • 추가적인 패키지가 필요하지 않음
  • 소규모 앱이나 프로토타입에 적합

단점:

  • 위젯 트리가 깊어지면 상태 전달이 복잡해짐 (prop drilling 문제)
  • 재사용 가능한 상태 로직을 만들기 어려움
  • 앱 규모가 커질수록 관리하기 어려워짐

2. InheritedWidget & InheritedModel

Flutter의 내장 위젯으로, 위젯 트리를 통해 데이터를 효율적으로 전달할 수 있습니다.

예제 코드:

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

  CounterProvider({
    Key? key,
    required Widget child,
    required this.counter,
    required this.incrementCounter,
  }) : super(key: key, child: child);

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

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

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,
      incrementCounter: _incrementCounter,
      child: MaterialApp(
        home: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final provider = CounterProvider.of(context);

    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget 예제')),
      body: Center(
        child: Text('카운터: ${provider.counter}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => provider.incrementCounter(),
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • Flutter 내장 기능으로 추가 패키지가 필요하지 않음
  • 위젯 트리의 깊은 레벨까지 효율적으로 데이터 전달 가능
  • 상태 변경 시 필요한 위젯만 다시 빌드됨

단점:

  • 복잡한 상태 관리를 위해 많은 보일러플레이트 코드가 필요
  • 비동기 작업 처리가 번거로움
  • Provider나 Riverpod 같은 솔루션이 이를 더 간결하게 해결함

3. Provider

InheritedWidget의 단점을 보완하기 위해 만들어진 인기 있는 상태 관리 라이브러리입니다.

설정:

dependencies:
  provider: ^6.0.5

예제 코드:

// 상태 모델
class Counter with ChangeNotifier {
  int _count = 0;
  int get count => _count;

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

// 메인 앱
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MaterialApp(
        home: CounterPage(),
      ),
    );
  }
}

// 소비자 위젯
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Provider 예제')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('현재 카운터 값:'),
            // 1. Consumer 위젯 사용
            Consumer<Counter>(
              builder: (context, counter, child) {
                return Text(
                  '${counter.count}',
                  style: TextStyle(fontSize: 36),
                );
              },
            ),

            // 2. 직접 Provider에서 읽기
            ElevatedButton(
              onPressed: () {
                // 읽기만 할 때 (UI 변경에 반응하지 않음)
                final counter = Provider.of<Counter>(context, listen: false);
                counter.increment();
              },
              child: Text('증가'),
            ),
          ],
        ),
      ),
    );
  }
}

장점:

  • InheritedWidget보다 간결한 API
  • 의존성 주입 패턴으로 테스트하기 쉬움
  • 여러 Provider를 조합하여 복잡한 상태 관리 가능
  • 상태 변경 시 효율적인 리빌드 (필요한 위젯만 업데이트)
  • 다양한 Provider 타입 제공 (ChangeNotifierProvider, FutureProvider, StreamProvider 등)

단점:

  • 앱 규모가 매우 커지면 Provider 간의 의존성 관리가 복잡해질 수 있음
  • 비동기 작업에서 일부 제한적인 경우가 있음
  • 코드 생성 기능이 없어 타입 안전성이 일부 부족할 수 있음

4. Riverpod

Provider의 향상된 버전으로, Provider의 한계를 극복하기 위해 개발되었습니다.

설정:

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 = state + 1;
}

// 메인 앱
void main() {
  runApp(
    // ProviderScope는 Riverpod의 상태를 관리하는 컨테이너
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterPage(),
    );
  }
}

// 소비자 위젯
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 프로바이더에서 상태 읽기
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod 예제')),
      body: Center(
        child: Text(
          '카운터: $count',
          style: TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • Provider의 모든 장점을 가지면서 몇 가지 제한을 해결
  • 컴파일 타임 안전성 강화
  • Provider의 전역 참조가 가능해 의존성 주입이 더 용이함
  • Provider 간의 의존성 관리가 더 명확함
  • 테스트 용이성 향상
  • 여러 Provider가 동일한 타입을 제공할 수 있음

단점:

  • Provider보다 약간 더 많은 보일러플레이트 코드가 필요할 수 있음
  • 러닝 커브가 조금 높을 수 있음
  • Provider 대비 상대적으로 커뮤니티 리소스가 적을 수 있음 (빠르게 성장 중)

5. Bloc (Business Logic Component)

복잡한 앱을 위한 강력한 상태 관리 라이브러리로, 이벤트 기반 아키텍처를 사용합니다.

설정:

dependencies:
  flutter_bloc: ^8.1.2

예제 코드:

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

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

// Bloc 구현
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<IncrementEvent>((event, emit) => emit(state + 1));
    on<DecrementEvent>((event, emit) => emit(state - 1));
  }
}

// 메인 앱
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: MaterialApp(
        home: CounterPage(),
      ),
    );
  }
}

// UI 구현
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc 예제')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text(
              '카운터: $count',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'increment',
            child: Icon(Icons.add),
            onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
          ),
          SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'decrement',
            child: Icon(Icons.remove),
            onPressed: () => context.read<CounterBloc>().add(DecrementEvent()),
          ),
        ],
      ),
    );
  }
}

장점:

  • 체계적인 이벤트 기반 아키텍처
  • 상태 변화를 예측 가능하게 관리 (단방향 데이터 흐름)
  • 대규모 애플리케이션에 적합
  • 기능 테스트가 용이함
  • 개발자 도구를 통한 디버깅 지원
  • 비동기 작업을 효과적으로 처리

단점:

  • 간단한 앱에는 과도한 보일러플레이트 코드가 필요할 수 있음
  • 러닝 커브가 상대적으로 높음
  • 소규모 프로젝트에는 오버엔지니어링일 수 있음

6. GetX

상태 관리, 라우팅, 의존성 주입 등 다양한 기능을 제공하는 경량 패키지입니다.

설정:

dependencies:
  get: ^4.6.5

예제 코드:

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

// 컨트롤러
class CounterController extends GetxController {
  var count = 0.obs;  // observable 변수

  void increment() => count++;
}

// 메인 앱
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(  // MaterialApp 대신 GetMaterialApp 사용
      home: CounterPage(),
    );
  }
}

// UI 구현
class CounterPage extends StatelessWidget {
  // 컨트롤러 등록
  final CounterController controller = Get.put(CounterController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GetX 예제')),
      body: Center(
        child: Obx(  // 반응형 상태 위젯
          () => Text(
            '카운터: ${controller.count.value}',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: controller.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • 경량화된 API로 간결한 코드 작성 가능
  • 상태 관리, 라우팅, 의존성 주입 등 통합 솔루션
  • 빠른 개발 속도
  • 성능 최적화
  • 간편한 전역 상태 접근

단점:

  • 코딩 스타일이 Flutter의 일반적인 패턴과 다름
  • 프로젝트 구조가 GetX에 의존적이 됨
  • 일부 개발자들은 "마법같은" API가 코드 가독성을 해친다고 생각함
  • 대규모 팀 프로젝트에서는 신중한 사용이 필요

7. MobX

Observable 패턴 기반의 반응형 프로그래밍 라이브러리입니다.

설정:

dependencies:
  mobx: ^2.2.0
  flutter_mobx: ^2.0.6+5

dev_dependencies:
  build_runner: ^2.4.6
  mobx_codegen: ^2.3.0

예제 코드:

// counter_store.dart
import 'package:mobx/mobx.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
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'counter_store.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterPage(),
    );
  }
}

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final CounterStore store = CounterStore();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('MobX 예제')),
      body: Center(
        child: Observer(  // Observer 위젯으로 감싸기
          builder: (_) => Text(
            '카운터: ${store.count}',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: store.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • 직관적인 Observable/Observer 패턴
  • 명시적인 코드 구조
  • 자동 코드 생성으로 보일러플레이트 감소
  • 비동기 작업 처리에 강점
  • 확장성과 유연성이 좋음

단점:

  • 코드 생성을 위한 설정이 필요
  • 러닝 커브가 있음
  • 복잡한 상태에서는 디버깅이 어려울 수 있음

8. Redux

Flux 아키텍처 기반의 예측 가능한 상태 관리 라이브러리입니다.

설정:

dependencies:
  redux: ^5.0.0
  flutter_redux: ^0.10.0

예제 코드:

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

// 액션
enum Actions { Increment }

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

void main() {
  // Store 생성
  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(
      appBar: AppBar(title: Text('Redux 예제')),
      body: Center(
        child: StoreConnector<int, String>(
          converter: (store) => store.state.toString(),
          builder: (context, count) {
            return Text(
              '카운터: $count',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          StoreProvider.of<int>(context).dispatch(Actions.Increment);
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • 예측 가능한 단방향 데이터 흐름
  • 시간 여행 디버깅 가능
  • 엄격한 아키텍처로 일관된 코드베이스 유지
  • 미들웨어를 통한 비동기 작업 처리
  • Redux DevTools를 통한 강력한 디버깅

단점:

  • 많은 보일러플레이트 코드 필요
  • 단순한 앱에는 과도한 구조일 수 있음
  • 상대적으로 가파른 러닝 커브
  • 간단한 상태 변경에도 복잡한 과정이 필요할 수 있음

9. Hooks (flutter_hooks)

React Hooks에서 영감을 받은 상태 관리 방식입니다.

설정:

dependencies:
  flutter_hooks: ^0.20.0

예제 코드:

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

void main() {
  runApp(MaterialApp(home: CounterApp()));
}

class CounterApp extends HookWidget {
  @override
  Widget build(BuildContext context) {
    // useState를 사용하여 상태 관리
    final counter = useState(0);

    return Scaffold(
      appBar: AppBar(title: Text('Hooks 예제')),
      body: Center(
        child: Text(
          '카운터: ${counter.value}',
          style: TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.value++,
        child: Icon(Icons.add),
      ),
    );
  }
}

장점:

  • 함수형 프로그래밍 스타일
  • 상태 관리 로직 재사용 용이
  • 코드 간결성
  • 복잡한 위젯 상태를 더 쉽게 관리

단점:

  • 함수형 프로그래밍에 익숙하지 않은 개발자에게는 어려울 수 있음
  • Flutter의 StatelessWidget/StatefulWidget 패러다임과 차이가 있음
  • 규모가 큰 앱에서는 다른 상태 관리 솔루션과 함께 사용해야 할 수 있음

상태 관리 방법 선택 가이드

각 애플리케이션의 특성과 요구사항에 따라 적절한 상태 관리 방법을 선택하는 것이 중요합니다:

  1. 소규모 앱 또는 프로토타입:

    • setState 또는 Provider
  2. 중간 규모 앱:

    • Provider 또는 Riverpod
    • GetX (빠른 개발 속도가 필요한 경우)
  3. 대규모 복잡한 앱:

    • Bloc (체계적인 아키텍처가 중요한 경우)
    • Riverpod (유연성과 타입 안전성이 중요한 경우)
    • Redux (엄격한 상태 관리가 필요한 경우)
  4. 특별한 요구사항:

    • 반응형 프로그래밍 스타일 선호: MobX
    • 함수형 스타일 선호: flutter_hooks
    • 간결한 API와 빠른 개발 속도 필요: GetX

여러 상태 관리 솔루션 조합하기

대규모 애플리케이션에서는 여러 상태 관리 방법을 조합하여 사용하는 것도 좋은 접근법입니다:

  1. 로컬 UI 상태: setState 또는 flutter_hooks
  2. 중간 범위 상태: Provider 또는 Riverpod
  3. 전역 애플리케이션 상태: Bloc 또는 Redux

결론

Flutter의 상태 관리는 앱의 복잡성, 팀의 경험, 프로젝트 요구사항에 따라 달라집니다. 각 솔루션은 저마다의 장단점이 있으므로, 프로젝트에 가장 적합한 방법을 선택하는 것이 중요합니다. 작은 앱에서는 간단한 방법으로 시작하고, 앱이 성장함에 따라 필요한 경우 더 강력한 상태 관리 솔루션으로 마이그레이션하는 접근법을 고려해볼 수 있습니다.

results matching ""

    No results matching ""