🤖

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

⚠️

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

이미지 로딩 중...

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

AI Generated

2025. 12. 11. · 8 Views

Riverpod 3.0 requireValue로 Provider 결합하기

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


목차

  1. requireValue란_무엇인가
  2. 두_API_결과_합치기
  3. 기존_await_방식과_비교
  4. 에러_발생_시_동작
  5. 사용_조건과_주의사항
  6. 대시보드_데이터_결합_예제

1. requireValue란 무엇인가

어느 날 김개발 씨는 대시보드 화면을 만들고 있었습니다. 사용자 정보와 최근 주문 목록, 두 개의 API 데이터를 동시에 보여줘야 했죠.

선배 개발자 박시니어 씨가 다가와 말했습니다. "Riverpod 3.0부터 requireValue라는 편리한 기능이 생겼어요."

requireValue는 Riverpod 3.0에서 새로 추가된 기능으로, AsyncValue에서 데이터가 준비되었다고 확신할 때 바로 값을 꺼내는 방법입니다. 마치 택배 상자를 열기 전에 이미 물건이 들어있다는 걸 알고 있는 것과 같습니다.

이를 통해 여러 Provider의 데이터를 깔끔하게 결합할 수 있습니다.

다음 코드를 살펴봅시다.

// 사용자 정보 Provider
final userProvider = FutureProvider<User>((ref) async {
  // API 호출
  return await fetchUser();
});

// requireValue로 사용자 ID 꺼내기
final userPostsProvider = FutureProvider<List<Post>>((ref) async {
  // userProvider의 데이터를 requireValue로 바로 접근
  final userId = ref.watch(userProvider).requireValue.id;

  // 해당 사용자의 게시글 조회
  return await fetchPosts(userId);
});

김개발 씨는 Flutter로 앱을 만든 지 두 달이 되었습니다. Riverpod도 어느 정도 익숙해졌다고 생각했죠.

그런데 오늘 마주한 요구사항은 조금 까다로웠습니다. "사용자 정보를 먼저 가져온 다음, 그 사용자의 주문 목록을 보여주세요." 간단해 보였지만 막상 코드를 작성하려니 머리가 복잡해졌습니다.

Provider 두 개를 어떻게 연결해야 할까요? requireValue란 정확히 무엇일까요? 쉽게 비유하자면, requireValue는 마치 배달 앱에서 "배달 완료" 알림을 받고 현관문을 여는 것과 같습니다.

이미 음식이 도착했다는 걸 알고 있으니, 확인 절차 없이 바로 문을 열어 음식을 가져오는 것이죠. AsyncValue도 마찬가지입니다.

데이터가 이미 준비되었다는 확신이 있을 때, requireValue로 바로 꺼내 쓸 수 있습니다. Riverpod 3.0 이전에는 어땠을까요? 예전에는 AsyncValue에서 값을 꺼내려면 when이나 maybeWhen 같은 메서드를 사용해야 했습니다.

코드가 길어지고, 중첩이 깊어졌습니다. 특히 여러 Provider를 조합할 때는 코드가 금세 복잡해져서 읽기 어려웠습니다.

더 큰 문제는 불필요한 보일러플레이트 코드였습니다. 분명히 데이터가 준비되었다는 걸 개발자는 알고 있는데, Riverpod에게 그걸 설명하기 위해 when 블록을 작성해야 했죠.

마치 이미 신분증을 확인했는데 또 확인하는 것 같은 느낌이었습니다. requireValue의 등장 바로 이런 불편함을 해결하기 위해 Riverpod 3.0에서 requireValue가 추가되었습니다.

requireValue를 사용하면 AsyncValue에서 값을 한 줄로 꺼낼 수 있습니다. 또한 코드의 의도가 명확해집니다.

"나는 이 시점에 데이터가 준비되었다고 확신한다"는 의미를 코드로 표현하는 것이죠. 무엇보다 여러 Provider를 조합할 때 코드가 훨씬 간결해집니다는 큰 이점이 있습니다.

코드를 자세히 살펴보겠습니다 먼저 위의 예제를 한 줄씩 분석해봅시다. userProvider는 사용자 정보를 가져오는 일반적인 FutureProvider입니다.

그 다음, userPostsProvider가 핵심입니다. 여기서 ref.watch(userProvider).requireValue.id라는 코드를 볼 수 있습니다.

이 한 줄이 마법을 부리는 부분입니다. ref.watch(userProvider)는 AsyncValue<User>를 반환합니다.

여기에 requireValue를 호출하면 User 객체를 바로 얻을 수 있습니다. 그리고 .id로 사용자 ID에 접근하죠.

이 ID를 사용해서 해당 사용자의 게시글을 조회합니다. 실무에서는 어떻게 활용할까요? 쇼핑몰 앱을 만든다고 가정해봅시다.

먼저 사용자가 로그인하면 사용자 정보를 가져옵니다. 그 다음 사용자의 장바구니, 최근 본 상품, 주문 내역을 각각 다른 Provider로 관리해야 합니다.

requireValue를 활용하면 이런 의존 관계를 깔끔하게 표현할 수 있습니다. 토스, 당근마켓 같은 많은 앱에서 이런 패턴을 사용하고 있습니다.

주의할 점이 있습니다 초보 개발자들이 흔히 하는 실수는 데이터가 준비되지 않았는데 requireValue를 호출하는 것입니다. 예를 들어 userProvider가 아직 로딩 중인데 requireValue를 호출하면 에러가 발생합니다.

따라서 Provider 간의 의존 관계가 명확할 때만 사용해야 합니다. 정리하면 박시니어 씨의 설명을 들은 김개발 씨는 환하게 웃었습니다.

"이렇게 간단하게 Provider를 연결할 수 있다니!" requireValue를 사용하면 복잡한 비동기 로직을 마치 동기 코드처럼 깔끔하게 작성할 수 있습니다. 여러분도 다음 프로젝트에서 Provider를 조합할 일이 있다면 requireValue를 떠올려 보세요.

실전 팁

💡 - requireValue는 데이터가 준비되었다는 확신이 있을 때만 사용하세요

  • Provider 간 의존 관계가 명확한 경우에 활용하면 코드가 훨씬 깔끔해집니다

2. 두 API 결과 합치기

김개발 씨에게 새로운 과제가 주어졌습니다. 사용자 프로필 화면에 사용자 정보와 통계 데이터를 함께 보여줘야 했죠.

두 개의 독립적인 API를 호출하고, 결과를 하나로 합쳐야 했습니다. "requireValue를 쓰면 이것도 쉽게 할 수 있을 것 같은데..." 김개발 씨가 중얼거렸습니다.

여러 Provider의 데이터를 하나로 결합하는 것은 실무에서 매우 흔한 패턴입니다. requireValue를 사용하면 각 Provider의 데이터를 마치 레고 블록처럼 조립할 수 있습니다.

두 개 이상의 비동기 데이터를 기다렸다가, 모두 준비되면 합치는 작업이 단 몇 줄로 가능합니다.

다음 코드를 살펴봅시다.

// 사용자 기본 정보
final userInfoProvider = FutureProvider<UserInfo>((ref) async {
  return await fetchUserInfo();
});

// 사용자 통계 데이터
final userStatsProvider = FutureProvider<UserStats>((ref) async {
  return await fetchUserStats();
});

// 두 데이터를 결합한 프로필
final userProfileProvider = FutureProvider<UserProfile>((ref) async {
  // 두 Provider가 모두 완료될 때까지 대기
  final info = ref.watch(userInfoProvider).requireValue;
  final stats = ref.watch(userStatsProvider).requireValue;

  // 결합하여 새로운 객체 생성
  return UserProfile(info: info, stats: stats);
});

김개발 씨는 화면을 스케치했습니다. 상단에는 사용자 이름과 프로필 사진이 필요했고, 하단에는 게시글 수, 팔로워 수, 좋아요 수가 표시되어야 했습니다.

문제는 이 데이터들이 서로 다른 API에서 온다는 점이었습니다. "두 API를 따로 호출하고, 둘 다 완료되면 화면에 보여주면 되겠지?" 김개발 씨는 생각했습니다.

하지만 어떻게 구현해야 할까요? 데이터 결합은 왜 필요할까요? 실무에서 API는 보통 기능별로 분리되어 있습니다.

사용자 정보를 주는 API, 통계를 주는 API, 설정을 주는 API가 각각 따로 존재하죠. 하지만 화면에서는 이 모든 데이터를 한꺼번에 보여줘야 할 때가 많습니다.

마치 요리를 할 때 재료를 각각 다른 곳에서 사 와야 하는 것과 같습니다. 채소는 채소 가게, 고기는 정육점, 양념은 마트에서 구매합니다.

그리고 집에서 이 모든 재료를 합쳐 하나의 요리를 완성하는 것이죠. 예전 방식의 문제점 Riverpod 3.0 이전에는 이런 데이터 결합이 상당히 번거로웠습니다.

when 메서드를 중첩해서 사용해야 했고, 코드가 피라미드처럼 깊어졌습니다. 가독성이 떨어졌고, 실수하기도 쉬웠습니다.

특히 세 개 이상의 Provider를 결합할 때는 코드가 거의 읽을 수 없을 정도로 복잡해졌습니다. 신입 개발자가 그 코드를 보면 "이게 대체 뭐지?" 하고 당황할 정도였죠.

requireValue로 깔끔하게 해결 requireValue를 사용하면 이 모든 문제가 해결됩니다. 위 코드를 보면, userProfileProvider에서 두 개의 Provider를 watch하고 있습니다.

각각 requireValue로 값을 꺼냅니다. 그리고 UserProfile이라는 새로운 객체를 만들어 두 데이터를 담습니다.

코드가 직관적이고 읽기 쉽습니다. 어떻게 동작하는 걸까요? Riverpod는 똑똑합니다.

userProfileProvider가 userInfoProvider와 userStatsProvider를 watch하고 있다는 걸 알고 있습니다. 따라서 두 Provider가 모두 데이터를 준비할 때까지 기다립니다.

두 Provider 중 하나라도 아직 로딩 중이면, userProfileProvider도 로딩 상태로 남아 있습니다. 둘 다 완료되면 그때 requireValue가 실행되고, UserProfile 객체가 만들어집니다.

마치 모든 재료가 준비될 때까지 기다렸다가 요리를 시작하는 것과 같습니다. 실전 예시를 볼까요? 블로그 앱을 만든다고 가정해봅시다.

게시글 상세 화면에는 게시글 내용, 작성자 정보, 댓글 목록이 모두 표시되어야 합니다. 이 세 가지 데이터는 각각 다른 API에서 가져옵니다.

requireValue를 활용하면 postProvider, authorProvider, commentsProvider를 각각 만들고, 이들을 결합한 postDetailProvider를 쉽게 구성할 수 있습니다. 코드는 간결하고, 각 Provider의 역할은 명확합니다.

주의해야 할 점 모든 Provider가 성공적으로 완료되어야 requireValue가 작동합니다. 만약 userInfoProvider는 성공했지만 userStatsProvider가 실패하면 어떻게 될까요?

이 경우 userProfileProvider는 에러 상태가 됩니다. 따라서 에러 처리를 꼭 고려해야 합니다.

UI에서 AsyncValue의 when 메서드를 사용해 로딩, 에러, 성공 상태를 각각 처리하는 것이 좋습니다. 정리 김개발 씨는 코드를 완성하고 뿌듯해했습니다.

두 개의 API 데이터를 깔끔하게 결합했고, 코드는 누가 봐도 이해하기 쉬웠습니다. requireValue는 마치 퍼즐 조각을 맞추듯 여러 데이터를 자연스럽게 결합할 수 있게 해줍니다.

여러분도 다음에 여러 API 데이터를 합쳐야 할 일이 생기면 이 패턴을 떠올려 보세요.

실전 팁

💡 - 두 개 이상의 Provider를 결합할 때 requireValue를 활용하면 코드가 간결해집니다

  • UI 레이어에서는 AsyncValue.when을 사용해 로딩/에러/성공 상태를 처리하세요

3. 기존 await 방식과 비교

점심시간, 김개발 씨는 같은 팀 이주니어 씨와 코드 리뷰를 하고 있었습니다. 이주니어 씨가 물었습니다.

"저는 그냥 async/await로 작성했는데, requireValue를 꼭 써야 하나요? 뭐가 다른 건가요?" 좋은 질문이었습니다.

async/await 방식requireValue 방식은 언뜻 비슷해 보이지만, Riverpod의 반응성을 활용한다는 점에서 큰 차이가 있습니다. await는 단순히 비동기 작업을 기다리는 것이지만, requireValue는 Provider의 변화를 추적하고 자동으로 재계산합니다.

다음 코드를 살펴봅시다.

// 방식 1: async/await (추천하지 않음)
final userPostsProviderOld = FutureProvider<List<Post>>((ref) async {
  final userAsync = await ref.read(userProvider.future);
  return await fetchPosts(userAsync.id);
});

// 방식 2: requireValue (권장)
final userPostsProviderNew = FutureProvider<List<Post>>((ref) async {
  final userId = ref.watch(userProvider).requireValue.id;
  return await fetchPosts(userId);
});

// 차이점: userProvider가 변경되면?
// 방식 1: 재계산되지 않음 (read 사용)
// 방식 2: 자동으로 재계산됨 (watch 사용)

이주니어 씨의 코드를 보니, ref.read(userProvider.future)를 await하고 있었습니다. 동작은 했지만, 박시니어 씨는 고개를 가로저었습니다.

"동작은 하지만, Riverpod의 강력한 기능을 놓치고 있어요." async/await는 우리에게 익숙합니다 Dart를 배우면 가장 먼저 접하는 것이 Future와 async/await입니다. 비동기 작업을 기다리고, 결과를 받아 다음 작업을 진행하는 방식이죠.

직관적이고 이해하기 쉽습니다. 마치 커피숍에서 주문하고 기다리는 것과 같습니다.

주문하면 진동벨을 받고, 진동이 울리면 카운터로 가서 커피를 받습니다. async/await도 이처럼 "작업이 완료될 때까지 기다렸다가 결과를 받는" 방식입니다.

그런데 왜 requireValue를 써야 할까요? 핵심은 반응성입니다. Riverpod의 가장 큰 장점은 데이터가 변경되면 자동으로 UI를 업데이트한다는 것입니다.

이를 위해서는 ref.watch를 사용해야 합니다. ref.read는 단순히 현재 값을 한 번 읽어오는 것입니다.

이후 Provider가 변경되어도 알아채지 못합니다. 반면 ref.watch는 Provider를 구독합니다.

Provider가 변경되면 자동으로 알림을 받고, 다시 계산합니다. 실제 시나리오로 비교해봅시다 사용자가 로그아웃하고 다른 계정으로 로그인했다고 가정해봅시다.

방식 1 (await + read): userProvider는 새로운 사용자 정보로 업데이트됩니다. 하지만 userPostsProviderOld는 여전히 이전 사용자의 게시글을 보여줍니다.

ref.read를 사용했기 때문에 userProvider의 변경을 감지하지 못하는 것이죠. 버그가 발생합니다.

방식 2 (requireValue + watch): userProvider가 새로운 사용자로 업데이트되면, ref.watch 덕분에 userPostsProviderNew도 자동으로 재계산됩니다. 새로운 사용자의 게시글을 가져옵니다.

완벽하게 동작합니다. 코드를 다시 살펴봅시다 첫 번째 방식은 ref.read(userProvider.future)를 사용합니다.

.future는 FutureProvider의 Future를 직접 가져오는 방법입니다. await로 기다렸다가 결과를 받습니다.

간단하지만, 구독하지 않습니다. 두 번째 방식은 ref.watch(userProvider)를 사용합니다.

이것은 AsyncValue<User>를 반환합니다. requireValue로 User 객체를 꺼냅니다.

이 과정에서 userProvider를 구독하게 됩니다. 성능 차이도 있을까요? 거의 없습니다.

두 방식 모두 비동기 작업을 기다리는 것은 동일합니다. 차이는 반응성에 있습니다.

requireValue 방식이 Riverpod의 철학에 더 부합하고, 유지보수하기 쉬운 코드를 만듭니다. 그럼 언제 read를 쓰나요? ref.read는 이벤트 핸들러버튼 클릭 시 사용합니다.

예를 들어 "저장" 버튼을 눌렀을 때 현재 사용자 정보를 한 번만 읽어오는 경우입니다. 이런 경우에는 구독할 필요가 없으므로 read가 적합합니다.

하지만 Provider 간의 의존 관계를 표현할 때는 항상 watch를 사용해야 합니다. 이것이 Riverpod를 제대로 활용하는 방법입니다.

정리 박시니어 씨의 설명을 듣고, 이주니어 씨는 "아하!" 하고 이해했습니다. async/await는 단순히 작업을 기다리는 것이고, requireValue는 Riverpod의 반응성을 활용하는 것입니다.

비슷해 보이지만 본질적으로 다른 접근 방식입니다. 여러분도 Provider를 조합할 때는 꼭 watch + requireValue 패턴을 사용하세요.

실전 팁

💡 - Provider 간 의존 관계는 ref.watch + requireValue로 표현하세요

  • ref.read는 이벤트 핸들러나 일회성 작업에만 사용합니다

4. 에러 발생 시 동작

김개발 씨는 requireValue를 활용해 코드를 작성했습니다. 잘 동작했죠.

그런데 갑자기 앱이 멈췄습니다. 에러 메시지를 보니 "Bad state: requireValue called on AsyncError"라고 나왔습니다.

"이게 무슨 일이지?" 김개발 씨는 당황했습니다.

requireValue는 데이터가 준비되었을 때만 안전하게 사용할 수 있습니다. 만약 Provider가 에러 상태이거나 로딩 중일 때 requireValue를 호출하면 예외가 발생합니다.

이는 버그를 조기에 발견하게 해주는 안전장치이자, 동시에 주의해서 사용해야 하는 이유입니다.

다음 코드를 살펴봅시다.

// API 호출이 실패할 수 있는 Provider
final userProvider = FutureProvider<User>((ref) async {
  final response = await fetchUser();
  if (response == null) {
    throw Exception('사용자를 찾을 수 없습니다');
  }
  return response;
});

// requireValue 사용 - 주의!
final userPostsProvider = FutureProvider<List<Post>>((ref) async {
  // userProvider가 에러 상태라면?
  // -> StateError 예외 발생!
  final userId = ref.watch(userProvider).requireValue.id;
  return await fetchPosts(userId);
});

// 안전한 처리 방법
final userPostsSafeProvider = FutureProvider<List<Post>?>((ref) async {
  final userAsync = ref.watch(userProvider);

  // 에러나 로딩 상태면 null 반환
  if (!userAsync.hasValue) return null;

  final userId = userAsync.requireValue.id;
  return await fetchPosts(userId);
});

김개발 씨는 디버깅을 시작했습니다. 로그를 살펴보니 userProvider에서 API 호출이 실패했다는 걸 발견했습니다.

서버가 일시적으로 응답하지 않았던 것이죠. 그런데 왜 앱이 멈춘 걸까요?

requireValue는 확신을 요구합니다 requireValue라는 이름에는 "require", 즉 "요구한다"는 의미가 담겨 있습니다. 데이터가 반드시 있어야 한다는 뜻이죠.

만약 데이터가 없으면 어떻게 될까요? 예외가 발생합니다.

마치 은행 ATM에서 돈을 뽑는 것과 비슷합니다. 계좌에 잔액이 있다는 확신이 있을 때 출금 버튼을 누릅니다.

만약 잔액이 없는데 출금을 시도하면 에러 메시지가 나오죠. requireValue도 마찬가지입니다.

AsyncValue의 세 가지 상태 AsyncValue는 세 가지 상태를 가질 수 있습니다. 첫 번째는 AsyncLoading입니다.

아직 데이터를 가져오는 중입니다. 두 번째는 AsyncData입니다.

데이터를 성공적으로 가져왔습니다. 세 번째는 AsyncError입니다.

에러가 발생했습니다. requireValue는 오직 AsyncData 상태일 때만 안전합니다.

AsyncLoading이나 AsyncError 상태에서 호출하면 StateError 예외가 던져집니다. 언제 이런 문제가 생길까요? 실무에서는 API 호출이 항상 성공하지 않습니다.

네트워크가 끊길 수 있고, 서버가 다운될 수 있습니다. 사용자의 인증 토큰이 만료될 수도 있죠.

만약 userProvider가 네트워크 에러로 실패했는데, userPostsProvider에서 requireValue를 호출한다면 어떻게 될까요? StateError가 발생하고, 앱이 크래시될 수 있습니다.

사용자에게는 최악의 경험입니다. 어떻게 안전하게 사용할까요? 위 코드의 userPostsSafeProvider를 보세요.

requireValue를 호출하기 전에 userAsync.hasValue로 확인합니다. hasValue는 AsyncData 상태일 때만 true를 반환합니다.

이렇게 하면 에러나 로딩 상태일 때는 null을 반환하고, 데이터가 준비되었을 때만 requireValue를 호출합니다. 안전합니다.

또 다른 방법도 있습니다 AsyncValue의 when 메서드를 사용할 수도 있습니다. when은 로딩, 에러, 데이터 상태를 각각 처리할 수 있게 해줍니다.

하지만 이렇게 하면 requireValue의 장점인 간결함이 사라집니다. 따라서 Provider 간 의존 관계가 명확하고, 상위 Provider가 거의 항상 성공한다는 보장이 있을 때 requireValue를 사용하는 것이 좋습니다.

그렇지 않다면 hasValue 체크를 추가하거나, when 메서드를 사용하세요. Riverpod의 에러 전파 한 가지 좋은 소식이 있습니다.

userPostsProvider에서 StateError가 발생하면, 이 Provider 자체가 AsyncError 상태가 됩니다. 따라서 UI에서 AsyncValue.when을 사용하면 에러를 잡아서 적절한 메시지를 보여줄 수 있습니다.

즉, requireValue를 사용해도 앱이 완전히 크래시되지는 않습니다. 다만 예외가 발생하므로, 디버그 모드에서는 눈에 띄게 됩니다.

이것은 오히려 장점입니다. 개발 중에 문제를 빨리 발견할 수 있으니까요.

정리 김개발 씨는 이제 이해했습니다. requireValue는 강력하지만, 데이터가 준비되었다는 확신이 있을 때만 사용해야 합니다.

필요하다면 hasValue로 체크하고, UI 레이어에서는 항상 when 메서드로 에러를 처리해야 합니다. 이렇게 하면 안전하고 견고한 앱을 만들 수 있습니다.

실전 팁

💡 - requireValue 호출 전에 hasValue로 체크하면 안전합니다

  • UI 레이어에서는 AsyncValue.when으로 로딩/에러/성공 상태를 모두 처리하세요

5. 사용 조건과 주의사항

김개발 씨는 requireValue가 편리하다는 것을 알았습니다. 그래서 모든 곳에 requireValue를 사용하기 시작했죠.

그런데 박시니어 씨가 코드 리뷰에서 댓글을 남겼습니다. "여기는 requireValue를 쓰면 안 됩니다.

조건을 다시 확인해보세요."

requireValue는 만능이 아닙니다. Provider 간 의존 관계가 명확하고, 상위 Provider의 성공이 보장될 때만 안전하게 사용할 수 있습니다.

잘못 사용하면 예상치 못한 에러가 발생하거나, 코드의 의도가 불명확해질 수 있습니다.

다음 코드를 살펴봅시다.

// ❌ 잘못된 사용: 독립적인 두 Provider
final userProvider = FutureProvider<User>((ref) async {
  return await fetchUser();
});

final settingsProvider = FutureProvider<Settings>((ref) async {
  return await fetchSettings();
});

// 이건 위험합니다!
final dashboardProvider = FutureProvider<Dashboard>((ref) async {
  final user = ref.watch(userProvider).requireValue;
  final settings = ref.watch(settingsProvider).requireValue;
  // userProvider나 settingsProvider 중 하나라도 에러면?
  return Dashboard(user: user, settings: settings);
});

// ✅ 올바른 사용: 명확한 의존 관계
final userIdProvider = Provider<String>((ref) {
  // 이미 확정된 값
  return 'user123';
});

final userDataProvider = FutureProvider<User>((ref) async {
  final userId = ref.watch(userIdProvider); // requireValue 필요 없음
  return await fetchUser(userId);
});

김개발 씨는 박시니어 씨의 댓글을 보고 자신의 코드를 다시 살펴봤습니다. dashboardProvider에서 두 개의 독립적인 Provider를 requireValue로 가져오고 있었습니다.

"뭐가 문제지? 둘 다 watch하고 있는데?" 독립적인 Provider는 위험합니다 userProvider와 settingsProvider는 서로 관계가 없습니다.

각각 독립적으로 API를 호출하죠. 만약 settingsProvider는 성공했지만 userProvider가 실패하면 어떻게 될까요?

requireValue에서 예외가 발생합니다. 마치 두 개의 주사위를 동시에 던지는 것과 같습니다.

각각 1부터 6까지 나올 수 있죠. 두 주사위가 모두 6이 나올 거라고 확신할 수 있나요?

없습니다. requireValue를 사용하는 것은 "두 주사위가 모두 6이 나올 것이다"라고 확신하는 것과 같습니다.

명확한 의존 관계란 무엇일까요? "A가 있어야 B를 가져올 수 있다"는 관계가 명확한 의존 관계입니다. 예를 들어 사용자 ID가 있어야 사용자 정보를 가져올 수 있습니다.

게시글 ID가 있어야 댓글 목록을 가져올 수 있습니다. 이런 경우에는 상위 Provider(사용자 ID)가 성공해야만 하위 Provider(사용자 정보)를 호출합니다.

따라서 requireValue를 안전하게 사용할 수 있습니다. 올바른 사용 예시를 봅시다 위 코드의 userDataProvider를 보세요.

userIdProvider는 단순한 Provider입니다. FutureProvider가 아니죠.

즉, 이미 값이 확정되어 있습니다. 비동기가 아니므로 실패할 가능성도 없습니다.

이런 경우에는 ref.watch(userIdProvider)로 값을 바로 가져올 수 있습니다. requireValue도 필요 없습니다.

완벽하게 안전합니다. 그럼 독립적인 Provider는 어떻게 결합하나요? 독립적인 여러 Provider를 결합해야 할 때는 어떻게 해야 할까요?

두 가지 방법이 있습니다. 방법 1: hasValue 체크를 사용합니다.

모든 Provider가 hasValue를 만족하는지 확인하고, 그때만 requireValue를 호출합니다. 하나라도 준비되지 않았다면 null을 반환하거나 로딩 상태로 유지합니다.

방법 2: AsyncValue를 그대로 노출합니다. Provider를 결합하지 않고, UI 레이어에서 여러 AsyncValue를 각각 when으로 처리합니다.

코드가 조금 길어지지만, 더 안전하고 명확합니다. FutureProvider끼리의 결합 FutureProvider끼리 의존 관계가 있다면 requireValue를 사용할 수 있습니다.

예를 들어 userProvider가 성공해야만 userPostsProvider를 호출하는 구조라면, userPostsProvider에서 requireValue를 써도 됩니다. 하지만 병렬로 실행되는 두 FutureProvider를 결합하는 것은 위험합니다.

둘 중 하나라도 실패하면 에러가 발생하기 때문이죠. 주의사항 정리 requireValue를 사용하기 전에 자문해보세요.

"이 Provider는 상위 Provider가 성공했을 때만 실행되는가?" "상위 Provider의 데이터가 반드시 필요한가?" 두 질문에 모두 "예"라고 답할 수 있다면 requireValue를 사용하세요. 그렇지 않다면 hasValue 체크를 추가하거나, when 메서드를 사용하는 것이 안전합니다.

requireValue는 편리하지만, 남용하면 예상치 못한 버그를 만들 수 있습니다. 정리 박시니어 씨의 조언을 듣고, 김개발 씨는 dashboardProvider를 수정했습니다.

hasValue 체크를 추가하고, 에러 처리를 명확히 했습니다. requireValue는 강력한 도구이지만, 올바른 상황에서만 사용해야 한다는 것을 배웠습니다.

여러분도 requireValue를 사용하기 전에 의존 관계가 명확한지 꼭 확인하세요.

실전 팁

💡 - Provider 간 의존 관계가 명확한지 확인하세요 (A가 있어야 B를 가져올 수 있는 구조)

  • 독립적인 Provider를 결합할 때는 hasValue 체크를 추가하거나 UI 레이어에서 처리하세요

6. 대시보드 데이터 결합 예제

드디어 김개발 씨는 실전 프로젝트를 맡았습니다. 관리자 대시보드를 만드는 것이었죠.

사용자 수, 오늘의 매출, 최근 주문, 인기 상품을 한 화면에 보여줘야 했습니다. "지금까지 배운 requireValue를 총동원할 시간이군!" 김개발 씨는 의욕이 넘쳤습니다.

실무에서 가장 흔한 패턴 중 하나는 대시보드 화면입니다. 여러 개의 독립적인 데이터를 한 화면에 모아서 보여주죠.

이때 requireValue를 적절히 활용하면 코드를 깔끔하게 유지하면서도, 각 데이터의 로딩과 에러를 개별적으로 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// 각각 독립적인 데이터 Provider
final userCountProvider = FutureProvider<int>((ref) async {
  return await fetchUserCount();
});

final todaySalesProvider = FutureProvider<double>((ref) async {
  return await fetchTodaySales();
});

final recentOrdersProvider = FutureProvider<List<Order>>((ref) async {
  return await fetchRecentOrders();
});

// UI에서 사용 - 각각 개별적으로 처리
class DashboardScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userCountAsync = ref.watch(userCountProvider);
    final salesAsync = ref.watch(todaySalesProvider);
    final ordersAsync = ref.watch(recentOrdersProvider);

    return Column(
      children: [
        // 각 데이터를 독립적으로 표시
        userCountAsync.when(
          data: (count) => Text('사용자: $count명'),
          loading: () => CircularProgressIndicator(),
          error: (e, s) => Text('에러 발생'),
        ),
        salesAsync.when(
          data: (sales) => Text('매출: ${sales}원'),
          loading: () => CircularProgressIndicator(),
          error: (e, s) => Text('에러 발생'),
        ),
        // ... 나머지도 동일
      ],
    );
  }
}

김개발 씨는 설계를 시작했습니다. 대시보드에는 네 가지 정보가 필요했습니다.

각각 다른 API에서 가져와야 하는 데이터였죠. "이걸 어떻게 구성해야 하지?" 대시보드의 특성을 이해하기 대시보드는 일반적인 상세 화면과 다릅니다.

상세 화면은 모든 데이터가 필요합니다. 예를 들어 게시글 상세 화면에서 제목이 없으면 화면을 보여줄 수 없죠.

하지만 대시보드는 다릅니다. 사용자 수를 가져오는 API가 실패했다고 해서 매출 정보까지 안 보여줄 필요는 없습니다.

각 위젯은 독립적으로 동작해야 합니다. 마치 신문의 각 섹션처럼 말이죠.

잘못된 접근: 모든 데이터를 결합 초보 개발자는 흔히 이렇게 생각합니다. "모든 데이터를 하나의 DashboardData 클래스로 만들고, 하나의 Provider로 관리하자." 그리고 requireValue로 모든 데이터를 결합합니다.

이렇게 하면 큰 문제가 생깁니다. 네 개의 API 중 하나라도 실패하면 전체 대시보드가 에러 화면이 됩니다.

사용자는 아무 정보도 볼 수 없죠. 나쁜 사용자 경험입니다.

올바른 접근: 독립적인 Provider 위 코드를 보세요. 각 데이터마다 별도의 FutureProvider를 만들었습니다.

userCountProvider, todaySalesProvider, recentOrdersProvider가 각각 독립적으로 존재합니다. UI에서는 각 Provider를 watch하고, 각각 when 메서드로 처리합니다.

이렇게 하면 사용자 수 API가 실패해도, 매출 정보는 정상적으로 표시됩니다. 훨씬 견고한 구조입니다.

부분 결합이 필요하다면? 때로는 일부 데이터를 결합해야 할 때도 있습니다. 예를 들어 "평균 주문 금액"을 계산하려면 매출과 주문 수가 모두 필요합니다.

이런 경우에는 별도의 Provider를 만들 수 있습니다. dart final avgOrderAmountProvider = FutureProvider<double?>((ref) async { final salesAsync = ref.watch(todaySalesProvider); final ordersAsync = ref.watch(recentOrdersProvider); // 둘 다 준비되었는지 확인 if (!salesAsync.hasValue || !ordersAsync.hasValue) { return null; } final sales = salesAsync.requireValue; final orders = ordersAsync.requireValue; if (orders.isEmpty) return 0; return sales / orders.length; }); hasValue로 체크하고, 둘 다 준비되었을 때만 계산합니다.

안전합니다. 로딩 상태 처리 대시보드는 처음 로드될 때 모든 데이터가 로딩 중일 수 있습니다.

네 개의 로딩 인디케이터가 동시에 돌아가는 것이죠. 이것은 정상입니다.

다만 사용자 경험을 위해 스켈레톤 UI를 사용하는 것도 좋습니다. 로딩 중일 때 빈 공간 대신 회색 박스를 보여주는 것이죠.

사용자는 "아, 여기에 뭔가 표시될 거구나"라고 예상할 수 있습니다. 에러 처리 전략 대시보드에서 에러가 발생하면 어떻게 보여줄까요?

두 가지 전략이 있습니다. 전략 1: 인라인 에러 메시지.

각 위젯 자리에 작은 에러 메시지를 표시합니다. "데이터를 불러올 수 없습니다" 같은 메시지죠.

다른 데이터는 정상적으로 표시됩니다. 전략 2: 재시도 버튼.

에러가 발생한 위젯에 "다시 시도" 버튼을 추가합니다. 사용자가 클릭하면 해당 Provider를 invalidate해서 다시 로드합니다.

실전 팁 실무에서는 대시보드 데이터를 주기적으로 새로고침하는 경우가 많습니다. 예를 들어 30초마다 최신 데이터를 가져오는 것이죠.

이때 ref.invalidate를 사용할 수 있습니다. Timer를 설정하고, 30초마다 각 Provider를 invalidate하면 자동으로 재조회됩니다.

사용자는 항상 최신 데이터를 볼 수 있습니다. 정리 김개발 씨는 대시보드를 완성했습니다.

각 데이터는 독립적으로 로드되고, 하나가 실패해도 다른 정보는 정상적으로 표시되었습니다. requireValue는 필요한 곳에만 사용하고, 대부분은 when 메서드로 개별 처리했습니다.

사용자는 만족했고, 김개발 씨는 뿌듯했습니다. 여러분도 대시보드를 만들 때 이런 패턴을 떠올려 보세요.

실전 팁

💡 - 대시보드는 각 데이터를 독립적인 Provider로 관리하세요

  • UI에서 when 메서드로 각각 처리하면 부분 실패에도 견고합니다
  • 재시도 버튼과 스켈레톤 UI로 사용자 경험을 개선하세요

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

#Flutter#Riverpod#Provider#AsyncValue#StateManagement#Flutter,Riverpod

댓글 (0)

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

함께 보면 좋은 카드 뉴스