[Flutter] 상태 관리 알아보기 - GetX, BLoC, Provider

MinGyun_06·2025년 2월 21일

Flutter

목록 보기
1/1

이 글을 시작하며🎈

Flutter의 복잡한 앱 구조에서 가장 중요한 부분 중 하나는 상태관리이다.
상태를 어떻게 효과적으로 관리하느냐에 따라 앱의 유지보수성과 성능이 크게
달라질 수 있다.
이번 글에서는 다양한 상태관리 기법을 살펴보고, 각각의 장단점과 적합한 사용 사례를 소개하고자 한다.

0. setState

Flutter에서 setState는 StatefulWidget에서 상태를 관리하는 핵심 메서드이다.
주로 간단한 상태의 변화가 생길 때 사용되며, setState는 해당 위젯의 상태가 변경되었음을 프레임워크에 알리고, UI 갱신을 위해 위젯의 build메서드를 다시 호출하게 된다.

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

위 코드처럼 setState는 간단한 상태 관리에 매우 유용하지만, 앱의 규모가 커지면 복잡한 상태 관리 기법을 사용해야 한다. 이제 Flutter의 여러 상태관리 라이브러리에 대해 알아보자.

1. GetX

GetX는 Flutter의 상태관리 뿐만 아니라, 의존성 및 라우트 관리도 가능하다.
적은 코드로 많은 기능들을 구현할 수 있으며 반응형/비반응형 상태 관리를 모두 지원한다.

장점

  • 코드가 매우 가볍고 빠르며 간단한 문법을 가져오기 쉽다.
  • 직관적인 API로 빠르게 배우고 사용할 수 있다.
  • 상태관리 뿐만 아니라 의존성 주입, 라우팅 등을 한 패키지로 제공한다.

단점

  • 쉬운 코드 때문에 해당 로직 또는 내부 동작을 잘 이해하지 못하거나 GetX에 대한 의존성이 높아질 수 있다.
  • 과도하게 사용하면 추후에 다른 상태관리 기법으로 전환하기가 어렵다.

class CounterController extends GetxController {
  var count = 0.obs;
  increment() => count++;
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GetX Counter Example')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('You have pushed the button this many times:'),
            Obx(() => Text(
                  '${controller.count}',
                  style: TextStyle(fontSize: 40),
                )),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: controller.increment,
      ),
    );
  }
}

작동 흐름

  1. HomePage 생성자에서 CounterController()가 호출된다.
    이 호출로 CounterController의 인스턴스가 생성되고 GetX의 의존성 관리 시스템에 등록된다.

  2. CounterController가 초기화된다.
    count 변수가 0.obs로 초기화되어 반응형 변수가 된다.

  3. 컨트롤러의 GetConsumer가 변경을 감지한다.

  4. 컨트롤러의 상태를 업데이트한다.

  5. 상태 변경에 따라 UI를 자동으로 재구성한다.

2. BLoC

BloC(Business Logic Component)는 플러터 전용으로 구축된 라이브러리로 이벤트 기반의 상태 관리를 제공하며, 애플리케이션의 비즈니스 로직을 UI로부터
분리한다. 테스트가 용이하고, 특히 복잡한 비즈니스 로직을 가진 대규모 앱에서 강점을 발휘한다.

장점

  1. 관심사의 분리
    • UI와 비즈니스 로직이 명확히 분리되어 코드의 구조가 개선된다.
    • 각 컴포넌트의 역할이 명확해져 유지보수가 용이해진다.
  2. 테스트 용이성
    • 비즈니스 로직을 UI와 독립적으로 테스트할 수 있다.
  3. 코드 재사용
    • 플랫폼 간(ex: iOS, Android, Web)로직 공유가 가능해진다.
  4. 확장성
    • 새로운 기능을 추가하거나 기존 기능을 수정하기 쉽다.
    • 대규모 앱에서도 구조를 깔끔하게 유지할 수 있다.

단점

  • 초보 개발자나 처음 접하는 개발자들에게는 BLoC 패턴과 관련 개념(Streams, Reactive Programming)을 이해하는데 시간이 필요할 수 있다.
  • 간단한 기능을 구현할 때도 많은 기능을 작성해야 할 수 있다.
  • 간단한 로직이나 작은 규모의 앱에서 BLoC을 적용하면 불필요하게 복잡해질 수 있다.
// 이벤트 정의
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}

// 상태 정의
class CounterState {
  final int count;
  CounterState(this.count);
}

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

// UI에서 사용 예시
class CounterPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Scaffold(
            appBar: AppBar(title: Text('Counter')),
            body: Center(child: Text('Count: ${state.count}')),
            floatingActionButton: FloatingActionButton(
              onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
              child: Icon(Icons.add),
            ),
          );
        },
      ),
    );
  }
}

작동 흐름

  1. BLoC이 생성되고 초기 상태가 설정된다.

  2. BlocBuilder, BlocListener, 또는 BlocConsumer위젯이 UI를 구성한다.
    이 위젯들은 특정 BLoC의 상태 변화를 감지하도록 설정된다.

  3. 사용자와 UI의 상호작용에 따른 이벤트가 생성된다.
    이벤트는 BLoC.add(event())메서드를 통해 BLoC에 전달된다.

  4. BLoC의 <onEvent>핸들러가 이벤트를 받아 처리한다.

  5. 비즈니스 로직 처리 결과에 따라 emit(newState)메서드를 통해 새 상태 스트림이 추가된다.

  6. BLoC의 상태 스트림에 변화를 BlocBuilderBlocConsumer가 이 변화를 감지하고 UI를 다시 빌드한다.

  7. 업데이트된 상태가 모든 위젯에 전파된다.

3. Provider

  • 간단하고 직관적인 API

    • Provider상태관리를 매우 간단하고 직관적으로 구현할 수 있도록 돕는다.
      ChangeNotifier, Stream, Future등을 이용해 데이터들을 관리하고, UI와의 연결을 쉽게 만들 수 있다.
    • ConsumerSelector 위젯을 통해 상태를 구독하고, 상태가 변경될 때 UI를 갱신하는 방식으로 작동한다.
  • 의존성 주입 (Dependency Injection)

    • Provider의존성 주입을 지원하여, 특정 상태를 위젯 트리의 하위 위젯들에 전달하는 역할을 한다. 이 덕분에 다른 위젯들이 전역적으로 상태를 관리할 필요 없이 필요한 곳에서만 상태를 참조 할 수 있다.
  • 단방향 데이터 흐름

    • Provider단방향 데이터 흐름을 따른다. 상태는 위젯 트리의 하위에서 상위로 전달되지 않고, 상위 위젯에서 하위 위젯으로 상태를 주입하여 관리된다. 이는 코드의 예측 가능성을 높이고 디버깅을 쉽게 한다.
  • 효율적인 상태 변경 감지

    • Provider는 상태 변경이 일어날 때만 UI를 갱신하므로, 필요하지 않은 위젯까지 불필요하게 다시 빌드되지 않습니다. Consumer위젯을 사용하여 상태의 변화를 선택적으로 구독할 수 있어 성능 최적화가 가능합니다.
  • 상태와 비즈니스 로직의 분리

    • Provider는 비즈니스 로직과 UI를 명확히 분리할 수 있도록 설계되었습니다. ChangeNotifier 클래스를 통해 상태 변화 및 로직을 별도로 관리하고, UI는 상태가 변경될 때 자동으로 갱신됩니다.

0개의 댓글