결과처리

topics 500-모바일개발 501 Flutter 0_SubTopics/505 상태관리
types 이론 실습
tags

결과 처리

API 호출이나 비동기 작업의 결과를 처리하는 패턴이다.

스크린샷 2024-03-16 오전 1.10.05.png

왜 Result 패턴을 쓸까

// 나쁜 예 - try-catch만 사용
try {
  final user = await fetchUser();
  // 성공 처리
} catch (e) {
  // 에러 처리
}

문제점: 에러 타입을 명시적으로 다루기 어렵다. catch 블록에서 모든 에러를 처리해야 한다. 코드가 분산된다.

Result 타입 구현

Sealed Class 활용 (Dart 3.0+)

sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}

class Failure<T> extends Result<T> {
  final String message;
  final Exception? exception;
  Failure(this.message, [this.exception]);
}

사용 예시

Future<Result<User>> fetchUser(String id) async {
  try {
    final response = await api.getUser(id);
    return Success(User.fromJson(response));
  } on NetworkException catch (e) {
    return Failure('네트워크 오류', e);
  } on ServerException catch (e) {
    return Failure('서버 오류', e);
  } catch (e) {
    return Failure('알 수 없는 오류');
  }
}

UI에서 처리

Widget build(BuildContext context) {
  return FutureBuilder<Result<User>>(
    future: fetchUser('123'),
    builder: (context, snapshot) {
      if (!snapshot.hasData) {
        return CircularProgressIndicator();
      }

      return switch (snapshot.data!) {
        Success(data: var user) => UserProfile(user: user),
        Failure(message: var msg) => ErrorWidget(message: msg),
      };
    },
  );
}

로딩 상태 포함

sealed class AsyncResult<T> {}

class Loading<T> extends AsyncResult<T> {}

class Success<T> extends AsyncResult<T> {
  final T data;
  Success(this.data);
}

class Failure<T> extends AsyncResult<T> {
  final String message;
  Failure(this.message);
}

Notifier에서 사용

class UserNotifier extends Notifier<AsyncResult<User>> {
  @override
  AsyncResult<User> build() => Loading();

  Future<void> fetch(String id) async {
    state = Loading();

    final result = await repository.getUser(id);

    state = switch (result) {
      Success(data: var user) => Success(user),
      Failure(message: var msg) => Failure(msg),
      Loading() => Loading(),
    };
  }
}

Riverpod의 AsyncValue

Riverpod을 사용한다면 직접 만들 필요 없다. AsyncValue가 이미 있다.

final userProvider = FutureProvider<User>((ref) async {
  return await repository.getUser();
});

// UI
ref.watch(userProvider).when(
  loading: () => CircularProgressIndicator(),
  error: (err, stack) => Text('Error: $err'),
  data: (user) => UserProfile(user: user),
);

왜 AsyncValue를 권장하냐면: 로딩/에러/데이터 상태가 이미 정의되어 있고, when, maybeWhen 등 편리한 메소드를 제공한다.

Either 패턴 (fpdart)

함수형 프로그래밍 스타일을 선호한다면 fpdart 패키지의 Either를 사용할 수 있다.

import 'package:fpdart/fpdart.dart';

Future<Either<Failure, User>> fetchUser() async {
  try {
    final user = await api.getUser();
    return Right(user);
  } catch (e) {
    return Left(Failure('에러 발생'));
  }
}

// 사용
final result = await fetchUser();
result.fold(
  (failure) => print(failure.message),
  (user) => print(user.name),
);

언제 뭘 쓸까

상황 추천
Riverpod 사용 중 AsyncValue
간단한 Result 필요 Sealed Class 직접 구현
함수형 프로그래밍 선호 fpdartEither
복잡한 에러 타입 분기 Sealed Class + 패턴 매칭

관련 문서