본문 바로가기
공부/프로그래밍

Flutter 앱 개발 시리즈: Riverpod과 비동기 데이터 초기화 문제 해결하기

by demonic_ 2024. 11. 27.
반응형

들어가며

Flutter로 앱을 개발하다 보면 앱 시작 시 여러 데이터를 동시에 불러와야 하는 상황이 자주 발생한다. 특히 로컬저장소, SQLite, API 등 여러 데이터소스를 활용할때는 더욱 그렇다. 오늘은 앱을 개발하면서 마주친 비동기 초기화 문제와 그 해결 과정을 공유하려 한다.

 

 

문제상황

최근 개발중인 사진 앱에서는 시작 시 두가지 데이터를 불러와야 했다.

  1. 로컬 파일 시스템의 파일 정보
  2. SQLite 데이터베이스에 저장된 ID 정보

 

 

처음에는 단순하게 다음과 같이 구현하였다.

class HomeScreen extends ConsumerStatefulWidget {
  @override
  ConsumerState<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  void initState() {
    super.initState();
    fetchAlbums().then((result) {
      ref.read(localAlbumProvider.notifier).state = result;
    });

    fetchMyAlbum().then((data) {
      final albumIds = data.map((item) => item.albumId).toList();
      ref.read(albumStateProvider.notifier).setAlbums(albumIds);
    });
  }
  // ...
}

 

 

그러나 이 방식은 몇가지 문제가 있었다.

 

발생한 문제점들

  1. 데이터 동기화 문제
    1. 두 데이터 로딩이 독립적으로 실행되어 완료 시점이 달랐음
    2. 데이터가 로딩되지 않은 상태에서 화면이 그려져 데이터가 없는 상태로 진행되었음.
  2. 빈 화면 노출
    1. 데이터 로딩 전에 화면이 렌더링되어 사용자가 빈 화면을 보게 됨
    2. 좋지 않은 사용자 경험을 제공
  3. 타입 안정성 부족
    1. 암시적 타입 변환으로 인한 잠재적 런타임 에러 가능성이 있었음.

 

 

그래서 이 문제를 해결하기 위해 아래와 같이 코드를 수정했다.

// 로딩 상태 관리를 위한 Provider
final initLoadingProvider = StateProvider<bool>((ref) => true);

class HomeScreen extends ConsumerStatefulWidget {
  const HomeScreen({super.key});

  @override
  ConsumerState<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  void initState() {
    super.initState();
    _initData();
  }

  Future<void> _initData() async {
    try {
      // 병렬로 데이터 로딩
      final results = await Future.wait<dynamic>([
        fetchAlbums(),
        fetchMyAlbum(),
      ]);

      // 명시적 타입 캐스팅
      final albums = results[0] as List<AssetPathEntity>;
      final myAlbums = results[1] as List<MyAlbum>;

      // Provider 상태 업데이트
      ref.read(localAlbumProvider.notifier).state = albums;
      final albumIds = myAlbums.map((item) => item.albumId).toList();
      ref.read(albumStateProvider.notifier).setAlbums(albumIds);

      // 로딩 완료 처리
      ref.read(initLoadingProvider.notifier).state = false;
    } catch (e) {
      print('초기화 중 에러 발생: $e');
      // 에러 처리 로직
    }
  }

  @override
  Widget build(BuildContext context) {
    final isLoading = ref.watch(initLoadingProvider);

    if (isLoading) {
      return const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }

    return // 메인 화면 위젯
  }
}

 

 

개선된 점

  1. 데이터 동기화: 두 데이터 소스의 로딩이 완료되기 까지 loading 을 보여줌
  2. 사용자 경험: 로딩중에 로딩 인디케이터 표시
  3. 에러처리: try - catch 블록으로 에러 상황 관리
  4. 타입 안정성: 명시적인 타입 지정으로 런타임 에러 방지
  5. 성능: 병렬 데이터 로딩으로 초기화 시간 단축

 

 

이로 인해 배운점은 다음과 같다.

  1. Riverpod에서 비동기 초기화를 다룰 때는 loading 상태 관리가 중요
  2. Future.wait 를 활용한 병렬 처리로 성능 최적화 가능
  3. 사용자 경험을 고려한 로딩 상태 표기의 중요성.

 

 

마무리하며

이번 경험을 통해 다음에는 여러 api 호출이 필요할때 어떻게 핸들링 할 수 있을지를 알게 되었고, 화면이 랜더링 되기 전어떻게 사전로딩을 하여 시작할지를 배웠다. 이것을 응용해 여기저기서 많이 사용할 수 있을거 같다.

 

반응형

댓글