LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


Flutter 인기 아키텍처 라이브러리 3종 비교 분석 - GetX vs BLoC vs Provider

안녕하세요. LINE+ ABC Studio에서 앱을 개발하고 있는 윤기영입니다. 최근 Flutter로 진행하는 새로운 앱 개발 업무를 맡아서 어떤 아키텍처 라이브러리를 사용할지 선정하는 작업을 진행했습니다. 여러 라이브러리 중 현재 가장 인기 있는 라이브러리인 GetX와 Provider, BLoC를 후보로 정한 뒤 샘플 앱을 구현했는데요. 이번 글에서는 구현한 각 코드를 다양한 상황에서 살펴보며 각 라이브러리의 장단점을 비교해 보겠습니다.

GetX Provider BLoC
pub.dev 최초 등록일 2019년 11월 2018년 10월 2018년 10월
pub.dev 좋아요 수 11,792
(https://pub.dev/packages/get)
8,241
(https://pub.dev/packages/provider)
2,210
(https://pub.dev/packages/bloc)
GitHub 좋아요 수 8,265
(https://github.com/jonataslaw/getx)
4,681
(https://github.com/rrousselGit/provider)
10,194
(https://github.com/felangel/bloc)
GitHub 기여자 수 100명 이상 100명 이상 79명
각 라이브러리 인기도(2023년 3월 기준)

오픈소스 라이브러리를 사용하는 이유

하나의 앱을 만들 때는 한 가지 구조로 통일하는 것이 좋습니다. 작업자가 다르다고 화면마다 각각 다른 패턴으로 결과물을 만들면 유지 보수하기가 어렵습니다. 패턴화할 수 있는 부분을 뽑아 아키텍처 라이브러리로 만들어 사용하면 작업자가 다르더라도 통일된 산출물을 얻을 수 있습니다.

이때 아키텍처 라이브러리는 직접 만들어도 되고 오픈소스를 사용해도 됩니다. 다만 직접 만들면 검증도 혼자서 해야 하고, 매뉴얼도 직접 만들어야 하며, 각종 상황이 발생했을 때 참고할 만한 레퍼런스도 부족합니다. 반면 인기 많은 오픈소스 라이브러리는 오랜 기간 다양한 사람들이 토론하며 기능을 개선하고 테스트한 결과물입니다. 많은 사람들이 꾸준히 검증하면서 지속적으로 업데이트하고 있으며, 매뉴얼도 잘 갖춰져 있고, 다양한 문제 상황에 대비할 레퍼런스도 쉽게 찾을 수 있습니다.

비록 직접 라이브러리를 만들어 쓰고 싶은 욕망이 간절했지만, 같이 일하는 동료들(혹은 1년 뒤의 나)을 위해 아키텍처는 오픈소스 라이브러리를 사용하기로 결정했습니다. Flutter에서 사용하는 대표적인 오픈소스 아키텍처 라이브러리로는 GetX와 Provider, BLoC가 있습니다. 각 라이브러리의 비슷한 점과 차이점을 비교해 볼 텐데요. 그전에 먼저 MVVM 패턴에 대해 간략히 설명하겠습니다. 

MVVM 패턴

GetX와 Provider, BLoC는 모두 MVVM 패턴을 사용합니다. MVVM은 뷰(view), 뷰모델(viewmodel), 모델(model)의 세 개 영역으로 나눠 클래스를 만듭니다. 아래는 Flutter MVVM 아키텍처의 데이트 흐름도입니다. 다른 용어와 혼동하지 않도록 이 글에서는 '모델'을 '서비스'로 지칭하겠습니다. 

기존 개발 방식에서는 액션이 발생하면 하나의 함수에서 모든 일을 다 처리합니다.

// 뷰모델을 사용하지 않는 기존 개발 방식
// func1 기능 정의가 복잡 → 데이터를 받아와 화면을 업데이트하고 에러가 발생하면 창 닫기
// func1 결과는 어떻게 테스트하지?
func1() {
  // 데이터 받아오기
  final data = await requestArticle();

  // 화면 반영
  view.updateTitle(data.title);
  view.updateText(data.text);

  // 에러가 있다면 현재 화면 닫기
  if (data.hasError) {
    Navigator.pop();
  }
}

MVVM 패턴을 이용하면 아래와 같이 액션 결과가 데이터(data)로 반영됩니다. 데이터로 결과를 얻으면 코드 작성 목적이 명확해지고 테스트가 쉬워집니다.

// 뷰모델을 사용하는 경우
// func1 기능 정의가 명확 → 외부에서 데이터를 받아와 내부에 저장
// 함수 호출 전/후 데이터를 비교해 기능 테스트 실시
func2() {
  // 외부에서 데이터 받아와 내부에 저장
  this.data = await requestArticle();

  // 에러 시 이벤트 발행
  if (data.hasError) {
    emit(HasError());
  }
}

MVVM 아키텍처에서는 추가로 아래 규칙도 적용합니다.

라이브러리 비교에 사용할 샘플 앱 소개

라이브러리를 비교하기 위해 앱 개발을 학습할 때 일반적으로 접하는 'To Do' 앱을 하나 만들었습니다. 이 앱은 아래와 같은 기능과 특징이 있습니다. 

  • 외부에서 데이터를 받아 화면 갱신

  • 컴포넌트 간 데이터 공유

  • 앱을 사용하는 중간에 언제든지 비동기 요청 발생 가능

  • 다른 화면 간 데이터 공유

실제 코드를 작성할 때는 추가해야 하지만, 샘플 코드에서는 이번 글의 목적에 집중하기 위해 아래 내용을 생략했습니다.

  • 위젯 생성 시 사용하는 const 키워드
  • 위젯 유효성 여부를 검사하는 mounted 조사
  • StreamSubscription.cancel()
  • CancelableOperation.cancel()

StatefulWidget으로 MVVM 패턴 구현

먼저 라이브러리를 사용하지 않고 StatefulWidget으로 MVVM 패턴을 구현해 보겠습니다. 클래스는 아래와 같이 구성합니다.

아래 코드는 뷰모델 역할을 하는 ListViewModel 구현부입니다.

class ListViewModel {
  ListViewModel() {
    // clickCount는 화면 간 공유하므로 서비스에 선언해 놓고 참조
    LocalService.instance.clickCount.listen((value) {
      clickCount = value;
      onUpdated();
    });
  }
 
 
 
  // ==============================================
  // 뷰에 반영될 상태를 내부 변수로 선언
  // ==============================================
 
  // 리스트 로딩 중
  bool listLoading = false;
 
  // add/delete request 진행 중
  bool loading = false;
 
  // 화면에 표시되는 clickCount
  int clickCount = 0;
 
  // 화면에 표시되는 list
  List<Article> list = [];
 
 
 
  // ==============================================
  // 뷰 통지를 위한 콜백 함수
  // ==============================================
 
  // 위젯에 전달하는 콜백
  void Function() onUpdated = () {};
  void Function(String) onAlert = (msg) {};
 
 
 
  // ==============================================
  // 액션이 발생하면 내부 상태를 바꾸고 뷰에 통지
  // ==============================================
 
  // 외부에서 list 받아오기
  reloadList() async {
    listLoading = true; // 1. 로딩 상태로 바꾸고
    onUpdated();
    list = await RemoteService.instance.getArticleList(); // 2. 데이터를 채우고
    listLoading = false;
    onUpdated(); // 3. 화면을 갱신
  }
 
  // 체크된 리스트 아이템 삭제하기
  removeChecked() async {
    loading = true; // 로딩 상태로 바꾸고
    onUpdated();
    final result = await RemoteService.instance.remove(list.checked); // request
    if (result) {
      list = await RemoteService.instance.getArticleList();
      loading = false;
      onUpdated();
      onAlert('SUCCESS'); // success
    } else {
      loading = false;
      onUpdated();
      onAlert('ERROR'); // error
    }
  }
 
  // 리스트 아이템 체크 상태 변경
  toggleCheck(Article article) {
    article.checked = !article.checked;
    onUpdated();
  }
 
 
  // clickCount 증가
  addClickCount() {
    // clickCount는 화면 간 공유하므로 서비스 내 변수 참조
    LocalService.instance.addClickCount();
  }
}

아래 코드는 뷰 역할을 하는 ListPage 구현부입니다.  

class ListPageState extends State<ListPage> {
  ListViewModel vm = ListViewModel();
 
  @override
  void initState() {
    super.initState();
    // 뷰모델을 listen
    vm.onUpdated = () => setState(() {});
    vm.onAlert = (msg) => context.showAlert(msg);
  }
 
  @override
  Widget build(BuildContext context) {
    return Frame(
      layer: vm.loading ? LoadingView() : null, // View ← ViewModel.State
      children: [
        // -------- UI counter --------
        CounterButton(
          text: Text('CLICK COUNT : ${vm.clickCount}'), // View ← ViewModel.State
          onTapCount: () => vm.addClickCount(), // 뷰모델의 액션 실행
          onTapOpen: () => context.pushCountView(),
        ),
 
        // -------- UI remove button --------
        RemoveButton(
          text: Text('REMOVE : ${vm.list.checkedCount}'), // View ← ViewModel.State
          onPressed: () => vm.removeChecked(), // 뷰모델의 액션 실행
        ),
 
        // -------- UI list --------
        ArticleListView(
          layer: vm.listLoading ? ListLoadingView() : null,
          itemBuilder: (context, index) {
            return ListItem(
              article: vm.list[index], // View ← ViewModel.State
              onTap: () => vm.toggleCheck(vm.list[index]), // 뷰모델의 액션 실행
            );
          },
        ),
      ],
    ); // Frame
  }
}

위 코드는 clickCount만 변경돼도 onUpdated()가 호출돼 화면 전체가 업데이트되는 단점이 있습니다. 이를 보완한 게 미니멀 빌드로, 데이터가 변경될 때 관련 있는 뷰만 업데이트합니다.

위 코드를 미니멀 빌드가 되도록 수정할 수 있습니다. 하지만 파라미터와 콜백이 많아져서 코드가 복잡해집니다. 이때 GetX를 이용하면 간단히 구현할 수 있습니다.

GetX로 MVVM 패턴 구현

pub.dev에서 가장 많은 '좋아요'를 받은 GetX를 사용해 보겠습니다. 클래스 구성은 StatefulWidget을 사용했을 때와 거의 동일합니다. 아래 두 가지 내용만 다른데요. 코드 내 주석으로 설명하겠습니다.

  • 서비스와 뷰모델 객체를 등록하고 불러오기
  • RxObx를 이용해 관련 있는 위젯만 업데이트하기
    • Rx: 변수 값이 변경됐을 때 이를 외부에 알려주는 클래스
    • Obx: Rx 변경이 발생했을 때 화면을 다시 그리는 위젯 
// ==============================================
// 서비스
// ==============================================
 
// 서비스는 모두 GetxService를 상속해야 함
class LocalService extends GetxService {
  ...
 
 
// 서비스 등록
Get.put(LocalService());
...
 
 
// 등록한 서비스를 불러올 때는 아래처럼 호출(어디서든 호출 가능)
LocalService obj = Get.find();
...
 
 
 
// ==============================================
// 뷰모델
// ==============================================
 
// 뷰모델은 모두 GetxController를 상속해야 함
class ListViewModel extends GetxController {
 
  // 필드 하나 하나를 Rx로 선언해 Obx 위젯이 변경 내역을 통보받을 수 있게 함
  RxInt clickCount = 0.obs;
  RxBool listLoading = false.obs;
  Rx<List<Article>> list = [].obs;
  ...
 
  addClickCount() {
    clickCount.value = clickCount.value + 1;
  }
  ...
 
 
// 뷰모델은 Widget build가 호출되는 시점에 등록
// 이렇게 등록한 뷰모델은 해당 위젯이 소멸될 때 같이 소멸됨
class HomePage extends StatelessWidget {
  const ListPage({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    // 등록
    Get.put(ListViewModel());
    ...
 
 
// 등록한 뷰모델을 불러올 때는 아래처럼 호출(어디서든 호출 가능)
ListViewModel obj = Get.find();
...
 
 
 
// ==============================================
// 뷰
// ==============================================
 
// 뷰 구현부
class ListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...
        // Obx는 Rx 변경을 추적하는 위젯
        // Rx로 선언한 clickCount가 변경되면 Obx 내부에 선언된 Text 위젯만 업데이트됨
        Obx(() => Text('CLICK COUNT : ${viewModel.clickCount.value}'))
        ...

Provider와 BLoC은 뷰모델을 얻으려면 항상 BuildContext를 이용해야 하지만 GetX는 BuildContext가 없어도 객체를 불러올 수 있습니다. 

// 뷰모델 가져오기

// GetX - 현재 메모리에 존재하는 동일한 타입의 객체 리턴
ListViewModel obj = Get.find();

// Provider - 항상 BuildContext를 파라미터로 전달받아야 함
ListViewModel obj = context.read();

// BLoC - 항상 BuildContext를 파라미터로 전달받아야 함
ListViewModel obj = BlocProvider.of(context);

처음에는 BuildContext를 알고 있어야 뷰모델을 불러올 수 있다는 점이 불편할 것 같아서 GetX를 선택했지만, 이는 이후 GetX 사용을 포기하게 만든 점이 되기도 했습니다. GetX는 현재 메모리에 존재하는 뷰모델 중 유형이 동일한 인스턴스를 가져다 주는 방식을 사용하는 반면, Provider와 BLoC은 BuildContext를 이용한 의존성 주입을 사용합니다.

BuildContext는 Flutter 위젯이 작동하는 주요 개념 중 하나입니다. Flutter와 같은 선언형 UI 방식 개발에서는 화면을 그리기 위해 위젯 트리 최상단에서부터 자식들을 하나씩 그려나가는데요. 각 위젯이 화면에 그려질 때 BuildContext를 전달받습니다. BuildContext는 위젯 트리에서 현재 그려야 할 위젯의 위치를 나타냅니다. 위젯을 그릴 때 이 값을 역추적해 부모 노드를 탐색하면 현재 내가 그려야 할 화면 영역이 어디인지와 색상과 폰트 등의 테마 값을 알 수 있고, 화면 내비게이터 정보도 알아낼 수 있습니다. 굳이 위젯의 생성자에 이런 값을 하나하나 다 전달하지 않더라도 BuildContext를 통해 의존성을 주입받을 수 있습니다.

아래 위젯 트리 그림은 BuildContext를 이용한 의존성 주입을 어떻게 사용하는지 보다 자세히 설명한 그림입니다.

BuildContext 사용 예시 1. 의존성 주입

BuildContext 사용 예시 2. 생명 주기 관리

BuildContext 사용 예시 3. 의존성 변경

BuildContext를 통하지 않는 객체 참조 방식은 예기치 못한 곳에서 문제를 일으킬 가능성이 있습니다. 한 예로 GetX 사용 중 아래와 같이 동일한 클래스의 인스턴스를 두 개 이상 등록할 때 문제가 발생했습니다.

final tab1 = TabViewModel('tab1');
Get.put(tab1)
 
final tab2 = TabViewModel('tab2'); // 동일한 클래스로 다른 인스턴스를 생성
Get.put(tab2)
 
// tab1을 받을지 tab2를 받을지 알 수 없음
TabViewModel tab = Get.find()

...

// 이 문제는 GetX에서 태그를 지정하는 방식을 사용해야 해결할 수 있음

// 태그 지정해야 함
Get.put(TabViewModel(), tag: 'tab1');
Get.put(TabViewModel(), tag: 'tab2');
 
// 뷰모델 사용, 태그를 알아야 가져올 수 있음
TabViewModel tab1 = Get.find(tag: 'tab1'); 
TabViewModel tab2 = Get.find(tag: 'tab2');
// ← 뷰모델을 사용하는 자식 위젯에게 태그 값을 매번 전달해야 해서 불편, BuildContext를 사용하는 게 낫지 않을까?

Provider로 MVVM 패턴 구현

Provider에서는 GetX와 다르게 Provider라는 객체를 이용해 BuildContext 의존성 주입을 이용한 뷰모델을 제공합니다. 화면이 구성될 때 위젯 트리는 아래와 같이 구성됩니다.

코드 구현부를 살펴보겠습니다.

// ==============================================
// 서비스
// ==============================================
 
// 서비스 객체, 특별한 규칙이 필요하지 않음
class LocalService {
  ...
 
 
// 위젯 트리 중간에 Provider를 이용해 서비스 주입
// 서비스는 BuildContext의 의존성 주입 룰을 따름
class MyApp extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (context) => LocalService(),
      child: HomePage(),
      ...
 
 
// 서비스 객체를 얻는 코드, BuildContext 내에서만 접근 가능
LocalService local = context.read();
...
 
 
 
// ==============================================
// 뷰모델
// ==============================================
 
// 뷰모델은 ChangeNotifier를 상속받아야 함
class ListViewModel extends ChangeNotifier {
  int clickCount = 0;
  ...
 
  addClickCount() {
    clickCount += 1;
    notifyListeners(); // 변경 내역을 뷰에 통보하기 위해 notifyListeners() 호출
  }
  ...
 
 
// 위젯 트리 중간에 ChangeNotifierProvider를 이용해 뷰모델 주입
// 뷰모델은 BuildContext의 의존성 주입 룰을 따름
class HomePage extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => ListViewModel(),
      child: ListView(),
      ...
 
 
// 아래와 같이 호출해 뷰모델 사용, BuildContext 내에서만 접근 가능
ListViewModel list = context.read();
...
 
 
 
// ==============================================
// 뷰
// ==============================================
 
// 뷰모델을 읽어 화면을 업데이트
class ListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...
        Text('CLICK COUNT : ${context.select((ListViewModel vm) => vm.clickCount)}'), // select viewModel
        ...

화면에서 특정 부분만 업데이트하고 싶다면 Provider를 나눠 구현하면 됩니다. 예를 들어 아래와 같이 매초마다 새로 그려야 하는 정도로 자주 업데이트하는 위젯을 만든다면 Provider를 나눠 구현합니다. 

매초 현재 시간을 업데이트하는 화면

Provider는 뷰모델 스트림만 리슨(listen)할 수 있다는 단점이 있으며, 경고창 생성 같은 일회성 이벤트를 지원하지 않아서 직접 구현해야 하는 불편함이 있습니다. 저는 Provider를 이용하면서 이와 같은 경우에만 MVVM 규칙을 어기고 사용했습니다. 

class _View extends StatelessWidget {
  ...
  
  // 현재 선택한 List 항목을 삭제하고 결과를 경고 창으로 보여줌
  remove(BuildContext context) async {
    final result = await context.listVm.removeChecked(); // 규칙 위반, 뷰에서 뷰모델 데이터가 아닌 함수 호출 결과 사용
    if (!mounted) return;
    if (result) {
      context.showAlert('SUCCESS');
    } else {
      context.showAlert('ERROR');
    }
  }

BLoC로 MVVM 패턴 구현

BLoC는 Provider와 매우 비슷합니다. Provider에서 제공하는 기능에 추가로 몇 가지 기능을 더 제공하기 때문에 마치 Provider 확장판처럼 느껴집니다. 다만 한 가지 큰 차이점이 있습니다. Provider는 '상태가 변경됐다'는 통지만 하는 반면 BLoC은 '미리 정의된 상태'만 통지한다는 점입니다. 아래 코드로 살펴보겠습니다.

// Provider ViewModel
count = 1;
listLoading = true;
notifyListeners(); // 나 자신이 변경됨 
// BLoC CountViewModel
class CountViewModel {
	count = 1
	emit(CountChanged(count)); // count 변경
	...

// BLoC ListViewModel
class ListViewModel {
	emit(ListLoading()); // list가 로딩 중 상태로 변경
	...
 	emit(ListLoaded());  // list 로딩 완료
	...

BLoC은 동시에 두 가지 상태를 가질 수 없습니다.

// BLoC Error - 두 가지 상태를 동시에 가질 수 없음
class ViewModel {
	emit(CountChanged(count), ListLoading()); // 두 가지 상태를 동시에 emit할 수 없음 
	...
	print(state) // 마지막 emit된 상태 하나를 가짐
	...

이런 특성 때문에 자연스럽게 뷰모델은 성격에 따라 여러 개의 클래스로 나뉩니다. 이런 서로 다른 기능은 분리해서 각각 다른 Cubit으로 만듭니다. 아래 그림을 살펴보겠습니다.

Provider와 비교하며 위젯 트리를 살펴보겠습니다.

물론 ProviderCubit처럼 클래스를 분리해 코드를 개발할 수 있지만, Provider는 자율성이 있는 반면 BLoC은 강제로 클래스를 나눠야 합니다. 강제 분리는 객체 지향의 캡슐화를 구현해 주긴 하지만 종종 이런 분리가 더 불편할 때가 있는데요. 예를 들어 Cubit 간 데이터 공유가 필요한 경우에는 해야 할 일이 많아집니다. MVVM 패턴에서는 뷰모델 간 참조를 금지하고 대신 상위 레이어인 뷰 혹은 하위 레이어에서 통신할 것을 권장합니다. BLoC도 같은 규칙을 사용합니다. BLoC의 Repository는 이와 같은 경우에 사용합니다.

아래는 BLoC을 사용한 코드입니다.

// ==============================================
// 서비스
// ==============================================
 
// 서비스 객체, 특별한 규칙이 필요하지 않음
class LocalService {
  ...
 
 
// 위젯 트리 중간에 RepositoryProvider를 이용해 서비스 주입
// 서비스는 BuildContext의 의존성 주입 룰 따름
class MyApp extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return RepositoryProvider(
      create: (context) => LocalService(),
      child: HomePage(),
      ...
 
 
// 아래와 같이 호출해 서비스 사용, BuildContext 내에서만 접근 가능
LocalService local = RepositoryProvider.of(context);
...
 
 
 
// ==============================================
// 뷰모델
// ==============================================
 
// 뷰모델은 Cubit을 상속받아야 함
class ClickCountCubit extends Cubit {
  addClickCount() {
    final count = state.count + 1;
    emit(ClickCountChanged(count)); // 변경 내역을 뷰에 통보합니다.
  }
  ...
 
 
// Cubit이 발생하는 이벤트는 상태로 정의
class ClickCountChanged {
  int count;
  ...
 
 
// 위젯 트리 중간에 BlocProvider를 이용해 Cubit 주입
// Cubit은 BuildContext의 의존성 주입 룰 따름
class HomePage extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider(create: (context) => ListCubit()),
        BlocProvider(create: (context) => ClickCountCubit()),
      ],
      child: ListView(),
      ...
 
 
// 아래와 같이 호출해 Cubit 사용, BuildContext 내에서만 접근 가능
final CountCubit cubit = BlocProvider.of(context);
...
 
 
 
// ==============================================
// 뷰
// ==============================================
 
// Cubit 상태는 BlocBuilder를 이용해 리슨(listen)
class ListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...
        BlocBuilder<ClickCountCubit, ClickCountState>(builder: (context, state) {
            return Text('CLICK COUNT : ${state.clickCount}');
        }),
        ...
 
 
// Provider가 지원하지 않는 일회성 이벤트 리슨도 지원
class ListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
      return BlocListener(listener: (context, state) {
        if (state is LoadingError) {
          showAlert('ERROR');
        }
    })

결론

세 가지 라이브러리는 각각 장단점이 뚜렷합니다.

GetX Provider BLoC
화면에서 원하는 부분만 업데이트 하는 것이 간편한가? O
(Rx, Obx를 이용해 가장 편리하게 지원)
BuildContext를 전달하는 번거로움이 없는가? O
(전역 변수 사용하듯 편하게 접근 가능)
X X
라이브러리 사용법을 배우기 쉬운가? O X
뷰모델 작성이 간편한가? O O X
(상태 그룹별로 Cubit을 만들어야 하고, Cubit 간 데이터를 공유하는 경우에는 추가로 Repository 클래스 생성 필요)
BuildContext를 이용한 의존성 주입을 사용할 수 있는가? X
(위험 요소)
O O
뷰모델에서 일회성 이벤트(경고창 생성 같은 이벤트)를 발행할 수 있는가? X X O

GetX는 생산성이 가장 높지만 BuildContext를 사용하지 않는 코드 전개는 예기치 못한 곳에서 문제를 일으킬 가능성이 있습니다. BLoC는 Cubit 간 독립성이 강제로 보장된다는 점이 좋지만 작성해야 하는 코드가 너무 많고 러닝커브가 높습니다.

세 라이브러리에 대한 제 평가는 다음과 같이 요약할 수 있습니다.

  • 보통의 경우라면 Provider가 무난합니다.
  • 많은 인원이 참여하는 복잡한 기능의 앱을 개발한다면 BLoC을 추천합니다.
  • GetX의 생산성은 놀랍지만 저는 추천하지 않습니다. BuildContext를 사용한 의존성 주입은 Flutter의 주요 작동 원리 중 하나이기 때문입니다.

이번에 진행한 프로젝트는 복잡하지 않은 소규모 프로젝트였기 때문에 최종적으로 Provider를 선택했습니다. 하지만 아키텍처에 정답은 없습니다. 각자의 팀 상황과 프로젝트에 따라 다양한 선택이 가능하니 각 장단점을 잘 비교하고 적절한 선택을 내리시길 바라겠습니다. 제 글이 선택의 순간에 조금이나마 도움이 되기를 바라며 글을 마치겠습니다. 

참고 자료