
안녕하세요, 성조입니다.
이번 포스팅은 상태관리에 대해 공부한 내용들을 작성하는 것을 중점으로 해요.
제가 다음 사이트들에 있는 콘텐츠들보다 중복성 없이 더 잘 정의할 수 있는가에 대해 스스로 질문했을 때 현실적으로 어려움이 크게 있을 것이라 판단되어서 제 블로그 포스팅은 주관적인 생각을 읽고 싶은 분만 보시고, 학습이 목표라면 참조 링크들을 적극적으로 공부하는 것을 권장드려요.
- 추천 자료 링크 및 참조 링크 -
Riverpod
어디서나 공유 상태 선언하기 더 이상 main.dart과 UI 파일 사이를 오갈 필요가 없습니다. 공유 상태의 코드를 별도의 패키지에 넣든, 필요한 위젯 바로 옆에 넣든, 테스트 가능성을 잃지 않고 적절
riverpod.dev
프로그래밍에서 모르는 것이 나왔을 때는 공식 문서가 근본이 된다 생각해요.
한글이 있길래 이렇게 유저가 많았나? 했는데 한글과 영어가 섞여있는 버전이었습니다. 그래도 기능에 대해 잘 정리한 문서인 만큼 확인해 보면 좋겠어요
2. https://techblog.lycorp.co.jp/ko/how-to-make-the-most-of-flutter-riverpod
Flutter Riverpod 200% 활용하기
Flutter에는 다양한 상태 관리 라이브러리가 존재합니다. 대표적으로 Provider와 GetX, BLoC 등이 있는데요. 이전에 Flutter 인기 아키텍처 라이브러리 3종 비...
techblog.lycorp.co.jp
라인에서 작성한 기술 블로그 콘텐츠로 동작 원리와 실무 팁 등을 잘 작성한 자료로 보기 좋은 자료라 생각된다. 버전에 따라 상황에 따라 사용 방법들이 다르게 적용되겠지만, 한글로 친절하게 어떤 것이다 하고 gif로 동작하는 것까지 보여주는 경우는
Riverpod(리버팟)이란?
StateProvider, FutureProvider, StateNotifierProvider와 같은 메서드들 때문에 진입 장벽이 높아지면서 복잡한 상태 관리가 불편함을 전하는 것 때문에 Provider의 창시자 Remi Rousselet가 직접 단점을 개선하기 위해 만든 새로운 상태 관리 라이브러리이다.
'살짝 답답하니 내가 개선한다'라는 마인드로 기존 메서드 암기 후 적용 방식을 대체하기 위해 코드 제너레이션(코드를 자동으로 생성해주는 도구) 방식을 도입하여 다트 코드를 일반 코딩하는 것처럼 '@~무슨 코드'의 형태로 생산성 증가를 도와줄 수 있도록 개선한 것이다.
Provider나 BLOC 등의 상태 관리 라이브러리도 좋지만, 중형급 프로젝트에는 리버팟이 효율성이 가장 좋다. 개발 문서에서는 영문은 'A Reactive Caching and Data-binding Framework' 한글로는 '리액티브 캐싱 및 바인딩 프레임워크'라 정의되어 있다.
조금 더 한글로 풀어 작성하면 캐싱(Caching)은 자주 사용하는 데이터를 고속 메모리에 임시 저장하여 시스템 성능을 향상시키는 기술이고, 바인딩(Binding)은 프로그램의 변수나 UI 요소를 특정 데이터/속성에 연결하는 과정을 의미한다.
Provider의 단점이 뭐였는데?
1) UI 트리 밖에서상태를 읽거나 수정하기 어려운 BuildContext에 대한 의존성 문제가 있다.
2) ProviderNotFoundException과 같이 앱을 실행해 봐야지만 알 수 있는 에러가 존재하여 런타임 에러 발생 가능성이 높아지는 문제가 존재.
3) 두 개의 Provider<String>을 동시에 사용하는 게 어렵고, 같은 타입의 Provider를 중복 사용할 수 없는 문제가 있다.
리버팟은 무엇을 해결했나?
1) BuildContext 없이도 어디서든 안전하게 상태에 접근할 수 있다.
2) 서버 데이터의 최적화.
3) 동일한 타입의 상태를 여러 개 만들어도 충돌하지 않는다. (중복성 관리가 어려울 수 있기에 주의가 필요)
4) 다만, 보일러플레이트(수정 없이 반복적으로 재사용되는 표준화된 코드 블록이나 템플릿들을 의미하는 정의) 코드를 풀어주는 코드 제너레이션(Riverpod Generator)을 공식 지원.
5) 컴파일 타임에 안정성(Compile-time safety)을 보장하여 런타임 에러를 방지하여 안정성을 향상한다.
코드 제너레이션을 왜 사용하는 가?
1) 문법의 단순화 -> FutureProvider, NotifierProvider 등 상황에 맞는 Provider를 고민할 필요 없이 함수나 클래스 위에 @riverpod 만 붙이면 자동으로 생성해 준다.
2) Family의 한계 극복 -> 기존 프로바이더는 하나의 파라미터만 넘기는 게 가능했는데 리버팟을 통해 여러 개의 파라미터를 전달하는데 위치 기반, 네임드 파라미터 등 모두 자유롭게 전달할 수 있게 되는 장점이 있다.
3) AutoDispose 기본 적용 -> 메모리 누수 방지를 위해 상태가 더 이상 사용되지 않으면 자동으로 메모리 해제되는 것이 기본적인 설정이다.
4) 디버깅 향상 -> 생성된 코드가 Provider의 이름을 자동으로 지정해 주어 디버깅이 훨씬 수월해지는 구조이다.
이런 다양한 이유로 리버팟의 코드 제너레이터를 매우 장점으로 보고 사용한다.
기본 세팅 방법
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
// ProviderScope로 앱 전체를 감싸준다.
const ProviderScope(
child: MyApp(),
),
);
}
패키지를 가져오고, 정의만 처리하면 된다.
리버팟의 상태 정의? 그리고, 사용 방법
상태 정의를 진행하려면 코드를 작성한 다음 반드시 flutter pub run build_runner watch -d를 실행하여 코드 자동 생성(. g.dart)을 진행해야 한다.
1) 읽기 전용 데이터(또는 비동기 Fetch)의 함수형
API에서 데이터를 가져오거나, 변하지 않는 설정값을 제공할 때는 일반 함수에 @riverpod을 붙인다. 내부적으로는 FutureProvider와 동일하게 작동한다.
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 코드 제너레이터가 생성할 파일명
part 'user_provider.g.dart';
// @riverpod 어노테이션을 붙이면 자동으로 UserProvider가 생성.
@riverpod
Future<String> fetchUser(FetchUserRef ref, {required int userId}) async {
// 가상의 API 호출
await Future.delayed(const Duration(seconds: 2));
return 'User_$userId';
}
2) 상태 변경이 필요한 비즈니스 로직 - 클래스형
UI에서 버튼을 눌러 값을 바꾸는 등의 로직이 필요하다면 다트 코드로 클래스를 작성하고 riverpod을 사용하면 된다.
다음과 같이 구성하면 NotifierProvider로 작동한다.
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';
@riverpod
class Counter extends _$Counter {
// 초기 상태값을 설정하는 build 메서드 (필수)
@override
int build() {
return 0;
}
// 상태를 변경하는 커스텀 메서드
void increment() {
state++; // state 변수에 접근하여 값을 변경하면 UI가 자동 업데이트됨
}
}
클래스 이름이 Counter라면 반드시 _$Counter를 상속(extends) 받는 구조여야 한다.
3) 상태 유지방법
코드 제너레이션을 사용하면 기본적으로 화면을 벗어날 때 상태가 파괴(autoDispose)가 되는데 탭이 이동되어도 상태나 캐시 데이터를 유지하고 싶다면 KeepAliver를 통해 상태를 유지할 수 있다. @Riverpod(keepAlive: true) 옵션이다.
예를 들면 유저 데이터의 상태가 변경되지 않아야 하는 특정 상태 이거나 사라지면 안 되는 데이터를 들고 있을 때 이 조건이 특정 조건으로 변경됐을 때 해제의 기준이 된다.
@Riverpod(keepAlive: true)
class AuthToken extends _$AuthToken {
@override
String? build() => null;
// ...
}
4) UI에서 상태 읽고 작성하는 방법 - ConsumerWidget
UI에서 상태를 사용할 때는 ConsumerWidget과 WidgetRef를 사용하고 이전과 동일
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'counter_provider.dart'; // 생성된 provider import
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 1. 상태 읽기 (watch - 값이 바뀌면 UI 재빌드)
// 코드 제너레이터가 클래스 이름에 'Provider'를 붙인 객체를 자동 생성 (counterProvider)
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Modern Riverpod')),
body: Center(
child: Text('Count: $count', style: const TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 2. 상태 변경 메서드 호출 (read)
// 클래스 내부에 정의한 메서드를 호출할 때는 .notifier를 붙여 접근.
ref.read(counterProvider.notifier).increment();
},
child: const Icon(Icons.add),
),
);
}
}
* 비동기 데이터 처리 (AsyncValue)
함수형으로 만든 비동기 Provider (fetchUserProvider)를 UI에서 사용할 때는 when 패턴을 사용해 로딩, 에러, 성공 상태를 깔끔하게 분기처리 해준다.
Widget build(BuildContext context, WidgetRef ref) {
// 파라미터를 넘김.
final asyncUser = ref.watch(fetchUserProvider(userId: 1));
return asyncUser.when(
data: (user) => Text('안녕하세요, $user님'),
error: (err, stack) => Text('에러 발생: $err'),
loading: () => const CircularProgressIndicator(),
);
}
* 자주 사용되는 것들을 이어서 소개한다.
5) Provider
final helloWorldProvider = Provider<String>((ref) {
return 'Hello, Riverpod!';
});
기본으로 사용하는 방법으로 변하지 않는 값(Read-only)이나 객체를 제공할 때 사용하고, 주로 의존성 주입(DI), 설정 값, 저장소 (Repository) 인스턴스 등을 제공할 때 사용된다.
6) StateProvider
final counterProvider = StateProvider<int>((ref) => 0);
// 값 변경 시: ref.read(counterProvider.notifier).state++;
가장 단순한 형태의 변경 가능한 상태를 관리하는 목적으로 사용한다.
정수(int), 문자열(String), 불리언(boolean), 열거형(enum) 등 간단한 상태(예시 - UI의 토글 버튼, 카운터 숫자, 선택된 필터 등)에 적합하고, 비즈니스 로직이 포함되지 않은 순수한 상태 관리에 좋은 것이다.
7) NotifierProvider
// 1. Notifier 클래스 정의
class CounterNotifier extends Notifier<int> {
@override
int build() {
return 0; // 초기 상태 값 반환
}
// 상태를 변경하는 비즈니스 로직 (메서드)
void increment() {
state++;
}
}
// 2. Provider 생성
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(() {
return CounterNotifier();
});
비동기 작업(API 호출, DB 읽기 등)의 상태를 관리할 때 필수적으로 사용되며, 데이터가 로딩 중인지(loading), 에러가 났는지(error), 성공적으로 데이터를 가져왔는지(data)를 AsyncValue라는 객체로 안전하게 래핑 하여 제공
Ref 객체 활용 방법 정리 한 스푼
1. ref.watch(provider) UI 렌더링 용도
- 상태의 값을 읽어오고, 해당 상태가 변경될 때마다 위젯을 재빌드(rebuild)한다.
- 반드시 build 메서드 내부에서만 사용해야 하는 것을 주의해야 된다.
2. ref.read(provider) 1회성 읽기의 이벤트 핸들러 용도
- 상태의 값을 단 한 번만 읽어올 때 사용하며, 상태가 변경되어도 재빌드되지 않는다
- 버튼의 onPressed 등 콜백 함수나 이벤트 핸들러 내부에서만 사용해야 한다. watch처럼 build 메서드 내에서 쓰면 안 된다
3. ref.listen(provider) 부수 효과 처리 방법
- 상태가 변경될 때마다 특정 액션을 실행한다. (ex. 스낵바 띄우기나 화면 이동, 모달 띄우기 등)
- build 메서드 안에서 호출하지만 UI 렌더링에 직접 관여하지 않는 것으로 사용.
정리
결론적으로 Riverpod은 Provider가 가졌던 태생적 한계(런타임 에러, 중복 사용 불가, BuildContext 의존성 등)를 완벽히 극복한 가장 모던한 상태 관리 프레임워크에 가깝다고 판단한다.
대형은 BLOC가 유리하다는 얘기가 있어서 해당 상태 관리를 중점으로 더 디테일하게 보고 리버팟 학습을 미뤘다.
초기 세팅이나 build_runner를 주기적으로 실행하여 생성한다는 조금 낯설지만 컴파일 타임의 강력한 안정성과 유연성 덕분에 중대형급의 프로젝트에도 적절하고 생산성이나 코드 컨벤션 등이 매우 유연하고 좋은 방향으로 잘 구성되어 있다 판단되어 유지보수성과 개발 속도를 개선해 줄 수 있는 든든한 상태관리 라이브러리라고 생각하게 됐다.
추후, 추가 프로젝트가 생긴다면 충분히 도전해 볼 만하다 판단함.
감사합니다.
다음 포스팅에서 뵙겠습니다.