본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 30. · 20 Views
Riverpod 3.0 Mutation으로 폼 제출 처리 완벽 가이드
Riverpod 3.0에서 새롭게 도입된 Mutation을 활용하여 폼 제출을 처리하는 방법을 알아봅니다. 로딩 상태, 에러 핸들링, 재시도 로직까지 실무에서 바로 적용할 수 있는 패턴을 배웁니다.
목차
1. Mutation이란
김개발 씨는 회원가입 폼을 만들고 있었습니다. 버튼을 누르면 서버에 데이터를 보내고, 성공하면 다음 화면으로 이동하고, 실패하면 에러를 보여줘야 합니다.
"로딩 상태는 어떻게 관리하지? isLoading 변수를 따로 만들어야 하나?" 고민하던 중, 선배가 다가와 말했습니다.
"Riverpod 3.0의 Mutation을 써봐. 이런 상황에 딱이야."
Mutation은 Riverpod 3.0에서 새롭게 도입된 기능으로, 서버에 데이터를 변경하는 작업을 깔끔하게 처리하는 도구입니다. 마치 은행 창구에서 입금이나 출금 같은 거래 요청을 처리하는 것과 같습니다.
Query가 데이터를 읽어오는 작업이라면, Mutation은 데이터를 변경하는 작업을 담당합니다. 폼 제출, 좋아요 누르기, 장바구니 추가 등 사용자의 액션으로 서버 상태를 바꾸는 모든 작업에 활용할 수 있습니다.
다음 코드를 살펴봅시다.
// Mutation Provider 정의
@riverpod
class SignUp extends _$SignUp {
@override
FutureOr<void> build() {
// 초기 상태는 비어있음
}
// 실제 회원가입 로직을 수행하는 메서드
Future<void> submit(String email, String password) async {
// mutate가 로딩/성공/에러 상태를 자동 관리
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(authRepositoryProvider).signUp(email, password);
});
}
}
김개발 씨는 입사 6개월 차 Flutter 개발자입니다. 오늘 팀장님이 회원가입 기능 개발을 맡겼습니다.
"간단하지 뭐"라고 생각했지만, 막상 코드를 작성하려니 고민이 생겼습니다. 버튼을 누르면 로딩 스피너를 보여주고, 성공하면 홈 화면으로, 실패하면 에러 메시지를 띄워야 합니다.
상태 관리가 생각보다 복잡해 보였습니다. 선배 개발자 박시니어 씨가 김개발 씨의 화면을 힐끗 보더니 말했습니다.
"아직도 isLoading 변수 따로 만들어서 쓰고 있어? Riverpod 3.0에 Mutation이라는 게 생겼는데, 이걸 쓰면 훨씬 편해." 그렇다면 Mutation이란 정확히 무엇일까요?
쉽게 비유하자면, Mutation은 마치 은행 창구의 거래 처리 시스템과 같습니다. 고객이 입금을 요청하면, 창구 직원이 요청을 접수하고, 처리 중임을 알려주고, 완료되면 결과를 알려줍니다.
만약 문제가 생기면 왜 실패했는지도 설명해줍니다. Mutation도 똑같이 사용자의 요청을 받아서 처리하고, 그 과정의 모든 상태를 자동으로 관리해줍니다.
Riverpod에서 데이터를 다루는 방식은 크게 두 가지로 나눌 수 있습니다. 하나는 Query로, 서버에서 데이터를 읽어오는 작업입니다.
다른 하나는 Mutation으로, 서버의 데이터를 변경하는 작업입니다. 마치 데이터베이스의 SELECT와 INSERT/UPDATE/DELETE의 관계와 같습니다.
예전에는 이런 작업을 어떻게 처리했을까요? 개발자들은 isLoading, error, isSuccess 같은 변수들을 각각 만들어서 관리해야 했습니다.
버튼을 누르면 isLoading을 true로 바꾸고, API 호출이 끝나면 false로 바꾸고, 에러가 나면 error 변수에 저장하고... 코드가 복잡해지기 쉬웠습니다.
Mutation을 사용하면 이 모든 상태가 자동으로 관리됩니다. 개발자는 비즈니스 로직에만 집중하면 됩니다.
"회원가입 API를 호출한다"라는 핵심 로직만 작성하면, 로딩 상태, 성공 상태, 에러 상태는 Mutation이 알아서 처리합니다. 위 코드를 살펴보면, @riverpod 어노테이션으로 Provider를 정의하고 있습니다.
build 메서드는 초기 상태를 정의하는데, Mutation은 처음에는 아무 작업도 하지 않으므로 비어있습니다. 핵심은 submit 메서드입니다.
이 메서드가 실제 회원가입 로직을 수행합니다. AsyncValue.guard는 try-catch를 우아하게 처리해주는 도우미입니다.
내부에서 예외가 발생하면 자동으로 AsyncError 상태가 되고, 성공하면 AsyncData 상태가 됩니다. 개발자가 직접 try-catch를 작성하지 않아도 됩니다.
실제 현업에서 Mutation은 정말 다양한 곳에서 활용됩니다. 회원가입, 로그인, 게시글 작성, 댓글 달기, 좋아요 누르기, 장바구니에 상품 추가하기 등 사용자의 액션으로 서버 데이터가 바뀌는 모든 곳에서 사용할 수 있습니다.
김개발 씨는 고개를 끄덕였습니다. "아, Query는 데이터를 가져오는 거고, Mutation은 데이터를 바꾸는 거군요.
그리고 Mutation이 로딩이나 에러 상태를 자동으로 관리해주니까 코드가 훨씬 깔끔해지겠네요!"
실전 팁
💡 - Mutation은 데이터를 변경하는 작업에만 사용하세요. 데이터를 읽어오는 작업은 일반 Provider나 FutureProvider를 사용합니다.
- AsyncValue.guard를 사용하면 try-catch 없이도 에러를 우아하게 처리할 수 있습니다.
2. MutationState 상태 속성들
김개발 씨가 Mutation을 적용하고 보니, 상태에 따라 UI를 다르게 보여줘야 했습니다. "로딩 중일 때는 스피너를, 에러일 때는 에러 메시지를, 성공하면 완료 표시를..." 어떻게 각 상태를 구분할 수 있을까요?
박시니어 씨가 말했습니다. "AsyncValue에는 상태를 확인할 수 있는 여러 속성들이 있어.
하나씩 알아보자."
AsyncValue는 비동기 작업의 상태를 나타내는 클래스로, isLoading, hasValue, hasError 등의 속성을 제공합니다. 마치 배송 조회 시스템에서 "배송 준비 중", "배송 중", "배송 완료", "배송 실패"를 보여주는 것과 같습니다.
이 속성들을 활용하면 현재 Mutation이 어떤 단계에 있는지 정확히 파악하고, 그에 맞는 UI를 보여줄 수 있습니다.
다음 코드를 살펴봅시다.
// AsyncValue의 주요 상태 속성들
final signUpState = ref.watch(signUpProvider);
// 로딩 중인지 확인
if (signUpState.isLoading) {
return CircularProgressIndicator();
}
// 에러가 있는지 확인
if (signUpState.hasError) {
return Text('에러: ${signUpState.error}');
}
// 값이 있는지 확인 (성공)
if (signUpState.hasValue) {
return Text('성공!');
}
// when을 사용한 패턴 매칭
return signUpState.when(
data: (_) => Text('완료'),
loading: () => CircularProgressIndicator(),
error: (e, st) => Text('에러: $e'),
);
김개발 씨는 Mutation을 적용했지만, 아직 UI 처리가 남아있었습니다. 버튼을 눌렀을 때 로딩 스피너를 보여주고, 성공하면 "가입 완료" 메시지를, 실패하면 에러 메시지를 보여줘야 합니다.
상태가 세 가지나 되는데, 어떻게 구분해야 할까요? 박시니어 씨가 설명을 시작했습니다.
"Riverpod의 AsyncValue는 비동기 작업의 모든 상태를 담고 있어. 마치 택배 조회 시스템처럼 현재 어떤 단계인지 알려주지." AsyncValue는 세 가지 주요 상태를 가집니다.
첫 번째는 AsyncLoading으로, 작업이 진행 중임을 나타냅니다. 두 번째는 AsyncData로, 작업이 성공적으로 완료되었음을 나타냅니다.
세 번째는 AsyncError로, 작업 중 에러가 발생했음을 나타냅니다. 이 상태들을 확인하는 방법은 여러 가지가 있습니다.
가장 직관적인 방법은 속성을 사용하는 것입니다. isLoading은 현재 로딩 중인지, hasError는 에러가 있는지, hasValue는 값이 있는지 알려줍니다.
여기서 주의할 점이 있습니다. isLoading과 hasValue가 동시에 true일 수 있습니다.
예를 들어, 이전에 성공한 데이터가 있는 상태에서 새로운 요청이 진행 중이라면, 둘 다 true가 됩니다. 이런 특성을 활용하면 로딩 중에도 이전 데이터를 보여주는 UX를 구현할 수 있습니다.
더 우아한 방법은 when 메서드를 사용하는 것입니다. when은 패턴 매칭을 제공하여, 각 상태에 맞는 위젯을 반환할 수 있게 해줍니다.
data, loading, error 세 가지 케이스를 모두 처리해야 하므로, 실수로 어떤 상태를 빠뜨리는 일이 없습니다. maybeWhen이라는 변형도 있습니다.
이건 특정 상태만 처리하고 나머지는 orElse로 기본값을 반환할 때 사용합니다. 모든 케이스를 처리할 필요가 없을 때 편리합니다.
실무에서는 상황에 따라 적절한 방법을 선택하면 됩니다. 단순히 로딩 여부만 확인할 때는 isLoading 속성을, 모든 상태에 대해 다른 UI를 보여줄 때는 when 메서드를 사용하는 것이 일반적입니다.
김개발 씨가 물었습니다. "그럼 Mutation이 아직 한 번도 실행되지 않은 초기 상태는 어떻게 확인해요?" 좋은 질문입니다.
초기 상태에서는 hasValue가 false이고 isLoading도 false입니다. 이 조합으로 "아직 시작하지 않은 상태"를 구분할 수 있습니다.
박시니어 씨가 정리해주었습니다. "정리하면, isLoading으로 로딩 중인지, hasError로 실패했는지, hasValue로 성공했는지 확인할 수 있어.
그리고 when을 쓰면 세 가지 상태를 빠짐없이 처리할 수 있지."
실전 팁
💡 - when 메서드는 모든 상태를 처리하도록 강제하므로, 상태 누락 실수를 방지할 수 있습니다.
- 초기 상태(idle)는 !isLoading && !hasValue && !hasError로 확인할 수 있습니다.
- isLoading과 hasValue가 동시에 true일 수 있다는 점을 기억하세요.
3. 폼 제출 예제
이론은 충분히 배웠습니다. 이제 김개발 씨는 실제 회원가입 폼을 만들어볼 차례입니다.
이메일과 비밀번호를 입력받고, 가입 버튼을 누르면 서버에 요청을 보내는 전체 플로우를 구현해야 합니다. "자, 이제 진짜로 만들어보자." 김개발 씨는 키보드에 손을 올렸습니다.
실제 폼 제출은 TextEditingController로 입력값을 관리하고, Mutation으로 제출 로직을 처리하는 구조로 만듭니다. 마치 종이 서류를 작성하고 접수 창구에 제출하는 것과 같습니다.
사용자가 폼을 작성하면 Controller가 값을 보관하고, 제출 버튼을 누르면 Mutation이 서버로 전송합니다. 이 패턴을 익히면 어떤 종류의 폼이든 동일한 방식으로 처리할 수 있습니다.
다음 코드를 살펴봅시다.
class SignUpScreen extends ConsumerStatefulWidget {
@override
ConsumerState<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends ConsumerState<SignUpScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
void _onSubmit() {
final email = _emailController.text;
final password = _passwordController.text;
// Mutation 실행
ref.read(signUpProvider.notifier).submit(email, password);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(controller: _emailController),
TextField(controller: _passwordController, obscureText: true),
ElevatedButton(onPressed: _onSubmit, child: Text('가입하기')),
],
);
}
}
김개발 씨는 드디어 실제 코드를 작성하기 시작했습니다. 먼저 화면 구조부터 잡아야 합니다.
이메일 입력 필드, 비밀번호 입력 필드, 그리고 가입 버튼이 필요합니다. Flutter에서 폼을 만들 때는 TextEditingController를 사용합니다.
이 컨트롤러는 TextField에 입력된 값을 저장하고 관리해줍니다. 마치 종이 서류의 각 칸에 적힌 내용을 기억하는 비서와 같습니다.
여기서 중요한 점은 ConsumerStatefulWidget을 사용한다는 것입니다. 일반 StatefulWidget과 달리, ConsumerStatefulWidget은 ref 객체에 접근할 수 있습니다.
ref를 통해 Provider를 읽거나 감시할 수 있습니다. _onSubmit 메서드를 보면, 컨트롤러에서 텍스트를 꺼내 Mutation의 submit 메서드에 전달하고 있습니다.
ref.read를 사용한 것에 주목하세요. 이벤트 핸들러에서는 watch가 아닌 read를 사용합니다.
watch는 build 메서드 안에서만 사용해야 합니다. signUpProvider.notifier는 Provider의 Notifier 객체를 가져옵니다.
Notifier 객체에는 우리가 정의한 submit 메서드가 있습니다. 이 메서드를 호출하면 회원가입 프로세스가 시작됩니다.
버튼의 onPressed에 _onSubmit을 연결하면 됩니다. 사용자가 버튼을 누르면 _onSubmit이 호출되고, submit 메서드가 실행되어 서버에 회원가입 요청을 보냅니다.
하지만 아직 부족합니다. 지금 상태로는 버튼을 눌러도 로딩 표시가 없고, 성공이나 실패에 대한 피드백도 없습니다.
사용자는 자신의 요청이 처리되고 있는지 알 수 없습니다. 박시니어 씨가 말했습니다.
"기본 구조는 잘 잡았어. 이제 다음 단계에서 로딩 상태랑 에러 처리를 추가해보자." 김개발 씨는 코드를 저장하고 다음 단계를 준비했습니다.
기본 구조가 완성되었으니, 이제 사용자 경험을 개선할 차례입니다.
실전 팁
💡 - 이벤트 핸들러에서는 ref.read를, build 메서드에서는 ref.watch를 사용하세요.
- TextEditingController는 dispose에서 반드시 정리해주세요. 메모리 누수를 방지합니다.
- ConsumerStatefulWidget 대신 HookConsumerWidget을 사용하면 더 간결한 코드를 작성할 수 있습니다.
4. 버튼 로딩 상태 처리
김개발 씨가 만든 폼을 테스트해보니 문제가 있었습니다. 가입 버튼을 눌러도 아무런 반응이 없어 보였습니다.
서버 응답이 올 때까지 2-3초가 걸리는데, 그 동안 사용자는 버튼이 눌린 건지 아닌 건지 알 수 없었습니다. "이러면 사용자가 버튼을 여러 번 누를 수도 있겠네요." UX 개선이 필요한 순간입니다.
로딩 상태 처리는 사용자 경험의 핵심입니다. Mutation의 isLoading 속성을 활용하면, 요청이 진행 중일 때 버튼에 로딩 스피너를 보여주거나 버튼을 비활성화할 수 있습니다.
마치 엘리베이터 버튼을 누르면 불이 들어오고, 다시 누를 수 없게 되는 것과 같습니다. 이렇게 하면 사용자는 자신의 요청이 처리되고 있음을 알 수 있고, 중복 요청도 방지할 수 있습니다.
다음 코드를 살펴봅시다.
@override
Widget build(BuildContext context) {
// Mutation 상태를 감시
final signUpState = ref.watch(signUpProvider);
final isLoading = signUpState.isLoading;
return Column(
children: [
TextField(controller: _emailController, enabled: !isLoading),
TextField(controller: _passwordController, enabled: !isLoading),
ElevatedButton(
// 로딩 중에는 버튼 비활성화
onPressed: isLoading ? null : _onSubmit,
child: isLoading
? SizedBox(
width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text('가입하기'),
),
],
);
}
좋은 앱은 사용자에게 항상 피드백을 제공합니다. 버튼을 눌렀는데 아무 반응이 없으면, 사용자는 불안해집니다.
"눌린 건가? 안 눌린 건가?" 그래서 여러 번 누르게 되고, 그러면 중복 요청이 발생합니다.
이 문제를 해결하는 방법은 간단합니다. Mutation의 isLoading 상태를 활용하면 됩니다.
요청이 진행 중일 때는 버튼에 로딩 스피너를 보여주고, 버튼을 비활성화합니다. 코드를 살펴보면, **ref.watch(signUpProvider)**로 Mutation의 상태를 감시하고 있습니다.
상태가 바뀔 때마다 build 메서드가 다시 호출되어 UI가 업데이트됩니다. isLoading 속성으로 현재 로딩 중인지 확인합니다.
버튼의 onPressed를 보세요. 삼항 연산자를 사용해서, 로딩 중이면 null을, 아니면 _onSubmit을 전달합니다.
Flutter에서 onPressed에 null을 전달하면 버튼이 비활성화됩니다. 회색으로 변하고 눌러도 반응하지 않습니다.
버튼의 child도 상태에 따라 달라집니다. 로딩 중이면 CircularProgressIndicator를, 아니면 텍스트를 보여줍니다.
스피너의 크기를 SizedBox로 제한한 것에 주목하세요. 기본 크기가 너무 크기 때문에 버튼 안에 들어가도록 조절한 것입니다.
TextField도 로딩 중에는 비활성화했습니다. enabled: !isLoading으로 설정하면, 요청이 진행되는 동안 사용자가 입력값을 바꾸지 못합니다.
이건 선택사항이지만, 일관된 UX를 위해 권장됩니다. 박시니어 씨가 덧붙였습니다.
"버튼 비활성화는 정말 중요해. 결제 같은 중요한 작업에서 중복 요청이 발생하면 큰 문제가 생길 수 있거든." 김개발 씨가 고개를 끄덕였습니다.
"맞아요. 예전에 결제 버튼을 여러 번 눌러서 카드가 여러 번 결제된 사고를 들은 적 있어요.
이렇게 처리하면 그런 문제를 방지할 수 있겠네요." 이제 사용자는 버튼을 누르면 스피너가 돌아가는 걸 보고, 자신의 요청이 처리되고 있음을 알 수 있습니다. 그리고 로딩 중에는 버튼을 다시 누를 수 없으니 중복 요청도 걱정 없습니다.
실전 팁
💡 - 버튼뿐 아니라 TextField도 로딩 중에 비활성화하면 더 일관된 UX를 제공할 수 있습니다.
- CircularProgressIndicator의 크기는 SizedBox로 제한하고, strokeWidth를 조절하면 더 예쁜 스피너를 만들 수 있습니다.
5. 에러 핸들링과 재시도
테스트 중에 김개발 씨가 일부러 잘못된 이메일을 입력해봤습니다. 서버에서 에러가 돌아왔는데, 화면에는 아무 표시도 없었습니다.
"사용자가 뭐가 잘못됐는지 모르면 답답하겠죠?" 에러 처리도 중요한 사용자 경험입니다. 에러 메시지를 보여주고, 다시 시도할 수 있는 기능도 추가해야 합니다.
에러 핸들링은 실패를 우아하게 처리하는 기술입니다. Mutation의 hasError와 error 속성을 활용하면 에러 발생 여부와 에러 내용을 확인할 수 있습니다.
마치 ATM에서 거래 실패 시 "잔액이 부족합니다"라고 알려주는 것과 같습니다. 사용자에게 무엇이 잘못되었는지 알려주고, 다시 시도할 기회를 주는 것이 좋은 UX입니다.
다음 코드를 살펴봅시다.
@override
Widget build(BuildContext context) {
final signUpState = ref.watch(signUpProvider);
// 에러 발생 시 SnackBar로 알림
ref.listen(signUpProvider, (prev, next) {
if (next.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('에러: ${next.error}'),
action: SnackBarAction(
label: '다시 시도',
onPressed: _onSubmit,
),
),
);
}
// 성공 시 다음 화면으로 이동
if (next.hasValue && prev?.isLoading == true) {
Navigator.pushReplacementNamed(context, '/home');
}
});
return /* 기존 UI 코드 */;
}
에러는 언제든 발생할 수 있습니다. 네트워크가 불안정하거나, 서버에 문제가 있거나, 사용자가 잘못된 정보를 입력했거나.
중요한 건 에러가 발생했을 때 어떻게 대응하느냐입니다. 좋은 에러 처리의 핵심은 세 가지입니다.
첫째, 사용자에게 무엇이 잘못되었는지 알려줍니다. 둘째, 어떻게 해결할 수 있는지 안내합니다.
셋째, 쉽게 다시 시도할 수 있게 합니다. 여기서 ref.listen이 등장합니다.
ref.watch와 다르게, ref.listen은 상태가 변할 때 콜백 함수를 실행합니다. UI를 다시 그리지 않고, 특정 액션을 수행할 때 사용합니다.
SnackBar 표시나 화면 이동 같은 사이드 이펙트에 적합합니다. 코드를 보면, ref.listen 안에서 next.hasError를 확인합니다.
에러가 발생하면 SnackBar를 띄워 사용자에게 알립니다. next.error에는 에러 객체가 담겨 있어서, 에러 메시지를 보여줄 수 있습니다.
SnackBarAction으로 "다시 시도" 버튼을 추가했습니다. 사용자가 이 버튼을 누르면 _onSubmit이 다시 호출되어 요청을 재시도합니다.
입력값은 그대로 유지되니까, 사용자가 다시 입력할 필요가 없습니다. 성공 처리도 중요합니다.
next.hasValue가 true이고, 이전 상태가 로딩 중이었다면 요청이 성공한 것입니다. 이때 Navigator로 다음 화면으로 이동합니다.
pushReplacementNamed를 사용하면 현재 화면을 스택에서 제거하고 새 화면으로 교체합니다. 뒤로 가기를 눌러도 회원가입 화면으로 돌아오지 않습니다.
박시니어 씨가 팁을 알려줬습니다. "에러 메시지는 사용자가 이해할 수 있어야 해.
'Error 500'이라고 보여주면 일반 사용자는 뭔지 모르거든. '서버 오류가 발생했습니다.
잠시 후 다시 시도해주세요.' 이런 식으로 바꿔서 보여주는 게 좋아." 실무에서는 에러 타입에 따라 다른 메시지를 보여주는 경우가 많습니다. 네트워크 에러면 "인터넷 연결을 확인해주세요", 인증 에러면 "이메일 또는 비밀번호를 확인해주세요" 같은 식으로요.
김개발 씨는 이제 에러 처리의 중요성을 깨달았습니다. 잘 작동하는 것도 중요하지만, 잘못됐을 때 우아하게 처리하는 것도 그만큼 중요합니다.
실전 팁
💡 - ref.listen은 SnackBar, Dialog, Navigation 같은 사이드 이펙트에 사용합니다. UI 렌더링에는 ref.watch를 사용하세요.
- 에러 메시지는 기술적인 내용보다 사용자가 이해할 수 있는 언어로 변환해서 보여주세요.
- 재시도 가능한 에러(네트워크 오류 등)와 재시도해도 안 되는 에러(잘못된 입력 등)를 구분해서 처리하면 더 좋습니다.
6. Mutation vs AsyncNotifier 비교
김개발 씨가 Mutation을 배우고 나니 궁금증이 생겼습니다. "그런데 선배, 예전에 AsyncNotifier로도 비슷한 거 했잖아요?
뭐가 다른 거예요?" 박시니어 씨가 빙긋 웃었습니다. "좋은 질문이야.
둘 다 비동기 작업을 처리하지만, 목적과 사용법이 조금 달라. 언제 뭘 써야 하는지 알아보자."
Mutation과 AsyncNotifier는 모두 비동기 작업을 처리하지만, 용도가 다릅니다. AsyncNotifier는 데이터를 조회하고 캐싱하는 데 최적화되어 있습니다.
반면 Mutation은 데이터를 변경하는 일회성 작업에 적합합니다. 마치 도서관에서 책을 검색하는 것(AsyncNotifier)과 책을 대출/반납하는 것(Mutation)의 차이와 같습니다.
상황에 맞는 도구를 선택하면 더 깔끔한 코드를 작성할 수 있습니다.
다음 코드를 살펴봅시다.
// AsyncNotifier - 데이터 조회에 적합
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async {
// 초기화 시 자동으로 데이터 로드
return await ref.read(userRepositoryProvider).getProfile();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(userRepositoryProvider).getProfile());
}
}
// Mutation - 데이터 변경에 적합
@riverpod
class UpdateProfile extends _$UpdateProfile {
@override
FutureOr<void> build() {} // 초기에는 아무것도 안 함
Future<void> submit(User user) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(userRepositoryProvider).updateProfile(user));
}
}
Riverpod을 사용하다 보면 비슷해 보이는 여러 도구들을 만나게 됩니다. AsyncNotifier, FutureProvider, 그리고 이제 Mutation까지.
언제 무엇을 써야 할지 헷갈릴 수 있습니다. 핵심적인 차이를 이해하면 선택이 쉬워집니다.
AsyncNotifier와 FutureProvider는 데이터를 조회하는 데 최적화되어 있습니다. Provider가 처음 읽힐 때 자동으로 데이터를 가져오고, 그 결과를 캐싱합니다.
같은 데이터를 여러 위젯에서 사용해도 API 호출은 한 번만 일어납니다. 반면 Mutation은 데이터를 변경하는 작업에 적합합니다.
초기에는 아무 작업도 하지 않다가, 사용자가 명시적으로 메서드를 호출할 때 비로소 동작합니다. 폼 제출, 좋아요 누르기, 삭제하기 같은 액션에 사용합니다.
코드를 비교해보면 차이가 명확합니다. UserProfile(AsyncNotifier)의 build 메서드는 **Future<User>**를 반환합니다.
Provider가 처음 사용될 때 자동으로 호출되어 사용자 정보를 가져옵니다. 반면 UpdateProfile(Mutation)의 build 메서드는 void입니다.
아무것도 하지 않습니다. 대신 submit 메서드를 따로 정의해서, 사용자가 버튼을 눌렀을 때만 실행되도록 합니다.
실무에서 이 구분을 적용해보겠습니다. 사용자 프로필 화면을 만든다고 가정해봅시다.
화면이 열릴 때 프로필 정보를 조회하는 건 AsyncNotifier로 처리합니다. 프로필 수정 버튼을 눌러 정보를 업데이트하는 건 Mutation으로 처리합니다.
박시니어 씨가 정리해줬습니다. "쉽게 말해서, 자동으로 데이터를 가져와야 하면 AsyncNotifier, 사용자 액션에 반응해서 뭔가를 바꿔야 하면 Mutation이야." 또 다른 차이점도 있습니다.
AsyncNotifier는 상태를 유지합니다. 사용자 정보를 한 번 가져오면 앱이 종료될 때까지 캐싱됩니다.
Mutation은 일회성입니다. 작업이 끝나면 상태는 초기화되어도 상관없습니다.
김개발 씨가 이해했습니다. "그러니까 읽기는 AsyncNotifier, 쓰기는 Mutation이군요!" 박시니어 씨가 고개를 끄덕였습니다.
"정확해. CRUD에서 R은 AsyncNotifier, CUD는 Mutation이라고 생각하면 돼." 이제 김개발 씨는 상황에 맞는 도구를 선택할 수 있게 되었습니다.
적절한 도구를 사용하면 코드가 더 명확해지고, 유지보수도 쉬워집니다.
실전 팁
💡 - CRUD 관점에서 생각하세요. Read는 AsyncNotifier, Create/Update/Delete는 Mutation입니다.
- Mutation 후에 관련 데이터를 갱신해야 할 때는 ref.invalidate를 사용해서 AsyncNotifier를 새로고침하세요.
- 복잡한 상태 관리가 필요하면 StateNotifier나 Notifier를 고려하세요. Mutation은 단순한 비동기 작업에 최적화되어 있습니다.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
LangGraph Fault Tolerance 장애 복구 완벽 가이드
LangGraph 애플리케이션의 장애 복구 메커니즘을 실무 중심으로 배웁니다. Durable Execution부터 Graph Migrations까지 체크포인트 기반 복구 시스템을 스토리텔링으로 쉽게 이해할 수 있습니다.
LangGraph Time Travel 완벽 가이드
LangGraph의 Time Travel 기능으로 특정 시점으로 돌아가고, 상태를 포크하여 분기하고, 대안 경로를 탐색하는 방법을 배웁니다. 실무에서 디버깅과 실험에 활용하는 실전 가이드입니다.
LangGraph Persistence 완벽 가이드
LangGraph의 Persistence 기능을 활용하여 대화 상태를 저장하고 관리하는 방법을 배웁니다. Thread와 Checkpoint를 이해하고, 상태 조회 및 수정 방법을 실무 중심으로 학습합니다.
Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드
Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.
Riverpod 3.0 Retry 자동 재시도 완벽 가이드
Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.