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 아키텍처의 데이트 흐름도입니다. 다른 용어와 혼동하지 않도록 이 글에서는 '모델'을 '서비스'로 지칭하겠습니다.
기존 개발 방식에서는 액션이 발생하면 하나의 함수에서 모든 일을 다 처리합니다.
MVVM 패턴을 이용하면 아래와 같이 액션 결과가 데이터(data)로 반영됩니다. 데이터로 결과를 얻으면 코드 작성 목적이 명확해지고 테스트가 쉬워집니다.
MVVM 아키텍처에서는 추가로 아래 규칙도 적용합니다.
라이브러리 비교에 사용할 샘플 앱 소개
라이브러리를 비교하기 위해 앱 개발을 학습할 때 일반적으로 접하는 'To Do' 앱을 하나 만들었습니다. 이 앱은 아래와 같은 기능과 특징이 있습니다.
- 외부에서 데이터를 받아 화면 갱신
- 컴포넌트 간 데이터 공유
- 앱을 사용하는 중간에 언제든지 비동기 요청 발생 가능
- 다른 화면 간 데이터 공유
실제 코드를 작성할 때는 추가해야 하지만, 샘플 코드에서는 이번 글의 목적에 집중하기 위해 아래 내용을 생략했습니다.
- 위젯 생성 시 사용하는
const
키워드 - 위젯 유효성 여부를 검사하는
mounted
조사 StreamSubscription.cancel()
CancelableOperation.cancel()
StatefulWidget으로 MVVM 패턴 구현
먼저 라이브러리를 사용하지 않고 StatefulWidget으로 MVVM 패턴을 구현해 보겠습니다. 클래스는 아래와 같이 구성합니다.
아래 코드는 뷰모델 역할을 하는 ListViewModel 구현부입니다.
아래 코드는 뷰 역할을 하는 ListPage 구현부입니다.
위 코드는 clickCount
만 변경돼도 onUpdated()
가 호출돼 화면 전체가 업데이트되는 단점이 있습니다. 이를 보완한 게 미니멀 빌드로, 데이터가 변경될 때 관련 있는 뷰만 업데이트합니다.
위 코드를 미니멀 빌드가 되도록 수정할 수 있습니다. 하지만 파라미터와 콜백이 많아져서 코드가 복잡해집니다. 이때 GetX를 이용하면 간단히 구현할 수 있습니다.
GetX로 MVVM 패턴 구현
pub.dev에서 가장 많은 '좋아요'를 받은 GetX를 사용해 보겠습니다. 클래스 구성은 StatefulWidget
을 사용했을 때와 거의 동일합니다. 아래 두 가지 내용만 다른데요. 코드 내 주석으로 설명하겠습니다.
- 서비스와 뷰모델 객체를 등록하고 불러오기
Rx
와Obx
를 이용해 관련 있는 위젯만 업데이트하기Rx
: 변수 값이 변경됐을 때 이를 외부에 알려주는 클래스Obx
:Rx
변경이 발생했을 때 화면을 다시 그리는 위젯
Provider와 BLoC은 뷰모델을 얻으려면 항상 BuildContext
를 이용해야 하지만 GetX는 BuildContext
가 없어도 객체를 불러올 수 있습니다.
처음에는 BuildContext
를 알고 있어야 뷰모델을 불러올 수 있다는 점이 불편할 것 같아서 GetX를 선택했지만, 이는 이후 GetX 사용을 포기하게 만든 점이 되기도 했습니다. GetX는 현재 메모리에 존재하는 뷰모델 중 유형이 동일한 인스턴스를 가져다 주는 방식을 사용하는 반면, Provider와 BLoC은 BuildContext
를 이용한 의존성 주입을 사용합니다.
BuildContext
는 Flutter 위젯이 작동하는 주요 개념 중 하나입니다. Flutter와 같은 선언형 UI 방식 개발에서는 화면을 그리기 위해 위젯 트리 최상단에서부터 자식들을 하나씩 그려나가는데요. 각 위젯이 화면에 그려질 때 BuildContext
를 전달받습니다. BuildContext
는 위젯 트리에서 현재 그려야 할 위젯의 위치를 나타냅니다. 위젯을 그릴 때 이 값을 역추적해 부모 노드를 탐색하면 현재 내가 그려야 할 화면 영역이 어디인지와 색상과 폰트 등의 테마 값을 알 수 있고, 화면 내비게이터 정보도 알아낼 수 있습니다. 굳이 위젯의 생성자에 이런 값을 하나하나 다 전달하지 않더라도 BuildContext
를 통해 의존성을 주입받을 수 있습니다.
아래 위젯 트리 그림은 BuildContext
를 이용한 의존성 주입을 어떻게 사용하는지 보다 자세히 설명한 그림입니다.
BuildContext 사용 예시 1. 의존성 주입
BuildContext 사용 예시 2. 생명 주기 관리
BuildContext 사용 예시 3. 의존성 변경
BuildContext
를 통하지 않는 객체 참조 방식은 예기치 못한 곳에서 문제를 일으킬 가능성이 있습니다. 한 예로 GetX 사용 중 아래와 같이 동일한 클래스의 인스턴스를 두 개 이상 등록할 때 문제가 발생했습니다.
Provider로 MVVM 패턴 구현
Provider에서는 GetX와 다르게 Provider
라는 객체를 이용해 BuildContext
의존성 주입을 이용한 뷰모델을 제공합니다. 화면이 구성될 때 위젯 트리는 아래와 같이 구성됩니다.
코드 구현부를 살펴보겠습니다.
화면에서 특정 부분만 업데이트하고 싶다면 Provider
를 나눠 구현하면 됩니다. 예를 들어 아래와 같이 매초마다 새로 그려야 하는 정도로 자주 업데이트하는 위젯을 만든다면 Provider
를 나눠 구현합니다.
Provider는 뷰모델 스트림만 리슨(listen)할 수 있다는 단점이 있으며, 경고창 생성 같은 일회성 이벤트를 지원하지 않아서 직접 구현해야 하는 불편함이 있습니다. 저는 Provider를 이용하면서 이와 같은 경우에만 MVVM 규칙을 어기고 사용했습니다.
BLoC로 MVVM 패턴 구현
BLoC는 Provider와 매우 비슷합니다. Provider에서 제공하는 기능에 추가로 몇 가지 기능을 더 제공하기 때문에 마치 Provider 확장판처럼 느껴집니다. 다만 한 가지 큰 차이점이 있습니다. Provider는 '상태가 변경됐다'는 통지만 하는 반면 BLoC은 '미리 정의된 상태'만 통지한다는 점입니다. 아래 코드로 살펴보겠습니다.
BLoC은 동시에 두 가지 상태를 가질 수 없습니다.
이런 특성 때문에 자연스럽게 뷰모델은 성격에 따라 여러 개의 클래스로 나뉩니다. 이런 서로 다른 기능은 분리해서 각각 다른 Cubit으로 만듭니다. 아래 그림을 살펴보겠습니다.
Provider와 비교하며 위젯 트리를 살펴보겠습니다.
물론 Provider
도 Cubit
처럼 클래스를 분리해 코드를 개발할 수 있지만, Provider는 자율성이 있는 반면 BLoC은 강제로 클래스를 나눠야 합니다. 강제 분리는 객체 지향의 캡슐화를 구현해 주긴 하지만 종종 이런 분리가 더 불편할 때가 있는데요. 예를 들어 Cubit
간 데이터 공유가 필요한 경우에는 해야 할 일이 많아집니다. MVVM 패턴에서는 뷰모델 간 참조를 금지하고 대신 상위 레이어인 뷰 혹은 하위 레이어에서 통신할 것을 권장합니다. BLoC도 같은 규칙을 사용합니다. BLoC의 Repository
는 이와 같은 경우에 사용합니다.
아래는 BLoC을 사용한 코드입니다.
결론
세 가지 라이브러리는 각각 장단점이 뚜렷합니다.
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를 선택했습니다. 하지만 아키텍처에 정답은 없습니다. 각자의 팀 상황과 프로젝트에 따라 다양한 선택이 가능하니 각 장단점을 잘 비교하고 적절한 선택을 내리시길 바라겠습니다. 제 글이 선택의 순간에 조금이나마 도움이 되기를 바라며 글을 마치겠습니다.