[Flutter] Result Pattern 적용하기
오늘은 저번에 학습하였던 Result Pattern을 실제로 내 프로젝트에 적용시켜서 카카오 로그인과 애플 로그인의 Error Handling을 진행해보겠다.
우선 어떻게 동작하는지 흐름을 정리해봤다
위에 Flow를 보면 View → ViewModel → Repository → Service의 패턴을 따르는 것을 확인할 수 있다.
- LoginView에서 간편 로그인 버튼을 누르게 되면 LoginViewModel에 있는 Result 타입을 반환하는 함수가 실행이 된다.
- 성공시 Router가 변경이 되고 Home으로 이동된다.
- 실패시 Result안에 ErrorMessage가 담기게 되고 그것을 출력하거나 화면에 표시할 수 있다.
- LoginViewModel에 있는 함수가 실행되면 LoginRepository에 있는 Result 타입을 반환하는 함수가 실행이 된다.
- 성공시 User 객체에 있는 데이터를 모두 갖고 올 수 있으며 User 객체를 생성하고 성공을 반환한다.
- 실패시 ErrorMessage를 Failure객체에 넣어 반환한다.
- LoginRepository에 있는 함수가 실행되면 최종적으로 Service에 있는 함수가 실행되어 Kakao와 FirebaseAuth를 통신하여 필요한 데이터를 가져오게 된다.
- 성공시 AuthUser 데이터를 받게 되며 User객체에 필요한 데이터들 중 일부를 얻게 된다. AuthUser를 Success객체에 넣어 반환한다.
- 실패시 ErrorMessage를 Failure객체에 넣어 반환한다.
- Service안에서는 먼저 카카오 로그인or 애플 로그인이 실행이 된 후 FirebaseAuth에 필요한 토큰들을 발급 받는다. 그 후 FirebaseAuth를 통해 현재 유저의 정보를 가져오게 된다.
이렇게 이미지를 글로 풀어 적어봤는데 너무 어지럽다.. 🫠
하나씩 코드로 작성해보겠다.
0. Result 클래스 생성
sealed class는 Class Modifier로 abstract class의 성질을 갖고 있다.
그리고 하위 클래스들이 모두 호출되지 않으면 컴파일 에러를 발생한다.
아래 이미지는 Failure를 주석처리하여 호출하지 않아서 생기는 문제이다.
sealed class Result<S, E extends Exception> {
const Result();
}
final class Success<S, E extends Exception> extends Result<S, E> {
const Success(this.value);
final S value;
}
final class Failure<S, E extends Exception> extends Result<S, E> {
const Failure(this.exception);
final E exception;
}
1. 간편 로그인 버튼 클릭
나는 signInButton안에 _loginVM.signInWithKakao()를 실행하는데 반환 값을 result에 넣어주었다.
await 때문에 저 메소드가 끝날때까지 기다릴 것이고 끝난다면 Success 혹은 Failure 결과를 가져올 것 이다.
만약 성공이라면 화면 전환을 하면 된다. 실패라면? 에러 발생 이유와 dialog를 출력하면 된다.
signInButton(
onPressed: () async {
final result = await _loginVM.signInWithKakao();
switch (result) {
case Success():
context.go('/home');
case Failure(exception: final e):
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('로그인 실패'),
content: Text('에러 발생: $e'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('확인'),
),
],
),
);
}
},
loginType: LoginType.kakao,
),
2. ViewModel에서 Repository 함수 실행
현재 프로젝트에서는 데이터 상태관리를 하지 않고 있다.
이 작업이 끝나면 RiverPod을 연결하여 데이터 상태를 관리 해줄 것 이다.
그렇다면 지금 기준으로는 LoginRepository를 불러와서 안에 있는 signInWithKakao함수를 실행시킨다.
성공한다면 User의 모든 데이터를 가져올 수 있으므로 User 객체를 생성하고 실패하면 에러 메세지를 담자.
final _loginRepository = LoginRepository();
final userVM = UserViewModel();
Future<Result<void, Exception>> signInWithKakao() async {
final result = await _loginRepository.signInWithKakao();
switch (result) {
case Success(value: final user):
userVM.user = user;
return Success('카카오 로그인 성공');
case Failure(exception: final e):
return Failure(Exception('로그인 실패: $e'));
}
}
3. Repository에서 Service 함수 실행
이제 외부와 통신하는 Service를 불러와 함수를 실행시킨다.
만약 성공한다면 이 때 Success안 value 값은 모든 User의 데이터가 아닌 User의 일부분 데이터를 갖고 있는 상황이다. 그래서 User 객체를 Success에 담아 반환하려면 User 데이터를 만드는 함수를 실행시켜 완전한 User 데이터를 만들어 성공에 담아 반환해주자. _createUserFromAuth함수를 사용하여 데이터를 생성하였다. 추후 레거시한 데이터를 지워주고 Firebase DataBase에서 가져온 데이터를 넣어주자.
final _firebaseAuthService = FirebaseAuthService();
Future<Result<User, Exception>> signInWithKakao() async {
final result = await _firebaseAuthService.signInWithKakao();
switch (result) {
case Success(value: final authUser):
return Success(_createUserFromAuth(authUser));
case Failure(exception: final exception):
return Failure(exception);
default:
throw Exception('Unexpected result type');
}
}
User _createUserFromAuth(authUser) {
Map<String, dynamic> data = {
'id': authUser.uid,
'simple_login_id': authUser.providerData.isNotEmpty ? authUser.providerData.first.providerId : 'unknown',
'user_type': '카카오',
'campus': '관악',
'course': 'Flutter',
'name': '하명관',
'create_date': authUser.metadata.creationTime?.toIso8601String(),
'last_login_date': authUser.metadata.lastSignInTime?.toIso8601String(),
};
return User.fromMap(data);
}
4. Service에서 외부와 통신하기
카카오 로그인
아래 코드는 카카오 로그인을 하여 토큰을 발급받는 함수이다.
마찬가지로 성공한다면 User를 반환하는데 이 때 아래 이미지를 보면 User는 App내의 개발자가 생성한 User가 아닌 FirebaseAuth의 User이다.
/// 카카오 로그인 - Firebase Auth User 전달
Future<Result<User, Exception>> signInWithKakao() async {
try {
// 토큰 발급하기
kakao.OAuthToken? token;
if (await kakao.isKakaoTalkInstalled()) {
token = await kakao.UserApi.instance.loginWithKakaoTalk();
} else {
token = await kakao.UserApi.instance.loginWithKakaoAccount();
}
if (token.idToken != null) {
final provider = 'oidc.kakao';
final idToken = token.idToken!; // idToken 언래핑
final accessToken = token.accessToken;
final result = await _connectFirebaseAuth(provider, idToken, accessToken);
final authUser = switch (result) {
Success(value: final user) => user,
Failure(exception: final e) => throw e,
};
return Success(authUser);
} else {
return Failure(Exception('signInWithKakao Error'));
}
} on Exception catch (error) {
return Failure(error);
}
}
Firbase Auth 연동
애플 로그인이든 카카오 로그인이든 연결하여 토큰을 발급 받는다면 토큰들을 갖고 Firebase의 OAuthCredential를 만들어 현재 로그인한 사용자의 정보를 가져올 수 있다.
마찬가지로 아래 User도 Firebase Auth에서 정의한 User이다.
이렇게 모두 성공적으로 데이터를 가져오면 계속 상위로 데이터를 보낸다.
/// Firebase Auth
Future<Result<User, Exception>> _connectFirebaseAuth(String provider, String idToken, String accessToken) async {
try {
// OAuthCredential 생성
final OAuthCredential credential = OAuthProvider(provider).credential(
idToken: idToken,
accessToken: accessToken,
);
// Firebase에 자격 증명으로 로그인
await FirebaseAuth.instance.signInWithCredential(credential);
// 현재 로그인된 사용자 정보 가져오기
User? user = FirebaseAuth.instance.currentUser;
if (user != null) {
return Success(user);
} else {
return Failure(Exception('User 정보 가져오기 실패 에러 발생 Error'));
}
} catch (error) {
return Failure(Exception(error));
}
}
결론
이제 아래 Flow Chart를 이해할 수 있다.. 하지만 이런 상황(결과를 계속 받는 비동기 함수를 동기적으로 실행해야하는 상황)을 처리해주는 Package가 있다고 하는데 솔직히 Package를 사용하고 싶지는 않다.
이것이 Result Pattern을 잘 적용했다고 말할 수 있을까? 일단 나는 충분히 고민하여 이렇게 만들었다.
앞으로 Error Handling은 모두 이런 방식으로 처리하려고 한다.
코드
전체적인 코드는 아래에서 확인할 수 있다.
'Flutter' 카테고리의 다른 글
[Flutter] RiverPod - provider 알아보기(with 네트워크 요청) (0) | 2025.01.06 |
---|---|
[Flutter] RiverPod 학습 - State management (0) | 2025.01.03 |
[Flutter] [Error Handling] Result Pattern (1) | 2024.12.24 |
[Flutter] [Firbase] 간편 로그인 구현하기(Apple🍎) (0) | 2024.12.24 |
[Flutter] [Firbase] 간편 로그인 구현하기(kakao💬) (1) | 2024.12.19 |