개요
Flutter로 페이지를 만들고 라우팅까지 할 수 있게 된 뒤, 각 페이지에 네트워킹을 붙이게 될 쯤 굉장히 난처한 부분을 마주하게 된다. 바로 이전 페이지로 돌아가면서 데이터 패칭을 하는 것이다.
A(Page)에서 B로 이동한 뒤, B에서 A에 연결된 데이터에 영향이 가는 작업을 수행한 뒤 다시 A로 돌아오면 변경된 사항을 반영해주어야 한다. 이를 해결하는 가장 쉬운 방법은 강제 새로고침을 넣어 사용자에게 이를 전가하는 방식인데 이는 유저 친화적이지 않기에 보통은 뒤로 이동하면 자동으로 데이터 패칭이 되게끔 개발한다.
하지만 보통 데이터 패치는 페이지가 생성되는 `init`에서 수행되고, 각 페이지와 연결된 Controller(혹은 Notifier가 있다면)는 다른 Controller들과 독립적이기에 다른 Controller의 작업을 수행하지 못한다. A에 붙어있는 A Controller가 수행하는 데이터 패칭 메소드를 B에 붙어있는 B Controller를 수행할 수 없다는 것이다.
각 Controller는 독립적이어야한다는 규칙을 깨고 B Controller가 A Controller에 접근해서 뒤로 돌아가는 순간 A Controller의 메소드를 수행하게 할 수는 있지만, 이는 결국 돌아가기만 할 뿐, 유지보수가 어려워지는 코드만 남게 된다.
그래서 이를 해결하기 위해 필자는 Event Bus 패턴이라는 것을 도입했다.
Event Bus 패턴
Event Bus 패턴이란?
Event Bus 패턴은 컴포넌트 간의 비동기 통신을 가능하게 해주는 디자인 패턴이다. 이를 통해 독립적인 컴포넌트들이 직접 참조하지 않고 서로 통신을 할 수 있게 된다. 즉, 컴포넌트들은 각자의 독립성을 지키면서 각 컴포넌트에 간섭할 수 있게 된다는 것이다.
Event Bus의 구조와 요소
Event Bus의 구조는 중요한 세 가지 요소로 구성된다. 하나는 이벤트 발행자(Event Publisher), 다른 하나는 이벤트 수신자(Event Subsciber)이며, 마지막은 이 둘을 연결해주는 이벤트 버스(Event Bus)이다. 구조를 쉽게 요약해주면, 이벤트 발행자가 발행하는 이벤트를 이벤트 버스를 통해 이벤트 수신자가 받는 구조라고 할 수 있다. ▼
Event Bus 구조는 크게 4가지로 수행이 된다. 일단 하나씩 보고, 그 다음에 코드와 함께 보자.
1. 특정 이벤트 구독
가장 먼저 이벤트 수신자와 이벤트 클래스를 정의한다. 이벤트 수신자가 이벤트 버스에 어떤 이벤트를 전달 받을 것인지를 구독한다. 개발자는 커스텀 이벤트 클래스를 만들고, 생성한 커스텀 이벤트 클래스를 이벤트 버스에 구독을 통해 등록한다.
예를 들어, B에서 A로 돌아갈 때 데이터를 다시 패칭하고 싶다면 데이터를 다시 패칭한다는 이벤트를 받고 수행하는 부분은 A이기에 A에서 구독을 수행해준다. ▼
2. 특정 이벤트 발행
그 다음 이벤트 발행자 클래스를 정의하고, 개발자가 이벤트를 발행하고 싶은 위치를 정하여 발행시킨다.
예를 들어, B에서 A로 돌아가는 시점에서 A Controller의 데이터를 다시 패칭하고 싶다고 하면 해당 시점에 데이터를 다시 패칭하는 이벤트를 발행한다. ▼
3. 특정 이벤트 전달
이벤트 발행자가 발행한 이벤트를 이벤트 버스가 전달해준다. 이 부분은 Event Bus 코드가 수행을 해주는 부분이며, 코드로 한 번 작성해놓으면 특별한 문제가 생기지 않는한 개발자가 관리를 해야하는 부분은 아니다.
예를 들어, B에서 A로 돌아가는 시점에서 이벤트 발행자가 데이터를 다시 패칭하라는 이벤트를 발행했다면, 이벤트 버스가 이를 받아서 A에 전달해준다. ▼
4. 구독한 특정 이벤트 수신
이제 이벤트 수신자는 발행한 이벤트를 이벤트 버스를 통해 받게 된다. 이벤트를 받으면 사전에 정의해놓은 메소드를 수행하게 된다.
예를 들어, B에서 A로 돌아가는 시점에서 보낸 데이터를 다시 패칭하는 이벤트를 이벤트 버스가 이벤트 수신자에게 전달하여 이벤트 수신자가 이를 수신하게 된다. 이벤트를 수신한 이벤트 수신자는 사전에 정의해놓은 메소드, 데이터를 다시 패칭하는 메소드를 수행하여 데이터를 패칭하게 된다. ▼
이렇게 위의 4가지 과정을 통해 컴포넌트 간에 비동기 통신을 할 수 있게 된다.
코드와 함께 보는 Event Bus
백문이 불여일견이라고 코드와 함께 보자.
구성 요소들의 코드
EventBus 클래스
가장 먼저 `EventBus` 클래스 코드이다. ▼
/*
* 만약 publishing과 unSubscribe가 동시에 발생할 경우 오류가 발생한다.
*
* 이유 -> for문으로 iterable객체에 순회를 하는중간에 특정 요소를 제거할 수 없다.
* 즉 publishing에서 순회를 하고있는데 동시에 unSubscribe가발생한다면
* 동일객체에 순회와 수정이 동시에 발생하여 오류를 일으킨다.
* 이것을 대응하기 위해 publish에서는 메서드는 초입부에 copy본을 생성하여
* 그것을 이용하여 순회하고 event를 발행하도록 한다.
* */
class EventBus {
EventBus._();
static final EventBus _instance = EventBus._();
static EventBus get instance => _instance;
// 각 이벤트 타입에 대한 구독자 목록을 관리합니다.
final Map<Type, List<EventBusSubscriber<EventBusBaseEvent>>> _subscribers =
{};
/// 구독
void subscribe<T extends EventBusBaseEvent>(
{required EventBusSubscriber<T> subscriber}) {
_subscribers.putIfAbsent(T, () => []);
_subscribers[T]?.removeWhere(
(element) => element.runtimeType == subscriber.runtimeType);
_subscribers[T]!.add(subscriber);
AppInitiator.logger.i("subscriber - ${subscriber.runtimeType}");
}
/// 구독 해제
void unSubscribe<T extends EventBusBaseEvent>(
{required EventBusSubscriber<T> subscriber}) {
_subscribers[T]?.removeWhere(
(element) => element.runtimeType == subscriber.runtimeType);
_subscribers[T]?.remove(subscriber);
}
/// 퍼블리싱
void publish<T extends EventBusBaseEvent>({required T event}) {
List<EventBusSubscriber<EventBusBaseEvent>>? subscribersOfType =
_subscribers[T];
if (subscribersOfType != null) {
// 구독자 목록의 복사본을 만들어 반복
List<EventBusSubscriber<EventBusBaseEvent>> subscribersSnapshot =
List.from(subscribersOfType);
for (var subscriber in subscribersSnapshot) {
subscriber.onEvent(event);
}
}
}
}
싱글톤 패턴으로 되어있으며, 앱이 실행되는 동안 메모리에서 제거되지 않고 이벤트를 보관하고 전달해준다.
EventSubscriber 클래스
다음은 `EventSubscriber` 클래스이다. ▼
abstract class EventBusSubscriber<T extends EventBusBaseEvent> {
void subscribe();
void unSubscribe();
void onEvent(T event);
}
추상 클래스로 작성이 되어있어 개발자는 이를 컨트롤러에 `implement` 하여 총 3가지 메소드를 구현해야한다. 각각 어떻게 구현해야하는 지는 각 단계별 코드에서 보겠다.
EventBusBaseEvent 클래스
다음은 `EventBusBaseEvent` 클래스이다. ▼
abstract class EventBusBaseEvent {}
딱히 제약 사항이 없는 껍데기만 있는 추상 클래스이다. 개발자는 이를 상속받아 이벤트가 발생하여 이벤트가 전달 될 때 같이 전달할 값들을 정의할 수 있다.
여기까지가 기본적으로 작성해야할 EventBus 코드들이다. 필자는 아래와 같이 폴더를 구성했다. ▼
어 근데 EventPublisher는 없는데?
Publish는 EventBus 클래스에 내장되어있고, 이벤트 발행이 필요한 시점에서 EventBus 인스턴스를 호출하여 발행해주면 되는 구조로 되어있다. 그래서 EventPublisher는 EventBus 내부에 있다고 생각하면 좋다.
예시 코드
그러면 이제 앞에서 줄곧 언급한 A에서 B로 이동하고, B에서 A로 돌아올 때 데이터를 패칭하는 예시 코드를 보자.
⚠️ 주의 ⚠️
필자는 Provider 상태관리 툴을 사용했으며, Clean Architecture가 적용된 프로젝트를 예시로 들고 있다. 다른 상태관리 툴과 디자인 패턴에 직접 적용하는데에는 큰 무리는 없겠지만 어느정도는 주의를 하고 보는 것이 좋다.
1. 발행할 Event 작성
우선 필자는 아래와 같이 작성했다. ▼
abstract class FetchEvent extends EventBusBaseEvent {}
class FetchListEvent extends FetchEvent {
FetchListEvent();
}
EventBus 클래스가 EventBusBaseEvent 부모 클래스 타입을 받기에 만드는 모든 Event들은 이를 상속해줘야한다. 그리고 이를 상속한 이벤트를 다시 FetchListEvent에 상속시켰다.
'왜 번거롭게 두 번 상속시킨거?'
추상 클래스를 만들고, 이를 다시 상속한 이유는 해당 이벤트에 여러 바리에이션이 있을 수 있기 때문이다. `FetchEvent`라는 이벤트가 있고, 이와 같은 관심사를 가진 `FetchListEvent`, `FetchDataEvent`, `FetchDetailEvent` 등등 여러가지 이벤트가 생길 수 있다. 만약에 단순하게 이를 `EventBusBaseEvent`를 바로 상속받아서 `Event`를 만들게 되면 확장성이 떨어지게 될 수 있다.
하지만 이런 이벤트가 적다면 바로 상속해도 상관은 없다.
그림으로는 이 부분에 해당한다. ▼
2. EventSubscriber 작성
EventSubscriber는 위와 같이 별도의 클래스 파일로 작성하지 않고 notifier(controller)에 implement 해주었다. ▼
class APageNotifier extends BaseNotifier
implements EventBusSubscriber<FetchEvent>
앞에서 왜 두 번 상속했는가에 대한 명확한 해답을 여기서 볼 수 있다. 만약에 여러 이벤트에 각각 EventBusBaseEvent를 상속시켰다면, 해당 Notifier에서 같은 유형의 여러가지 이벤트를 받아야 할 때 각각의 이벤트들을 모두 작성해줘야한다. 하지만 위와 같이 특정 유형에 대해 부모 클래스를 작성한 뒤 이를 다시 상속받게 하면 해당 부모 클래스만 implements하여 코드를 쉽게 작성할 수 있게 된다.
그리고 해당 Notifier(혹은 Controller)에 `subscribe`, `unSubscribe`, `onEvent`를 아래와 같이 작성해준다. ▼
@override
void onEvent(UnitEvent event) {
/*데이터 패칭하는 메소드, 혹은 로직 구현*/
}
@override
void subscribe() {
EventBus.instance.subscribe<FetchEvent>(subscriber: this);
}
@override
void unSubscribe() {
EventBus.instance.unSubscribe<FetchEvent>(subscriber: this);
}
@override
void onInit() {
subscribe();
super.onInit();
}
@override
void dispose() {
unSubscribe();
super.dispose();
}
onEvent가 이벤트를 수신할 때 수행할 메소드를 정의해놓는 부분이기에 해당 부분에 어떻게 데이터를 패칭해올 건지를 작성하면 된다.
허나 가장 중요한 것은 구독과 구독 해제를 하는 부분이다. 구독을 해야 이벤트를 수신받고, 구독 해제를 해야 제대로 된 메모리 관리나 구독 관리가 되기에 onInit과 dispose에 각각 구독과 구독 해제 메소드를 꼭 작성해줘야한다.
그림으로 보면 이 부분에 해당한다. ▼
3. Publish
다 작성했다면 발행을 해줘야한다. B에서 뒤로 돌아가는 시점에 아래와 같이 작성해주면 쉽게 발행할 수 있다. ▼
IconButton(
onPressed: () {
EventBus.instance.publish<FetchEvent>(event: FetchListEvent());
// 제네릭에는 FetchEvent, event: 에는 이를 상속받은 구체화 된 FetchListEvent 인스턴스
Navigator.of(context).pop();
},
icon: const Icon(
Icons.close,
color: AppColors.grayscale500,
),
),
이렇게 되면 뒤로 돌아가는 시점에 A로 이벤트를 날리고, A Notifier가 살아있다면(Replace나 GetX의 offAllAndNamed 처럼 페이지를 날린게 아니라면) 데이터 패칭을 하게 된다.
그림으로 보면 나머지 모든 동작에 해당한다. ▼
이렇게 작성하면 Notifier를 싱글톤으로 만들고, 이를 불러내서 데이터 패칭을 시키고 할 필요 없이 깔끔하게 뒤로 가면서 혹은 다른 동작을 할 때 데이터 패칭을 요청할 수 있게 된다.
사실 백엔드와 연결해놓고 데이터 스트림을 받아서 하면 다시 패칭할 필요없이 가장 깔끔하게 구현이 가능하겠지만, 이는 백엔드와 상의를 해야하고 백엔드의 부담이 커지기에 일반적으로 수행할 수 있는 방법중에 가장 깔끔한 것 같다.
마치며
Event Bus 패턴이란 것을 잘 모르고 있을 때는 코드를 굉장히 지저분하고 객체지향 규칙을 모두 어기면서 코드를 짰었는데 해당 패턴을 알고난 뒤로는 코드가 상당히 깔끔해졌다. 역시 많이 알아야 좋은 코드를 짤 수 있구나를 또 한 번 느꼈다...
'Develop > Flutter' 카테고리의 다른 글
[Flutter][Widget][Issue] TabBar 왼쪽에 공간이 생기는 현상 (0) | 2024.12.11 |
---|---|
[Flutter][Error] Lexical or Preprocessor Issue (Xcode): 에러 해결 (0) | 2024.10.16 |
[Flutter] Dart는 싱글 스레드 언어 (0) | 2024.08.04 |
[Flutter] Dart의 컴파일 과정 (0) | 2024.08.04 |
[Flutter][Widget] CustomPaint로 나만의 위젯 만들기 (2) | 2024.04.08 |