[TIL] 21.04.10, 21.04.12

BloC 패턴

BloC(Business Logic Component) 패턴은 구글 개발자들이 추천하는, 플러터 개발에 적용되는 가장 대표적인 디자인 패턴이다.
안드로이드에서의 MVVM 패턴(Model - View - ViewModel)과 거의 동일하며 ViewModel 대신 BloC이 들어가는 구조이다.

지난번 내용이 여기까지였는데 이어서 공부해보았다.

BloC 패턴의 구조

앞서 계속 말했듯 BloC은 패턴이다. 따라서 프로젝트 구조를 어떻게 잡고, 기능을 구현할 때 어떤 요소들을 어떤 단위로 나눠 흐름이 이어지도록 만드는가에 초점을 맞춰 공부해야한다. 이를 파악하기 위한 제일 쉬운 방법 중 하나는 데이터의 흐름(어디서 나와서 어디로 전달되는가)을 확인하는 것이다.

BloC 패턴

위의 그림을 보면 아주 간단한 형태로 BloC 패턴을 이해할 수 있다.

  1. UI: 말그대로 화면 관련 부분
  2. BloC: Presenter 또는 ViewModel의 역할이라고 한다. 그게 무슨 역할이냐면 데이터를 다루는 부분과 화면을 다루는 부분 사이에서 데이터를 잘 주고 받을 수 있도록 제어하는 역할을 하는 것이다. 쉽게 생각해 상태 관리 관련 모든 부분은 여기서 한다고 보면 된다. Repository에 데이터를 요청하고, UI에 데이터를 제공한다.
  3. Repository: 아래의 Data Provider와 엮어서 모델 영역으로 생각하면 되며, 여러 Data Provider를 관리 및 BloC 부분과 통신하는 역할을 한다.
  4. Network/Local Data Provider: API 연동을 위한 코드 영역이다.

BloC 패턴의 구조(보충)

여러 문서를 보다가 결국엔 공식 문서(킹갓 Felix)로 돌아왔다. 아래 구조 이미지를 보자.

BloC 구조

위에서 설명했던 것과 비슷하지만 좀 더 명확한 느낌이다. 위 구조에서 주목할 점은 UI와 data의 명확한 구분이다. 그 사이에 BloC이 위치하여 중간자 역할을 수행한다. data 영역은 앞서 봤던 것처럼 Repository와 Data Provider로 구분된다. 설명은 위와 동일하다.

패키지 [bloc]

https://bloclibrary.dev/#/coreconcepts

bloc 공식 문서에 들어가보면 두가지 패키지가 존재한다. 하나는 bloc이고 하나는 flutter_bloc이다. 이 사람의 문서나 예제를 보면 뭔가 이름들이 헷갈리게끔 짓는다는 느낌을 받는데, 이 패키지명들도 마찬가지이다…

내가 이해한 bloc과 flutter_bloc의 차이점은 bloc은 말그대로 위의 패턴에서 가운데에 있는 bloc을 구현하기 위한 패키지이고, flutter_bloc은 Flutter의 UI 영역에서 bloc 관련 내용을 반영하기 위한 위젯들을 모아놓은 패키지라는 것이다. 쉽게 말해 bloc은 BloC 영역을, flutter_bloc은 UI 영역을 위한 패키지라는 것이고 결국 둘 다 필요한 것이다!

결론은 둘 다 알아야 한다는 것이다.^^

아래 내용에 들어가기에 앞서 이 패키지 내에는 BlocBase라는 기본 클래스가 있다는 사실을 알고 넘어가자. 이 기본 클래스는 Bloc 영역의 구현을 위한 가장 기본 기능들을 모아놓은 클래스이다.

용어 정의(Cubit vs Bloc)

Cubit: BlocBase를 상속받은 클래스로, 간단한 상태에 대한 구현을 아주 간결하게 할 수 있도록 도와주는 Bloc 영역의 클래스이다.

Bloc: BlocBase를 상속받은 클래스로, 좀 더 복잡한 상태에 대한 구현을 위한 Bloc 영역의 클래스이다.

이 두가지의 방법 중 하나로 Bloc을 구현할 수 있다. 용어가 혼재되어 헷갈리는데, BloC 패턴은 말그대로 디자인 패턴이고, UI와 Data 사이의 중간자 역할을 하는 영역의 이름도 Bloc이며, 그 영역을 구현하기 위한 클래스들(방법들)이 Cubit, Bloc이라는 클래스이다.

Bloc이라는 단어가 너무 많은 영역에서 쓰여서 좀 헷갈린다. 일단은 Bloc 영역을 구현하기 위한 클래스로 Cubit을 쓸지 Bloc을 쓸지 고민하는 것이라고만 기억해두자.

Cubit 기본

각각의 용법에 대해 보충 설명을 해보겠다. Cubit의 경우 간단한 상태에 대한 구현을 한다고 했는데, 그만큼 사용법도 간결하다. 카운터 앱을 예제로 만들건데, 카운터 상태를 위한 CounterCubit은 아래와 같이 만들 수 있다.

1
2
3
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
}

Cubit 클래스를 상속받으면서 라는 타입을 지정해주었다. 이는 타입의 상태를 다루는 Cubit이라는 뜻으로 선언한 것이다. 또한 super(0)을 통해 이 Cubit의 초기 상태 값을 0으로 설정해주었다. 초기화를 외부에서 하게끔(CounterCubit을 생성할 때) 하려면 아래와 같이 쓰면 된다.

1
2
3
class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);
}

상태의 변화에 대해 다루려면 아래와 같이 한다. emit이라는 키워드로 상태를 변화시킬 수 있는데, 그 예시는 아래와 같다.

1
2
3
4
5
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);

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

CounterCubit 클래스 내에 increment()라는 메소드를 선언해주었고, 이 메소드는 emit()을 활용해 state + 1 값을 CounterCubit으로(내부 상태 값으로) 전달해주었다. 외부에서 CounterCubit의 상태 값을 변경하려면 이 increment() 메소드를 통해 변경할 수 있다.

여기까지 작성한 내용으로 CounterCubit을 사용하려면(이라기보단 확인해보려면) 아래와 같이 작성하면 된다.

1
2
3
4
5
6
7
void main() {
final cubit = CounterCubit();
print(cubit.state); // 0
cubit.increment();
print(cubit.state); // 1
cubit.close();
}

아주아주 간단하다. 이전에 그냥 setState()로 관리했던 것과 비슷한 수준인 것 같다.

Cubit 스트림

하지만 우리의 Cubit과 Bloc의 근본이 되는 BlocBase는 Stream을 확장시킨 클래스이다. Stream 개념을 쓰지 않을 것이라면 굳이 bloc을 쓸 이유가 없다. 우리는 이를 통해 Cubit과 Bloc을 사용함으로 비동기로 로드되는 데이터에 대한 처리를 가능하게끔 할 수 있다. 이래서 앞서 Stream을 알아봤던 것이다!

1
2
3
4
5
6
7
8
Future<void> main() async {
final cubit = CounterCubit();
final subscription = cubit.stream.listen(print); // 1
cubit.increment();
await Future.delayed(Duration.zero);
await subscription.cancel();
await cubit.close();
}

위의 예시를 살펴보면 Cubit과 Stream의 만남을 확인할 수 있다. 사실 카운터 예제는 굳이 Stream이 필요 없다보니 조금 이상할 수 있지만, 일단 사용법만 본다고 생각하자.

우리는 subscription이라는 것을 선언하여 cubit의 데이터 변화에 listen하고 있다. cubit의 데이터를 구독하고 있다는 표현과 동일하며 그 데이터에 대한 콜백으로 print 함수를 넣어 변화하는 데이터 각각을 출력하도록 했다.

cubit.increment()를 통해 상태 변화를 시켰고, 다 끝나고 나서는 subscription.cancel(), cubit.close()로 구독을 끊고 cubit을 닫았다.

Cubit 관찰하기

Cubit을 스트림으로 다뤄 비동기적인 데이터 처리를 시킬 수도 있지만, 기본적으로 Cubit의 상태 변화를 관찰하게 할 수도 있다. Cubit의 기본 메소드인 onChange()를 오버라이딩하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);

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

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

void main() {
CounterCubit()
..increment()
..close();
}

이렇게 하면 increment()가 수행된 후에 close()된다. 그리고 increment()가 실행이 되면 emit을 통해 상태가 변화하고, 이 변화를 onChange()가 감지하여 변화했다는 것을 인지하게 되는 것이다. 실행 결과는 아래와 같다.

1
Change { currentState: 0, nextState: 1 }

BlocObserver로 앱 내 모든 Cubit 관찰하기

위의 방법인 onChange()는 단 하나의 Cubit 상태에 대한 변화만을 인지한다. 앱이 조금만 커져도 다루는 상태 값들이 많아질 것이고, 그 개수만큼 Cubit도 있을 것이다. 이 모든 Cubit에 대한 변화를 전부 다 관찰하고 있을 수 있는 슈퍼 구독자가 있다. 이것이 BlocObserver이다.

BlocObserver는 bloc 패키지 내에 있는 기본 클래스 중 하나이다. 이 클래스를 확장하여 나만의 BlocObserver를 만들면 앱 전역에 있는 모든 Cubit을 이것 하나로 구독하게끔 할 수 있다. 모든 Cubit들의 변화를 얘가 혼자 다 알려준다.

1
2
3
4
5
6
7
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
}

이렇게 만들 수 있다. BlocObserver의 경우 앱 내에 하나만 있으면 될 것이다. onChange()를 오버라이딩할 때 BlocBase로 선언되어 있기 때문에 Cubit과 Bloc 둘다 처리해줄 수 있으며, 앱 전체의 Cubit & Bloc을 관찰하기 때문에 앱 내에 하나만 있으면 된다. 그래서 주로 lib 폴더 바로 밑에 넣어두곤 한다.

1
2
3
4
5
6
void main() {
Bloc.observer = SimpleBlocObserver();
CounterCubit()
..increment()
..close();
}

아까 만들었던 CounterCubit에 BlocObserver를 붙여보자. 놀랍게도 단지 저 한줄의 코드만으로 CounterCubit의 변화를 감지할 수 있게 되었다.

1
2
Change { currentState: 0, nextState: 1 }
CounterCubit Change { currentState: 0, nextState: 1 }

일단 오늘은 Cubit에 대해 알아보았고, 좀 더 복잡한 상태를 위한 Bloc을 다음번에 알아보자. 어쨌든 하는 역할은 같으니 어렵지 않을 것이다.

Author

TaeBbong Kwon

Posted on

2021-04-12

Updated on

2023-01-02

Licensed under

Comments