brownglasses / gbfb

0 stars 0 forks source link

[feature] 에러 처리하기 (useCase , ViewModel) #8

Closed brownglasses closed 1 month ago

brownglasses commented 1 month ago

uscase 와 viewModel 에서 에러가 나타났을 때, try ~ catch 하는 구문을 작성


보충 설명

Flutter 앱에서 모든 에러를 깔끔하게 처리하는 것은 사용자 경험을 향상시키고 디버깅을 용이하게 만드는 중요한 부분입니다. 에러를 체계적으로 관리하기 위해서는 적절한 아키텍처와 에러 핸들링 전략을 구축해야 합니다. 여기서는 MVVM 패턴을 기반으로 Riverpod을 활용하여 에러를 일관되게 처리하는 방법을 살펴보겠습니다.

1. 에러 처리 전략 설계

에러 처리 전략은 다음과 같은 사항을 포함해야 합니다.

  1. 에러 분류: 에러의 종류에 따라 다른 처리를 합니다 (네트워크, 데이터베이스, 사용자 입력 등).
  2. 에러 로깅: 디버깅을 위해 에러를 기록합니다.
  3. 사용자 피드백: 사용자에게 적절한 피드백을 제공합니다 (알림, 다이얼로그 등).
  4. 전역 에러 핸들링: 앱의 최상위 레벨에서 에러를 포착합니다.

2. 에러 클래스 정의

에러를 관리하기 위한 전용 클래스를 정의합니다.

app_exception.dart

class AppException implements Exception {
  final String message;
  final String? details;

  AppException(this.message, {this.details});

  @override
  String toString() => 'AppException: $message, Details: $details';
}

class NetworkException extends AppException {
  NetworkException(super.message, {super.details});
}

class DatabaseException extends AppException {
  DatabaseException(super.message, {super.details});
}

class ValidationException extends AppException {
  ValidationException(super.message, {super.details});
}

class AuthorizationException extends AppException {
  AuthorizationException(super.message, {super.details});
}

class FileNotFoundException extends AppException {
  FileNotFoundException(super.message, {super.details});
}

class TimeoutException extends AppException {
  TimeoutException(super.message, {super.details});
}

단계별 가이드

1. UseCase에서 커스텀 예외 사용하기

CreateProfileUseCase는 프로필을 생성하는 것과 관련된 로직을 담당하며, 프로필 사진을 업로드하고 Firestore에 프로필을 저장합니다. 이 과정에서 발생할 수 있는 다양한 에러를 커스텀 예외로 캡슐화하여 처리하는 것이 좋은 방법입니다.

아래는 CreateProfileUseCase에서 커스텀 예외를 사용하는 방법입니다:

import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:gfbf/models/profile_model.dart';
import 'package:gfbf/utils/log.dart'; // Log 클래스를 가져옵니다.
import 'exceptions.dart'; // 커스텀 예외 클래스 import

class CreateProfileUseCase {
  final FirebaseAuth firebaseAuth;
  final FirebaseFirestore firebaseFirestore;
  final FirebaseStorage firebaseStorage;

  CreateProfileUseCase(
    this.firebaseAuth,
    this.firebaseFirestore,
    this.firebaseStorage,
  );

  Future<void> execute(ProfileModel profileModel, File? file) async {
    try {
      Log.info("프로필 생성 시작");

      // 현재 로그인된 사용자 가져오기
      User? user = firebaseAuth.currentUser;
      if (user == null) {
        Log.error('로그인된 사용자가 없습니다.');
        throw AuthorizationException('User not logged in');
      }

      Log.info('로그인된 사용자 UID: ${user.uid}');

      // 프로필 모델에 UID 설정
      profileModel = profileModel.copyWith(uid: user.uid);

      // 프로필 사진 업로드
      String photoUrl = '';
      if (file != null) {
        Log.info('프로필 사진 업로드 시작 - UID: ${user.uid}');
        photoUrl = await _uploadProfilePhoto(file, user.uid);
        profileModel = profileModel.copyWith(photoUrl: photoUrl);
        Log.info('프로필 사진 업로드 성공: $photoUrl');
      }

      // Firestore에 프로필 저장
      Log.info('Firestore에 프로필 저장 - UID: ${user.uid}');
      await firebaseFirestore
          .collection('profiles')
          .doc(user.uid)
          .set(profileModel.toMap());
      Log.info('프로필 저장 성공 - UID: ${user.uid}');
    } on FirebaseAuthException catch (e) {
      // Firebase 인증 관련 오류 처리
      Log.error('Firebase 인증 오류 발생', e);
      throw AuthorizationException('Firebase Auth error: ${e.message}');
    } on FirebaseException catch (e) {
      // Firebase 관련 오류 처리
      Log.error('Firebase 오류 발생', e);
      throw DatabaseException('Firestore error: ${e.message}');
    } on SocketException catch (e) {
      // 네트워크 관련 오류 처리
      Log.error('네트워크 오류 발생', e);
      throw NetworkException('No Internet connection');
    } catch (e, stackTrace) {
      Log.error('프로필 생성 중 예상치 못한 오류 발생', e, stackTrace);
      throw AppException('Unexpected error: $e');
    }
  }

  Future<String> _uploadProfilePhoto(File file, String uid) async {
    try {
      Log.info('Firebase Storage에 파일 업로드 시작: profile_photos/$uid');
      TaskSnapshot snapshot =
          await firebaseStorage.ref('profile_photos/$uid').putFile(file);

      String downloadUrl = await snapshot.ref.getDownloadURL();
      Log.info('파일 업로드 성공, 다운로드 URL: $downloadUrl');
      return downloadUrl;
    } on FirebaseException catch (e) {
      Log.error('Firebase Storage 오류 발생', e);
      throw FileNotFoundException('Failed to upload file: ${e.message}');
    } on SocketException catch (e) {
      Log.error('네트워크 오류 발생', e);
      throw NetworkException('No Internet connection');
    } catch (e, stackTrace) {
      Log.error('파일 업로드 중 예상치 못한 오류 발생', e, stackTrace);
      throw AppException('Unexpected error during file upload: $e');
    }
  }
}

설명:

2. ViewModel에서 예외 처리

ProfileCreateViewModel에서는 UseCase에서 던진 예외를 처리하고 UI 상태를 적절히 업데이트해야 합니다. 아래는 ViewModel을 수정하는 방법입니다:

import 'dart:io';
import 'package:gfbf/models/profile_model.dart';
import 'package:gfbf/models/user_model.dart';
import 'package:gfbf/provider.dart';
import 'package:gfbf/state/profile_create_state.dart';
import 'package:gfbf/usecase/create_profile_use_case.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'exceptions.dart'; // 커스텀 예외 클래스 import

class ProfileCreateViewModel extends StateNotifier<ProfileCreateState> {
  final CreateProfileUseCase createProfileUseCase;
  final UserModel? userModel;
  final Ref ref;

  ProfileCreateViewModel(this.createProfileUseCase, this.userModel, this.ref)
      : super(ProfileCreateState.create(userModel));

  Future<void> createProfile(ProfileModel profileModel, File? file) async {
    try {
      await createProfileUseCase.execute(profileModel, file);
      ref.read(profileNotifierProvider.notifier).setState(profileModel);
      state = ProfileCreateState.create(userModel);
      // 성공 메시지를 표시하거나 상태를 성공으로 설정할 수 있습니다.
    } on NetworkException catch (e) {
      state = ProfileCreateState.error('인터넷 연결이 없습니다: ${e.message}');
    } on DatabaseException catch (e) {
      state = ProfileCreateState.error('데이터베이스 오류: ${e.message}');
    } on FileNotFoundException catch (e) {
      state = ProfileCreateState.error('파일 업로드 오류: ${e.message}');
    } on AuthorizationException catch (e) {
      state = ProfileCreateState.error('인증 오류: ${e.message}');
    } on AppException catch (e) {
      state = ProfileCreateState.error('예상치 못한 오류: ${e.message}');
    } catch (e) {
      state = ProfileCreateState.error('처리되지 않은 오류: $e');
    }
  }
}

설명:

요약

커스텀 예외를 사용함으로써 다음을 달성할 수 있습니다:

이러한 설정을 통해 앱은 더욱 견고해지고, 유지보수가 쉬워지며, 사용자에게 명확한 오류 피드백을 제공할 수 있습니다.

brownglasses commented 1 month ago

VerifyPhoneNumberUseCase 의 구조를 잘 보기, 다른 예외 처리와는 조금 다름,

VerifyPhoneNumberUseCase에서의 예외 처리는 다른 use case와 다음과 같은 점에서 차이가 있습니다:

  1. 콜백 사용: FirebaseAuth의 비동기 인증 프로세스는 여러 단계의 콜백을 필요로 하며, 이로 인해 예외를 콜백 내에서 직접 처리하고 사용자에게 즉시 피드백을 제공해야 합니다.

  2. Firebase 특화 예외 처리: FirebaseAuthException과 같은 Firebase 특화 예외를 여러 위치에서 다루며, 일반적인 use case보다 특정한 에러 상황을 세분화하여 처리합니다.

  3. 실시간 처리: 전화번호 인증은 실시간으로 진행되기 때문에, 자동 인증, 코드 전송, 타임아웃 등 다양한 단계에서 예외를 세심하게 다룹니다.

  4. 사용자 경험 초점: 사용자 경험을 개선하기 위해 명확하고 이해하기 쉬운 오류 메시지를 제공하며, 사용자에게 즉각적인 피드백을 주기 위해 verificationFailed와 같은 콜백을 사용합니다.

  5. 상태 관리: 인증 과정의 여러 상태(자동 인증 완료, 실패, 코드 전송 등)를 개별적으로 관리하여 각 상태에 맞는 적절한 처리를 수행합니다.

즉 예외를 처리하는 콜백이 존재하므로, 조금 다르게 처리할 필요가 있다.