상태 관리는 모든 Flutter 애플리케이션의 핵심입니다. 앱이 복잡해짐에 따라 상태를 효과적으로 관리하는 것은 유지 보수성, 테스트 용이성 및 성능에 매우 중요해집니다. Riverpod는 강력하고 인기 있는 컴파일 안전 상태 관리 라이브러리로 부상했으며, Provider와 같은 이전 솔루션에 비해 더 유연하고 강력한 대안을 제공합니다. 개발자가 애플리케이션 상태를 효율적으로 관리하고 코드 재사용성을 높이며 전반적인 앱 성능을 크게 향상시키는 데 도움이 됩니다.
1. Riverpod 이해하기: 핵심 개념
Riverpod는 Provider 패키지의 작성자인 Remi Rousselet이 Provider의 몇 가지 본질적인 한계를 해결하고 보다 현대적이고 컴파일 안전하며 테스트 가능한 접근 방식을 제공하기 위해 만들었습니다.
1.1. 왜 Riverpod인가? Provider의 한계 해결
Provider는 유능한 도구이지만 Riverpod가 해결하고자 하는 특정 단점이 있습니다.
- 런타임 오류: Provider는 종종 위젯 트리 조회에 의존하므로 프로바이더를 찾을 수 없거나 잘못된 유형인 경우 런타임 오류가 발생할 수 있습니다. Riverpod는 컴파일 안전하며 이러한 문제 중 상당수를 컴파일 타임에 포착합니다.
- 위젯 트리 종속성: Provider에서 프로바이더에 액세스하는 것은
BuildContext
에 연결되어 있어 위젯 트리 외부(예: 서비스 또는 유틸리티 클래스)에서 상태에 액세스하기가 더 어렵습니다. Riverpod는 상태를 위젯 트리에서 분리합니다. - 리빌드 세분화: Provider는 리빌드를 최적화하는 방법(예:
Selector
또는context.select
)을 제공하지만 Riverpod는 처음부터 더 세분화된 리빌드를 위해 설계되어 기본적으로 더 나은 성능을 제공하는 경우가 많습니다. 특정 상태 조각을 명시적으로 수신 대기하는 위젯만 다시 빌드합니다. - 테스트 용이성: Provider 기반 로직 테스트는 위젯 트리에 의존하기 때문에 때때로 번거로울 수 있습니다. Riverpod의 설계를 통해 프로바이더와 해당 로직을 격리하여 더 쉽게 테스트할 수 있습니다.
- 유연성: Riverpod는 더 복잡한 상태 관리 시나리오에 맞춰 더 다양한 프로바이더 유형과 수정자를 제공합니다.
1.2. Riverpod의 "프로바이더" 개념
Riverpod의 핵심에는 프로바이더라는 개념이 있습니다. 프로바이더는 상태 조각을 캡슐화하고 애플리케이션의 다른 부분이 해당 상태를 수신 대기할 수 있도록 하는 객체입니다. 프로바이더는 다음을 수행할 수 있습니다.
- 값 노출: 단순한 값, 복잡한 객체 또는 비동기 작업의 결과일 수 있습니다.
- 수신 대기 가능: 위젯 또는 다른 프로바이더가 프로바이더를 "감시"하여 상태 변경에 반응할 수 있습니다.
- 불변성: 프로바이더 자체는 불변입니다. 제공하는 상태는 변경 가능할 수 있지만(예:
StateNotifier
사용) 프로바이더 선언 자체는 상수입니다.
이러한 선언적 접근 방식은 상태 관리를 단순화하고 코드 재사용성을 향상시키며(프로바이더를 위젯 트리를 통해 전달하지 않고 전역적으로 액세스할 수 있으므로) 테스트 용이성을 개선합니다.
1.3. 상태 읽기: Consumer, ConsumerWidget 및 WidgetRef
Riverpod는 위젯이 프로바이더와 상호 작용하는 여러 가지 방법을 제공합니다.
ConsumerWidget
:build
메서드에서WidgetRef
를 제공하는 상태 비저장 위젯입니다.WidgetRef
는 프로바이더를 읽고 상호 작용하는 데 사용됩니다.ConsumerStatefulWidget
및ConsumerState
: 상태 저장 위젯의 경우ref
를 통해WidgetRef
에 액세스할 수 있습니다.Consumer
위젯: 위젯 트리의 어느 곳에나 배치하여 프로바이더를 수신 대기하고 전체 상위 위젯을 다시 빌드하지 않고 UI의 일부를 다시 빌드할 수 있는 위젯입니다.WidgetRef
:ConsumerWidget
의build
메서드에 전달되거나(또는ConsumerState
에서ref
로 사용 가능) 다음을 수행할 수 있는 객체입니다.ref.watch(myProvider)
: 프로바이더를 수신 대기합니다. 프로바이더의 상태가 변경되면 위젯이 다시 빌드됩니다.ref.read(myProvider)
: 변경 사항을 수신 대기하지 않고 프로바이더의 현재 상태를 한 번 읽습니다. 버튼 콜백과 같은 일회성 작업에 유용합니다.ref.listen(myProvider, (previous, next) { ... })
: 위젯을 다시 빌드하지 않고 부작용(예: 대화 상자 표시, 탐색)을 위해 프로바이더를 수신 대기합니다.
이러한 메커니즘은 특정 상태 조각이 변경될 때 필요한 위젯만 다시 빌드되도록 하여 성능을 향상시킵니다.
1.4. 자동 상태 폐기: .autoDispose
수정자
Riverpod는 프로바이더를 위한 강력한 .autoDispose
수정자를 제공합니다. .autoDispose
로 표시된 프로바이더가 더 이상 수신 대기되지 않으면(즉, 위젯이나 다른 프로바이더가 "감시"하지 않는 경우) 해당 상태가 자동으로 폐기됩니다. 이는 다음과 같은 경우에 매우 유용합니다.
- 메모리 누수 방지: 프로바이더와 관련된 리소스(예: 네트워크 연결 또는 타이머)가 더 이상 필요하지 않을 때 정리되도록 합니다.
- 상태 재설정: 사용자가 화면에서 벗어났다가 다시 돌아오면 자동 폐기 프로바이더가 자동으로 상태를 초기 값으로 재설정할 수 있으며, 이는 종종 화면별 상태에 대해 원하는 동작입니다.
2. Riverpod 모범 사례
Riverpod를 사용할 때 모범 사례를 따르면 코드 품질을 크게 향상시키고 앱 성능을 높이며 버그 발생 가능성을 줄일 수 있습니다.
- 프로바이더 범위 적절하게 지정하기:
- 프로바이더는 전역적으로 액세스할 수 있지만 상태가 실제로 필요한 위치를 고려하십시오.
- 화면별 상태의 경우 화면이 더 이상 표시되지 않을 때 상태가 정리되도록
.autoDispose
사용을 고려하십시오. - 더 로컬하게 관리할 수 있는 경우 변경 가능한 상태를 전역적으로 과도하게 노출하지 마십시오.
- UI 리빌드에는
ref.watch
, 작업에는ref.read
사용하기:ConsumerWidget
또는ConsumerStatefulWidget
의build
메서드에서 상태 변경을 수신 대기하고 리빌드를 트리거하려면ref.watch(myProvider)
를 사용하십시오.- 이벤트 핸들러(예:
onPressed
콜백)에서 작업을 트리거하거나 리빌드를 유발하지 않고 상태를 읽으려면ref.read(myProvider.notifier)
(StateNotifierProvider
의 경우) 또는ref.read(myProvider)
를 사용하십시오.
- 불변 상태 선호하기:
StateNotifier
를 사용할 때 상태 클래스가 불변인지 확인하십시오(예:final
필드 및copyWith
메서드 사용). 이렇게 하면 상태 변경을 더 예측 가능하게 만들고 디버깅하기 쉽게 만듭니다.- Riverpod는 불변 상태 클래스를 생성하기 위한
freezed
와 같은 패키지와 잘 작동합니다.
- 올바른 프로바이더 유형 선택하기:
Provider
: 변경되지 않는 단순한 읽기 전용 값 또는 서비스용.StateProvider
: UI에서 변경할 수 있는 단순한 변경 가능한 상태(예: 부울 플래그 또는 카운터)용. 종종 로컬 위젯 상태에 적합합니다.StateNotifierProvider
: 비즈니스 로직을 포함하는 더 복잡한 변경 가능한 상태용. 사용자 정의StateNotifier
클래스와 함께 사용합니다. 이는 매우 일반적이고 권장되는 선택입니다.FutureProvider
: 단일 값을 반환하는 비동기 작업(예: API에서 데이터 가져오기) 관리용.StreamProvider
: 시간이 지남에 따라 여러 값을 내보내는 비동기 작업(예: Firebase 스트림 수신 대기) 관리용.
.autoDispose
적극적으로 활용하기:- 더 이상 사용되지 않을 때 재설정하거나 정리해야 하는 상태(예: 특정 화면에 연결된 상태)의 경우 항상 프로바이더에
.autoDispose
수정자를 추가하십시오. 이렇게 하면 메모리 누수를 방지하고 필요할 때 새로운 상태를 보장하는 데 도움이 됩니다. - 예:
final myDataProvider = FutureProvider.autoDispose
((ref) async { ... });
- 더 이상 사용되지 않을 때 재설정하거나 정리해야 하는 상태(예: 특정 화면에 연결된 상태)의 경우 항상 프로바이더에
- UI 로직과 비즈니스 로직 분리하기:
- 위젯이 상태에 따라 UI를 렌더링하는 데 집중하도록 하십시오.
- 비즈니스 로직을
StateNotifier
클래스 또는 프로바이더에서 노출하는 전용 서비스 클래스 내에 캡슐화하십시오.
- 부작용에
ref.listen
활용하기:- UI 리빌드를 직접 유발하지 않지만 상태 변경에 반응해야 하는 작업(예: SnackBar 표시, 다른 화면으로 이동, 로깅)에는
ref.listen
을 사용하십시오.
- UI 리빌드를 직접 유발하지 않지만 상태 변경에 반응해야 하는 작업(예: SnackBar 표시, 다른 화면으로 이동, 로깅)에는
- 최신 정보 유지하기:
- Riverpod는 활발하게 유지 관리됩니다. 공식 문서 및 커뮤니티 리소스(예: Riverpod Discord 서버 또는 GitHub 토론)를 정기적으로 확인하여 최신 정보, 패턴 및 모범 사례를 확인하십시오.
3. 실전 예제를 통한 Riverpod 적용
Riverpod의 실제 동작을 보여주기 위해 간단한 Flutter 앱을 만들어 보겠습니다. 이 앱은 사용자 이름을 입력받아 개인화된 환영 메시지를 표시합니다.
3.1. Riverpod 의존성 추가
먼저 pubspec.yaml
파일에 flutter_riverpod
를 추가합니다.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1 # 최신 버전 사용
dev_dependencies:
flutter_test:
sdk: flutter
터미널에서 flutter pub get
을 실행합니다.
3.2. Riverpod 초기화: ProviderScope
main.dart
파일에서 루트 위젯(일반적으로 MyApp
)을 ProviderScope
로 래핑합니다. 이 위젯은 모든 프로바이더의 상태를 저장합니다.
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:your_app_name/home_screen.dart'; // HomeScreen이 있다고 가정
void main() {
runApp(
// ProviderScope는 전체 애플리케이션에 대해 Riverpod를 활성화합니다.
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod 데모',
home: HomeScreen(), // 기본 화면
);
}
}
3.3. 이름에 대한 StateNotifier 및 Provider 생성
사용자 이름(변경 가능한 문자열)을 관리하기 위해 StateNotifier
를 사용합니다. StateNotifierProvider
가 이 StateNotifier
를 노출합니다.
// name_state.dart (새 파일 생성, 예: lib/providers/)
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. StateNotifier 클래스 정의
class NameNotifier extends StateNotifier {
// 상태 초기화 (예: 빈 문자열로)
NameNotifier() : super('');
// 이름 업데이트 메서드
void updateName(String newName) {
state = newName;
}
void clearName() {
state = '';
}
}
// 2. StateNotifierProvider 생성
// 프로바이더가 더 이상 수신 대기되지 않을 때 이름을 지우기 위해 .autoDispose 사용
final nameProvider = StateNotifierProvider.autoDispose((ref) {
return NameNotifier();
});
3.4. 이름 입력 및 표시를 위한 위젯 생성
이제 입력을 위한 TextField
와 환영 메시지를 표시하기 위한 Text
위젯을 포함하는 HomeScreen
을 만들어 보겠습니다. 프로바이더에 액세스하기 위해 ConsumerWidget
을 사용합니다.
// home_screen.dart (새 파일 생성, 예: lib/screens/)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:your_app_name/providers/name_state.dart'; // 프로바이더 가져오기
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// nameProvider를 감시하여 현재 이름을 가져오고 변경 시 다시 빌드
final String currentName = ref.watch(nameProvider);
// 메서드(예: updateName)를 호출하기 위해 notifier 가져오기
final NameNotifier nameNotifier = ref.read(nameProvider.notifier);
final TextEditingController controller = TextEditingController(text: currentName);
// 텍스트가 미리 채워져 있으면 커서가 끝에 있는지 확인
controller.selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
return Scaffold(
appBar: AppBar(
title: const Text('Riverpod 이름 앱'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: controller,
decoration: const InputDecoration(
labelText: '이름을 입력하세요',
border: OutlineInputBorder(),
),
onChanged: (newName) {
// notifier의 메서드를 사용하여 상태 업데이트
nameNotifier.updateName(newName);
},
),
const SizedBox(height: 20),
// nameProvider의 변경 사항에 반응하여 환영 메시지 표시
if (currentName.isNotEmpty)
Text(
'안녕하세요, $currentName님!',
style: Theme.of(context).textTheme.headlineMedium,
)
else
Text(
'이름을 입력해주세요.',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
nameNotifier.clearName();
controller.clear(); // TextField도 지우기
},
child: const Text('이름 지우기'),
)
],
),
),
);
}
}
이 예제에서:
HomeScreen
은ConsumerWidget
입니다.ref.watch(nameProvider)
는 이름 상태를 수신 대기합니다.updateName
이 호출되면nameProvider
는 수신자에게 알리고 이 위젯은 새 이름을 표시하기 위해 다시 빌드됩니다.ref.read(nameProvider.notifier)
는NameNotifier
의 인스턴스를 가져오므로TextField
의onChanged
콜백에서updateName
메서드를 호출할 수 있습니다. 여기서는ref.read
를 사용합니다. 이름이 변경될 때TextField
자체가 다시 빌드되는 것을 원하지 않기 때문입니다(이름을 표시하는Text
위젯만 다시 빌드되어야 함).
4. Riverpod를 이용한 Flutter 개발의 장점
Flutter 프로젝트에서 상태 관리에 Riverpod를 채택하면 다음과 같은 수많은 이점이 있습니다.
- 컴파일 안전성: 컴파일 타임에 프로바이더 관련 문제를 포착하여 런타임 오류를 줄여 더 강력한 애플리케이션을 만들 수 있습니다.
- 분리된 상태: 상태가 위젯 트리나
BuildContext
에 연결되지 않아 애플리케이션의 어느 곳에서나(서비스, 리포지토리 등) 액세스할 수 있으므로 테스트 용이성과 아키텍처 유연성이 크게 향상됩니다. - 성능 향상:
- 기본적으로 Riverpod는 세분화된 리빌드를 권장합니다. 프로바이더를 명시적으로 "감시"하는 위젯만 상태가 변경될 때 다시 빌드됩니다.
.autoDispose
수정자는 더 이상 사용되지 않을 때 상태를 자동으로 정리하여 메모리 누수를 방지하고 장기적인 성능과 안정성에 기여합니다.
- 테스트 용이성 향상: 프로바이더 및 관련 로직(예:
StateNotifier
)은 위젯 트리나 복잡한 모의 작업 없이 격리하여 쉽게 테스트할 수 있습니다. - 유연성 및 확장성: 단순한 로컬 상태에서 복잡한 전역 애플리케이션 상태에 이르기까지 다양한 상태 관리 시나리오를 처리하기 위해 풍부한 프로바이더 유형(
Provider
,StateProvider
,StateNotifierProvider
,FutureProvider
,StreamProvider
)과 수정자(.family
,.autoDispose
)를 제공합니다. - 단순화된 상태 액세스:
WidgetRef
객체는 프로바이더와 상호 작용하기 위한 명확하고 일관된 API(watch
,read
,listen
)를 제공합니다. - InheritedWidget 상용구 없음:
InheritedWidget
을 수동으로 사용하거나 더 고급 사용 사례에 대한 Provider 설정의 복잡성과 관련된 많은 상용구를 제거합니다. - 활발한 커뮤니티 및 개발: Riverpod는 우수한 문서와 지원 커뮤니티를 통해 잘 유지 관리되므로 지속적인 개선과 즉각적인 도움을 받을 수 있습니다.
이러한 이점을 활용함으로써 개발자는 더 확장 가능하고 유지 관리 가능하며 성능이 뛰어난 Flutter 애플리케이션을 더 큰 자신감을 갖고 구축할 수 있습니다.