시리즈

[Flutter] Riverpod 은 좋은 상태관리 라이브러리 인가?

20221123
app state
architecture
dart
flutter
getx
provider
riverpod
state
state management

리버팟 (Riverpod) 은 좋은 상태관리 라이브러리 일까?

‘좋다’ 라는 것을 말하려면 기준 비교 대상이 있어야 한다. 비교 대상은 여기에 나와 있는 플러그인들이 있다. 개인적으로 인지도가 있다고 판단한 것들을 중심으로 설명하겠다.

그 전에 잠깐, 상태관리의 정의를 하고 넘어가야 한다.


상태관리란?

상태 라는 것은 데이터를 말한다. 조금 더 구체적으로는 앱에서 관리해야 하는 데이터를 의미한다.

그러니 상태관리라는 것은 앱에서 관리해야 할 데이터를 관리하는 행위이다.

여기까지만 들으면 조금 말장난으로 들릴 지도 모른다. 조금 더 깊게 들어가보자.

플러터에 한정해서 설명하자면, 앱을 구성하는 모든 것(거의 대부분)은 위젯이고, 모든 위젯은 상태를 가지고 있다.

StatelessWidget 또한 State가 변하지 않는 것이지, State를 가지고 있다. (좀 어렵게는 immutable state 를 가진다고 말한다.)

즉, 위젯과 상태는 1:1로 단단하게 결합되어 있는 것이다.

이러면 상태의 접근 범위(scope) 는 확실하게 상태를 가진 위젯으로 한정되는 장점이 있지만, 바꿔 말하면 하나의 상태를 여러 위젯에서 공유하기는 어렵다는 단점이 된다.

상태 관리는 이 문제에서 시작된다. 플러터 상태관리의 목적은 하나의 상태를 하나의 위젯 보다 더 큰 범위에서 공유하자는 것이다.

그러려면 위젯과 상태의 결합도를 떨어뜨려야 한다.

이제 여러 상태관리 플러그인들을 살펴보자.


Provider

제일 기본이 되는 Provider 의 경우를 보자. Provider의 많은 프로바이더 위젯들은 결국 위젯이다.

Provider의 Metadata

정확히는 InheritedWidget (이하 IW) 을 조금 사용하기 편하게 해둔 위젯들이다. IW 은 플러터 SDK 에 들어있는 위젯이라, 플러터 팀에서는 Provider를 매우 권장하고 있다. Provider의 본질을 이해하려면 IW 을 이해하면 된다.

inheritedWidget

이 위젯이 하는 일은 간단하다. 위젯 트리 상에서 자기의 자식 위젯들에게 상태를 전파해준다.

자식 위젯에서 IW 이 전파한 상태에 접근하려면 BuildContext 를 통해서 접근해야 한다. 주의할 점은, 같은 타입의 상태를 가진 InheritedWidget 부모가 여러 개 있다면 가장 가까운 조상 IW의 상태에만 접근할 수 있다.

상태 접근 범위 : 위젯 트리에서 자식 노드들.

Provider 는 이해하기 쉽고, 간단하고, 플러터 팀에서 안정성을 보장해주는 좋은 상태관리 방법이다.

BLoC

BloC은 BloC 패턴을 빠르게 구현하기 위해서 만들어진 플러그인이다. BLoC 은 provider로 만들어지고 있기 때문에, 상태 관리 측면에서 보면 provider 와 다를 것은 없다.

BLoC 패턴과 아키텍처는 이 글의 범위를 많이 벗어나기 때문에 다루지 않겠다.

BLoC 은 좋은 아키텍처 패턴을 빠르게 적용할 수 있는 상태 관리 방법이다.


Get_It

get_it 은 꼭 짚고 넘어가야 한다. 왜냐하면 BuildContext를 사용하지 않는 상태 관리를 하기 때문이다. provider나 bloc 처럼 BuildContext 가 필요한 것들은 context 에 의존하는 문제가 있다.

context는 위젯에서만 접근 가능하기 때문에, 위젯 외부에서 상태에 접근하려면 반드시 Context로 부터 독립해서 위젯과 상태의 결합도를 떨어뜨려야 한다.

위젯에서는 BuildContext 에 쉽게 접근할 수 있어서 별 문제가 없겠지만, 위젯 외부에 있는 비즈니스 로직에서 상태에 접근하려면 BuildContext 를 매개 변수로 넘겨주거나 해야하는데, 이것은 성능 측면에서도 좋지 않고 비동기 연산의 경우에는 에러가 발생하기 쉬우며, 보기에도 좋지 않다.

위젯에서 DB 나 비즈니스 로직 같은 위젯 밖에 있는 레이어에 접근할 때 사용하기에 딱 좋다.

접근은 잘 되는데 상태의 변화를 리슨하는 것은 get_it 만으로는 좀 어려울 수 있다.

이런 것을 위해서는 이 글에 나와있는 것 처럼 ValueListenableBuilder 를 이용해서 상태관리를 할 수 있다.

class FavoriteButton extends StatelessWidget {
const FavoriteButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final playPage = getIt<PlayPageManager>();
return ValueListenableBuilder<bool>(
valueListenable: playPage.favoriteNotifier,
builder: (context, value, child) {
return IconButton(
icon: Icon(
(value)
? Icons.favorite
: Icons.favorite_border,
),
onPressed: playPage.onFavoritePressed,
);
},
);
}
}

상태 접근 범위 : (등록을 했다면) 앱 전체

get_it 은 context에 의존하지 않는 좋은 상태관리이다. 상태 변화를 리슨하는 것은 조금 약하다.

Get

get_it과 이름이 비슷한 Get (또는 GetX) 은 전체 플러터 플러그인 중 좋아요를 가장 많이 받은 슈퍼스타다. Get은 정말 많은 기능을 가지고 있다. 여기서는 상태관리만 다루어 보자.

get의 상태관리를 완벽히 이해하는 것은 쉽지 않다. 왜냐하면 코드가 너무 비대해서 관리 안된채로 주석도 없이 방치된 코드가 많고, 공식 문서 조차 장황하다. get의 나쁜 점을 모두 다루기에는 이 글 전체 보다 더 많은 종이가 필요하니 여기서는 따로 언급하지 않겠다.

최대한 플러그인 문서 를 보면서 코드를 이해하려고 해보자.

문서에 따르면, 크게 2가지 방식이 있다. Reactive한 방식과, Simple 한 방식. 이 2방식은 혼용할 수는 있지만 서로 시너지를 내지는 않는다.

Get의 코드

위 그림에서 get_rx와 get_state_manager 디렉토리가 상태관리와 관련 있는 부분이다.

혹시 Get을 사용하고 있다면 들어가서 코드를 한 번 보기 바란다. 별 것 없다. StatefulWidget, InheritedWidget, rxdart, get_it 에 있는 코드와 같다. rx_flutter 부분은 rxdart의 하위호환이고, simple 부분은 get_it의 하위호환이다. 한 번 코드들을 비교하면서 살펴보길 바란다. (get_responsive.dart 는 상태관리하고 전혀 관련없는 코드인데 왜 들어가 있는지 의문이다.)

“와, 이렇게 많은 플러그인을 한꺼번에 사용할 수 있으니 좋다” 라고 생각했다면 매우 반성해야 한다. 쓰지도 않는 것들을 그냥 다 여러분이 만드는 앱에 같이 빌드하게 되는데 안 그래도 네이티브 보다 큰 플러터 앱의 용량이 더 커지게 된다. 빌드할 때 이걸 필요한 부분만 선택적으로 취할 수가 없다. 부분적으로 채용할 수 있는 좋은 플러그인들이 있는 데 왜 이걸 한꺼번에 다 사야하나?

Get이 간단하고 쉽다고 하는데, get_it을 ValueListenableBuilder 와 쓰는 것이 훨씬 간단하고 직관적이다.

웃긴 점은 Get에서 상태 관리는 그나마 괜찮은 부분이라는 것.


드디어 Riverpod

final counterProvider = StateNotifierProvider((ref) {
return Counter();
});

class Counter extends StateNotifier<int> {
Counter(): super(0);

void increment() => state++;
}

class Example extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text(count.toString());
}
}

get을 굳이 여기서 언급한 이유는 인기가 많아서 이기도 하지만, Riverpod의 탄생에 직간접적인 영향을 줬기 때문이다. get_it 도 BuildContext에 의존하지 않지만 like 수가 그리 많지 않았는데, get의 인기를 보고서는 Provider로는 뭔가 부족하다고 느꼈을 것이다.

Riverpod은 위에 있는 모든 플러그인의 단점을 극복하려고 애썼다.

Provider 의 단점인 BuildContext 의존 => BuildContext 없이 Ref 를 사용해서 상태 접근 (WidgetRef 의 경우는 BuildContext 를 내장하고 있다.)

Provider 의 단점인 가장 가까운 조상만 접근 가능 => 위젯 트리와 관계 없이 접근

get_it의 단점인 상태 변화 리슨 => ref.watch() 등을 이용한 상태 리슨

추가적인 장점

  • 컴파일 할 때 타입에러를 잡아낸다.
  • 위젯, 플러터 SDK에 의존성이 없어서 테스트하기 쉽다.
  • provider 를 private 하게 선언해서 접근 범위를 제한할 수 있다.
  • 전역 변수를 사용하는 것처럼 쉽게 상태에 접근할 수 있다.
  • 프로바이더에서 다른 프로바이더에 접근할 수 있다.
  • Scope, override 를 통한 명확한 접근 범위 지정 가능.
  • 코드 자동 생성 (2버전 이후)
  • 앞으로 Provider 는 riverpod 에 흡수될 것이다.

단점

  • 클래스가 아닌 top-level 에서 전역변수 스타일로 선언하다보니 OOP 에 익숙하다면 어색할 수 있다.
  • 아키텍처를 제대로 생각하지 않으면 코드 관리가 매우 어렵고, 접근 제한 또한 쉽지 않다. BloC을 쓸 때 처럼 프로바이더가 하나의 비즈니스 로직 레이어 그 자체라고 생각하면 안되고, 어떤 레이어에 접근하기 위한 방법으로 생각해야 한다. 마치 Get_it 을 쓸 때 처럼.

# riverpod은 좋은 상태관리 플러그인이다. 하지만 초보자에게는 추천하지 않는다. 강력하지만, 쉽지는 않다.

# 처음이라면 Get_it을 먼저 사용해보자.

.

piano (press key Q)

Categories

flutter ( 82 ) dart ( 34 ) android ( 32 ) kotlin ( 11 ) plugin ( 8 ) provider ( 8 ) vim ( 7 ) bloc ( 6 ) iOS ( 6 ) state management ( 6 ) 플러터 ( 6 ) PS ( 5 ) algorithm ( 5 ) architecture ( 5 ) async ( 5 ) getx ( 5 ) java ( 5 ) API ( 4 ) BOJ ( 4 ) class ( 4 ) daily ( 4 ) git ( 4 ) golang ( 4 ) memo ( 4 ) riverpod ( 4 ) state ( 4 ) stream ( 4 ) test ( 4 ) web ( 4 ) widget ( 4 ) windows ( 4 ) HTTP ( 3 ) androidX ( 3 ) app state ( 3 ) context ( 3 ) crash ( 3 ) db ( 3 ) editor ( 3 ) error ( 3 ) extension ( 3 ) github ( 3 ) hive ( 3 ) ide ( 3 ) package ( 3 ) pubspec ( 3 ) python ( 3 ) syntax ( 3 ) vscode ( 3 ) app icon ( 2 ) await ( 2 ) chocolatey ( 2 ) consumer ( 2 ) cp949 ( 2 ) deployment ( 2 ) dev ( 2 ) flavor ( 2 ) gesture ( 2 ) globalkey ( 2 ) go ( 2 ) google ( 2 ) hack ( 2 ) js ( 2 ) json ( 2 ) key ( 2 ) keystore ( 2 ) list ( 2 ) listview ( 2 ) lock ( 2 ) mac ( 2 ) map ( 2 ) navigation ( 2 ) nosql ( 2 ) project ( 2 ) pub ( 2 ) recyclerview ( 2 ) rxdart ( 2 ) sdk ( 2 ) selector ( 2 ) setting ( 2 ) size ( 2 ) soc ( 2 ) synchronized ( 2 ) tdd ( 2 ) tip ( 2 ) version ( 2 ) viewmodel ( 2 ) vundle ( 2 ) webview ( 2 ) xcode ( 2 ) yaml ( 2 ) ( 2 ) 플러터 단점 ( 2 ) 16.0 ( 1 ) 2.0 ( 1 ) 2023 ( 1 ) AATP2 ( 1 ) ChangeNotifierProvider ( 1 ) Example ( 1 ) Guava ( 1 ) ImageReader ( 1 ) Mo's algorithm ( 1 ) OAuth2 ( 1 ) OpenGL ( 1 ) Oreo ( 1 ) ProgressBar ( 1 ) REST API ( 1 ) Trie ( 1 ) activity ( 1 ) adaptive ( 1 ) android P ( 1 ) android context ( 1 ) android11 ( 1 ) apktool2 ( 1 ) app exit ( 1 ) append ( 1 ) appicon ( 1 ) arkit ( 1 ) array ( 1 ) asciidoc ( 1 ) async * ( 1 ) async* ( 1 ) audio ( 1 ) authorization ( 1 ) await for ( 1 ) behaviorsubject ( 1 ) beta ( 1 ) binary ( 1 ) binarysearch ( 1 ) blender ( 1 ) book ( 1 ) bottomsheet ( 1 ) break ( 1 ) broadcast ( 1 ) browser ( 1 ) bubbles ( 1 ) bug ( 1 ) build ( 1 ) buildcontext ( 1 ) buildnumber ( 1 ) bundle ( 1 ) button ( 1 ) bytecode ( 1 ) cache ( 1 ) camera2 ( 1 ) cameramanager ( 1 ) cd ( 1 ) chrome ( 1 ) ci ( 1 ) circle ( 1 ) clean ( 1 ) clean architecture ( 1 ) cli ( 1 ) clip ( 1 ) clipboard ( 1 ) cloud ide ( 1 ) cmdlet ( 1 ) code ( 1 ) coding test ( 1 ) command ( 1 ) comparator ( 1 ) complexity ( 1 ) concurrency ( 1 ) conditional ( 1 ) const ( 1 ) constraint ( 1 ) constraintlayout ( 1 ) controlc ( 1 ) controlv ( 1 ) converter ( 1 ) copy ( 1 ) copy project ( 1 ) coupling ( 1 ) coverage ( 1 ) cp ( 1 ) css ( 1 ) cupertino ( 1 ) cursor ( 1 ) cv ( 1 ) data class ( 1 ) data structure ( 1 ) dataBinding ( 1 ) database ( 1 ) debounce ( 1 ) decompile ( 1 ) delegate ( 1 ) deno ( 1 ) design pattern ( 1 ) development ( 1 ) device ( 1 ) di ( 1 ) dialog ( 1 ) dio ( 1 ) drawable ( 1 ) drug ( 1 ) emmet ( 1 ) encoding ( 1 ) english ( 1 ) entries ( 1 ) environment ( 1 ) equality ( 1 ) equatable ( 1 ) euc-kr ( 1 ) euckr ( 1 ) exit ( 1 ) expand ( 1 ) expanded ( 1 ) export ( 1 ) extension method ( 1 ) facade ( 1 ) fake ( 1 ) field ( 1 ) figma ( 1 ) final ( 1 ) fixed ( 1 ) flutter pub ( 1 ) flutter web ( 1 ) flutter_inappwebview ( 1 ) flutter_test ( 1 ) flutterflow ( 1 ) fold ( 1 ) fonts ( 1 ) form ( 1 ) frame ( 1 ) future ( 1 ) gestureDetector ( 1 ) gestureRecognizer ( 1 ) gesturearena ( 1 ) get-command ( 1 ) get_cli ( 1 ) getbuilder ( 1 ) getx단점 ( 1 ) gitignore ( 1 ) glut ( 1 ) google fonts ( 1 ) gopath ( 1 ) goto ( 1 ) gradient ( 1 ) graphics ( 1 ) gvim ( 1 ) hackaton ( 1 ) hash ( 1 ) hashmap ( 1 ) hot reload ( 1 ) how to ( 1 ) html ( 1 ) i18n ( 1 ) icon ( 1 ) id ( 1 ) impeller ( 1 ) implementation ( 1 ) import ( 1 ) indicator ( 1 ) inkwell ( 1 ) interrupt ( 1 ) intl ( 1 ) introduction ( 1 ) io ( 1 ) isar ( 1 ) iterable ( 1 ) iteration ( 1 ) javascript ( 1 ) julia ( 1 ) juno ( 1 ) jupyter ( 1 ) kakaomap ( 1 ) keytool ( 1 ) korean ( 1 ) kotlin syntax ( 1 ) l10n ( 1 ) lambda ( 1 ) language ( 1 ) layer ( 1 ) layout ( 1 ) lineageOS ( 1 ) localkey ( 1 ) localtoglobal ( 1 ) long list ( 1 ) ls ( 1 ) mac osx ( 1 ) markdown ( 1 ) markup ( 1 ) material ( 1 ) method ( 1 ) microtask ( 1 ) migrate ( 1 ) mintlify ( 1 ) mock ( 1 ) module ( 1 ) monitor ( 1 ) moor ( 1 ) mouse ( 1 ) mouseregion ( 1 ) multiplatform ( 1 ) multiset ( 1 ) multithread ( 1 ) mutable ( 1 ) mvvm ( 1 ) new ( 1 ) node ( 1 ) nodejs ( 1 ) nosuchmethod ( 1 ) null-safety ( 1 ) numberformat ( 1 ) nvim ( 1 ) object ( 1 ) objectbox ( 1 ) objectkey ( 1 ) obx ( 1 ) online ide ( 1 ) operator ( 1 ) orientation ( 1 ) parabeac ( 1 ) parse ( 1 ) paste ( 1 ) path ( 1 ) pattern ( 1 ) pitfall ( 1 ) play store ( 1 ) pod ( 1 ) podfile ( 1 ) pointer ( 1 ) pointers ( 1 ) powershell ( 1 ) private ( 1 ) programming ( 1 ) pull to refresh ( 1 ) puzzle ( 1 ) pycharm ( 1 ) realitykit ( 1 ) recursion ( 1 ) reduce ( 1 ) reference ( 1 ) regex ( 1 ) regular expression ( 1 ) release note ( 1 ) renderbox ( 1 ) renderobject ( 1 ) repl ( 1 ) repository ( 1 ) response ( 1 ) rm ( 1 ) rotue ( 1 ) round ( 1 ) run ( 1 ) scope ( 1 ) scroll ( 1 ) search ( 1 ) server ( 1 ) serverless ( 1 ) service ( 1 ) sharp ( 1 ) singlerepo ( 1 ) singleton ( 1 ) sketch ( 1 ) sliver ( 1 ) sliverlist ( 1 ) snippets ( 1 ) sogae ( 1 ) sorting ( 1 ) source ( 1 ) sparse ( 1 ) sparse array ( 1 ) spec ( 1 ) split ( 1 ) sqflite ( 1 ) sqlite ( 1 ) sqrt decomposition ( 1 ) stateful ( 1 ) statefulwidget ( 1 ) step ( 1 ) stepper ( 1 ) string ( 1 ) stringbuffer ( 1 ) stringbuilder ( 1 ) studio ( 1 ) study ( 1 ) sub-directory ( 1 ) svn ( 1 ) swiftui ( 1 ) swipe to refresh ( 1 ) system_alert_window ( 1 ) system_cache ( 1 ) systemnavigator ( 1 ) tail recursion ( 1 ) tailrec ( 1 ) tap test ( 1 ) text ( 1 ) texteditingcontroller ( 1 ) textfield ( 1 ) texttheme ( 1 ) themedata ( 1 ) then ( 1 ) thread ( 1 ) throttle ( 1 ) time ( 1 ) tool ( 1 ) tools ( 1 ) tooltip ( 1 ) ts ( 1 ) tutorial ( 1 ) typescript ( 1 ) ui ( 1 ) unittest ( 1 ) update ( 1 ) usb ( 1 ) utf8 ( 1 ) ux ( 1 ) valuekey ( 1 ) variable ( 1 ) vector ( 1 ) versioncode ( 1 ) very_good ( 1 ) view ( 1 ) vim plugin ( 1 ) vimrc ( 1 ) virtualenv ( 1 ) wasm ( 1 ) web app ( 1 ) webview_flutter ( 1 ) while ( 1 ) widget tree ( 1 ) window ( 1 ) wsl ( 1 ) yield ( 1 ) 강의 ( 1 ) 개발 ( 1 ) 개발 공부 ( 1 ) 공부법 ( 1 ) 그래픽스 ( 1 ) 꼬리재귀 ( 1 ) 꿀팁 ( 1 ) 데노 ( 1 ) 두줄 ( 1 ) 디노 ( 1 ) 번역 ( 1 ) 블록 ( 1 ) 상태관리 ( 1 ) 실험 ( 1 ) 안드로이드 ( 1 ) 안드로이드프로젝트 ( 1 ) 안드로이드프로젝트복사 ( 1 ) 어이없는 ( 1 ) 조건부 임포트 ( 1 ) 주절주절분노조절실패의식으흐름 ( 1 ) 패키지 ( 1 ) 프로젝트복사 ( 1 ) 플러그인 ( 1 )