자바스크립트가 비활성화 되어있습니다.
자바스크립트가 활성화 되어야 콘텐츠가 깨지지 않고 보이게 됩니다.
자바스크립트를 사용할수 있도록 옵션을 변경해 주세요.
- willbsoon

본문 바로가기
Mobile/Flutter

[flutter] Bloc 이란? - official document

by willbsoon 2022. 7. 19.

1.  웹 -> 앱 개발

오랜만에 글을 남긴다.

회사에서 모바일 웹페이지를 웹뷰로 앱제작을 하려고 한다.

flutter가 대세라서 flutter로 빠르게 만들어볼려고하였다. 근데 생각해보니 여기서 더 확장성있게 개발을 할려면 대충 하나의 파일에 개발을 하기 보다는 제대로된 설계가 필요할듯 해보였다. 그래서 검색하다보니 bloc에 대해서 찾아보게 되었다.

앵귤러 개발을 하다보니 프론트에 대한 어느정도 이해는 있지만 또 막상 새로운 dart와 flutter를 하려다보니 공부해야할게 많았다.

그래서 bloc 패턴에 대해 공부하고자 해서 글을 남겨본다.

 

내용이 많아 연재형식으로 가야할듯?

 

state 혹은 상태라고 번역한다

직역과 의역이 만연한 발번역이라 이해해주세요...

 

 

2022.07.19 - [Mobile/Flutter] - [flutter] Bloc 이란? - official document

2022.07.19 - [Mobile/Flutter] - [flutter] flutter_bloc 이란?(Bloc Widgets) - official document

2022.07.19 - [Mobile/Flutter] - [flutter] Bloc Usage - official document

 

 

2.  Bloc 공식 문서(링크)

 

 

 

 

 

 

 

 

 1) bloc의 목표

bloc의 목표는  비즈니스 로직으로부터 presentation을 쉽게 분리해 테스트 가능성과 재사용성을 용이하게 하는 것입니다. bloc의 의미는 Business Logic Component 입니다. 이는 reactive 측면을 추상화하여 개발자가 비즈니스 로직 작성에 집중할 수 있도록 합니다.

 

 

이제 각각의 구성 하나하나 살펴가보도록 합시다.

Bloc 패턴에서는 먼저 2가지에 대해 말하고있는데 Cubit과 Bloc이다.

 2) Cubit

Cubit은 BlocBase를 상속하는 클래스이고, 이것은 어느 타입의 상태라도 확장이 가능하다. Cubit은 emit 메서드를 호출하기 전에 초기상태를 가지고 있어야 한다.

그리고 Cubit의 현재 상태는 state getter를 통해 접근이 가능하고 Cubit의 상태는 emit 메서드를 통해서 새로운 상태로 업데이트가 가능하다.

 

위 이미지를 보면 Cubit은 미리 정의된 어떠한 함수를 통해서 호출 받게 되면 현재 state를 배출하게 된다. emit을 통해서 state를 업데이트 할 수 있고, 상태가 변경되기 직전에 onChange를 통해서 현재 상태와 다음 상태를 매개변수로 받아 custom한 작업을 할 수있다.

 

*Cubit 만들기

** cubit 관측하기(observing)

`onChange`는 하나의 cubit의 상태를 관측하도록 재정의할수있다.

`onError`는 하나의 cubit의 에러를 관측하도록 재정의할수 있다.

// CounterCubit 에서는 int형의 상태를 관리한다.
class CounterCubit extends Cubit<int> {
  // CounterCubit의 초기값은 0 이다.
  CounterCubit() : super(0);

  // increment가 호출되면 현재 상태는 state로 접근할수있고, emit을 통해 새로운 상태를 얻을 수 있다.
  void increment() => emit(state + 1);
  
  // Observing Cubit
  // cubit의 상태를 관측하기 위해 재정의할수 있다.
  @override
  void onChange(Change<int> change) {
    super.onChange(change);
    print(change);
  }

  // cubit의 에러를 관측하기 위해 재정의할수 있다.
  @override
  void onError(Object error, StackTrace stackTrace) {
    print('$error, $stackTrace');
    super.onError(error, stackTrace);
  }
  
}

 

*Cubit 사용하기

void main() {
  // CounterCubit 인스턴스를 생성한다.
  final cubit = CounterCubit();

  // cubit의 state필드를 통해 현재 state를 접근할수 있다.
  print(cubit.state); // 0

  // cubit 안에 미리 정의해둔 increment 메서드를 통해 state를 변경할 수 있다.
  cubit.increment();

  // cubit의 state필드를 통해 현재 state를 접근할수 있다.
  print(cubit.state); // 1

  // Cubit을 더이상 사용하지 않는다면 종료한다.
  cubit.close();
}

 

 

 

 

 

 

 

 

 

 

 

 3) BlocObserver - Cubit 관측

BlocObserver를 사용하면 모든 Cubit을 관측할 수 있다. 아래 예시를 보자.

 

*BlocObserver

class MyBlocObserver extends BlocObserver {
  // BlocBase는 Cubit에서 상속하는 Class. 이후 Cubit의 상태 변경을 관측할수 있다.
  
  @override
  void onCreate(BlocBase bloc) {
    super.onCreate(bloc);
    print('onCreate -- ${bloc.runtimeType}');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('onChange -- ${bloc.runtimeType}, $change');
  }

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

  @override
  void onClose(BlocBase bloc) {
    super.onClose(bloc);
    print('onClose -- ${bloc.runtimeType}');
  }
}

* BlocObserver 사용하기

void main() {
  BlocOverrides.runZoned(
    () {
      // Cubit 사용하기.......
    },
    blocObserver: MyBlocObserver(),
  );
}

 

 

 

4) Bloc

드디어 Bloc에 대해 알아보자.

 

Bloc은 함수보다 상태를 변경하기위한 이벤트에 의존하는 고급 클래스이다. 그리고 BlocBase를 상속받아서 Cubit과 비슷한 공통 api를 가지고 있다. 하지만 Bloc에서 함수를 호출하거나 직접 새로운 상태를 출력하는것 보다는 Bloc은 이벤트를 수신하고 들어온 이벤트를 나가는 state 로 변경해준다

 

 

Cubit과는 다르게 Bloc에 event를 입력받고 state를 Emit하는것을 볼수 있다. 그리고 몇가지 메서드들이 추가된것을 볼수 있다. 

1. Bloc에서 상태 변경은 이벤트가 추가되어 onEvent가 발생했을때 시작된다.

2. 그리고 이벤트는 EventTransformer로 유입된다. 이미지로 보면 EventTransformer는 들어온 이벤트를 어떻게 처리할지 조작하도록 지정할수있다고 한다. 기본적으로 각 이벤트는 동시에 처리되지만, EventTransformer는 들어오는 이벤트 스트림을 조작하는데 제공될 수 있다.

3. 그러면 등록된 모든 EventHandler는 수신된 이벤트와 함께 호출된다.

4. 각 EventHandler는 이벤트의 응답으로 0 혹은 그 이상의 상태를 emit 하는 역할을 한다.

5. 그리고 마지막으로 onTransition은 state가 업데이트되지 전에 호출되고, 현재 상태와 이벤트, 다음 상태를 포함한다.

 

예시를 통해 살펴보자.

 

*Bloc 만들기

 

Bloc은 BlocBase를 상속하기 때문에 Cubit과 같이 onChange, onError를 재정의 가능하다.

추가로 onEvent와 onTransition도 재정의할 수 있다.  onEvent는 Event가 Bloc에 추가되었을때 호출된다. 

onTransition은 onChange와 비슷하지만 이것은 상태를 변경하는 event와 현재상태와 다음 상태를 포함한다.

// CounterBloc이 반응할 이벤트를 추상 클래스로 만든다.
abstract class CounterEvent {}

// 증가 상태를 bloc에 알려줄 이벤트. 추상클래스를 상속받는다.
class CounterIncrementPressed extends CounterEvent {}

// CounterEvent를 int로 변환하는 것을 처리할 CounterBloc을 만든다.
class CounterBloc extends Bloc<CounterEvent, int> {
  // CounterBloc의 초기값을 0으로 잡는다.
  CounterBloc() : super(0) {
    // CounterBloc에 CounterIncrementPressed 이벤트를 추가할때
    // 현재 상태는 state 변수로 접근할수 있고 새로운 상태는 emit함수를 통해 출력한다.
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  }
  
  

  // bloc을 observing 하기
  @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);
  }

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

* Bloc 사용하기

Future<void> main() async {
  //CounterBloc 인스턴스를 생성
  final bloc = CounterBloc();

  // CounterBloc의 상태는 state 필드로 접근가능한다.
  print(bloc.state); // 0

  // 이벤트를 추가함으로 상태를 변경할수 있다.
  bloc.add(CounterIncrementPressed());

  // 이벤트가 처리되었는지 확인하기위해 이벤트 루프의 다음 반복을 기다린다.
  await Future.delayed(Duration.zero);

  // CounterBloc의 상태는 state 필드로 접근가능한다.
  print(bloc.state); // 1

  // bloc을 사용하지 않으면 종료한다.
  await bloc.close();
}

 

5) BlocObserver - Bloc 관찰

BlocObserver는 bloc도 마찬가지로 BlocOverrides.runZoned()를 통해 관찰할 수 있다.

class MyBlocObserver extends BlocObserver {
  @override
  void onCreate(BlocBase bloc) {
    super.onCreate(bloc);
    print('onCreate -- ${bloc.runtimeType}');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('onChange -- ${bloc.runtimeType}, $change');
  }

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

  @override
  void onClose(BlocBase bloc) {
    super.onClose(bloc);
    print('onClose -- ${bloc.runtimeType}');
  }
  // 위 코드는
  // cubit 관측할때과 같음
  
  
  
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('onEvent -- ${bloc.runtimeType}, $event');
  }
  
  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('onTransition -- ${bloc.runtimeType}, $transition');
  }
}

* BlocObserver 사용

Cubit을 관측할때와 같음..

void main() {
  BlocOverrides.runZoned(
    () {
    // example...
      CounterBloc()
        ..add(CounterIncrementPressed())
        ..close();
    },
    blocObserver: MyBlocObserver(),
  );
}

 

 

6) Cubit vs Bloc (링크)

그럼 둘이 뭐가 다른거냐? 살펴보자.

 

Cubit - 간단함

Cubit을 생성할때 오직 State와 상태 변경을 위해 우리가 노출하기 위한 함수만 정의하면 된다. 대조적으로 Bloc을 만들땐 state, event, EventHandler를 정의해야한다. 이렇게 하면 Cubit을 더 쉽게 이해할 수 있고 관련된 코드가 줄어듭니다.

Cubit 구현은 보다 간결하며 이벤트를 별도로 정의하는 대신 함수가 이벤트처럼 작동합니다. 또한 Cubit를 사용할 때 상태 변경을 트리거하기 위해 어디에서나 간단히 emit을 호출할 수 있습니다.

 

Bloc - 추적성

Bloc 사용의 가장 큰 장점 중 하나는 상태 변경의 순서와 이러한 변경을 촉발한 정확한 원인을 아는 것입니다. 응용 프로그램의 기능에 중요한 상태의 경우 상태 변경 외에 모든 이벤트를 캡처하기 위해 보다 이벤트 중심 접근 방식을 사용하는 것이 매우 유용할 수 있습니다.

 

Bloc - 고급 이벤트 변환

Bloc이 Cubit보다 뛰어난 또 다른 영역은 buffer, debounceTime, throttle 등과 같은 reactive 연산자를 활용해야 할 때입니다.

예를 들어 실시간 검색의 경우 벡엔드의 부하를 줄이기 위해 디바운스타임을 주고싶을때가 있다. Bloc은 들어온 이벤트가 처리하는 방식을 변경하기 위해 custom EventTransformer를 제공한다.

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

CounterBloc() : super(0) {
  on<Increment>(
    (event, emit) => emit(state + 1),
    /// custom EventTransformer를 EventHandler 에 적용한다.
    transformer: debounce(const Duration(milliseconds: 300)),
  );
}

 

 

7) Streams

Bloc을 사용하기 위해서는 먼저 stream에 대한 이해가 있어야 한다. 그래서 dart에 stream을 먼저 공부해야할 필요가 있다. 사실 rxjs를 사용해본 터라 비슷한 개념인것을 알고는 있지만 기회가 되면 정리해봐야할듯하다.

Stream는 비동기 데이터의 순서? 정도라고 볼 수 있다. 시간의 흐름에 따라 연속적으로 들어오는 데이터를 처리하기 위한 데이터 타입인것.

 

dart 에서 이를 처리하기 위한 간단한 예시를 보자.

Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}

Int 형의 Stream을 볼수 있다. 함수를 선언하는 과정에서 쓰인 async*를 볼수 있는데 async generator라고 한다.  async* 와 yield 키워드를 사용하면 Stream을 만들 수 있다.

async* 함수에서 yield 할때마다 Stream에 해당 데이터 조각을 밀어넣는다. 따라서 아래와 같이 Stream 데이터를 소비할 수 있다.

Future<int> sumStream(Stream<int> stream) async {
    int sum = 0;
    await for (int value in stream) {
        sum += value;
    }
    return sum;
}

async 키워드로 마킹된 함수에서 우리는 await 키워드를 사용하려 동기적 처리가 가능하고, Future 데이터타입을 리턴할수 있다. 위 함수를 통해 매개변수로 받은 stream 안의 리턴받은 값들의 합을 리턴받는다.

 

위 함수를 사용해서 아래와 같은 예시를 만들수 있다.

void main() async {
    /// 0-9 값으로 초기화하는 stream을 만든다.
    Stream<int> stream = countStream(10);
    /// 합계를 계산한다. await 키워드를 통해 동기적으로 작동한다.
    int sum = await sumStream(stream);
    /// 합계를 계산
    print(sum); // 45
}

 

 

 

3. 결론

Bloc 패턴에 대해 기본인 cubit과 bloc에 대해 알아봤다.

이것은 기본이고 더 나아가 문서를 쭈욱 훑어 보자.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

댓글