🤖

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

⚠️

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

이미지 로딩 중...

Riverpod invalidate와 refresh로 데이터 새로고침 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 11. · 8 Views

Riverpod invalidate와 refresh로 데이터 새로고침 완벽 가이드

Riverpod에서 데이터를 새로고침하는 다양한 방법을 배웁니다. invalidate와 refresh의 차이를 이해하고, pull-to-refresh, 버튼 새로고침, 연관 Provider 갱신, 자동 새로고침까지 실무에서 바로 쓸 수 있는 패턴을 익힙니다.


목차

  1. invalidate vs refresh 차이
  2. Pull-to-refresh 구현
  3. 버튼으로 새로고침
  4. 연관된 Provider 함께 갱신
  5. invalidateSelf 사용
  6. 자동 새로고침 타이머

1. invalidate vs refresh 차이

어느 날 김개발 씨는 사용자 목록 화면을 개발하고 있었습니다. 사용자가 새로고침 버튼을 누르면 최신 데이터를 가져와야 하는데, 선배 박시니어 씨가 코드 리뷰를 하다가 물었습니다.

"여기서 invalidate를 쓴 이유가 있나요? refresh를 쓰는 게 나을 것 같은데요."

invalidate는 Provider의 상태를 완전히 초기화하여 다음에 접근할 때 다시 빌드하도록 만드는 방법입니다. 반면 refresh는 즉시 Provider를 재실행하여 새로운 값을 반환받습니다.

마치 invalidate는 예약을 취소하는 것이고, refresh는 지금 당장 다시 주문하는 것과 같습니다.

다음 코드를 살펴봅시다.

// invalidate: 상태를 초기화만 함 (다음 접근 시 재빌드)
ref.invalidate(userListProvider);

// refresh: 즉시 재실행하고 새 값을 반환받음
final newUsers = await ref.refresh(userListProvider.future);

// invalidate는 비동기 작업을 기다리지 않음
ref.invalidate(userListProvider);
// 여기서는 아직 새 데이터가 로드되지 않음

// refresh는 새 데이터를 기다릴 수 있음
final users = await ref.refresh(userListProvider.future);
// 여기서는 새 데이터가 준비됨

김개발 씨는 입사 4개월 차 주니어 개발자입니다. Flutter와 Riverpod을 사용하여 회원 관리 앱을 개발하고 있습니다.

사용자가 새로고침 버튼을 누르면 서버에서 최신 데이터를 가져와야 하는데, 어떤 방법을 써야 할지 고민이 됩니다. 선배 개발자 박시니어 씨가 코드를 살펴보더니 말했습니다.

"invalidate와 refresh의 차이를 제대로 알고 사용하는 게 중요해요." 그렇다면 이 둘의 차이는 정확히 무엇일까요? 쉽게 비유하자면, invalidate는 마치 도서관에서 책을 반납하는 것과 같습니다.

책을 서가에 돌려놓고, 다음에 누군가 필요할 때 다시 빌려가면 됩니다. 반면 refresh는 지금 당장 새 책을 빌리는 것입니다.

반납과 동시에 새 책을 받아오는 셈이죠. invalidate가 없던 시절에는 어땠을까요?

개발자들은 Provider의 상태를 수동으로 관리해야 했습니다. 언제 데이터가 오래되었는지 추적하고, 직접 새로운 데이터를 요청하는 코드를 작성해야 했습니다.

더 큰 문제는 여러 화면에서 같은 데이터를 사용할 때 일관성을 유지하기가 매우 어려웠다는 점입니다. 바로 이런 문제를 해결하기 위해 invalidaterefresh가 등장했습니다.

invalidate를 사용하면 Provider의 상태를 깔끔하게 초기화할 수 있습니다. 이 Provider를 구독하고 있는 모든 위젯이 자동으로 다시 빌드됩니다.

또한 다음에 이 Provider에 접근할 때 자동으로 새 데이터를 가져옵니다. refresh는 더 직접적입니다.

호출하는 즉시 Provider가 재실행되고, Future를 반환하므로 새로운 값을 기다릴 수 있습니다. 새로고침 결과를 바로 사용해야 할 때 유용합니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 첫 번째 줄의 ref.invalidate는 userListProvider의 상태를 초기화합니다.

이 시점에서는 아직 새 데이터를 가져오지 않습니다. 다음으로 ref.refresh는 즉시 Provider를 재실행하고 Future를 반환합니다.

await를 사용하면 새 데이터가 로드될 때까지 기다릴 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다. 사용자가 장바구니에 상품을 추가한 후 목록 화면으로 돌아갈 때, invalidate를 사용하면 목록 화면이 자동으로 새로고침됩니다.

하지만 사용자가 새로고침 버튼을 눌렀을 때는 refresh를 사용하여 즉시 결과를 보여주는 것이 좋습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 invalidate 직후에 바로 데이터를 사용하려는 것입니다. invalidate는 비동기적으로 작동하므로, 호출 직후에는 아직 새 데이터가 준비되지 않았을 수 있습니다.

따라서 즉시 새 데이터가 필요하다면 refresh를 사용해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 버튼을 눌렀을 때는 refresh를 써서 로딩 인디케이터를 보여주는 게 낫겠네요!" invalidaterefresh의 차이를 제대로 이해하면 사용자 경험을 크게 개선할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 화면 전환 시 자동 새로고침이 필요하면 invalidate를 사용하세요

  • 사용자가 명시적으로 새로고침을 요청하면 refresh를 사용하세요
  • refresh의 반환값을 사용하여 새로고침 성공/실패를 처리할 수 있습니다

2. Pull-to-refresh 구현

김개발 씨는 다음 작업으로 pull-to-refresh 기능을 구현해야 했습니다. 사용자가 화면을 아래로 당기면 데이터를 새로고침하는 그 익숙한 기능 말이죠.

하지만 Riverpod과 어떻게 연동해야 할지 막막했습니다.

Pull-to-refresh는 사용자가 스크롤을 아래로 당겨서 데이터를 새로고침하는 UI 패턴입니다. Flutter의 RefreshIndicator 위젯과 Riverpod의 refresh 메서드를 결합하면 간단하게 구현할 수 있습니다.

사용자 친화적인 새로고침 경험을 제공하는 필수 기능입니다.

다음 코드를 살펴봅시다.

class UserListScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(userListProvider);

    return RefreshIndicator(
      // pull-to-refresh 시 호출되는 콜백
      onRefresh: () async {
        // refresh로 즉시 재실행하고 결과를 기다림
        return ref.refresh(userListProvider.future);
      },
      child: usersAsync.when(
        data: (users) => ListView.builder(
          itemCount: users.length,
          itemBuilder: (context, index) => ListTile(
            title: Text(users[index].name),
          ),
        ),
        loading: () => CircularProgressIndicator(),
        error: (err, stack) => Text('오류: $err'),
      ),
    );
  }
}

김개발 씨는 사용자 목록 화면을 완성했지만, 한 가지 문제가 있었습니다. 사용자들이 새로운 데이터를 보려면 앱을 껐다 켜야 했습니다.

이것은 좋지 않은 사용자 경험이었습니다. 박시니어 씨가 조언했습니다.

"모바일 앱에서는 pull-to-refresh가 거의 필수예요. 사용자들이 직관적으로 데이터를 새로고침할 수 있거든요." 그렇다면 pull-to-refresh는 어떻게 구현할까요?

쉽게 비유하자면, pull-to-refresh는 마치 자판기에서 음료수를 뽑을 때 레버를 당기는 것과 같습니다. 사용자가 화면을 아래로 당기면 새로운 데이터가 나옵니다.

이 동작은 너무나 직관적이어서 별도의 설명이 필요 없습니다. pull-to-refresh가 없던 시절에는 어땠을까요?

사용자들은 새로고침 버튼을 찾아야 했습니다. 버튼이 어디 있는지 찾기 어려웠고, 때로는 버튼이 아예 없는 경우도 있었습니다.

더 큰 문제는 사용자들이 데이터가 오래되었다는 것을 알기 어려웠다는 점입니다. 바로 이런 문제를 해결하기 위해 pull-to-refresh 패턴이 등장했습니다.

Flutter에서는 RefreshIndicator 위젯이 이 기능을 제공합니다. 사용자가 스크롤 가능한 위젯을 아래로 당기면 자동으로 로딩 인디케이터를 보여줍니다.

또한 onRefresh 콜백을 통해 데이터를 새로고침할 수 있습니다. Riverpod과 결합하면 더욱 강력해집니다.

ref.refresh를 사용하여 Provider를 즉시 재실행하고, Future를 반환하므로 RefreshIndicator가 자동으로 로딩 상태를 관리합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 RefreshIndicator로 전체 화면을 감쌉니다. onRefresh 콜백에서는 **ref.refresh(userListProvider.future)**를 호출합니다.

이것은 Future를 반환하므로 async/await와 완벽하게 호환됩니다. RefreshIndicator는 이 Future가 완료될 때까지 로딩 인디케이터를 보여줍니다.

usersAsync.when을 사용하여 로딩, 데이터, 에러 상태를 각각 처리합니다. 사용자가 화면을 당기면 자동으로 loading 상태로 전환되고, 데이터가 로드되면 data 상태로 돌아갑니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 SNS 앱을 개발한다고 가정해봅시다.

사용자가 피드 화면에서 아래로 당기면 최신 게시물을 가져옵니다. 이때 pull-to-refresh를 사용하면 별도의 버튼 없이도 직관적으로 새로고침할 수 있습니다.

인스타그램, 트위터, 페이스북 등 거의 모든 SNS 앱이 이 패턴을 사용합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 onRefresh에서 invalidate를 사용하는 것입니다. invalidate는 Future를 반환하지 않으므로 RefreshIndicator가 언제 로딩을 멈춰야 할지 알 수 없습니다.

따라서 반드시 refresh를 사용해야 합니다. 또 다른 주의사항은 스크롤 가능한 위젯이 반드시 필요하다는 점입니다.

ListView, GridView, CustomScrollView 등이 없으면 pull-to-refresh가 작동하지 않습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

코드를 완성한 김개발 씨는 앱을 실행해 보았습니다. 화면을 아래로 당기자 로딩 인디케이터가 나타나고, 잠시 후 새로운 데이터가 로드되었습니다.

"와, 정말 간단하네요!" pull-to-refresh는 사용자 경험을 크게 향상시키는 필수 기능입니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - onRefresh는 반드시 Future를 반환해야 합니다

  • 스크롤 가능한 위젯(ListView 등)이 없으면 작동하지 않습니다
  • 에러가 발생해도 로딩 인디케이터가 자동으로 사라집니다

3. 버튼으로 새로고침

박시니어 씨가 김개발 씨에게 추가 요구사항을 전달했습니다. "pull-to-refresh는 좋은데, 일부 사용자들은 명시적인 버튼을 선호해요.

새로고침 버튼도 추가해 주세요." 김개발 씨는 이번에는 버튼 클릭으로 데이터를 새로고침하는 방법을 고민하기 시작했습니다.

버튼을 통한 새로고침은 사용자가 명시적으로 데이터 갱신을 요청할 수 있는 방법입니다. ElevatedButton이나 IconButton의 onPressed 콜백에서 ref.refresh를 호출하면 됩니다.

로딩 상태를 직접 관리하여 사용자에게 피드백을 제공할 수 있습니다.

다음 코드를 살펴봅시다.

class UserListScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(userListProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('사용자 목록'),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            // 버튼 클릭 시 새로고침
            onPressed: () {
              // refresh로 즉시 재실행
              ref.refresh(userListProvider);
            },
          ),
        ],
      ),
      body: usersAsync.when(
        data: (users) => ListView.builder(
          itemCount: users.length,
          itemBuilder: (context, index) => ListTile(
            title: Text(users[index].name),
          ),
        ),
        // 로딩 중일 때 인디케이터 표시
        loading: () => Center(child: CircularProgressIndicator()),
        error: (err, stack) => Text('오류: $err'),
      ),
    );
  }
}

김개발 씨는 pull-to-refresh를 구현했지만, 또 다른 과제가 생겼습니다. UX 디자이너가 피드백을 주었습니다.

"일부 사용자들은 pull-to-refresh를 모를 수 있어요. 명확한 새로고침 버튼도 있으면 좋겠어요." 박시니어 씨가 동의했습니다.

"맞아요. 특히 데스크톱 경험이 많은 사용자들은 버튼을 선호하는 경향이 있어요." 그렇다면 버튼으로 새로고침을 구현하는 방법은 무엇일까요?

쉽게 비유하자면, 버튼을 통한 새로고침은 마치 엘리베이터 버튼을 누르는 것과 같습니다. 명확하고 직관적입니다.

버튼을 누르면 정확히 무슨 일이 일어날지 사용자가 알 수 있습니다. 버튼이 없던 시절에는 어땠을까요?

웹 초창기에는 브라우저의 새로고침 버튼을 눌러야 했습니다. 이렇게 하면 전체 페이지가 다시 로드되어 사용자 경험이 좋지 않았습니다.

더 큰 문제는 입력하던 데이터가 모두 사라진다는 점이었습니다. 바로 이런 문제를 해결하기 위해 부분 새로고침 개념이 등장했습니다.

Riverpod에서는 ref.refresh를 사용하여 특정 Provider만 새로고침할 수 있습니다. 전체 화면을 다시 그리지 않고도 필요한 데이터만 갱신할 수 있습니다.

또한 AsyncValue를 통해 로딩 상태를 자동으로 관리합니다. 버튼 클릭으로 새로고침하면 사용자에게 명확한 피드백을 제공할 수 있습니다.

버튼을 누르는 순간 로딩 인디케이터가 나타나고, 데이터가 로드되면 자동으로 화면이 업데이트됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 AppBaractions에 IconButton을 추가합니다. onPressed 콜백에서 **ref.refresh(userListProvider)**를 호출합니다.

이것만으로 충분합니다. Riverpod이 자동으로 Provider를 재실행하고, 모든 구독자에게 알립니다.

usersAsync.when에서 loading 케이스를 처리합니다. 버튼을 누르면 자동으로 loading 상태로 전환되어 CircularProgressIndicator가 표시됩니다.

사용자는 데이터가 로드 중임을 시각적으로 확인할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 주식 거래 앱을 개발한다고 가정해봅시다. 실시간 주가 정보를 보여주는 화면에서 사용자가 새로고침 버튼을 누르면 최신 데이터를 가져옵니다.

이때 버튼 옆에 마지막 업데이트 시간을 표시하면 더욱 좋습니다. 또 다른 예로, 날씨 앱에서도 버튼 새로고침이 유용합니다.

사용자가 원할 때마다 최신 날씨 정보를 가져올 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 버튼을 너무 자주 누를 수 있게 만드는 것입니다. 사용자가 연속으로 버튼을 누르면 불필요한 네트워크 요청이 발생할 수 있습니다.

따라서 로딩 중일 때는 버튼을 비활성화하는 것이 좋습니다. 이렇게 개선할 수 있습니다.

usersAsync.isLoading을 확인하여 버튼을 비활성화하면 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

김개발 씨는 새로고침 버튼을 추가하고 로딩 중일 때 버튼을 비활성화하는 코드까지 완성했습니다. UX 디자이너가 결과를 보고 만족스러워했습니다.

버튼을 통한 새로고침은 명확하고 사용자 친화적인 패턴입니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 로딩 중일 때는 버튼을 비활성화하여 중복 요청을 방지하세요

  • 마지막 업데이트 시간을 표시하면 사용자 경험이 향상됩니다
  • 새로고침 성공/실패를 SnackBar로 알려주면 더 좋습니다

4. 연관된 Provider 함께 갱신

김개발 씨는 새로운 문제에 직면했습니다. 사용자 목록을 새로고침하면 사용자 상세 정보도 함께 갱신되어야 하는데, 어떻게 해야 할지 막막했습니다.

박시니어 씨가 말했습니다. "연관된 Provider들을 함께 갱신해야 할 때가 있어요."

연관된 Provider를 함께 갱신하는 것은 데이터 일관성을 유지하는 핵심 패턴입니다. 하나의 Provider를 새로고침할 때 관련된 다른 Provider들도 함께 invalidate하면, 다음 접근 시 자동으로 최신 데이터를 가져옵니다.

특히 마스터-디테일 패턴에서 유용합니다.

다음 코드를 살펴봅시다.

// 사용자 목록 Provider
final userListProvider = FutureProvider<List<User>>((ref) async {
  final api = ref.watch(apiProvider);
  return api.fetchUsers();
});

// 특정 사용자 상세 정보 Provider (ID를 인자로 받음)
final userDetailProvider = FutureProvider.family<User, int>((ref, userId) async {
  final api = ref.watch(apiProvider);
  return api.fetchUserDetail(userId);
});

// 새로고침 함수: 연관된 Provider들을 함께 갱신
Future<void> refreshUserData(WidgetRef ref) async {
  // 1. 사용자 목록 새로고침
  await ref.refresh(userListProvider.future);

  // 2. 모든 사용자 상세 정보 캐시 무효화
  ref.invalidate(userDetailProvider);

  // 또는 특정 사용자만 갱신
  // ref.invalidate(userDetailProvider(123));
}

김개발 씨는 사용자 목록 화면과 상세 화면을 모두 구현했습니다. 그런데 이상한 버그를 발견했습니다.

목록 화면에서 새로고침하면 목록은 업데이트되는데, 이미 열어둔 상세 화면은 여전히 오래된 데이터를 보여주고 있었습니다. 박시니어 씨가 설명했습니다.

"Provider들은 독립적으로 캐시되거든요. 하나를 갱신해도 다른 것은 그대로 남아있어요." 그렇다면 연관된 데이터를 어떻게 함께 갱신할까요?

쉽게 비유하자면, 연관된 Provider 갱신은 마치 백과사전을 업데이트하는 것과 같습니다. 본문을 수정하면 목차와 색인도 함께 업데이트해야 합니다.

그렇지 않으면 사용자가 잘못된 정보를 보게 됩니다. 연관 데이터 갱신이 없던 시절에는 어땠을까요?

개발자들은 각 화면에서 개별적으로 데이터를 관리해야 했습니다. 한 화면에서 데이터를 수정하면 다른 모든 화면에 일일이 알려야 했습니다.

더 큰 문제는 어떤 화면들이 연관되어 있는지 추적하기가 매우 어려웠다는 점입니다. 바로 이런 문제를 해결하기 위해 Provider 무효화 패턴이 등장했습니다.

Riverpod에서는 invalidate를 사용하여 관련된 Provider들을 한 번에 무효화할 수 있습니다. 무효화된 Provider는 다음에 접근할 때 자동으로 새 데이터를 가져옵니다.

또한 family Provider의 경우 특정 인자의 인스턴스만 무효화할 수도 있습니다. 연관 Provider를 함께 갱신하면 데이터 일관성이 보장됩니다.

사용자가 어느 화면을 보든 항상 최신 데이터를 볼 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 userListProvider는 전체 사용자 목록을 제공합니다. userDetailProviderfamily 한정자를 사용하여 특정 사용자의 상세 정보를 제공합니다.

userId를 인자로 받아 각각 독립적으로 캐시됩니다. refreshUserData 함수에서는 먼저 **ref.refresh(userListProvider.future)**로 목록을 갱신합니다.

그 다음 **ref.invalidate(userDetailProvider)**로 모든 상세 정보 캐시를 무효화합니다. 이렇게 하면 다음에 상세 화면을 열 때 자동으로 새 데이터를 가져옵니다.

만약 특정 사용자만 갱신하고 싶다면 **ref.invalidate(userDetailProvider(123))**처럼 인자를 전달하면 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 전자상거래 앱을 개발한다고 가정해봅시다. 사용자가 상품을 장바구니에 추가하면, 상품 목록뿐만 아니라 장바구니 개수, 총 금액 등 여러 Provider를 함께 갱신해야 합니다.

이때 연관 Provider 무효화 패턴을 사용하면 코드가 간결해집니다. 또 다른 예로, 소셜 미디어 앱에서 사용자가 프로필을 수정하면, 피드의 게시물, 댓글 작성자 정보, 팔로워 수 등 여러 곳에서 사용되는 데이터를 모두 갱신해야 합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 Provider를 한 번에 무효화하는 것입니다.

불필요한 네트워크 요청이 발생하여 성능이 저하될 수 있습니다. 따라서 정말 연관된 Provider만 선택적으로 무효화해야 합니다.

또 다른 주의사항은 순환 의존성입니다. A Provider를 갱신할 때 B를 무효화하고, B를 갱신할 때 A를 무효화하면 무한 루프에 빠질 수 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 김개발 씨는 refreshUserData 함수를 만들고, 목록 새로고침 시 관련 Provider들을 함께 무효화하도록 수정했습니다.

이제 어느 화면에서든 항상 최신 데이터가 표시됩니다. 연관된 Provider를 함께 갱신하는 패턴은 복잡한 앱에서 데이터 일관성을 유지하는 핵심입니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - family Provider는 특정 인자의 인스턴스만 무효화할 수 있습니다

  • 너무 많은 Provider를 무효화하면 성능이 저하될 수 있습니다
  • 순환 의존성을 조심하세요

5. invalidateSelf 사용

박시니어 씨가 김개발 씨의 코드를 리뷰하다가 흥미로운 패턴을 알려주었습니다. "Provider 내부에서 자기 자신을 무효화할 수 있다는 거 알아요?" 김개발 씨는 처음 듣는 이야기였습니다.

언제 이런 패턴이 필요할까요?

invalidateSelf는 Provider가 자기 자신을 무효화하는 방법입니다. 주로 일정 시간이 지나면 자동으로 데이터를 갱신하거나, 특정 조건에서 캐시를 무효화해야 할 때 사용합니다.

Provider 내부에서 **ref.invalidateSelf()**를 호출하면 됩니다.

다음 코드를 살펴봅시다.

// 10초 후 자동으로 무효화되는 Provider
final autoRefreshProvider = FutureProvider<List<User>>((ref) async {
  // 10초 후 자기 자신을 무효화
  final timer = Timer(Duration(seconds: 10), () {
    ref.invalidateSelf();
  });

  // Provider가 dispose될 때 타이머도 취소
  ref.onDispose(() {
    timer.cancel();
  });

  final api = ref.watch(apiProvider);
  return api.fetchUsers();
});

// 또는 keepAlive를 false로 설정하여 자동 정리
final cacheProvider = FutureProvider.autoDispose<List<User>>((ref) async {
  // 캐시 유효 시간 설정
  ref.cacheFor(Duration(seconds: 30));

  final api = ref.watch(apiProvider);
  return api.fetchUsers();
});

// cacheFor 헬퍼 확장 메서드
extension CacheForExtension on AutoDisposeRef<Object?> {
  void cacheFor(Duration duration) {
    final link = keepAlive();
    final timer = Timer(duration, link.close);
    onDispose(timer.cancel);
  }
}

김개발 씨는 실시간 주식 정보를 보여주는 화면을 개발하고 있었습니다. 데이터가 빠르게 변하므로 주기적으로 새로고침해야 했습니다.

하지만 사용자가 버튼을 누르지 않아도 자동으로 갱신되었으면 좋겠다고 생각했습니다. 박시니어 씨가 해결책을 제시했습니다.

"Provider가 스스로를 무효화하도록 만들 수 있어요." 그렇다면 invalidateSelf는 어떻게 작동할까요? 쉽게 비유하자면, invalidateSelf는 마치 우유 팩의 유통기한과 같습니다.

일정 시간이 지나면 자동으로 신선도가 떨어진 것으로 간주하고, 새 우유로 교체합니다. 사용자가 직접 확인하지 않아도 시스템이 알아서 관리합니다.

자동 무효화가 없던 시절에는 어땠을까요? 개발자들은 복잡한 타이머 로직을 직접 작성해야 했습니다.

화면마다 setInterval이나 Timer를 관리하고, 메모리 누수를 방지하기 위해 정리 코드도 작성해야 했습니다. 더 큰 문제는 화면이 닫혔을 때 타이머를 제대로 정리하지 못하는 버그가 자주 발생했다는 점입니다.

바로 이런 문제를 해결하기 위해 invalidateSelf 패턴이 등장했습니다. Provider 내부에서 **ref.invalidateSelf()**를 호출하면 자기 자신이 무효화됩니다.

이것은 외부에서 **ref.invalidate(provider)**를 호출하는 것과 동일한 효과입니다. 또한 ref.onDispose를 사용하여 정리 코드를 등록하면 메모리 누수를 방지할 수 있습니다.

더 나아가 autoDisposekeepAlive를 조합하면 캐시 유효 시간을 정확하게 제어할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 autoRefreshProvider에서는 Timer를 생성하여 10초 후에 **ref.invalidateSelf()**를 호출합니다. 이렇게 하면 Provider가 자동으로 무효화되고, 다음 접근 시 새 데이터를 가져옵니다.

ref.onDispose에서 타이머를 취소합니다. Provider가 더 이상 사용되지 않을 때 자동으로 정리됩니다.

이것은 매우 중요한 패턴입니다. cacheProvider는 더 세련된 방법을 보여줍니다.

autoDispose를 사용하면 Provider가 사용되지 않을 때 자동으로 정리됩니다. cacheFor 확장 메서드는 일정 시간 동안만 캐시를 유지합니다.

**keepAlive()**를 호출하면 link 객체를 받습니다. 이 link를 닫으면 Provider가 dispose됩니다.

Timer를 사용하여 정확한 시간에 link를 닫습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 날씨 앱을 개발한다고 가정해봅시다. 날씨 데이터는 1시간마다 자동으로 갱신되어야 합니다.

invalidateSelf를 사용하면 사용자가 앱을 켜놓기만 해도 최신 날씨 정보를 볼 수 있습니다. 또 다른 예로, 암호화폐 거래소 앱에서는 가격이 실시간으로 변합니다.

몇 초마다 자동으로 데이터를 갱신하여 사용자에게 최신 정보를 제공할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 짧은 간격으로 새로고침하는 것입니다. 서버에 과도한 부하를 주고, 사용자의 데이터 사용량도 증가합니다.

따라서 적절한 새로고침 간격을 설정해야 합니다. 또 다른 주의사항은 타이머를 반드시 정리해야 한다는 점입니다.

onDispose를 사용하지 않으면 메모리 누수가 발생합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

김개발 씨는 invalidateSelf를 사용하여 주식 정보가 자동으로 갱신되도록 구현했습니다. 사용자들이 항상 최신 정보를 볼 수 있게 되었습니다.

invalidateSelf는 자동 새로고침을 구현하는 강력한 도구입니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 타이머는 반드시 onDispose에서 정리하세요

  • 새로고침 간격은 너무 짧지 않게 설정하세요
  • autoDispose와 keepAlive를 조합하면 캐시 시간을 정확히 제어할 수 있습니다

6. 자동 새로고침 타이머

김개발 씨는 마지막 요구사항을 받았습니다. "사용자가 화면을 보고 있는 동안에만 주기적으로 데이터를 새로고침해 주세요." 화면을 떠나면 타이머를 멈추고, 다시 돌아오면 재개해야 했습니다.

이번에는 조금 복잡해 보였습니다.

자동 새로고침 타이머는 일정 간격으로 데이터를 자동으로 갱신하는 패턴입니다. Timer.periodic을 사용하여 주기적으로 **ref.invalidateSelf()**를 호출하면 됩니다.

StatefulWidget과 결합하면 화면의 생명주기에 따라 타이머를 제어할 수 있습니다.

다음 코드를 살펴봅시다.

// 자동 새로고침 Provider
final autoRefreshDataProvider = FutureProvider<List<Stock>>((ref) async {
  // 30초마다 자동 새로고침
  final timer = Timer.periodic(Duration(seconds: 30), (_) {
    ref.invalidateSelf();
  });

  // Provider dispose 시 타이머 정리
  ref.onDispose(timer.cancel);

  final api = ref.watch(stockApiProvider);
  return api.fetchStocks();
});

// 화면 생명주기에 따른 타이머 제어
class StockListScreen extends ConsumerStatefulWidget {
  @override
  ConsumerState<StockListScreen> createState() => _StockListScreenState();
}

class _StockListScreenState extends ConsumerState<StockListScreen>
    with WidgetsBindingObserver {

  @override
  void initState() {
    super.initState();
    // 생명주기 관찰 시작
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    // 생명주기 관찰 중지
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // 앱이 포그라운드로 돌아오면 새로고침
      ref.invalidate(autoRefreshDataProvider);
    }
  }

  @override
  Widget build(BuildContext context) {
    final stocksAsync = ref.watch(autoRefreshDataProvider);

    return stocksAsync.when(
      data: (stocks) => ListView.builder(
        itemCount: stocks.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(stocks[index].name),
          subtitle: Text('\$${stocks[index].price}'),
        ),
      ),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('오류: $err'),
    );
  }
}

김개발 씨는 주식 정보 앱을 거의 완성했습니다. 하지만 QA 팀에서 문제를 발견했습니다.

"사용자가 앱을 백그라운드로 보내도 계속 새로고침이 되네요. 배터리와 데이터가 낭비됩니다." 박시니어 씨가 조언했습니다.

"자동 새로고침은 좋지만, 화면 생명주기를 고려해야 해요." 그렇다면 똑똑한 자동 새로고침은 어떻게 구현할까요? 쉽게 비유하자면, 자동 새로고침 타이머는 마치 라디오 방송과 같습니다.

사람들이 듣고 있을 때만 방송하면 됩니다. 아무도 듣지 않는데 계속 전파를 쏘는 것은 낭비입니다.

앱도 마찬가지입니다. 사용자가 보고 있을 때만 데이터를 갱신하면 됩니다.

똑똑한 새로고침이 없던 시절에는 어땠을까요? 앱들은 백그라운드에서도 계속 데이터를 가져왔습니다.

배터리가 빨리 닳고, 데이터 사용량도 증가했습니다. 더 큰 문제는 백그라운드 네트워크 요청이 OS에 의해 제한되면서 예상치 못한 버그가 발생했다는 점입니다.

바로 이런 문제를 해결하기 위해 생명주기 기반 새로고침 패턴이 등장했습니다. Flutter의 WidgetsBindingObserver를 사용하면 앱의 생명주기 변화를 감지할 수 있습니다.

앱이 포그라운드로 돌아올 때, 백그라운드로 갈 때를 알 수 있습니다. Riverpod의 타이머와 결합하면 효율적인 자동 새로고침을 구현할 수 있습니다.

또한 autoDispose를 사용하면 화면을 떠날 때 자동으로 타이머가 정리됩니다. 메모리 누수 걱정 없이 안전하게 사용할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 autoRefreshDataProvider에서 Timer.periodic을 생성합니다.

30초마다 콜백이 실행되며, **ref.invalidateSelf()**를 호출합니다. 이렇게 하면 Provider가 주기적으로 무효화되어 새 데이터를 가져옵니다.

**ref.onDispose(timer.cancel)**로 타이머를 정리합니다. Provider가 더 이상 사용되지 않으면 자동으로 타이머가 중지됩니다.

ConsumerStatefulWidget을 사용하여 생명주기를 관리합니다. WidgetsBindingObserver를 믹스인하면 앱의 생명주기 이벤트를 받을 수 있습니다.

initState에서 addObserver를 호출하여 관찰을 시작합니다. dispose에서는 removeObserver로 정리합니다.

이 패턴을 반드시 지켜야 합니다. didChangeAppLifecycleState에서 앱 상태 변화를 감지합니다.

AppLifecycleState.resumed는 앱이 포그라운드로 돌아왔다는 의미입니다. 이때 ref.invalidate를 호출하여 즉시 새로고침합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 메신저 앱을 개발한다고 가정해봅시다.

사용자가 채팅방을 보고 있을 때는 1초마다 새 메시지를 확인합니다. 하지만 다른 앱을 사용하거나 화면이 꺼지면 푸시 알림에만 의존합니다.

이렇게 하면 배터리와 데이터를 절약할 수 있습니다. 또 다른 예로, 스포츠 실시간 스코어 앱에서는 경기 중일 때만 자동 새로고침을 활성화합니다.

경기가 끝나면 타이머를 중지하여 불필요한 요청을 방지합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 observer를 제거하지 않는 것입니다. removeObserver를 호출하지 않으면 메모리 누수가 발생합니다.

반드시 dispose에서 정리해야 합니다. 또 다른 주의사항은 너무 짧은 간격으로 새로고침하지 않는 것입니다.

1초 이하의 간격은 대부분 불필요하며, 서버와 클라이언트 모두에 부담을 줍니다. 마지막으로 백그라운드에서 완전히 타이머를 중지하고 싶다면, AppLifecycleState.paused에서 타이머를 취소하고 resumed에서 다시 시작하는 로직을 추가할 수 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 김개발 씨는 생명주기 관리를 추가하여 앱이 백그라운드에 있을 때는 새로고침을 멈추도록 개선했습니다.

QA 팀이 다시 테스트해 보니 배터리와 데이터 사용량이 크게 줄어들었습니다. 자동 새로고침 타이머는 사용자 경험과 효율성 사이의 균형을 맞추는 핵심 패턴입니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - WidgetsBindingObserver는 반드시 dispose에서 제거하세요

  • 백그라운드에서는 타이머를 중지하여 리소스를 절약하세요
  • 새로고침 간격은 실제 데이터 업데이트 빈도에 맞춰 설정하세요

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

#Riverpod#invalidate#refresh#StateManagement#DataRefresh#Flutter,Riverpod

댓글 (0)

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

함께 보면 좋은 카드 뉴스