뚝딱뚝딱 모바일

[Flutter] Bloc에 대해 알아보자 본문

Flutter 지식

[Flutter] Bloc에 대해 알아보자

규석 2023. 10. 31. 19:31

안녕하세요!

오늘은 상태관리 라이브러리인 Bloc에 대해 알아보겠습니다.


https://pub.dev/packages/flutter_bloc

 

flutter_bloc | Flutter Package

Flutter Widgets that make it easy to implement the BLoC (Business Logic Component) design pattern. Built to be used with the bloc state management package.

pub.dev

Bloc은 Flutter Favorite Package에 선정된 라이브러리 중 하나이며, 6000개 이상의 Likes를 받은, Flutter 개발자들은 한 번쯤 듣거나 사용해 본 라이브러리입니다. Bloc 또한 제공해 주는 문서가 세세하고, 여러 사항에 대한 예제까지 있어 이 포스팅은 개념적인 내용에 대해서 간략하게 요약해보려 합니다.

[참고자료 : Bloc 공식 문서]

왜 Bloc을 사용해야 하나?

Bloc을 사용하는 이유에 대해서 Bloc 개발진들은 이렇게 말합니다.

  • Simple - 이해하기 쉽고, 다양한 수준의 개발자가 사용할 수 있게 하기 위해
  • Powerful - 어플리케이션을 작은 단위의 컴포넌트로 나눠주어 멋지고 복잡한 애플리케이션 개발을 돕기 위해
  • Testable - 어플리케이션의 모든 부분을 쉽게 테스트할 수 있게 하여 코드에 확신을 가지고 개발을 진행할 수 있게 위해

Bloc의 주요 개념

위의 주장한 Bloc의 강력한 요소들을 이루는 주요 개념들에 대해 알아보겠습니다.

Bloc을 익히기 전에, Stream에 대해 잘 모르신다면 먼저 공부하고 오시는 것을 추천합니다. Bloc을 사용할 때 주로 쓰이는 요소 중 하나이므로, 사전 지식 없이 이해하기 힘드실 수 있습니다. (이 글에선 Stream에 관한 내용이 나오지 않습니다...!!)

Cubit

출처 : Bloc 제공 문서

Cubit은 Cubit이 관리할 State를 가지고 있고, 상태를 변화시키는 함수들을 가지고 있습니다.

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

  void increment() => emit(state + 1);
}

 

 

Cubit의 state들은 primitive 타입 대신 class 또한 사용할 수 있습니다.

그리고 state를 변경시키는 emit 함수는 Cubit 내부에서만 사용할 수 있습니다.

 

이제 이 Cubit을 UI와 연결해 봅시다.

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider<CounterCubit>(
      create: (_) => CounterCubit(),
      child: Scaffold(
        body: BlocBuilder<CounterCubit, int>(
          builder: (context, state) {
            return Center(
              child: GestureDetector(
                  onTap: () => context.read<CounterCubit>().increment(),
                  child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.yellow,
                      child: Center(child: Text(state.toString()))
                  )
              ),
            );
          },
        ),
      ),
    );
  }
}

Cubit을 사용하기 위해 BlocProvider를 위젯 트리 최상단에 감싸주고, 사용할 부분의 위젯에 BlocBuild를 감싸주었습니다.

위 UI는 숫자가 적힌 노란색 Container를 클릭하면 숫자가 1씩 증가하는 매우 간단한 Counter입니다. onTap 함수를 보시면, increment() 함수를 불러와 state를 변경시켜 준다는 것을 알 수 있습니다.

 

또한, Cubit은 state의 변화를 감지하는 onChange, error를 캐칭 하는 onError를 재정의하여 사용할 수 있습니다.

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

  void increment() {
    addError(Exception('increment error!'), StackTrace.current);
    emit(state + 1);
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }

  @override
  void onError(Object error, StackTrace stackTrace) {
    print('$error, $stackTrace');
    super.onError(error, stackTrace);
  }
}

Bloc

출처 : Bloc 제공 문서

Bloc은 함수를 통한 state 변화가 아닌 event에 의존하는 클래스입니다.

sealed class CounterEvent {}

final class CounterIncrementPressed extends CounterEvent {}

CounterEvent 클래스를 정의하고, 이를 상속받는 CounterIncrementPressed 클래스도 만들어줍니다.

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

CounterBloc은 Bloc<Event, State>를 상속받아 만들어줍니다.

Bloc은 Cubit과 다르게 on<Event>를 사용하여 이벤트 핸들러를 등록하도록 합니다.

위 코드에서는 CounterIncrementPressed가 불리게 되면 state를 1 더해줍니다.

 

이제 Bloc도 UI와 연결해 줍시다.

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider<CounterBloc>(
      create: (_) => CounterBloc(),
      child: Scaffold(
        body: BlocBuilder<CounterBloc, int>(
          builder: (context, state) {
            return Center(
              child: GestureDetector(
                  onTap: () => context.read<CounterBloc>().add(CounterIncrementPressed()),
                  child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.yellow,
                      child: Center(child: Text(state.toString()))
                  )
              ),
            );
          },
        ),
      ),
    );
  }
}

Cubit과 구조가 거의 같습니다. 다만 onTap 부분을 보면 미리 정의해 둔 Event인 CounterIncrementPressed 클래스를 Bloc으로 넘겨줌으로써, state를 변경한다는 점이 Bloc과 Cubit의 차이점입니다.

 

Bloc도 마찬가지로 onChange, onError를 재정의하여 사용할 수 있습니다.

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }

  @override
  void onEvent(CounterEvent event) {
    super.onEvent(event);
    print(event);
  }

  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }

  @override
  void onTransition(Transition<CounterEvent, int> transition) {
    super.onTransition(transition);
    print(transition);
  }
}

Cubit과 Bloc, 언제 써야 할까?

위에서도 잠깐 설명했지만, Cubit과 Bloc은 명확한 차이를 보여줍니다. 그럼 이 둘을 어떻게 해야 잘 쓸 수 있을까요? 이 둘의 장점에 대해 알아보고, 분류해 봅시다.

Cubit

Cubit의 큰 장점은 간단하다는 것입니다. Cubit은 state와 state를 변경할 함수만 정의하면 됩니다. Event와 EventHandler까지 구현해야 하는 Bloc과 비교해 보면 쉽게 알 수 있습니다.

Bloc

Bloc은 대신, state 변화뿐만 아니라, 무엇이 변화를 트리거했는지 알 수 있습니다.

Bloc 문서에서는 토큰 인증에 관하여 쉽게 예시를 보여줍니다.

enum으로 알 수 없음, 인증됨, 인증되지 않음 이렇게 3개의 타입을 만들었습니다.

enum AuthenticationState { unknown, authenticated, unauthenticated }

authenticated에서 unauthenticated가 되는 경우에는 여러 가지가 있습니다.

(유저가 로그아웃을 했다, 토큰이 만료되어 로그아웃이 되었다 등)

이럴 때, Bloc을 사용하여, 어떤 이유 때문에 특정 상태로 설정되었는지 알 수 있습니다.

// Bloc을 활용했을 때
Transition {
  currentState: AuthenticationState.authenticated,
  event: LogoutRequested,
  nextState: AuthenticationState.unauthenticated
}

// Cubit을 활용했을 때
Change {
  currentState: AuthenticationState.authenticated,
  nextState: AuthenticationState.unauthenticated
}

위처럼 출력로그에 event가 같이 나오면서, 쉽게 사유를 파악할 수 있습니다.

 

또, Bloc은 buffer, debounceTime, throttle 같은 반응형 연산자를 사용할 수 있습니다.

본인이 필요한 것에 따라 커스텀 EventTransformer를 사용하여 이벤트 처리 방식을 제어할 수 있습니다.

EventTransformer<T> debounce<T>(Duration duration) {
  return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}

// CounterBloc
.
.
CounterBloc() : super(0) {
  on<Increment>(
    (event, emit) => emit(state + 1),
    transformer: debounce(const Duration(milliseconds: 300)),
  );
}

본인이 판단했을 때, 간단한 처리만 하면 되거나, 함수를 통한 상태 변화가 더 깔끔한 것 같다 싶으면 Cubit,

여러 부가적인 이벤트들이 있고, 이를 처리해야 할 때는 Bloc 이렇게 사용하면 될 것 같습니다.


이렇게 Bloc 라이브러리에 대해 알아보았습니다. Bloc 라이브러리가 러닝커브가 조금 있기에, 이제 막 개발을 시작하거나, Flutter를 처음 익히려는 분들에게는 어려울 수 있습니다. (이상한 게 아닙니다..! 다 그래요 다) 하지만, 상태 관리와 로직을 매우 깔끔하게 처리할 수 있는 라이브러리라고 생각됩니다.