[Riverpod] AsyncSnapshot 대신 AsyncValue를 사용해 보세요.

eltese·2024년 4월 7일
0

riverpod

목록 보기
3/3
post-thumbnail

API 응답값을 UI에 출력하기 위해 FutureBuilder, StreamBuilder를 한 번쯤은 사용해 본 적이 있을 것이다. 오늘은 FutureBuilder, StreamBuilder에서 사용되는 AsyncSnapshot의 문제점을 알아 보고 더 나은 대안인 AsyncValue에 대해 알아 보자.

AsyncSnapshot

StreamBuilder<Item>(
  stream: getData(),
  builder: (context, snapshot) {
  	// 로딩의 경우 
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    } else if (snapshot.hasData) {
      // 데이터를 가지고 있는 경우 
      final item = snapshot.data!;
      return ListTile(
        title: Text(item.title),
      );
    } else if (snapshot.hasError) {
      // 에러가 발생한 경우
      final error = snapshot.error!;
      return Text(error.toString());
    } 
  },
);

StreamBuilder는 위처럼 사용해왔을 것이다.
개발자가 직접 builder 내부에서 로딩(ConnectionState.waiting)일 때, 데이터가 있을 때(snapshot.hasData), 에러일 때(snapshot.hasError)를 if-else 문으로 분기처리해야 한다.
그 이유는 AsyncSnapshot의 내부 구현을 살펴보면 알 수 있다.

class AsyncSnapshot<T> {
  final ConnectionState connectionState;
  final T? data;
  final Object? error;
  final StackTrace? stackTrace;
  bool get hasData => data != null;
  bool get hasError => error != null;
}

connectionState, data, error, stackTrace 변수가 모두 상호 배타적이지 않고 독립적이기 때문에 개발자가 직접 if-else로 분기 처리를 해야하는 상황이 발생한 것이다.

AsyncValue

반면 AsyncValue를 사용하는 코드를 살펴보자.

asyncValue.when(
  // 데이터를 가지고 있는 경우
  data: (item) => ListTile(
    title: Text(item.title),
    subtitle: Text(item.description),
  ),
  // 로딩의 경우
  loading: () => const Center(child: CircularProgressIndicator()),
  // 에러가 발생한 경우
  error: (e, st) => Center(child: Text(e.toString())),
);

코드가 훨씬 간단해지고 가독성도 좋아졌다. 또한 when 메서드의 내부 구현을 살펴보면

R when<R>({
    bool skipLoadingOnReload = false,
    bool skipLoadingOnRefresh = true,
    bool skipError = false,
    required R Function(T data) data,
    required R Function(Object error, StackTrace stackTrace) error,
    required R Function() loading,
  }) {
    if (isLoading) {
      bool skip;
      if (isRefreshing) {
        skip = skipLoadingOnRefresh;
      } else if (isReloading) {
        skip = skipLoadingOnReload;
      } else {
        skip = false;
      }
      if (!skip) return loading();
    }

    if (hasError && (!hasValue || !skipError)) {
      return error(this.error!, stackTrace!);
    }

    return data(requireValue);
  }

AsyncSnapshot과는 다르게 세 경우가 when 메서드에서 이미 분기 처리가 되어 있다는 것을 확인해 볼 수 있다.

따라서 개발자가 직접 if-else문으로 상태를 구분하는 것이 아니라 미리 구분되어 있는 data, loading, error를 사용하기만 하면 된다.

profile
백엔드 주니어 개발자 EL과 앱 개발자 Altese가 함께 운영하는 블로그

0개의 댓글