🤖

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

⚠️

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

이미지 로딩 중...

AsyncValue.requireValue로 Provider 결합하기 - 슬라이드 1/7
A

AI Generated

2025. 11. 30. · 24 Views

AsyncValue.requireValue로 Provider 결합하기

Riverpod 3.0에서 새로 도입된 requireValue를 활용하여 여러 AsyncValue를 동기적으로 결합하는 방법을 알아봅니다. 기존 방식과의 비교를 통해 더 깔끔하고 안전한 코드를 작성하는 방법을 배웁니다.


목차

  1. requireValue란? (3.0 신규)
  2. 여러 AsyncValue 동기 결합
  3. 기존 방식 vs requireValue 비교
  4. 안전한 사용 조건
  5. 에러 처리 전략
  6. 실전 활용 패턴

1. requireValue란? (3.0 신규)

김개발 씨는 Riverpod을 사용하여 여러 비동기 데이터를 조합하는 코드를 작성하고 있었습니다. 매번 when이나 switch 문으로 로딩, 에러, 데이터 상태를 처리하다 보니 코드가 점점 복잡해졌습니다.

"분명 더 간단한 방법이 있을 텐데..." 하며 고민하던 중, Riverpod 3.0의 새로운 기능을 발견했습니다.

requireValue는 Riverpod 3.0에서 새롭게 도입된 AsyncValue의 속성입니다. 이 속성은 AsyncValue가 데이터를 가지고 있을 때 그 값을 직접 반환하고, 그렇지 않으면 예외를 던집니다.

마치 금고에서 열쇠가 있을 때만 문을 여는 것처럼, 데이터가 확실히 있는 상황에서만 사용해야 합니다.

다음 코드를 살펴봅시다.

// AsyncValue의 requireValue 속성 정의
extension AsyncValueX<T> on AsyncValue<T> {
  // 값이 있으면 반환, 없으면 StateError 발생
  T get requireValue {
    return switch (this) {
      AsyncData(:final value) => value,
      AsyncLoading() => throw StateError('Value is loading'),
      AsyncError(:final error) => throw StateError('Value has error: $error'),
    };
  }
}

// 사용 예시
final userAsync = ref.watch(userProvider);
final user = userAsync.requireValue; // 값이 있다고 확신할 때만 사용

김개발 씨는 입사 6개월 차 Flutter 개발자입니다. 회사에서 Riverpod을 적극적으로 사용하고 있어서, 비동기 상태 관리에는 어느 정도 익숙해진 상태였습니다.

그런데 오늘 코드 리뷰에서 선배 박시니어 씨가 의아한 표정으로 물었습니다. "이 부분, 아직도 when으로 처리하고 있네요?" 김개발 씨가 고개를 갸웃거리며 되물었습니다.

"네, 원래 이렇게 하는 거 아닌가요?" 박시니어 씨가 웃으며 대답했습니다. "Riverpod 3.0에 requireValue라는 게 새로 생겼어요.

특정 상황에서는 코드가 훨씬 간결해집니다." 그렇다면 requireValue란 정확히 무엇일까요? 쉽게 비유하자면, requireValue는 마치 택배 상자를 여는 것과 같습니다.

택배가 도착했다는 것을 이미 확인한 상태에서 상자를 열어 내용물을 꺼내는 행위입니다. 만약 택배가 아직 배송 중이거나 분실된 상태에서 상자를 열려고 하면 당연히 문제가 생깁니다.

requireValue도 마찬가지로, 데이터가 이미 준비되어 있을 때만 안전하게 사용할 수 있습니다. 기존에는 AsyncValue에서 값을 꺼내려면 어떻게 해야 했을까요?

개발자들은 when 메서드나 switch 문을 사용하여 세 가지 상태를 모두 처리해야 했습니다. 로딩 중일 때는 무엇을 보여줄지, 에러가 발생하면 어떻게 할지, 데이터가 있을 때는 무엇을 할지 일일이 명시해야 했습니다.

단순히 값만 필요한 상황에서도 이 모든 케이스를 처리하는 것은 번거로운 일이었습니다. requireValue는 이런 상황을 위해 탄생했습니다.

데이터가 이미 로드되었다고 확신할 수 있는 상황에서, requireValue를 사용하면 단 한 줄로 값을 꺼낼 수 있습니다. 더 이상 when이나 switch로 모든 케이스를 나열할 필요가 없습니다.

코드가 간결해지고, 의도가 명확해집니다. 위의 코드를 살펴보면, requireValue의 동작 원리를 이해할 수 있습니다.

AsyncData 상태일 때는 내부의 value를 그대로 반환합니다. 반면 AsyncLoading이나 AsyncError 상태에서 호출되면 StateError를 던집니다.

이것이 핵심입니다. requireValue는 "나는 값이 있다고 확신한다"는 개발자의 의도를 코드로 표현하는 것입니다.

실제 현업에서는 언제 이런 확신을 가질 수 있을까요? 대표적인 예가 다른 Provider가 이미 로딩을 완료한 후 그 값을 참조할 때입니다.

예를 들어 userProvider가 로드된 후에야 실행되는 profileProvider에서는 userProvider의 값이 이미 있다고 확신할 수 있습니다. 이런 상황에서 requireValue가 빛을 발합니다.

하지만 주의할 점이 있습니다. requireValue를 잘못 사용하면 런타임 에러가 발생합니다.

아직 로딩 중인 데이터에 requireValue를 호출하면 앱이 크래시될 수 있습니다. 따라서 반드시 데이터가 준비되었다고 보장할 수 있는 상황에서만 사용해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 그래서 이름이 require인 거군요. 값을 요구한다는 뜻이니까, 없으면 에러가 나는 거고요!" requireValue를 적재적소에 활용하면 코드가 훨씬 읽기 쉬워집니다.

다음 섹션에서는 이 기능을 활용하여 여러 AsyncValue를 결합하는 방법을 알아보겠습니다.

실전 팁

💡 - requireValue는 값이 확실히 있을 때만 사용하세요. 확신이 없다면 when이나 switch를 사용하는 것이 안전합니다.

  • 런타임 에러를 방지하려면 requireValue 호출 전에 해당 Provider의 로딩이 완료되었는지 확인하세요.

2. 여러 AsyncValue 동기 결합

김개발 씨가 새로운 대시보드 화면을 개발하고 있었습니다. 사용자 정보, 알림 목록, 최근 활동 내역 세 가지 데이터를 한꺼번에 보여줘야 하는데, 각각이 모두 비동기로 로드됩니다.

"이 세 개를 어떻게 깔끔하게 합칠 수 있을까?" 김개발 씨의 고민이 시작되었습니다.

여러 AsyncValue를 동기적으로 결합한다는 것은 각각의 비동기 값들이 모두 준비되었을 때 하나로 합치는 것을 의미합니다. 마치 요리를 할 때 모든 재료가 준비된 후에야 조리를 시작하는 것과 같습니다.

requireValue를 활용하면 이 과정을 매우 간결하게 표현할 수 있습니다.

다음 코드를 살펴봅시다.

// 세 개의 독립적인 비동기 Provider
@riverpod
Future<User> user(Ref ref) async {
  return await fetchUser();
}

@riverpod
Future<List<Notification>> notifications(Ref ref) async {
  return await fetchNotifications();
}

@riverpod
Future<List<Activity>> activities(Ref ref) async {
  return await fetchActivities();
}

// 세 Provider를 결합하는 Dashboard Provider
@riverpod
Future<DashboardData> dashboard(Ref ref) async {
  // 세 Provider 모두 watch하여 로딩 상태 동기화
  final userAsync = ref.watch(userProvider);
  final notificationsAsync = ref.watch(notificationsProvider);
  final activitiesAsync = ref.watch(activitiesProvider);

  // 하나라도 로딩 중이면 이 Provider도 로딩 상태
  // 모두 완료되면 requireValue로 값 추출
  return DashboardData(
    user: userAsync.requireValue,
    notifications: notificationsAsync.requireValue,
    activities: activitiesAsync.requireValue,
  );
}

김개발 씨는 대시보드 화면을 설계하면서 골치가 아팠습니다. 사용자 정보는 userProvider에서, 알림 목록은 notificationsProvider에서, 최근 활동은 activitiesProvider에서 각각 가져와야 합니다.

세 가지 데이터가 모두 준비되어야만 화면을 제대로 그릴 수 있는데, 이걸 어떻게 처리해야 할까요? 박시니어 씨가 조언했습니다.

"여러 AsyncValue를 결합할 때는 requireValue를 활용하면 깔끔해요." 그 원리를 살펴봅시다. 먼저, 세 개의 Provider를 모두 ref.watch로 구독합니다.

이렇게 하면 Riverpod이 각 Provider의 상태 변화를 자동으로 추적합니다. userProvider가 로딩을 완료하고, notificationsProvider가 완료되고, activitiesProvider도 완료되면 그때 dashboardProvider가 다시 빌드됩니다.

여기서 마법 같은 일이 일어납니다. dashboardProvider 자체도 Future를 반환하는 비동기 Provider입니다.

세 개의 하위 Provider 중 하나라도 아직 로딩 중이라면, ref.watch가 AsyncLoading 상태를 반환합니다. 이 상태에서 requireValue를 호출하면 예외가 발생하는데, Riverpod은 이 예외를 잡아서 dashboardProvider 자체를 로딩 상태로 만들어줍니다.

비유하자면, 세 명의 친구와 약속을 잡은 상황과 같습니다. 모든 친구가 도착해야 식사를 시작할 수 있습니다.

한 명이라도 오지 않았다면 "아직 기다리는 중"이라는 상태가 되고, 모두 도착하면 비로소 식사를 시작합니다. Riverpod이 바로 이 "기다림"을 자동으로 처리해주는 것입니다.

코드를 다시 살펴보겠습니다. dashboardProvider 내부에서 세 개의 AsyncValue를 watch한 후, 각각에 requireValue를 호출합니다.

모든 값이 준비되었다면 DashboardData 객체가 생성되어 반환됩니다. 하나라도 준비되지 않았다면 예외가 발생하고, Riverpod이 이를 처리하여 로딩 상태를 유지합니다.

이 패턴의 장점은 무엇일까요? 첫째, 코드가 매우 간결합니다.

복잡한 상태 조합 로직을 직접 작성할 필요가 없습니다. 둘째, 로딩 상태가 자동으로 전파됩니다.

하위 Provider의 로딩 상태가 상위 Provider에 그대로 반영됩니다. 셋째, 에러 처리도 자동입니다.

하나라도 에러가 발생하면 상위 Provider도 에러 상태가 됩니다. UI에서는 어떻게 사용할까요?

dashboardProvider를 watch하면 단일 AsyncValue를 얻습니다. 이것 하나만 when이나 switch로 처리하면 됩니다.

세 개의 상태를 각각 처리할 필요가 없어서 UI 코드도 깔끔해집니다. 김개발 씨가 눈을 빛냈습니다.

"아, 그러니까 하위 Provider들의 상태를 하나로 합쳐서 관리하는 거군요!" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.

이게 바로 Provider 결합의 핵심이에요. requireValue 덕분에 이 과정이 아주 자연스러워졌죠."

실전 팁

💡 - 결합된 Provider도 Future를 반환하도록 만들면 로딩 상태가 자동으로 관리됩니다.

  • 하위 Provider들은 독립적으로 캐시되므로 다른 곳에서 재사용해도 중복 요청이 발생하지 않습니다.
  • 결합하는 Provider가 많아지면 별도의 Record나 클래스로 묶어서 관리하세요.

3. 기존 방식 vs requireValue 비교

코드 리뷰 시간이 되었습니다. 김개발 씨는 requireValue를 배우기 전에 작성했던 코드와 지금 작성한 코드를 나란히 놓고 비교해보았습니다.

똑같은 기능을 하는데 코드 양이 확연히 줄어든 것을 보고 놀랐습니다. "와, 이렇게 다를 수가 있다니!"

기존 방식에서는 여러 AsyncValue를 결합할 때 중첩된 when 문이나 복잡한 조건 분기가 필요했습니다. 반면 requireValue를 사용하면 마치 동기 코드처럼 값을 직접 꺼내서 조합할 수 있습니다.

두 방식을 비교해보면 가독성과 유지보수성의 차이가 명확하게 드러납니다.

다음 코드를 살펴봅시다.

// 기존 방식: 중첩된 when으로 처리
@riverpod
Widget dashboardOld(Ref ref) {
  final userAsync = ref.watch(userProvider);
  final notificationsAsync = ref.watch(notificationsProvider);

  return userAsync.when(
    loading: () => LoadingWidget(),
    error: (e, s) => ErrorWidget(e),
    data: (user) => notificationsAsync.when(
      loading: () => LoadingWidget(),
      error: (e, s) => ErrorWidget(e),
      data: (notifications) => DashboardWidget(
        user: user,
        notifications: notifications,
      ),
    ),
  );
}

// 새로운 방식: requireValue로 간결하게
@riverpod
Future<DashboardData> dashboardNew(Ref ref) async {
  final user = ref.watch(userProvider).requireValue;
  final notifications = ref.watch(notificationsProvider).requireValue;

  return DashboardData(user: user, notifications: notifications);
}

// UI에서 단일 when으로 처리
Consumer(
  builder: (context, ref, _) {
    final dashboard = ref.watch(dashboardNewProvider);
    return dashboard.when(
      loading: () => LoadingWidget(),
      error: (e, s) => ErrorWidget(e),
      data: (data) => DashboardWidget(data: data),
    );
  },
)

김개발 씨가 예전에 작성한 코드를 보면서 한숨을 쉬었습니다. 불과 한 달 전에 작성한 건데 벌써 읽기가 힘듭니다.

when 안에 when이 들어가고, 그 안에 또 when이 들어가는 구조였습니다. 당시에는 "이게 최선인가 보다"라고 생각했는데, 이제 보니 더 좋은 방법이 있었던 것입니다.

기존 방식의 문제점을 살펴봅시다. 첫 번째 문제는 코드의 들여쓰기 지옥입니다.

두 개의 AsyncValue를 조합하려면 when이 두 번 중첩됩니다. 세 개면 세 번, 네 개면 네 번.

들여쓰기가 깊어질수록 코드를 읽기가 힘들어집니다. 어디서 어떤 값을 처리하는지 파악하려면 집중해서 따라가야 합니다.

두 번째 문제는 중복 코드입니다. 각각의 when마다 loading과 error 케이스를 처리해야 합니다.

로딩 위젯과 에러 위젯이 반복적으로 등장합니다. 만약 로딩 UI를 바꾸고 싶다면?

모든 when을 찾아서 수정해야 합니다. 세 번째 문제는 확장성입니다.

새로운 데이터 소스를 추가하려면 기존 코드를 크게 수정해야 합니다. 중첩 구조를 한 단계 더 깊게 만들어야 하니까요.

이런 구조는 시간이 지날수록 유지보수가 어려워집니다. 이제 requireValue 방식을 살펴봅시다.

결합 로직과 UI 로직이 분리되었습니다. dashboardNewProvider에서는 순수하게 데이터를 결합하는 일만 합니다.

UI에서는 결합된 단일 AsyncValue만 처리하면 됩니다. 관심사가 명확하게 분리된 것입니다.

코드의 양도 크게 줄었습니다. 중첩된 when이 사라지고, 각 Provider에서 requireValue로 값을 꺼내 바로 조합합니다.

마치 일반 변수를 다루는 것처럼 자연스럽습니다. 새로운 데이터 소스를 추가하려면?

그냥 한 줄 더 추가하면 됩니다. 로딩과 에러 처리도 단순해졌습니다.

UI에서 when을 딱 한 번만 사용합니다. 로딩 UI를 바꾸고 싶다면 그 한 곳만 수정하면 됩니다.

에러 처리도 마찬가지입니다. 중복이 사라지니 실수할 여지도 줄어듭니다.

박시니어 씨가 덧붙였습니다. "물론 기존 방식이 나쁜 건 아니에요.

각 데이터마다 다른 로딩 UI를 보여주고 싶다면 기존 방식이 더 적합할 수도 있어요." 그렇습니다. requireValue 방식은 모든 데이터가 함께 로드되어야 하는 상황에 최적화되어 있습니다.

"전부 아니면 전무" 방식으로 데이터를 처리할 때 빛을 발합니다. 반면 각 데이터의 로딩 상태를 개별적으로 보여주고 싶다면 기존 방식이 더 적합합니다.

김개발 씨가 정리했습니다. "결국 상황에 맞게 선택하는 거군요.

하지만 대부분의 경우에는 requireValue가 더 깔끔하겠네요!"

실전 팁

💡 - 데이터가 함께 로드되어야 하는 경우 requireValue 방식을 선택하세요.

  • 각 데이터의 로딩 상태를 개별 표시해야 한다면 기존의 중첩 when 방식이 적합합니다.
  • 기존 코드를 마이그레이션할 때는 먼저 테스트 코드가 있는지 확인하세요.

4. 안전한 사용 조건

김개발 씨가 신이 나서 requireValue를 여기저기 적용하기 시작했습니다. 그런데 갑자기 앱이 크래시되었습니다.

콘솔에는 "StateError: Value is loading"이라는 에러 메시지가 출력되어 있었습니다. 박시니어 씨가 다가와 말했습니다.

"requireValue는 아무 데서나 쓰면 안 돼요. 사용 조건이 있어요."

requireValue는 강력하지만, 특정 조건이 충족될 때만 안전하게 사용할 수 있습니다. 잘못 사용하면 런타임 에러가 발생합니다.

마치 높은 다이빙대에서 뛰어내리기 전에 수영장에 물이 있는지 확인해야 하는 것처럼, requireValue를 호출하기 전에 값이 준비되어 있는지 보장해야 합니다.

다음 코드를 살펴봅시다.

// 안전한 사용 조건 1: 비동기 Provider 내부에서 사용
@riverpod
Future<CombinedData> combined(Ref ref) async {
  // 이 Provider가 Future를 반환하므로 안전
  final a = ref.watch(providerA).requireValue;
  final b = ref.watch(providerB).requireValue;
  return CombinedData(a, b);
}

// 안전한 사용 조건 2: skipLoadingOnRefresh와 함께
@riverpod
Future<Data> dataWithCache(Ref ref) async {
  final result = await fetchData();
  return result;
}

// UI에서 이전 데이터를 유지하며 새로고침
ref.watch(dataWithCacheProvider).when(
  skipLoadingOnRefresh: true, // 리프레시 시 로딩 상태 건너뜀
  loading: () => previousData, // 이전 데이터 표시
  error: (e, s) => ErrorWidget(e),
  data: (data) => DataWidget(data),
);

// 위험한 사용: 동기 컨텍스트에서 직접 호출
// 절대 하지 마세요!
class MyWidget extends ConsumerWidget {
  Widget build(BuildContext context, WidgetRef ref) {
    // 위험! 로딩 중이면 크래시
    final data = ref.watch(asyncProvider).requireValue;
    return Text(data.name);
  }
}

김개발 씨는 당황했습니다. 분명 박시니어 씨가 알려준 대로 했는데 왜 에러가 난 걸까요?

코드를 자세히 살펴보니 문제가 보였습니다. 위젯의 build 메서드에서 직접 requireValue를 호출한 것입니다.

박시니어 씨가 차근차근 설명했습니다. "requireValue가 안전하려면 두 가지 조건 중 하나가 충족되어야 해요." 첫 번째 조건은 비동기 Provider 내부에서 사용하는 것입니다.

@riverpod 어노테이션으로 만든 Provider가 Future를 반환하면, 그 내부에서 requireValue를 사용해도 안전합니다. 왜냐하면 requireValue가 던지는 예외를 Riverpod이 잡아서 해당 Provider를 로딩 상태로 만들어주기 때문입니다.

비유하자면, 안전망이 있는 서커스 공연과 같습니다. 고공에서 실수로 떨어져도 안전망이 받쳐주니까 다치지 않습니다.

비동기 Provider가 바로 그 안전망 역할을 합니다. 두 번째 조건은 값이 확실히 존재함을 보장할 수 있는 상황입니다.

예를 들어, 특정 화면에 진입하기 전에 이미 데이터를 로드해둔 경우입니다. 또는 skipLoadingOnRefresh를 사용하여 리프레시 중에도 이전 값이 유지되는 경우입니다.

그렇다면 위젯의 build 메서드에서는 왜 위험할까요? 위젯이 처음 빌드될 때, 아직 데이터가 로드되지 않았을 수 있습니다.

이 상태에서 requireValue를 호출하면 StateError가 발생합니다. Flutter는 이 에러를 잡지 못하고 앱이 크래시됩니다.

안전망 없이 고공에서 뛰어내린 것과 같습니다. 이 문제를 해결하는 방법은 여러 가지가 있습니다.

가장 권장되는 방법은 결합 로직을 별도의 비동기 Provider로 분리하는 것입니다. 앞서 살펴본 것처럼 dashboardProvider를 만들어서 그 안에서 requireValue를 사용합니다.

UI에서는 그 Provider를 watch하고 when으로 처리합니다. 또 다른 방법은 hasValue를 먼저 확인하는 것입니다.

AsyncValue에는 값이 있는지 확인하는 hasValue 속성이 있습니다. 이것으로 먼저 확인한 후 requireValue를 호출하면 안전합니다.

하지만 이 방법은 결국 조건문이 필요하므로 코드가 깔끔하지 않습니다. 김개발 씨가 물었습니다.

"그러면 언제 requireValue가 필요한 건가요? 항상 when을 쓰면 되지 않나요?" 박시니어 씨가 대답했습니다.

"when은 UI를 렌더링할 때 쓰고, requireValue는 데이터를 결합하거나 변환할 때 써요. 역할이 달라요." 정리하면, requireValue는 비동기 Provider의 내부라는 안전한 공간에서 사용해야 합니다.

그 외의 장소에서는 hasValue로 확인하거나, 애초에 when을 사용하는 것이 안전합니다.

실전 팁

💡 - requireValue는 반드시 비동기 Provider 내부에서 사용하세요.

  • 위젯의 build 메서드에서 직접 requireValue를 호출하지 마세요.
  • 불확실한 상황에서는 hasValue로 먼저 확인하거나 when을 사용하세요.

5. 에러 처리 전략

김개발 씨가 requireValue를 안전하게 사용하는 방법은 익혔지만, 새로운 의문이 생겼습니다. "만약 결합하는 Provider 중 하나에서 에러가 발생하면 어떻게 되나요?" 박시니어 씨가 미소를 지었습니다.

"좋은 질문이에요. 에러 처리 전략도 알아야 완벽하게 활용할 수 있죠."

여러 AsyncValue를 결합할 때 에러가 발생하면, 그 에러는 상위 Provider로 자동 전파됩니다. 하지만 때로는 일부 에러를 무시하거나, 기본값으로 대체하거나, 특정 에러만 다르게 처리하고 싶을 수 있습니다.

Riverpod은 이런 다양한 에러 처리 전략을 유연하게 구현할 수 있도록 해줍니다.

다음 코드를 살펴봅시다.

// 전략 1: 에러 자동 전파 (기본 동작)
@riverpod
Future<DashboardData> dashboard(Ref ref) async {
  // 하나라도 에러면 이 Provider도 에러 상태
  final user = ref.watch(userProvider).requireValue;
  final notifications = ref.watch(notificationsProvider).requireValue;
  return DashboardData(user, notifications);
}

// 전략 2: 일부 데이터에 기본값 사용
@riverpod
Future<DashboardData> dashboardWithFallback(Ref ref) async {
  final user = ref.watch(userProvider).requireValue;

  // 알림은 실패해도 빈 리스트로 대체
  final notificationsAsync = ref.watch(notificationsProvider);
  final notifications = notificationsAsync.valueOrNull ?? [];

  return DashboardData(user, notifications);
}

// 전략 3: 특정 에러만 다르게 처리
@riverpod
Future<DashboardData> dashboardWithErrorHandling(Ref ref) async {
  final userAsync = ref.watch(userProvider);

  // 인증 에러는 상위로 전파, 나머지는 기본값 사용
  if (userAsync case AsyncError(:final error)) {
    if (error is AuthenticationException) {
      throw error; // 상위로 전파
    }
  }

  final user = userAsync.valueOrNull ?? User.guest();
  final notifications = ref.watch(notificationsProvider).valueOrNull ?? [];

  return DashboardData(user, notifications);
}

에러 처리는 실무에서 매우 중요합니다. 사용자에게 좋은 경험을 제공하려면 에러 상황도 우아하게 처리해야 하기 때문입니다.

requireValue를 사용할 때 에러가 어떻게 흐르는지 이해해봅시다. 기본적으로 에러는 자동으로 전파됩니다.

결합된 Provider 중 하나에서 에러가 발생하면, requireValue가 그 에러를 다시 던집니다. Riverpod은 이 에러를 잡아서 상위 Provider를 에러 상태로 만듭니다.

UI에서는 when의 error 콜백에서 이 에러를 받아 처리합니다. 별도의 코드 없이 에러가 올바르게 전달되는 것입니다.

하지만 모든 에러가 똑같이 중요하지는 않습니다. 예를 들어 대시보드에서 사용자 정보는 필수지만, 알림 목록은 없어도 화면을 보여줄 수 있습니다.

이런 경우 알림 로드가 실패해도 빈 리스트로 대체하고 나머지는 정상적으로 보여주는 것이 더 좋은 사용자 경험입니다. 이때 valueOrNull을 활용합니다.

valueOrNull은 값이 있으면 반환하고, 로딩이나 에러 상태면 null을 반환합니다. null 병합 연산자(??)와 함께 사용하면 기본값을 쉽게 지정할 수 있습니다.

코드의 두 번째 예시가 바로 이 패턴입니다. 더 정교한 에러 처리가 필요할 때도 있습니다.

인증 관련 에러는 사용자에게 알리고 로그인 화면으로 보내야 합니다. 하지만 네트워크 일시 에러는 기본값을 보여주고 나중에 재시도하는 것이 나을 수 있습니다.

세 번째 예시처럼 에러의 종류에 따라 다르게 처리할 수 있습니다. Dart 3의 패턴 매칭을 활용하면 코드가 더 깔끔해집니다.

if (userAsync case AsyncError(:final error))라는 문법은 userAsync가 AsyncError 타입인지 확인하고, 맞다면 error 변수에 에러 값을 바인딩합니다. 이렇게 하면 타입 캐스팅 없이 안전하게 에러에 접근할 수 있습니다.

에러 처리 전략을 선택할 때 고려할 점이 있습니다. 필수 데이터와 선택 데이터를 구분하세요.

필수 데이터의 에러는 전파하고, 선택 데이터는 기본값으로 대체합니다. 또한 복구 가능한 에러와 치명적 에러를 구분하세요.

복구 가능한 에러는 재시도 버튼을 제공하고, 치명적 에러는 사용자에게 명확히 알립니다. 김개발 씨가 정리했습니다.

"결국 에러도 비즈니스 요구사항에 맞게 처리해야 하는 거군요. 무조건 에러 화면을 보여주는 게 능사가 아니네요." 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 사용자 입장에서 생각하면 답이 보여요.

알림 목록 하나 못 불러왔다고 대시보드 전체가 안 보이면 답답하잖아요."

실전 팁

💡 - 필수 데이터는 requireValue로, 선택 데이터는 valueOrNull로 처리하세요.

  • 에러의 종류에 따라 전파할지, 무시할지, 기본값을 사용할지 결정하세요.
  • 사용자 경험을 최우선으로 고려하여 에러 처리 전략을 설계하세요.

6. 실전 활용 패턴

이제 김개발 씨는 requireValue의 개념과 안전한 사용법을 모두 익혔습니다. 마지막으로 박시니어 씨가 실전에서 자주 사용하는 패턴들을 공유해주었습니다.

"이 패턴들만 알면 대부분의 상황에서 깔끔하게 처리할 수 있어요."

실무에서 requireValue는 다양한 상황에서 활용됩니다. 사용자 인증 정보를 기반으로 다른 데이터를 조회하거나, 여러 API 응답을 조합하여 화면 데이터를 구성하거나, 캐시된 데이터를 활용하여 즉각적인 UI 렌더링을 하는 등 실전 패턴을 익혀두면 코드 품질이 크게 향상됩니다.

다음 코드를 살펴봅시다.

// 패턴 1: 인증 기반 데이터 조회
@riverpod
Future<UserProfile> userProfile(Ref ref) async {
  // 인증된 사용자 정보를 먼저 가져옴
  final auth = ref.watch(authProvider).requireValue;

  // 사용자 ID로 프로필 조회
  final profile = await ref.watch(
    profileProvider(userId: auth.userId).future
  );

  return profile;
}

// 패턴 2: 여러 API 응답 조합
@riverpod
Future<ProductDetail> productDetail(Ref ref, String productId) async {
  // 병렬로 여러 데이터 로드
  final product = ref.watch(productProvider(productId)).requireValue;
  final reviews = ref.watch(reviewsProvider(productId)).requireValue;
  final related = ref.watch(relatedProductsProvider(productId)).requireValue;

  return ProductDetail(
    product: product,
    reviews: reviews,
    relatedProducts: related,
  );
}

// 패턴 3: 데이터 변환 파이프라인
@riverpod
Future<List<ChartData>> salesChart(Ref ref) async {
  final sales = ref.watch(salesDataProvider).requireValue;
  final categories = ref.watch(categoryProvider).requireValue;

  // 원시 데이터를 차트용 데이터로 변환
  return sales.map((sale) {
    final category = categories.firstWhere((c) => c.id == sale.categoryId);
    return ChartData(
      label: category.name,
      value: sale.amount,
      color: category.color,
    );
  }).toList();
}

// 패턴 4: 조건부 데이터 로드
@riverpod
Future<AdminDashboard> adminDashboard(Ref ref) async {
  final user = ref.watch(userProvider).requireValue;

  // 관리자만 추가 데이터 로드
  if (!user.isAdmin) {
    throw UnauthorizedException('관리자 권한이 필요합니다');
  }

  final stats = ref.watch(adminStatsProvider).requireValue;
  final logs = ref.watch(auditLogsProvider).requireValue;

  return AdminDashboard(user: user, stats: stats, logs: logs);
}

김개발 씨는 박시니어 씨가 보여주는 코드들을 유심히 살펴보았습니다. 단순한 예제가 아니라 실제 프로젝트에서 바로 활용할 수 있는 패턴들이었습니다.

첫 번째 패턴은 인증 기반 데이터 조회입니다. 많은 앱에서 로그인한 사용자의 정보를 기반으로 추가 데이터를 조회합니다.

예를 들어 사용자 ID로 프로필을 가져오거나, 사용자의 권한에 따라 다른 데이터를 보여줍니다. 이때 authProvider를 requireValue로 먼저 가져온 후, 그 값을 사용하여 다음 Provider를 호출합니다.

두 번째 패턴은 여러 API 응답 조합입니다. 상품 상세 페이지를 예로 들어봅시다.

상품 기본 정보, 리뷰 목록, 연관 상품까지 세 가지 API를 호출해야 합니다. 각각을 독립된 Provider로 만들고, productDetailProvider에서 세 값을 requireValue로 가져와 하나의 객체로 합칩니다.

세 API가 병렬로 호출되므로 성능도 좋습니다. 세 번째 패턴은 데이터 변환 파이프라인입니다.

서버에서 받은 원시 데이터를 UI에 맞게 변환해야 할 때가 있습니다. 매출 데이터와 카테고리 정보를 조합하여 차트용 데이터로 변환하는 것이 좋은 예입니다.

requireValue로 두 데이터를 가져온 후, 변환 로직을 적용합니다. 이렇게 하면 변환 로직이 Provider 안에 캡슐화되어 재사용하기 좋습니다.

네 번째 패턴은 조건부 데이터 로드입니다. 관리자 대시보드처럼 특정 조건을 만족해야만 접근할 수 있는 화면이 있습니다.

먼저 사용자 정보를 가져와 권한을 확인하고, 권한이 없으면 예외를 던집니다. 권한이 있다면 추가 데이터를 로드합니다.

이렇게 하면 권한 체크와 데이터 로드가 하나의 흐름으로 자연스럽게 연결됩니다. 이 패턴들의 공통점이 있습니다.

모두 비동기 Provider 내부에서 requireValue를 사용합니다. 안전한 사용 조건을 지키고 있는 것입니다.

또한 각 하위 Provider가 독립적으로 캐시됩니다. productProvider는 여러 곳에서 재사용되어도 API를 중복 호출하지 않습니다.

김개발 씨가 감탄했습니다. "패턴만 익혀두면 복잡한 데이터 흐름도 깔끔하게 정리할 수 있겠네요!" 박시니어 씨가 조언을 덧붙였습니다.

"처음에는 간단한 패턴부터 적용해보세요. 익숙해지면 점점 복잡한 상황에도 자연스럽게 적용할 수 있게 됩니다." requireValue는 Riverpod 3.0에서 도입된 작은 기능처럼 보이지만, Provider 결합의 패러다임을 바꾸는 중요한 변화입니다.

이 기능을 잘 활용하면 비동기 상태 관리 코드가 훨씬 깔끔하고 유지보수하기 쉬워집니다. 여러분도 오늘 배운 내용을 프로젝트에 적용해보세요.

처음에는 작은 부분부터 시작하여 점점 범위를 넓혀가면 됩니다. 그러면 어느새 requireValue가 없던 시절로는 돌아가기 싫어질 것입니다.

실전 팁

💡 - 작은 기능부터 requireValue를 적용해보고 점점 범위를 넓혀가세요.

  • Provider를 작게 나누고 조합하는 방식으로 설계하면 재사용성이 높아집니다.
  • 팀과 패턴을 공유하여 일관된 코드 스타일을 유지하세요.

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

#Flutter#Riverpod#AsyncValue#requireValue#StateManagement#Flutter,State Management

댓글 (0)

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

함께 보면 좋은 카드 뉴스