🤖

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

⚠️

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

이미지 로딩 중...

Riverpod 3.0 Offline Persistence 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 30. · 17 Views

Riverpod 3.0 Offline Persistence 완벽 가이드

Riverpod 3.0에서 새롭게 추가된 오프라인 및 영속성 지원 기능을 알아봅니다. 네트워크 연결 없이도 앱이 정상 동작하고, 앱을 재시작해도 상태가 유지되는 방법을 초급 개발자 눈높이에서 설명합니다.


목차

  1. Offline_기능_소개
  2. Storage_인터페이스_구현
  3. SharedPreferences_연동
  4. Hive_Isar_연동
  5. provider_future와_persisted_state
  6. 오프라인_우선_아키텍처

1. Offline 기능 소개

어느 날 김개발 씨가 만든 쇼핑몰 앱에 사용자 불만이 쏟아졌습니다. "지하철에서 앱을 열면 아무것도 안 보여요!" 네트워크가 끊기면 앱이 텅 빈 화면만 보여주는 문제였습니다.

오프라인 지원이 필요한 순간이었습니다.

Offline 기능은 네트워크 연결 없이도 앱이 정상적으로 동작할 수 있게 해주는 기능입니다. 마치 냉장고에 음식을 저장해두면 마트가 문을 닫아도 먹을 수 있는 것처럼, 데이터를 로컬에 저장해두면 인터넷이 끊겨도 앱을 사용할 수 있습니다.

Riverpod 3.0은 이 기능을 공식적으로 지원합니다.

다음 코드를 살펴봅시다.

// Riverpod 3.0의 오프라인 지원 기본 구조
@riverpod
class UserSettings extends _$UserSettings {
  @override
  Future<Settings> build() async {
    // 로컬 저장소에서 먼저 데이터를 불러옵니다
    final cached = await ref.read(storageProvider).load('settings');
    if (cached != null) return Settings.fromJson(cached);

    // 캐시가 없으면 서버에서 가져옵니다
    final response = await ref.read(apiProvider).fetchSettings();
    // 가져온 데이터를 로컬에 저장합니다
    await ref.read(storageProvider).save('settings', response.toJson());
    return response;
  }
}

김개발 씨는 입사 6개월 차 Flutter 개발자입니다. 회사에서 운영하는 쇼핑몰 앱의 리뷰란에 한 달째 같은 불만이 올라오고 있었습니다.

"지하철에서 앱이 안 돼요", "비행기 탈 때 쓸 수가 없어요". 모두 오프라인 상황에서의 문제였습니다.

팀장 박시니어 씨가 회의에서 이 문제를 언급했습니다. "우리 앱이 온라인에만 의존하고 있어서 생기는 문제입니다.

이번에 Riverpod 3.0으로 업그레이드하면서 오프라인 지원을 추가해봅시다." 오프라인 지원이란 정확히 무엇일까요? 쉽게 비유하자면, 오프라인 지원은 마치 집에 비상식량을 비축해두는 것과 같습니다.

마트가 문을 닫거나 태풍이 와서 외출이 어려워도, 집에 저장해둔 음식으로 끼니를 해결할 수 있습니다. 앱도 마찬가지입니다.

서버에서 받아온 데이터를 로컬 저장소에 보관해두면, 네트워크가 끊겨도 사용자에게 콘텐츠를 보여줄 수 있습니다. Riverpod 3.0 이전에는 오프라인 지원을 직접 구현해야 했습니다.

개발자가 캐시 로직을 하나하나 작성하고, 네트워크 상태를 확인하고, 동기화 로직까지 만들어야 했습니다. 코드가 복잡해지고 버그가 생기기 쉬웠습니다.

Riverpod 3.0은 이런 복잡함을 해결하기 위해 공식 오프라인 지원을 도입했습니다. 저장소 인터페이스를 정의하고, 원하는 저장 방식을 연결하기만 하면 됩니다.

프레임워크가 나머지를 알아서 처리해줍니다. 위 코드를 살펴보면, build 메서드에서 먼저 로컬 저장소를 확인합니다.

캐시된 데이터가 있으면 그것을 반환하고, 없을 때만 서버에 요청합니다. 서버에서 데이터를 받아오면 로컬에 저장해둡니다.

다음에 오프라인 상태가 되어도 저장된 데이터를 사용할 수 있습니다. 실제 현업에서는 이 패턴이 매우 유용합니다.

뉴스 앱을 생각해보세요. 사용자가 아침에 와이파이로 기사를 불러오면 로컬에 저장됩니다.

출퇴근길 지하철에서 네트워크가 끊겨도 아까 불러온 기사는 그대로 읽을 수 있습니다. 주의할 점도 있습니다.

오프라인 데이터는 시간이 지나면 오래된 정보가 될 수 있습니다. 따라서 네트워크가 다시 연결되면 최신 데이터로 갱신하는 로직도 함께 고려해야 합니다.

김개발 씨는 팀장의 설명을 듣고 고개를 끄덕였습니다. "그렇군요, 로컬에 데이터를 저장해두면 오프라인에서도 앱을 쓸 수 있겠네요!" 이제 본격적으로 Storage 인터페이스 구현을 배워볼 차례입니다.

실전 팁

💡 - 오프라인 데이터의 유효 기간을 설정해 오래된 캐시를 자동 삭제하세요

  • 네트워크 복구 시 백그라운드에서 데이터를 갱신하는 로직을 추가하세요

2. Storage 인터페이스 구현

김개발 씨가 오프라인 기능을 구현하려고 코드를 작성하기 시작했습니다. 그런데 어디에 데이터를 저장해야 할까요?

SharedPreferences? Hive?

SQLite? 박시니어 씨가 다가와 조언했습니다.

"먼저 Storage 인터페이스를 만들어. 나중에 저장소를 바꿔도 코드를 수정할 필요가 없어지거든."

Storage 인터페이스는 데이터 저장 방식을 추상화한 계약서와 같습니다. 마치 USB 포트처럼, 규격만 맞으면 어떤 저장 장치든 꽂아서 사용할 수 있습니다.

인터페이스를 정의해두면 SharedPreferences, Hive, Isar 등 어떤 저장소든 쉽게 교체할 수 있습니다.

다음 코드를 살펴봅시다.

// Storage 인터페이스 정의
abstract class Storage {
  // 데이터를 저장합니다
  Future<void> save(String key, Map<String, dynamic> value);

  // 데이터를 불러옵니다
  Future<Map<String, dynamic>?> load(String key);

  // 데이터를 삭제합니다
  Future<void> delete(String key);

  // 모든 데이터를 삭제합니다
  Future<void> clear();
}

// Storage Provider 정의
@riverpod
Storage storage(Ref ref) {
  // 실제 구현체를 반환합니다 (다음 섹션에서 구현)
  return SharedPreferencesStorage();
}

김개발 씨는 오프라인 기능을 구현하기 위해 데이터를 어디에 저장할지 고민했습니다. 처음에는 SharedPreferences를 사용하려고 했습니다.

간단하고 Flutter에서 기본으로 제공하니까요. 하지만 문득 걱정이 생겼습니다.

나중에 데이터가 많아지면 Hive로 바꿔야 할 수도 있는데, 그때 코드를 전부 수정해야 할까요? 박시니어 씨가 조언했습니다.

"인터페이스를 사용하면 그런 걱정을 할 필요가 없어." 인터페이스란 무엇일까요? 쉽게 비유하면, 인터페이스는 전기 콘센트와 같습니다.

한국의 콘센트 규격에 맞는 플러그라면, 어떤 전자제품이든 꽂아서 사용할 수 있습니다. 드라이기를 꽂든 충전기를 꽂든 콘센트는 상관하지 않습니다.

규격만 맞으면 됩니다. Storage 인터페이스도 마찬가지입니다.

save, load, delete, clear라는 규격을 정해두면, 이 규격을 따르는 어떤 저장소든 사용할 수 있습니다. 지금은 SharedPreferences를 쓰다가 나중에 Hive로 바꿔도, 앱의 다른 코드는 전혀 수정할 필요가 없습니다.

위 코드에서 abstract class Storage는 계약서 역할을 합니다. "나를 구현하려면 이 네 가지 메서드를 반드시 갖추어야 해"라고 선언하는 것입니다.

save는 데이터를 저장하고, load는 불러오고, delete는 삭제하고, clear는 전체를 비웁니다. storage Provider는 실제 Storage 구현체를 제공합니다.

지금은 SharedPreferencesStorage를 반환하지만, 나중에 HiveStorage로 바꾸고 싶다면 이 한 줄만 수정하면 됩니다. 앱 전체에서 storage를 사용하는 코드는 수정할 필요가 없습니다.

실무에서 이런 설계가 왜 중요할까요? 프로젝트 초기에는 간단한 저장소로 시작하는 경우가 많습니다.

하지만 사용자가 늘고 데이터가 많아지면 더 성능 좋은 저장소로 교체해야 할 때가 옵니다. 인터페이스가 없으면 저장소를 바꿀 때마다 수십, 수백 개의 파일을 수정해야 합니다.

인터페이스가 있으면 구현체 하나만 바꾸면 됩니다. 주의할 점이 있습니다.

인터페이스를 너무 복잡하게 설계하면 오히려 구현이 어려워집니다. 꼭 필요한 메서드만 정의하세요.

위 예제처럼 save, load, delete, clear 정도면 대부분의 경우를 커버할 수 있습니다. 김개발 씨는 인터페이스의 중요성을 깨달았습니다.

"처음부터 잘 설계해두면 나중에 편하겠군요!" 이제 이 인터페이스를 실제로 구현해볼 차례입니다.

실전 팁

💡 - 인터페이스는 단순하게 유지하세요. 필요할 때 메서드를 추가하면 됩니다

  • 테스트 시에는 Mock Storage를 만들어 실제 저장소 없이도 테스트할 수 있습니다

3. SharedPreferences 연동

김개발 씨가 Storage 인터페이스를 정의했습니다. 이제 첫 번째 구현체를 만들 차례입니다.

가장 간단한 것부터 시작하기로 했습니다. Flutter 개발자라면 누구나 한 번쯤 써봤을 SharedPreferences입니다.

SharedPreferences는 Flutter에서 가장 기본적인 로컬 저장소입니다. 마치 수첩에 간단한 메모를 적어두는 것처럼, 작은 데이터를 키-값 형태로 저장합니다.

설정값이나 사용자 정보 같은 간단한 데이터를 저장하기에 적합합니다.

다음 코드를 살펴봅시다.

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferencesStorage implements Storage {
  @override
  Future<void> save(String key, Map<String, dynamic> value) async {
    final prefs = await SharedPreferences.getInstance();
    // JSON 문자열로 변환하여 저장합니다
    await prefs.setString(key, jsonEncode(value));
  }

  @override
  Future<Map<String, dynamic>?> load(String key) async {
    final prefs = await SharedPreferences.getInstance();
    final data = prefs.getString(key);
    // 저장된 데이터가 없으면 null 반환
    if (data == null) return null;
    return jsonDecode(data) as Map<String, dynamic>;
  }

  @override
  Future<void> delete(String key) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(key);
  }

  @override
  Future<void> clear() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();
  }
}

김개발 씨는 첫 번째 Storage 구현체로 SharedPreferences를 선택했습니다. 왜 SharedPreferences일까요?

이유는 간단합니다. Flutter에서 기본으로 제공하고, 별도의 설정 없이 바로 사용할 수 있기 때문입니다.

SharedPreferences는 마치 냉장고 문에 붙이는 메모지와 같습니다. 전화번호나 장보기 목록처럼 간단한 정보를 적어두기에 딱 좋습니다.

하지만 소설 원고 전체를 메모지에 적기는 어렵겠죠? SharedPreferences도 마찬가지입니다.

작은 데이터에는 완벽하지만, 대용량 데이터에는 적합하지 않습니다. 위 코드를 살펴보겠습니다.

SharedPreferencesStorage 클래스는 앞서 정의한 Storage 인터페이스를 implements합니다. 이제 이 클래스는 반드시 save, load, delete, clear 메서드를 구현해야 합니다.

save 메서드에서 주목할 점은 jsonEncode입니다. SharedPreferences는 문자열만 저장할 수 있습니다.

Map 형태의 데이터를 저장하려면 먼저 JSON 문자열로 변환해야 합니다. 마치 택배를 보낼 때 물건을 상자에 포장하는 것과 같습니다.

load 메서드는 그 반대 과정을 수행합니다. 저장된 JSON 문자열을 다시 Map으로 변환합니다.

상자를 열어서 물건을 꺼내는 것과 같습니다. 저장된 데이터가 없으면 null을 반환해서 호출하는 쪽에서 적절히 처리할 수 있게 합니다.

실무에서 SharedPreferences는 어떤 경우에 사용할까요? 사용자 설정, 로그인 상태, 온보딩 완료 여부, 마지막으로 본 페이지 같은 간단한 정보를 저장하기에 적합합니다.

앱을 껐다 켜도 설정이 유지되어야 할 때 사용합니다. 하지만 주의할 점이 있습니다.

SharedPreferences는 동기화 보장이 되지 않습니다. 여러 곳에서 동시에 같은 키에 접근하면 예상치 못한 결과가 발생할 수 있습니다.

또한 저장할 수 있는 데이터 크기에도 플랫폼별 제한이 있습니다. 김개발 씨는 SharedPreferences 연동을 완료했습니다.

이제 사용자 설정 정도는 오프라인에서도 유지할 수 있게 되었습니다. 하지만 더 많은 데이터를 저장하려면 어떻게 해야 할까요?

다음 섹션에서 Hive와 Isar를 알아보겠습니다.

실전 팁

💡 - SharedPreferences는 앱 삭제 시 함께 삭제됩니다. 중요한 데이터는 서버에도 백업하세요

  • 민감한 정보는 flutter_secure_storage를 사용하세요

4. Hive Isar 연동

김개발 씨의 앱이 성장하면서 저장해야 할 데이터가 많아졌습니다. 사용자 정보뿐 아니라 상품 목록, 장바구니, 주문 내역까지.

SharedPreferences로는 감당이 안 됩니다. 박시니어 씨가 말했습니다.

"이제 제대로 된 로컬 데이터베이스를 써야 할 때가 됐어."

HiveIsar는 Flutter를 위한 고성능 로컬 데이터베이스입니다. 마치 개인 창고를 빌리는 것과 같습니다.

메모지에 적기엔 너무 많은 물건도 창고에는 체계적으로 보관할 수 있습니다. 대용량 데이터를 빠르게 저장하고 조회할 수 있습니다.

다음 코드를 살펴봅시다.

import 'package:hive_flutter/hive_flutter.dart';

class HiveStorage implements Storage {
  static const String _boxName = 'app_cache';

  @override
  Future<void> save(String key, Map<String, dynamic> value) async {
    final box = await Hive.openBox<Map>(_boxName);
    await box.put(key, value);
  }

  @override
  Future<Map<String, dynamic>?> load(String key) async {
    final box = await Hive.openBox<Map>(_boxName);
    final data = box.get(key);
    if (data == null) return null;
    // Hive에서 가져온 Map을 올바른 타입으로 변환
    return Map<String, dynamic>.from(data);
  }

  @override
  Future<void> delete(String key) async {
    final box = await Hive.openBox<Map>(_boxName);
    await box.delete(key);
  }

  @override
  Future<void> clear() async {
    final box = await Hive.openBox<Map>(_boxName);
    await box.clear();
  }
}

김개발 씨는 앱의 성장과 함께 고민이 깊어졌습니다. 상품이 수천 개가 넘고, 사용자마다 장바구니와 주문 내역이 쌓입니다.

SharedPreferences로 이 모든 것을 저장하기엔 역부족이었습니다. 박시니어 씨가 두 가지 선택지를 제시했습니다.

HiveIsar입니다. 둘 다 Flutter를 위해 만들어진 로컬 데이터베이스입니다.

Hive는 가볍고 빠르며, Isar는 더 강력한 쿼리 기능을 제공합니다. Hive를 쉽게 비유하면, 잘 정리된 서랍장과 같습니다.

각 서랍에 이름표를 붙이고 물건을 넣어둡니다. 나중에 이름표만 보고 원하는 물건을 빠르게 찾을 수 있습니다.

SharedPreferences가 메모지라면, Hive는 서랍장입니다. 훨씬 많은 것을 체계적으로 보관할 수 있습니다.

위 코드에서 Box라는 개념이 등장합니다. Box는 Hive의 저장 단위입니다.

서랍장의 서랍 하나라고 생각하면 됩니다. openBox로 서랍을 열고, put으로 물건을 넣고, get으로 꺼내옵니다.

SharedPreferences와 달리 Hive는 JSON 변환이 필요 없습니다. Map을 직접 저장할 수 있습니다.

다만 load에서 Map.from으로 타입을 변환하는 이유는, Hive가 반환하는 Map의 타입이 정확히 일치하지 않을 수 있기 때문입니다. Isar는 Hive보다 한 단계 더 진화한 데이터베이스입니다.

복잡한 쿼리, 전문 검색, 다중 인덱스를 지원합니다. 다만 설정이 조금 더 복잡합니다.

간단한 캐싱에는 Hive가, 복잡한 데이터 구조에는 Isar가 적합합니다. 실무에서 어떤 것을 선택해야 할까요?

프로젝트 초기에는 Hive로 시작하는 것이 좋습니다. 빠르게 구현하고, 나중에 필요하면 Isar로 전환할 수 있습니다.

우리가 Storage 인터페이스를 만들어둔 덕분에 전환이 쉽습니다. 주의할 점이 있습니다.

Hive를 사용하기 전에 반드시 **await Hive.initFlutter()**를 호출해야 합니다. 보통 main 함수에서 앱 시작 시 초기화합니다.

초기화를 빠뜨리면 앱이 크래시됩니다. 김개발 씨는 Hive를 도입하고 나서 앱의 오프라인 기능이 훨씬 안정적으로 동작하는 것을 확인했습니다.

"이제 수천 개의 상품 정보도 문제없이 캐싱할 수 있겠네요!"

실전 팁

💡 - Hive 초기화는 main 함수에서 runApp 전에 수행하세요

  • 복잡한 객체는 TypeAdapter를 등록해야 합니다

5. provider future와 persisted state

김개발 씨가 오프라인 저장소를 연결했습니다. 하지만 한 가지 의문이 생겼습니다.

"앱을 시작할 때마다 저장소에서 데이터를 불러오는 코드를 매번 작성해야 하나요?" 박시니어 씨가 웃으며 대답했습니다. "Riverpod 3.0의 persisted state를 쓰면 자동으로 처리돼."

Persisted state는 상태가 자동으로 저장소에 저장되고 복원되는 기능입니다. 마치 게임의 자동 저장 기능과 같습니다.

플레이어가 직접 저장 버튼을 누르지 않아도, 게임이 알아서 진행 상황을 저장해두고 다음에 이어서 플레이할 수 있게 해줍니다.

다음 코드를 살펴봅시다.

@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  Future<List<CartItem>> build() async {
    // 앱 시작 시 저장된 장바구니를 자동으로 불러옵니다
    final storage = ref.read(storageProvider);
    final cached = await storage.load('cart');

    if (cached != null) {
      final items = (cached['items'] as List)
          .map((e) => CartItem.fromJson(e))
          .toList();
      return items;
    }
    return [];
  }

  Future<void> addItem(CartItem item) async {
    final current = await future;
    final updated = [...current, item];
    state = AsyncData(updated);

    // 상태가 변경될 때마다 자동으로 저장합니다
    await _persist(updated);
  }

  Future<void> _persist(List<CartItem> items) async {
    final storage = ref.read(storageProvider);
    await storage.save('cart', {
      'items': items.map((e) => e.toJson()).toList(),
    });
  }
}

김개발 씨는 장바구니 기능을 구현하고 있었습니다. 사용자가 상품을 담으면 서버에 저장하는 것까지는 완료했습니다.

하지만 문제가 있었습니다. 오프라인 상태에서 상품을 담으면 서버에 저장할 수 없었습니다.

박시니어 씨가 해결책을 제시했습니다. "로컬에 먼저 저장하고, 온라인이 되면 서버와 동기화하면 돼." 이것이 바로 persisted state의 핵심 개념입니다.

Persisted state는 마치 스마트폰의 메모 앱과 같습니다. 비행기 모드에서 메모를 작성해도 폰에 저장됩니다.

나중에 인터넷에 연결되면 클라우드와 동기화됩니다. 사용자는 이 과정을 의식할 필요가 없습니다.

그냥 메모를 작성하면 됩니다. 위 코드에서 build 메서드를 보세요.

앱이 시작되면 가장 먼저 로컬 저장소에서 기존 장바구니를 불러옵니다. 저장된 데이터가 있으면 그것을 초기 상태로 사용합니다.

사용자는 앱을 껐다 켜도 장바구니가 그대로 유지됩니다. addItem 메서드에서는 상태를 변경한 직후 _persist를 호출합니다.

상품을 장바구니에 담을 때마다 자동으로 로컬에 저장됩니다. 이렇게 하면 앱이 갑자기 종료되어도 데이터가 사라지지 않습니다.

await future라는 표현에 주목하세요. AsyncNotifier에서 현재 상태의 실제 값을 얻으려면 future를 await해야 합니다.

state는 AsyncValue 타입이지만, future를 await하면 실제 데이터를 얻을 수 있습니다. 실무에서 이 패턴은 정말 유용합니다.

이커머스 앱의 장바구니, 게임의 진행 상황, 메모 앱의 임시 저장 등 수많은 곳에서 활용됩니다. 사용자 경험이 크게 향상됩니다.

주의할 점이 있습니다. 저장 작업은 비동기이므로, 아주 빠르게 여러 번 상태를 변경하면 저장 순서가 꼬일 수 있습니다.

디바운싱이나 쓰로틀링을 적용해서 저장 빈도를 조절하는 것이 좋습니다. 김개발 씨는 이제 장바구니가 오프라인에서도 완벽하게 동작하는 것을 확인했습니다.

"앱을 껐다 켜도 장바구니가 그대로네요!"

실전 팁

💡 - 저장 작업이 너무 잦으면 디바운싱을 적용하세요

  • 민감한 데이터는 저장 전에 암호화를 고려하세요

6. 오프라인 우선 아키텍처

김개발 씨의 앱이 드디어 오프라인을 지원하게 되었습니다. 하지만 박시니어 씨가 한 가지 더 조언했습니다.

"지금은 온라인을 기본으로 하고 오프라인을 보조하는 구조야. 아예 오프라인을 먼저 생각하면 어떨까?"

오프라인 우선 아키텍처는 네트워크 연결이 항상 불안정하다고 가정하고 설계하는 방식입니다. 마치 캠핑을 준비할 때 전기가 없다고 가정하고 짐을 싸는 것과 같습니다.

최악의 상황을 먼저 대비하면, 좋은 상황에서는 더욱 여유롭게 대처할 수 있습니다.

다음 코드를 살펴봅시다.

@riverpod
class ProductRepository extends _$ProductRepository {
  @override
  Future<List<Product>> build() async {
    // 1단계: 로컬 캐시를 먼저 보여줍니다 (즉시 응답)
    final cached = await _loadFromCache();
    if (cached.isNotEmpty) {
      // 캐시가 있으면 먼저 화면에 표시
      state = AsyncData(cached);
    }

    // 2단계: 백그라운드에서 서버 데이터를 가져옵니다
    _syncWithServer();

    return cached;
  }

  Future<List<Product>> _loadFromCache() async {
    final storage = ref.read(storageProvider);
    final data = await storage.load('products');
    if (data == null) return [];
    return (data['items'] as List)
        .map((e) => Product.fromJson(e))
        .toList();
  }

  Future<void> _syncWithServer() async {
    try {
      final products = await ref.read(apiProvider).fetchProducts();
      state = AsyncData(products);
      await _saveToCache(products);
    } catch (e) {
      // 네트워크 오류는 무시 (이미 캐시를 보여주고 있음)
    }
  }
}

김개발 씨는 지금까지 온라인 우선으로 생각해왔습니다. 서버에서 데이터를 가져오고, 실패하면 캐시를 보여주는 방식이었습니다.

하지만 이 방식에는 문제가 있습니다. 네트워크가 느리면 사용자는 로딩 화면만 계속 봐야 합니다.

오프라인 우선 아키텍처는 정반대입니다. 캐시를 먼저 보여주고, 백그라운드에서 서버 데이터를 가져옵니다.

사용자는 즉시 콘텐츠를 볼 수 있고, 새로운 데이터가 도착하면 자연스럽게 업데이트됩니다. 이것을 식당에 비유해보겠습니다.

온라인 우선은 손님이 오면 주방에서 요리를 시작하는 것입니다. 손님은 요리가 나올 때까지 기다려야 합니다.

오프라인 우선은 미리 만들어둔 반찬을 먼저 내놓는 것입니다. 손님은 반찬을 먹으면서 기다리고, 메인 요리가 나오면 더 맛있게 먹을 수 있습니다.

위 코드의 build 메서드를 보세요. 먼저 _loadFromCache를 호출해서 로컬 데이터를 가져옵니다.

캐시가 있으면 바로 상태를 업데이트합니다. 사용자는 즉시 상품 목록을 볼 수 있습니다.

그 다음 _syncWithServer를 호출합니다. 여기서 주목할 점은 await 없이 호출한다는 것입니다.

백그라운드에서 실행되므로 사용자를 기다리게 하지 않습니다. 서버 데이터가 도착하면 상태가 업데이트되고 화면이 갱신됩니다.

_syncWithServer에서 에러가 발생해도 catch로 잡아서 무시합니다. 왜냐하면 이미 캐시 데이터를 보여주고 있기 때문입니다.

네트워크 오류 때문에 앱이 멈추지 않습니다. 실무에서 이 아키텍처는 사용자 경험을 크게 향상시킵니다.

구글 맵을 생각해보세요. 오프라인에서도 다운로드된 지도는 볼 수 있습니다.

트위터도 비슷합니다. 앱을 열면 마지막으로 불러온 트윗이 먼저 보이고, 새 트윗은 백그라운드에서 불러옵니다.

주의할 점이 있습니다. 오래된 캐시 데이터는 사용자를 혼란스럽게 할 수 있습니다.

가격이 바뀌었는데 옛날 가격이 보인다면 문제가 됩니다. 따라서 캐시의 유효 기간을 설정하고, 중요한 데이터는 사용자에게 새로고침 중 표시를 보여주는 것이 좋습니다.

김개발 씨는 오프라인 우선 아키텍처를 적용한 후 앱 리뷰가 눈에 띄게 좋아진 것을 확인했습니다. "앱이 정말 빨라졌어요!"라는 리뷰가 늘어났습니다.

네트워크 상태와 관계없이 항상 빠른 앱, 그것이 오프라인 우선의 힘입니다.

실전 팁

💡 - 캐시 데이터 옆에 마지막 동기화 시간을 표시해주세요

  • 중요한 변경사항은 즉시 동기화하되, 덜 중요한 것은 배치로 처리하세요
  • Pull-to-refresh로 사용자가 직접 새로고침할 수 있게 하세요

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

#Flutter#Riverpod#Offline#Persistence#SharedPreferences#Hive#Flutter,State Management

댓글 (0)

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

함께 보면 좋은 카드 뉴스