[Flutter] BLoC(Bussiness Login Component)란?

Flutter에서의 BLoC(Business Logic Component)은 애플리케이션의 비즈니스 로직을 UI와 분리하여 유지보수성, 테스트 용이성, 재사용성을 높이기 위해 사용하는 상태 관리(State Management) 아키텍처 패턴이다.


1. BLoC이란 무엇인가?

https://bloclibrary.dev/ko/why-bloc/

 

왜 Bloc인가?

어떤 요소가 Bloc을 견고한 상태 관리 솔루션으로 만드는 지에 대한 개요입니다.

bloclibrary.dev

BLoC (Business Logic Component) 패턴은 Reactive Programming (반응형 프로그래밍)을 기반으로 하며, Dart의 StreamSink를 사용하여 UI와 비즈니스 로직을 분리한다.

  • Input: 사용자의 이벤트 (예: 버튼 클릭)를 Sink에 전달
  • Output: 처리된 결과를 Stream을 통해 UI에 전달

Bloc은 다음 세 가지 핵심 가치를 염두에 두고 설계되었다:

  • 간단함: 이해하기 쉽고 다양한 스킬 수준을 가진 개발자가 사용할 수 있다.
  • 강력함: 더 작은 구성 요소로 구성하여 놀랍고 복잡한 애플리케이션을 만드는 데 도움을 준다.
  • 테스트 가능: 애플리케이션의 모든 측면을 쉽게 테스트할 수 있으므로 자신 있게 테스트를 반복할 수 있다.

전반적으로, Bloc은 상태 변경이 발생할 수 있는 시기를 규제하고 애플리케이션 전체에 걸쳐 단일한 상태를 변경하는 방법을 시행함으로써 상태 변경을 예측 가능하게 만들려고 시도한다.


Streams

  • Stream은 연속적인 비동기 데이터다.
  • Streams 을 쉽게 이해하기 위해서는 물이 흐르는 파이프를 생각하자. 파이프는 Streams이고 물은 비동기 데이터이다.
async* (비동기 생성기) 함수를 작성하여 Dart에서 Stream을 생성할 수 있다.
Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}
  • 함수를 async*로 표시하면 yield 키워드를 사용하여 데이터의 Stream을 반환할 수 있다. 위 예시에서는 max 정수 파라미터까지의 정수 Stream을 반환하고 있다.
  • async* 함수에서 yield 할 때 마다 해당 데이터를 Stream을 통해 푸시한다.

Cubit

cubit 은 BlocBase를 extends 한 클래스로, 모든 유형의 state를 관리하도록 확장할 수 있다. 즉 BLoC의 간단한 버전이다. flutter_bloc 패키지에서 제공하며 Stream 기반이다. 이벤트 클래스를 따로 만들지 않고, 메서드 호출 -> 상태 변경을 하는 구조다.


2. BLoC vs Cubit

위에서 Cubit 이 BLoC 의 간단한 버전이라고 했는데 더 자세히 알아보자
항목 BLoC Cubit
이벤트 (Event) 반드시 정의 필요 없음
코드 구조 Event → Bloc → State 직접 메서드 호출로 상태 변경
복잡도 복잡한 상태 흐름에 적합 간단한 로직에 적합
Stream 기반 on<Event>((e, emit) {...}) emit(State) 직접 호출
유즈케이스 로그인, 결제, 인증 등 다중 이벤트 로직 토글, 카운터, 단순 상태 변경

BLoC 작동 흐름

[UI] --이벤트--> [Bloc] --로직처리--> [State] --> [UI]

예: 로그인 버튼 클릭 → 로그인 Bloc에 이벤트 전달 → 로그인 로직 수행 → 결과 상태 방출 → UI가 상태에 따라 리렌더링

BLoC 사용 예시

1. 이벤트 정의

abstract class CounterEvent {}

class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

2. 상태 정의

class CounterState {
  final int count;
  const CounterState(this.count);
}

3. BLoC 구현

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<Increment>((event, emit) => emit(CounterState(state.count + 1)));
    on<Decrement>((event, emit) => emit(CounterState(state.count - 1)));
  }
}

4. UI에서 사용

BlocProvider(
  create: (_) => CounterBloc(),
  child: BlocBuilder<CounterBloc, CounterState>(
    builder: (context, state) {
      return Column(
        children: [
          Text('Count: ${state.count}'),
          ElevatedButton(
            onPressed: () => context.read<CounterBloc>().add(Increment()),
            child: Text('Increment'),
          ),
        ],
      );
    },
  ),
)

Cubit 사용 예시

1. 상태 정의

class CounterState {
  final int count;
  const CounterState(this.count);
}

2. cubit 클래스 정의

class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(CounterState(0));

  void increment() => emit(CounterState(state.count + 1));
  void decrement() => emit(CounterState(state.count - 1));
}

3. UI에서 사용

BlocProvider(
  create: (_) => CounterCubit(),
  child: BlocBuilder<CounterCubit, CounterState>(
    builder: (context, state) {
      return Column(
        children: [
          Text('${state.count}'),
          ElevatedButton(
            onPressed: () => context.read<CounterCubit>().increment(),
            child: Text('증가'),
          )
        ],
      );
    },
  ),
);

3. 실제 서비스에서의 사용 예

예시 1: 로그인 흐름(BLoC)

이벤트: LoginButtonPressed
↓
BLoC: 로그인 시도, 서버 요청
↓
상태: LoginLoading → LoginSuccess / LoginFailure
↓
UI: 로딩 표시 / 에러 메시지 / 홈 화면 이동

예시 2: 다크모드 토글(Cubit)

Cubit: ThemeCubit → toggleDarkMode()
↓
상태: DarkModeOn / DarkModeOff
↓
UI: 테마 변경