🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Riverpod 3.0 Mutation으로 폼 제출 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 11. · 14 Views

Riverpod 3.0 Mutation으로 폼 제출 완벽 가이드

Riverpod 3.0의 새로운 Mutation 기능으로 로그인과 회원가입 폼을 우아하게 처리하는 방법을 배웁니다. 로딩 상태, 에러 처리, 성공 처리까지 실무에서 바로 쓸 수 있는 패턴을 익혀보세요.


목차

  1. Mutation이란? (3.0 신규)
  2. 로그인 폼 Notifier 작성
  3. MutationState 상태 활용
  4. 버튼 로딩 표시
  5. 성공/실패 처리
  6. 회원가입 폼 확장 예제

1. Mutation이란? (3.0 신규)

김개발 씨는 Flutter로 로그인 화면을 만들고 있었습니다. 버튼을 누르면 서버에 요청을 보내고, 로딩 중에는 버튼을 비활성화하고, 성공하면 화면을 전환하고, 실패하면 에러 메시지를 표시해야 했습니다.

"이걸 어떻게 깔끔하게 만들지?" 고민하던 중 선배 박시니어 씨가 말했습니다. "Riverpod 3.0의 Mutation을 써보세요."

Mutation은 Riverpod 3.0에서 새롭게 추가된 기능으로, 폼 제출이나 API 호출 같은 일회성 작업을 처리하는 패턴입니다. 마치 레스토랑에서 주문을 받고 처리하는 것처럼, 사용자의 액션을 받아 비동기 작업을 수행하고 그 결과를 관리합니다.

기존의 AsyncNotifier와 달리 로딩, 에러, 성공 상태를 자동으로 추적해주기 때문에 폼 처리가 훨씬 간결해집니다.

다음 코드를 살펴봅시다.

// LoginNotifier - Mutation 패턴으로 로그인 처리
class LoginNotifier extends AutoDisposeAsyncNotifier<void> {
  @override
  FutureOr<void> build() {
    // 초기 상태는 비어있음
  }

  // 로그인 실행 - 이것이 Mutation!
  Future<void> login(String email, String password) async {
    state = const AsyncLoading(); // 로딩 상태로 변경
    state = await AsyncValue.guard(() async {
      // API 호출
      await ref.read(authRepositoryProvider).login(email, password);
    });
  }
}

final loginProvider = AsyncNotifierProvider.autoDispose<LoginNotifier, void>(
  () => LoginNotifier(),
);

김개발 씨는 입사 6개월 차 Flutter 개발자입니다. 오늘은 회사 앱의 로그인 화면을 만들어야 합니다.

얼핏 간단해 보이지만, 막상 구현하려니 고민거리가 많습니다. 버튼을 누르면 서버에 로그인 요청을 보내야 합니다.

요청 중에는 로딩 인디케이터를 표시해야 하고, 성공하면 홈 화면으로 이동해야 합니다. 실패하면 "아이디 또는 비밀번호가 틀렸습니다"라는 메시지를 보여줘야 합니다.

게다가 요청 중에는 버튼을 비활성화해서 중복 요청을 막아야 합니다. 선배 박시니어 씨가 김개발 씨의 고민을 듣고는 미소를 지었습니다.

"Riverpod 3.0의 Mutation 패턴을 아시나요?" 그렇다면 Mutation이란 정확히 무엇일까요? 쉽게 비유하자면, Mutation은 마치 레스토랑의 주문 시스템과 같습니다.

손님이 주문을 하면 주방에서 요리를 시작하고, 요리가 완료되면 손님에게 전달하거나, 재료가 없으면 "죄송합니다. 품절입니다"라고 알려줍니다.

Mutation도 마찬가지로 사용자의 액션을 받아 작업을 시작하고, 그 과정과 결과를 추적합니다. Mutation이 없던 시절에는 어땠을까요?

개발자들은 로딩 상태를 수동으로 관리해야 했습니다. bool isLoading = false처럼 변수를 만들고, API 호출 전에 setState(() { isLoading = true; })를 호출하고, 완료되면 다시 false로 바꿔야 했습니다.

에러 처리도 마찬가지였습니다. `String?

errorMessage`를 만들어 관리해야 했죠. 더 큰 문제는 이런 상태들이 화면 곳곳에 흩어져 있다는 것이었습니다.

로그인 버튼, 회원가입 버튼, 비밀번호 재설정 버튼마다 똑같은 패턴의 코드를 반복해서 작성해야 했습니다. 프로젝트가 커질수록 이런 보일러플레이트 코드는 눈덩이처럼 불어났습니다.

바로 이런 문제를 해결하기 위해 Riverpod 3.0에서 Mutation 패턴이 등장했습니다. Mutation을 사용하면 AsyncNotifier가 자동으로 로딩, 에러, 데이터 상태를 관리해줍니다.

또한 AsyncValue.guard를 사용해 에러 처리를 간결하게 작성할 수 있습니다. 무엇보다 폼 제출이라는 일회성 작업의 의도가 코드에서 명확히 드러난다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 LoginNotifierAutoDisposeAsyncNotifier<void>를 상속받습니다.

제네릭 타입이 void인 이유는 로그인 자체는 성공/실패만 중요하고 특별한 데이터를 반환하지 않기 때문입니다. build 메서드는 초기 상태를 설정하는데, 로그인은 사용자가 버튼을 누를 때까지 아무 작업도 하지 않으므로 비어있습니다.

핵심은 login 메서드입니다. 이 메서드가 바로 Mutation의 실체입니다.

먼저 state = const AsyncLoading()으로 상태를 로딩으로 변경합니다. 그러면 UI에서 자동으로 로딩 인디케이터가 표시됩니다.

다음으로 AsyncValue.guard가 API 호출을 감싸며, 성공하면 AsyncData, 실패하면 AsyncError로 자동 변환해줍니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 이커머스 앱을 개발한다고 가정해봅시다. 사용자가 상품을 장바구니에 담을 때, 찜 목록에 추가할 때, 주문을 완료할 때 모두 Mutation 패턴을 사용할 수 있습니다.

각 액션마다 로딩 상태를 표시하고, 실패하면 재시도 버튼을 보여주고, 성공하면 확인 메시지를 표시하는 것이 일관된 패턴으로 구현됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 Mutation을 데이터 조회에 사용하는 것입니다. Mutation은 폼 제출, 데이터 생성, 수정, 삭제 같은 변경 작업에 적합합니다.

단순히 데이터를 읽어오는 작업에는 일반적인 FutureProviderAsyncNotifierProvider를 사용하는 것이 더 적합합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 Mutation이라고 부르는군요.

상태를 변경하는 작업이니까요!" Mutation 패턴을 제대로 이해하면 폼 처리 코드가 훨씬 깔끔해지고, 버그도 줄어듭니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Mutation은 일회성 작업에만 사용하고, 데이터 조회에는 일반 Provider를 사용하세요

  • AsyncValue.guard를 사용하면 try-catch 없이 깔끔하게 에러 처리가 가능합니다
  • autoDispose를 사용하면 화면을 벗어날 때 자동으로 상태가 초기화됩니다

2. 로그인 폼 Notifier 작성

김개발 씨는 이제 실제로 로그인 폼을 만들 차례입니다. 이메일과 비밀번호를 입력받아 서버에 전송해야 합니다.

"Notifier를 어떻게 구조화해야 깔끔할까?" 고민하던 중, 박시니어 씨가 화면에 코드를 띄워주었습니다.

로그인 폼 Notifier는 로그인이라는 비즈니스 로직을 캡슐화한 상태 관리 클래스입니다. AsyncNotifier를 상속받아 비동기 로그인 작업의 로딩, 성공, 실패 상태를 자동으로 추적합니다.

UI는 Notifier를 watch하여 상태에 따라 버튼을 비활성화하거나 에러 메시지를 표시할 수 있습니다.

다음 코드를 살펴봅시다.

// 로그인 Notifier 전체 구조
class LoginNotifier extends AutoDisposeAsyncNotifier<void> {
  @override
  FutureOr<void> build() {
    // 초기 상태: 아무것도 하지 않음
  }

  // 로그인 실행
  Future<void> login(String email, String password) async {
    // 로딩 시작
    state = const AsyncLoading();

    // API 호출 및 에러 처리
    state = await AsyncValue.guard(() async {
      final authRepo = ref.read(authRepositoryProvider);
      await authRepo.login(email, password);

      // 성공 시 사용자 정보 갱신
      ref.invalidate(currentUserProvider);
    });
  }
}

// Provider 등록
final loginProvider = AsyncNotifierProvider.autoDispose<LoginNotifier, void>(
  () => LoginNotifier(),
);

김개발 씨는 이제 구체적인 코드를 작성할 준비가 되었습니다. 손가락을 풀고 키보드 앞에 앉았습니다.

첫 번째로 결정해야 할 것은 Notifier의 제네릭 타입입니다. "로그인이 성공하면 뭘 반환하지?" 김개발 씨는 잠시 고민했습니다.

로그인 자체는 성공/실패만 중요하고, 특별히 반환할 데이터가 없습니다. 사용자 정보는 별도의 Provider에서 관리하면 됩니다.

박시니어 씨가 말했습니다. "그럴 땐 void를 사용하세요.

반환값이 없다는 의미죠." 제네릭 타입을 void로 설정하는 것이 첫 번째 핵심입니다. AutoDisposeAsyncNotifier<void>를 상속받으면 Riverpod가 자동으로 세 가지 상태를 관리해줍니다.

AsyncLoading은 작업이 진행 중임을 나타내고, AsyncData는 성공을 나타내고, AsyncError는 실패와 에러 정보를 담습니다. 개발자는 이 상태들을 일일이 만들 필요가 없습니다.

다음으로 build 메서드를 살펴봅시다. 일반적인 Provider에서 build는 초기 데이터를 로드하는 역할을 합니다.

예를 들어 사용자 목록을 보여주는 화면이라면 build에서 API를 호출해 목록을 가져옵니다. 하지만 로그인은 다릅니다.

화면이 열리자마자 로그인을 시도하면 안 됩니다. 사용자가 버튼을 누를 때까지 기다려야 합니다.

따라서 build 메서드는 비어있습니다. 아무 작업도 하지 않는 것이 올바른 구현입니다.

이제 핵심인 login 메서드를 살펴보겠습니다. 메서드는 emailpassword를 파라미터로 받습니다.

UI에서 사용자가 입력한 값을 전달받는 것이죠. 메서드가 호출되면 가장 먼저 state = const AsyncLoading()을 실행합니다.

이 한 줄로 상태가 로딩으로 바뀌고, UI에서는 로딩 인디케이터가 자동으로 표시됩니다. 다음은 AsyncValue.guard입니다.

이 유틸리티는 try-catch 블록을 대신해주는 편리한 헬퍼입니다. 내부의 async 함수를 실행하고, 성공하면 AsyncData로, 예외가 발생하면 AsyncError로 자동 변환해줍니다.

개발자는 예외 처리를 신경 쓸 필요가 없습니다. guard 안에서는 실제 비즈니스 로직이 실행됩니다.

ref.read(authRepositoryProvider)로 인증 저장소를 가져오고, login 메서드를 호출합니다. API 통신이 일어나는 부분입니다.

로그인이 성공하면 한 가지 더 할 일이 있습니다. ref.invalidate(currentUserProvider)를 호출해 현재 사용자 정보를 갱신합니다.

로그인이 성공했으니 앱 전체에서 사용하는 사용자 정보가 업데이트되어야 합니다. invalidate는 Provider를 무효화해서 다음에 접근할 때 새로 데이터를 로드하도록 만듭니다.

마지막으로 Provider를 등록합니다. AsyncNotifierProvider.autoDispose를 사용하면 화면이 dispose될 때 Notifier도 자동으로 정리됩니다.

메모리 누수를 방지하는 안전장치입니다. 로그인 화면을 벗어나면 로그인 상태도 초기화되는 것이 자연스럽습니다.

실제 프로젝트에서는 더 많은 로직이 들어갈 수 있습니다. 예를 들어 로그인 시도 횟수를 제한하거나, 실패 시 로그를 기록하거나, 소셜 로그인 여부에 따라 다른 API를 호출할 수 있습니다.

하지만 기본 구조는 동일합니다. Notifier가 비즈니스 로직을 담당하고, UI는 상태만 관찰하는 것입니다.

김개발 씨는 코드를 작성하며 점점 자신감이 생겼습니다. "생각보다 간단하네요!" 박시니어 씨가 웃으며 말했습니다.

"Riverpod가 복잡한 부분을 다 처리해주니까요. 우리는 비즈니스 로직에만 집중하면 됩니다." 로그인 폼 Notifier를 제대로 구조화하면 테스트하기도 쉽고, 재사용하기도 편합니다.

여러분도 이 패턴을 다양한 폼에 적용해 보세요.

실전 팁

💡 - 제네릭 타입은 반환값이 없으면 void, 있으면 구체적인 타입을 사용하세요

  • AsyncValue.guard를 사용하면 예외 처리가 자동으로 됩니다
  • 로그인 성공 후 ref.invalidate로 관련 Provider들을 갱신하는 것을 잊지 마세요

3. MutationState 상태 활용

이제 UI를 만들 차례입니다. 김개발 씨는 로그인 버튼을 만들고 있었는데, "상태에 따라 어떻게 UI를 바꾸지?" 고민이 되었습니다.

박시니어 씨가 말했습니다. "AsyncValue의 상태를 체크하면 됩니다.

when이나 isLoading을 사용해보세요."

AsyncValue는 비동기 작업의 상태를 표현하는 Riverpod의 핵심 타입입니다. isLoading, hasError, value 같은 프로퍼티로 현재 상태를 확인할 수 있고, when 메서드로 패턴 매칭 방식으로 각 상태에 대응할 수 있습니다.

이를 활용하면 로딩 중에는 버튼을 비활성화하고, 에러 발생 시에는 메시지를 표시하는 등의 UI 처리가 간단해집니다.

다음 코드를 살펴봅시다.

// UI에서 로그인 상태 활용
class LoginScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 로그인 상태 구독
    final loginState = ref.watch(loginProvider);

    return Column(
      children: [
        // 로딩 중인지 체크
        ElevatedButton(
          onPressed: loginState.isLoading
            ? null // 로딩 중에는 버튼 비활성화
            : () => ref.read(loginProvider.notifier).login(email, password),
          child: Text('로그인'),
        ),

        // 에러 메시지 표시
        if (loginState.hasError)
          Text(
            '로그인 실패: ${loginState.error}',
            style: TextStyle(color: Colors.red),
          ),
      ],
    );
  }
}

김개발 씨는 이제 실제 화면을 만들어야 합니다. TextField 두 개와 버튼 하나만 있으면 되는 간단한 화면이지만, 상태 관리가 까다롭습니다.

"로딩 중에는 버튼을 비활성화해야 하고, 에러가 발생하면 메시지를 보여줘야 하는데..." 김개발 씨는 예전에 StatefulWidget으로 만들 때 얼마나 복잡했는지 기억났습니다. bool isLoading, `String?

errorMessage같은 변수들을 만들고setState`를 호출하는 것이 번거로웠습니다. 박시니어 씨가 말했습니다.

"Riverpod를 쓰면 훨씬 간단합니다. AsyncValue가 모든 상태를 담고 있으니까요." AsyncValue는 세 가지 상태를 표현할 수 있습니다.

첫째, AsyncLoading은 작업이 진행 중임을 나타냅니다. 마치 신호등의 노란불처럼 "잠시만 기다려주세요"라고 말하는 상태입니다.

둘째, AsyncData는 작업이 성공적으로 완료되었음을 나타냅니다. 신호등의 파란불입니다.

셋째, AsyncError는 작업이 실패했고 에러 정보를 담고 있습니다. 빨간불이죠.

UI에서는 이 상태를 어떻게 확인할까요? 가장 간단한 방법은 isLoading 프로퍼티를 사용하는 것입니다.

loginState.isLoadingbool 값을 반환하므로 조건문에 바로 사용할 수 있습니다. 로딩 중이면 버튼의 onPressednull로 설정해 비활성화하면 됩니다.

코드를 자세히 살펴봅시다. 먼저 ref.watch(loginProvider)로 로그인 상태를 구독합니다.

watch를 사용하면 상태가 바뀔 때마다 위젯이 자동으로 다시 빌드됩니다. 로딩이 시작되면 버튼이 비활성화되고, 완료되면 다시 활성화되는 것이 자연스럽게 일어납니다.

버튼의 onPressed를 보면 삼항 연산자를 사용합니다. loginState.isLoadingtruenull을 반환해 버튼을 비활성화하고, false면 로그인 함수를 호출합니다.

이 한 줄로 중복 요청 방지가 완성됩니다. 에러 처리는 어떻게 할까요?

hasError 프로퍼티를 사용하면 됩니다. if (loginState.hasError)로 조건을 걸고, 에러가 있을 때만 메시지를 표시합니다.

loginState.error로 실제 에러 객체에 접근할 수 있으므로, 에러 메시지를 사용자에게 보여줄 수 있습니다. 더 강력한 방법도 있습니다.

when 메서드를 사용하는 것입니다. when은 세 가지 콜백을 받습니다.

data, loading, error입니다. 현재 상태에 따라 적절한 콜백이 자동으로 호출됩니다.

예를 들어 로딩 중이면 CircularProgressIndicator를 반환하고, 에러면 에러 메시지를, 성공이면 성공 화면을 반환할 수 있습니다. 실무에서는 더 복잡한 경우도 있습니다.

예를 들어 로그인이 성공하면 자동으로 홈 화면으로 이동해야 할 수 있습니다. 이럴 때는 ref.listen을 사용합니다.

listen은 상태 변화를 감지하되 위젯을 다시 빌드하지 않고, 특정 동작만 실행할 때 유용합니다. 성공 시 Navigator.push를 호출하는 식이죠.

김개발 씨는 코드를 작성하며 감탄했습니다. "와, 정말 간단하네요.

예전에는 이것저것 신경 쓸 게 많았는데..." 박시니어 씨가 고개를 끄덕였습니다. "맞아요.

Riverpod가 상태 관리의 복잡함을 추상화해주니까 우리는 UI 로직에만 집중할 수 있어요." 한 가지 팁을 더 드리자면, AsyncValue에는 maybeWhen이라는 메서드도 있습니다. 모든 경우를 다 처리할 필요 없이 특정 상태만 처리하고 싶을 때 유용합니다.

예를 들어 에러만 처리하고 나머지는 기본 UI를 보여주고 싶다면 maybeWhen을 사용하면 됩니다. AsyncValue의 상태 활용법을 익히면 모든 비동기 작업의 UI 처리가 일관되고 간결해집니다.

여러분도 다양한 프로퍼티와 메서드를 실험해 보세요.

실전 팁

💡 - isLoading으로 간단한 로딩 체크, when으로 복잡한 상태별 UI 처리를 하세요

  • 화면 전환 같은 사이드 이펙트는 ref.listen을 사용하세요
  • maybeWhen을 사용하면 특정 상태만 선택적으로 처리할 수 있습니다

4. 버튼 로딩 표시

김개발 씨는 버튼 디자인을 개선하고 싶었습니다. "로딩 중일 때 버튼 안에 작은 인디케이터를 넣으면 더 멋질 것 같은데..." 박시니어 씨가 말했습니다.

"좋은 생각이에요. 버튼의 child를 동적으로 바꾸면 됩니다."

버튼 로딩 표시는 사용자에게 작업이 진행 중임을 시각적으로 알려주는 UX 패턴입니다. 버튼을 비활성화하는 것만으로는 사용자가 버튼이 작동하지 않는 것인지 로딩 중인지 구분하기 어렵습니다.

로딩 중에는 버튼 안에 CircularProgressIndicator를 표시하고, 평소에는 텍스트를 표시하는 식으로 구현합니다.

다음 코드를 살펴봅시다.

// 버튼 안에 로딩 인디케이터 표시
ElevatedButton(
  onPressed: loginState.isLoading
    ? null
    : () async {
        await ref.read(loginProvider.notifier).login(
          emailController.text,
          passwordController.text,
        );
      },
  child: loginState.isLoading
    ? SizedBox(
        width: 20,
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 2,
          valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
        ),
      )
    : Text('로그인'),
)

김개발 씨는 실제로 앱을 실행해보고 뭔가 어색함을 느꼈습니다. 로그인 버튼을 누르면 버튼이 회색으로 비활성화되긴 하는데, 뭔가가 부족했습니다.

"사용자가 이 버튼이 작동하지 않는 건지, 아니면 로딩 중인 건지 구분할 수 있을까?" 김개발 씨는 다른 앱들을 둘러봤습니다. 대부분의 앱은 로딩 중일 때 버튼 안에 작은 원형 인디케이터를 표시했습니다.

박시니어 씨가 말했습니다. "좋은 관찰이에요.

이게 바로 UX의 핵심입니다. 시각적 피드백을 주는 거죠." 버튼 로딩 표시는 단순해 보이지만 사용자 경험에 큰 영향을 미칩니다.

마치 엘리베이터의 층수 표시와 같습니다. 엘리베이터를 탔는데 아무 표시도 없다면 불안할 것입니다.

"이거 고장 난 거 아냐?" 하고 의심하게 되죠. 하지만 층수가 올라가는 걸 보면 안심이 됩니다.

버튼의 로딩 표시도 마찬가지입니다. 구현은 생각보다 간단합니다.

버튼의 child 속성을 조건에 따라 바꾸면 됩니다. loginState.isLoadingtrueCircularProgressIndicator를, falseText 위젯을 반환하는 것이죠.

Flutter의 선언적 UI 특성 덕분에 상태가 바뀌면 자동으로 UI가 업데이트됩니다. 코드의 세부사항을 살펴봅시다.

SizedBox로 인디케이터의 크기를 제한합니다. 기본 크기는 버튼 안에 넣기에 너무 크기 때문입니다.

20x20 픽셀 정도가 적당합니다. strokeWidth를 2로 설정해 선을 얇게 만들면 더 세련되어 보입니다.

valueColor는 인디케이터의 색상을 지정합니다. AlwaysStoppedAnimation을 사용하면 애니메이션 없이 고정된 색상을 사용할 수 있습니다.

일반적으로 버튼의 텍스트 색상과 동일하게 흰색으로 설정합니다. onPressed 콜백을 보면 async로 표시되어 있습니다.

이것은 선택 사항이지만, 로그인이 완료될 때까지 기다리고 싶을 때 유용합니다. await를 사용하면 로그인이 완료된 후에 추가 작업을 할 수 있습니다.

예를 들어 성공 시 토스트 메시지를 보여주거나, 실패 시 입력 필드를 초기화하는 등의 작업이 가능합니다. 실무에서는 더 다양한 변형이 있습니다.

예를 들어 버튼 전체가 로딩 인디케이터로 바뀌는 대신, 텍스트 옆에 작은 인디케이터를 추가할 수 있습니다. "로그인 중..." 같은 텍스트와 인디케이터를 Row로 배치하는 것이죠.

어떤 디자인이 더 나은지는 앱의 전체적인 디자인 시스템에 따라 다릅니다. 또 다른 패턴은 스켈레톤 로딩입니다.

버튼 자체는 그대로 두고, 버튼 아래나 위에 얇은 프로그레스 바를 표시하는 방식입니다. 이 방법은 버튼의 크기가 변하지 않아 레이아웃 시프트를 방지할 수 있다는 장점이 있습니다.

김개발 씨는 여러 방법을 시도해보며 가장 적합한 디자인을 찾았습니다. "이제 사용자도 로딩 중이라는 걸 명확히 알 수 있겠네요!" 박시니어 씨가 미소 지었습니다.

"네, 작은 디테일 하나가 사용자 경험을 크게 개선합니다." 한 가지 더 팁을 드리자면, 로딩 애니메이션의 속도를 조절할 수도 있습니다. 네트워크가 느린 환경에서는 긴 로딩 시간을 견뎌야 하므로, 애니메이션이 너무 빠르면 오히려 불안감을 줄 수 있습니다.

적절한 속도를 찾는 것도 중요합니다. 버튼 로딩 표시를 잘 구현하면 사용자가 앱을 더 신뢰하게 됩니다.

여러분도 다양한 로딩 패턴을 실험해 보세요.

실전 팁

💡 - 인디케이터 크기는 20x20 정도로 버튼 안에 자연스럽게 들어가도록 조절하세요

  • strokeWidth를 2 정도로 줄이면 더 세련되어 보입니다
  • 버튼 텍스트와 인디케이터 색상을 통일하면 시각적으로 일관됩니다

5. 성공/실패 처리

로그인이 완료되면 어떻게 해야 할까요? 김개발 씨는 성공 시 홈 화면으로 이동하고, 실패 시 에러 메시지를 보여주고 싶었습니다.

"상태가 바뀔 때마다 특정 동작을 실행하려면 어떻게 해야 하죠?" 박시니어 씨가 답했습니다. "ref.listen을 사용하면 됩니다."

ref.listen은 Provider의 상태 변화를 감지하되 위젯을 다시 빌드하지 않는 Riverpod의 특별한 메서드입니다. 화면 전환, 다이얼로그 표시, 토스트 메시지 같은 일회성 사이드 이펙트를 실행할 때 사용합니다.

상태가 성공으로 바뀌면 홈 화면으로 이동하고, 실패로 바뀌면 에러 메시지를 표시하는 식의 로직을 깔끔하게 구현할 수 있습니다.

다음 코드를 살펴봅시다.

// ref.listen으로 성공/실패 처리
class LoginScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 상태 변화 감지
    ref.listen<AsyncValue<void>>(loginProvider, (previous, next) {
      next.when(
        data: (_) {
          // 로그인 성공 - 홈 화면으로 이동
          Navigator.of(context).pushReplacement(
            MaterialPageRoute(builder: (_) => HomeScreen()),
          );
        },
        error: (error, stack) {
          // 로그인 실패 - 에러 메시지 표시
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('로그인 실패: $error')),
          );
        },
        loading: () {}, // 로딩은 무시
      );
    });

    // UI 구성...
  }
}

김개발 씨는 이제 마지막 퍼즐 조각을 맞출 차례입니다. 로그인이 성공하면 사용자를 홈 화면으로 안내해야 하고, 실패하면 무엇이 잘못되었는지 알려줘야 합니다.

"상태를 watch하면 위젯이 다시 빌드되잖아요. 그런데 화면 전환은 한 번만 일어나야 하는데..." 김개발 씨는 고민에 빠졌습니다.

위젯이 계속 다시 빌드되면 화면 전환도 계속 일어날까요? 박시니어 씨가 설명했습니다.

"그럴 때 쓰는 게 ref.listen입니다. watch와 달리 위젯을 다시 빌드하지 않고, 상태가 바뀔 때 콜백만 실행해요." ref.listen과 ref.watch의 차이를 이해하는 것이 핵심입니다.

마치 신문 구독과 알림의 차이와 같습니다. 신문 구독은 매일 새 신문을 받아서 읽습니다.

상태가 바뀔 때마다 위젯을 다시 그려야 하는 경우죠. 알림은 중요한 일이 생겼을 때만 한 번 울립니다.

화면 전환 같은 일회성 이벤트를 처리하는 경우입니다. ref.listen은 두 개의 파라미터를 받습니다.

첫 번째는 감지할 Provider입니다. 여기서는 loginProvider를 전달합니다.

두 번째는 콜백 함수입니다. (previous, next) 형태로, 이전 상태와 새로운 상태를 받습니다.

대부분의 경우 새로운 상태만 사용하지만, 이전 상태와 비교가 필요할 때도 있습니다. 콜백 안에서는 next.when을 사용해 상태별로 처리합니다.

data 콜백은 로그인이 성공했을 때 실행됩니다. 여기서 Navigator.pushReplacement를 호출해 현재 화면을 홈 화면으로 교체합니다.

pushReplacement를 사용하면 뒤로 가기 버튼을 눌러도 로그인 화면으로 돌아가지 않습니다. 이미 로그인했으니 당연한 동작이죠.

error 콜백은 로그인이 실패했을 때 실행됩니다. ScaffoldMessenger를 사용해 화면 하단에 스낵바를 표시합니다.

에러 메시지는 error 객체에서 가져옵니다. 사용자에게 "아이디 또는 비밀번호가 틀렸습니다" 같은 구체적인 메시지를 보여주는 것이 좋습니다.

loading 콜백은 비어있습니다. 로딩 상태는 버튼의 인디케이터로 이미 표시하고 있으므로, 여기서는 추가 동작이 필요 없습니다.

실무에서는 더 복잡한 처리가 필요할 수 있습니다. 예를 들어 로그인 실패 횟수를 추적해서 3번 실패하면 비밀번호 재설정 버튼을 표시할 수 있습니다.

또는 특정 에러 코드에 따라 다른 메시지를 보여줄 수도 있습니다. 예를 들어 401은 "인증 실패", 500은 "서버 오류가 발생했습니다.

잠시 후 다시 시도해주세요" 같은 식입니다. 또 다른 패턴은 낙관적 업데이트입니다.

로그인 버튼을 누르자마자 홈 화면으로 이동하고, 백그라운드에서 실제 로그인을 수행합니다. 실패하면 다시 로그인 화면으로 되돌립니다.

이 방법은 앱이 더 빠르게 느껴지지만, 실패 시 사용자 경험이 나빠질 수 있으므로 신중하게 사용해야 합니다. 김개발 씨는 ref.listen을 적용하고 테스트해봤습니다.

로그인이 성공하면 부드럽게 홈 화면으로 전환되고, 실패하면 스낵바가 나타났습니다. "완벽해요!" 박시니어 씨가 고개를 끄덕였습니다.

"이제 진짜 실무에서 쓸 수 있는 코드가 됐네요." 한 가지 주의할 점은 ref.listenbuild 메서드 안에서 호출해야 한다는 것입니다. initState나 다른 생명주기 메서드에서 호출하면 작동하지 않습니다.

Riverpod의 설계 철학은 모든 것을 선언적으로 표현하는 것이기 때문입니다. 성공/실패 처리를 제대로 구현하면 사용자가 앱의 상태를 항상 명확히 인지할 수 있습니다.

여러분도 다양한 사이드 이펙트 패턴을 연습해 보세요.

실전 팁

💡 - 화면 전환 같은 사이드 이펙트는 ref.listen을 사용하세요

  • 성공 시 pushReplacement를 사용하면 뒤로 가기로 로그인 화면에 돌아가지 않습니다
  • 에러 코드별로 다른 메시지를 보여주면 사용자 경험이 개선됩니다

6. 회원가입 폼 확장 예제

김개발 씨는 로그인 화면을 완성했습니다. 이제 같은 패턴으로 회원가입 화면도 만들어야 합니다.

"로그인과 거의 비슷한데, 필드만 더 많네요." 박시니어 씨가 말했습니다. "맞아요.

Mutation 패턴은 재사용하기 쉽습니다."

회원가입 폼은 로그인과 동일한 Mutation 패턴을 사용하되, 더 많은 입력 필드와 유효성 검증이 추가됩니다. 이메일, 비밀번호, 비밀번호 확인, 이름, 전화번호 등의 필드를 처리하고, 각 필드의 유효성을 검사한 후 서버에 전송합니다.

동일한 Notifier 구조를 사용하므로 로그인에서 배운 패턴을 그대로 적용할 수 있습니다.

다음 코드를 살펴봅시다.

// 회원가입 Notifier 확장
class SignUpNotifier extends AutoDisposeAsyncNotifier<void> {
  @override
  FutureOr<void> build() {}

  Future<void> signUp({
    required String email,
    required String password,
    required String confirmPassword,
    required String name,
  }) async {
    // 유효성 검증
    if (password != confirmPassword) {
      state = AsyncError('비밀번호가 일치하지 않습니다', StackTrace.current);
      return;
    }

    // 로딩 시작
    state = const AsyncLoading();

    // API 호출
    state = await AsyncValue.guard(() async {
      await ref.read(authRepositoryProvider).signUp(
        email: email,
        password: password,
        name: name,
      );
    });
  }
}

final signUpProvider = AsyncNotifierProvider.autoDispose<SignUpNotifier, void>(
  () => SignUpNotifier(),
);

김개발 씨는 자신감이 생겼습니다. 로그인 화면을 성공적으로 만들었으니, 회원가입 화면도 금방 만들 수 있을 것 같았습니다.

"필드가 더 많긴 한데, 기본 구조는 똑같네요." 김개발 씨는 LoginNotifier를 복사해서 SignUpNotifier를 만들기 시작했습니다. 박시니어 씨가 웃으며 말했습니다.

"바로 그겁니다. 좋은 패턴은 재사용하기 쉽죠." 회원가입과 로그인의 공통점을 먼저 이해해봅시다.

둘 다 사용자 입력을 받아 서버에 전송하는 일회성 작업입니다. 로딩 상태를 표시해야 하고, 성공하면 다음 화면으로 이동하고, 실패하면 에러를 보여줘야 합니다.

차이점은 필드 개수와 유효성 검증 로직뿐입니다. 회원가입에는 더 많은 필드가 필요합니다.

이메일, 비밀번호는 기본이고, 비밀번호 확인, 이름, 전화번호, 생년월일 등이 추가될 수 있습니다. 어떤 서비스는 약관 동의 체크박스도 있습니다.

하지만 Notifier의 구조는 동일합니다. signUp 메서드의 파라미터만 늘어날 뿐입니다.

유효성 검증이 핵심입니다. 가장 흔한 검증은 비밀번호 확인입니다.

사용자가 입력한 비밀번호와 확인용 비밀번호가 일치하는지 확인해야 합니다. 일치하지 않으면 서버에 요청을 보내기 전에 즉시 에러를 표시합니다.

AsyncError를 직접 생성해서 state에 할당하면 됩니다. 코드를 자세히 살펴봅시다.

signUp 메서드는 네 개의 필수 파라미터를 받습니다. required 키워드를 사용해 컴파일 타임에 누락을 방지합니다.

메서드 안에서 가장 먼저 하는 일은 비밀번호 일치 여부를 확인하는 것입니다. 만약 비밀번호가 일치하지 않으면 AsyncError를 생성합니다.

에러 메시지와 스택 트레이스를 전달하면 loginProvider와 동일한 방식으로 에러를 처리할 수 있습니다. return으로 메서드를 즉시 종료해 불필요한 API 호출을 방지합니다.

유효성 검증을 통과하면 로딩을 시작합니다. 이후 과정은 로그인과 완전히 동일합니다.

AsyncValue.guard로 API 호출을 감싸고, 성공하면 AsyncData, 실패하면 AsyncError로 자동 변환됩니다. Riverpod가 반복적인 패턴을 처리해주므로 우리는 비즈니스 로직에만 집중하면 됩니다.

UI는 어떻게 구성할까요? 로그인 화면과 거의 동일합니다.

ref.watch(signUpProvider)로 상태를 구독하고, isLoading으로 버튼을 제어하고, ref.listen으로 성공/실패를 처리합니다. TextField만 몇 개 더 추가하면 됩니다.

실무에서는 더 복잡한 검증이 필요할 수 있습니다. 예를 들어 이메일 형식 검증, 비밀번호 강도 검증(최소 8자, 대소문자 포함 등), 전화번호 형식 검증 등입니다.

이런 검증은 Notifier에 넣을 수도 있고, 별도의 유틸리티 함수로 분리할 수도 있습니다. 저는 복잡한 검증은 별도 클래스로 분리하는 것을 추천합니다.

또 다른 패턴은 실시간 검증입니다. TextField의 onChanged 콜백을 사용해 사용자가 타이핑할 때마다 검증을 수행하고, 필드 아래에 에러 메시지를 즉시 표시하는 방식입니다.

이렇게 하면 사용자가 폼을 제출하기 전에 실수를 미리 발견할 수 있습니다. 김개발 씨는 회원가입 화면을 완성하고 뿌듯해했습니다.

"로그인에서 배운 패턴을 그대로 적용하니까 정말 빠르게 만들 수 있네요!" 박시니어 씨가 박수를 쳤습니다. "잘했어요.

이제 Mutation 패턴을 완전히 이해한 것 같네요. 다른 폼에도 똑같이 적용할 수 있을 거예요." 한 가지 팁을 더 드리자면, 회원가입이 성공한 후 자동으로 로그인을 시도하는 것도 좋은 UX입니다.

회원가입 후 다시 로그인 화면으로 이동하는 것보다, 바로 홈 화면으로 안내하는 것이 더 자연스럽습니다. 회원가입 폼 패턴을 익히면 설문 조사, 피드백 폼, 주문서 등 모든 종류의 폼을 쉽게 만들 수 있습니다.

여러분도 다양한 폼을 만들어 보며 패턴을 체화해 보세요.

실전 팁

💡 - 유효성 검증은 서버 요청 전에 먼저 수행해 불필요한 API 호출을 줄이세요

  • 복잡한 검증 로직은 별도 클래스로 분리하면 테스트하기 쉽습니다
  • 회원가입 성공 후 자동 로그인을 제공하면 사용자 경험이 개선됩니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Flutter#Riverpod#Mutation#AsyncNotifier#FormHandling#Flutter,Riverpod

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드

Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.

Riverpod 3.0 Retry 자동 재시도 완벽 가이드

Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.

Riverpod 3.0 requireValue로 Provider 결합하기

Riverpod 3.0에 새로 추가된 requireValue를 활용하여 여러 Provider의 데이터를 효율적으로 결합하는 방법을 배웁니다. 비동기 데이터를 마치 동기 데이터처럼 다루는 실전 패턴을 소개합니다.

Flutter 3.0 Offline 데이터 영속화 완벽 가이드

Flutter 3.0에서 새롭게 추가된 Offline 데이터 영속화 기능을 배웁니다. Storage 인터페이스부터 SharedPreferences 활용, 실전 예제까지 실무에서 바로 사용할 수 있는 패턴을 배워봅시다.

Riverpod Provider 오버라이드 테스트 완벽 가이드

Riverpod의 overrideWithValue와 overrideWith를 활용한 테스트 작성 방법을 실무 예제로 배워봅니다. Repository 모킹부터 위젯 테스트, 통합 테스트까지 단계별로 익힐 수 있습니다.