BLoC(Business Logic Component)
BLoC(Business Logic Component)란 UI와 Business Logic을 분리하여 만드는 방식을 의미한다. BLoC는 Flutter의 state를 관리하는 디자인 패턴 중 하나이며 Google 개발자에 의해 고안되었다. Flutter는 state에 따라 렌더링이 일어나기 때문에, 상태 관리가 매우 중요하다. 또한 상태 관리는 Flutter에서 만의 문제가 아닌 모든 개발에서 중요하게 고려되어야 할 사항이다. 그러므로 이 BLoC Pattern은 Flutter를 위해 설계되었지만 다른 프레임워크나 언어에서도 적용 가능한 디자인 패턴이다.
예를 들어 React Native에서 setState라는 Hook을 이용해 상태를 갱신시키며 re-render를 수행하는데, 만약 setState를 잘못 사용하여 setState를 수행한 변수에 의해 다른 변수가 다시 상태가 변경되는 경우가 생겼다고 가정하자. 이때 React Native는 "Too many re-renders. React limits the number of renders to prevent an infinite loop."와 같은 infinite loop를 야기하게 될 수 있어 오류를 반환한다.
다음으로 BLoC의 장점 및 특징은 아래와 같다.
- 빠른 속도
- 테스트 용이
- 재사용성
- 한 UI에 여러 BLoC가 존재할 수 있음
- UI코드는 UI에서만, Logic은 BLoC에서만 집중해 관리할 수 있음
- UI에서는 BLoC의 내부 구현에 대해서 몰라도 됨
- setState()를 사용하는 것보다 코드를 더 단순하게 구현 가능
UI와 Business Logic을 분리한 구조 덕분에 각각 코드의 의존성을 낮아져 유지보수나 테스트를 진행하기에도 용이하다.
즉, BLoC는 각 UI 객체 들은 BLoC 객체를 구독하고 있는 모양이다.
BLoC 용어
BLoC에서 자주 사용되는 용어는 아래와 같다.
- Events: Bloc의 input
특정 input을 알리는 역할이다. - States: Bloc의 연산 결과.
항상 초기 State값이 설정이 되어 있어야 한다. - Transitions: State의 변화
Transitions은 current state, the event, next state로 이루어져 있다. - Stream: 비동기 data들의 연속
Stream은 Future와 비슷하게 생각을 할 수 있지만 future와 달리 여러 개의 데이터를 yield를 통해 동시에 반환을 시켜 줄 수가 있다. 그러므로 흐르는 물처럼 데이터들이 흘러나올 수 있다. - Bloc: 들어오는 event Stream을 나가는 state stream으로 바꿔주는 역할.
mapEventToState 함수가 꼭 존재해야 하는데 이 함수가 들어오는 event를 state로 바꾸어 주는 역할을 한다.
Stream vs Future
위 용어 중 개인적으로 BLoC의 핵심은 Stream이라 생각한다. 우리는 비동기(Async) 작업을 수행할 때 Stream과 Future를 자주 접하게 된다. 간단하게 구분하자면 아래와 같다.
- Stream
- 지속적으로 데이터를 받을 때 사용
- Iterable 한 비동기적 데이터
- async * 선언 필요
- await for(for loop), listen과 짝을 이룸 - Future
- 일시적으로 데이터를 받을 때 사용
e.g. http를 이용해 get 하는 경우
- await를 통한 결과는 바로 사용 가능하며, await가 없다면 <Instance of Future>가 반환
- async 선언 필요
- await과 짝을 이룸
BLoC 예제
이제 BLoC를 구현을 예제를 통해 진행해본다.
프로세스는 아래와 같다.
- BLoC 객체의 상태가 변경되면, BLoC의 상태를 구독 중인 UI 객체 들은 그 즉시 해당 상태로 UI를 변경
- BLoC 객체는 UI 객체로부터 이벤트를 전달
- BLoC 객체는 필요한 Provider 나 Repository로부터 데이터를 전달받아, Business Logic을 처리
- Business Logic을 처리한 후, BLoC 객체를 구독 중인 UI 객체들에게 상태를 전달
- UI 객체는 구독 중이던 BLoC 객체의 상태가 변경되면 상태를 전달받는데, 이때 얻은 상태를 이용해 화면을 재구성
아래 소스코드는 https://fre2-dom.tistory.com/304 블로그를 참조하였다.
/// main.dart
import 'package:bloc_pattern/src/ui/bloc_display_widget.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocDisplayWidget() // BlocDisplayWidget 호출
);
}
}
위 코드를 통해 home에서 BlocDisplayWidget()을 호출한다.
/// bloc_display_wideget.dart
import 'package:bloc_pattern/src/bloc/count_bloc.dart';
import 'package:bloc_pattern/src/components/count_view.dart';
import 'package:flutter/material.dart';
late CountBloc countBloc; // 전역 변수로 CountBloc을 호출하고 late를 통해 나중에 값을 받는다.
class BlocDisplayWidget extends StatefulWidget {
const BlocDisplayWidget({Key? key}) : super(key: key);
@override
_BlocDisplayWidgetState createState() => _BlocDisplayWidgetState();
}
class _BlocDisplayWidgetState extends State<BlocDisplayWidget> {
// initState() : 위젯이 생성될 때 처음으로 호출되는 메소드
// initState()을 통해 CountBloc()을 생성
@override
void initState() {
super.initState();
countBloc = CountBloc();
}
// dispose(): 위젯이 종료될 때 호출되는 메소드
// dispose()을 통해 countBloc을 종료시켜 메모리 누수를 방지한다.
@override
void dispose() {
super.dispose();
countBloc.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Bloc 패턴"),
centerTitle: true,
elevation: 0.0,
),
body: CountView(), // Count만을 관리하는 CountView 호출
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// countBloc에서 add() 이벤트를 호출
countBloc.add();
},
),
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
// countBloc에서 remove() 이벤트를 호출
countBloc.remove();
},
)
],
),
);
}
}
위 코드는 앱이 실행될 때 표시할 UI를 그린다.
- 전역 변수로 CountBloc을 호출하고 late를 통해 나중에 값을 받게 된다.
- initState()를 통해 CountBloc을 생성하게 되고 dispose()를 통해 위젯이 종료될 때 메모리 누수를 방지하게 된다.
- body 부분을 보게 되면 count만을 관리하는 CountView()를 호출하게 된다.
- floatingActionButton을 통해 더하기 버튼과 빼기 버튼을 만든 것을 확인할 수 있고 버튼을 누르게 되면 각각 CountBloc에 이벤트를 호출하게 된다.
/// count_bloc.dart
import 'dart:async';
// 비즈니스 로직 분리
class CountBloc {
int _count = 0;
// StreamController을 통해 여러 이벤트를 처리
final StreamController _countSubject = StreamController.broadcast();
// count는 _countSubject.stream 을 구독하고 있는 모든 위젯에게 변경된 상태를 알림
Stream get count => _countSubject.stream;
// count 덧셈 이벤트 처리
add() {
_count++;
_countSubject.sink.add(_count); // _countSubject.sink 에다가 _count를 넣어준다.
}
// count 뺄셈 이벤트 처리
remove() {
_count--;
_countSubject.sink.add(_count); // _countSubject.sink 에다가 _count를 넣어준다.
}
// _countSubject을 종료
dispose() {
_countSubject.close();
}
}
위 코드는 비즈니스 로직을 분리한 부분이다.
- StreamController을 통해 여러 이벤트를 처리하게 되고 count는 _countSubject.stream을 구독하고 있는 모든 위젯에게 변경된 상태를 알리게 된다.
- 다음으로 count 덧셈 이벤트 처리와 뺄셈 이벤트 처리의 함수는 _count를 연산 후 _countSubject에 sink를 통해 _count를 전달해주게 된다.
/// count_view.dart
import 'package:bloc_pattern/src/ui/bloc_display_widget.dart';
import 'package:flutter/material.dart';
// count만을 보여주는 코드
class CountView extends StatelessWidget {
CountView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
// 비동기 처리(StreamBuilder : 변화되는 값을 계속해서 감지)
// StreamBuilder를 통해 countBloc.count을 감지
child: StreamBuilder(
stream: countBloc.count, // countBloc.count => _countSubject.stream 을 구독중
initialData: 0,
builder: (BuildContext context, AsyncSnapshot snapshot) {
// AsyncSnapshot을 통해 들어온 snapshot을 UI에 뿌려준다.
if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: TextStyle(fontSize: 80),
);
}
return CircularProgressIndicator();
},
),
);
}
}
위 코드는 count에 변화를 UI에 뿌려주는 코드인 CountView이다.
- 비동기 처리인 StreamBulider를 통해 변화되는 값인 countBloc.count를 계속해서 감지한다.
- countBloc.count은 _countSubject.stream을 구독 중이기 때문에 _countSubject.stream에 변화에 따라 countBloc.count가 변화게 된다.
- 이 변화된 값을 StreamBulider는 계속해서 감지하고 있다가 변화가 생기면 AsyncSnapshot을 통해 들어온 snapshot을 UI에 뿌리게 된다.
결론
소규모 프로젝트에서는 Provider 하나로 모든 상태 관리를 수행할 수 있다. 하지만 조금만 프로젝트의 크기가 커질수록 이에 적절한 상태 관리가 필요하며 이때 BLoC는 좋은 선택지일 수 있다. BLoC는 model마다 bloc을 만들어서 관리해야 한다는 점이 코드 양이 많아질 수 있고 비교적 복잡한 구조로 인해 초보자들에게 사용하기 어려울 수 있지만 추후 유지보수 단계에 이르게 되었을 때 효율적으로 작업을 진행할 수 있다. 사용되는 기능들을 model + bloc + repository 이렇게 총 3개의 부분으로 관리를 해 상태에 맞는 UI를 지속적으로 사용자에게 보여줄 수 있다. setState(() {})로 화면을 계속 re-rendering 하는 것은 그렇게 좋은 방법은 아닌 듯하다.
참고 자료
https://everyday.codes/mobile/bloc-in-flutter-implement-clean-flux-like-architecture/
https://velog.io/@iamhch/flutter-future-vs-stream
https://velog.io/@seunghwanly/Flutter-BLoC-Pattern
https://fre2-dom.tistory.com/304
https://eory96study.tistory.com/14