
작업 일정
- #16 [state] AuthNotifier 및 인증 상태 관리 구현
- #17 [state] HomeNotifier 및 홈 대시보드 상태 관리 구현
issue 번호가 아주 엉망이다. 처음 스케줄 잡은 게 아키텍처 이해도가 많이 떨어졌던 시점이라 중간에 좀 엎었기 때문이다. 아무렴 어때.
class AuthViewModel extends AsyncNotifier<User?> 를 구현한다. Riverpod의 AsyncNotifier 를 extend하여 비동기 상태를 관리하는 AsyncValue<User?> 를 사용한다. 이 녀석은 다음과 같은 상태를 갖는다.
AsyncLoading: 인증 확인 및 로그인/회원가입 진행 중AsyncData(User): 로그인 완료 상태AsyncData(null): 미로그인 (게스트) 상태AsyncError: 로그인/가입 실패 및 네트워크 오류 상태
가령 로그인에 대한 method를 작성한다고 할 때, 로그인을 시도하면 state = AsyncLoading 로 둔 채 signInWithUsername() 를 하고, 그 결과에 따라 state = AsyncData(value); 하거나 state = AsyncError(error, StackTrace.current); 하여 consumer 쪽에서 이 값에 따라 판단하는 식이다. state = AsyncLoading 라면 로그인 버튼을 아무리 눌러도 요청이 중복으로 들어가지 않고, state = AsyncData(value); 이면 무사히 로그인 되도록 하며, state = AsyncError(error, StackTrace.current); 이면 로그인 실패 Snackbar를 띄워주는 식으로 구현할 수 있다.
이 부분은 확실히 Rust의 pattern-matching과 닮아 있다. 요즘 스타일인가.
그것과 별개로 여기선 AI로부터 코드 지적을 많이 받았다. 놓치는 부분이 너무 많다. 아무래도 처음 써 보는 package는 쉽지 않다. 주요 이슈는 다음과 같다. 이것도 몇 개 작성하다 보면 어떤 것들을 고려해야 하는지 요령이 생기려나.
- 레이스 컨디션 및 광클 방지 (
if (state.isLoading))
- 이슈:
네트워크 지연 상황에서 사용자가 로그인/가입/탈퇴 버튼을 빠르게 연타할 때, 동일한 요청이 백엔드/Firebase로 중복 전송되어email-already-in-use에러나 불필요한 트랜잭션이 폭증하는 현상.- 설계 방식:
모든 상태 변경 액션 진입부에if (state.isLoading) return const Result.error(ValidationException('이미 요청이 진행 중입니다.'));가드를 배치하여 첫 번째 요청이 끝날 때까지 후속 입력을 즉시 차단합니다.
- Provider 해제 후 메모리 누수 및 크래시 방지 (
ref.mounted)
- 이슈:
사용자가 로그인/가입/프로필 조회 비동기 처리를 기다리는 도중 화면을 이탈하거나 앱이 백그라운드로 전환될 때, 비동기 처리가 끝난 뒤 이미 해제된 Notifier의 상태를 바꾸려다 발생하는Bad state: Cannot use ref/state after dispose치명적 크래시.- 설계 방식: Riverpod 3.x 프레임워크 내장 표준인
ref.mounted를 모든 await 직후에 적용하여 Provider가 안전하게 살아있을 때만 상태를 변경하도록 보장합니다.
- 앱 기동 시 불필요한 Firestore 중복 쿼리 방지 (
_listenAuthState)
- 이슈:
Firebase Auth의authStateChanges는 구독 직후 즉시 1회 초기 이벤트를 방출합니다. 이때build()에서 프로필을 조회하고 있는데 리스너도 동시에 조회를 시작하여 앱 켤 때마다 Firestore 읽기 비용이 2배로 청구되는 낭비.- 설계 방식:
if (state.isLoading) return;으로build()초기화 중 발생하는 초기 스트림 이벤트를 무시.
state.value?.uid != uid조건으로 계정이 실제로 바뀌었을 때만 프로필 조회를 트리거하여 불필요한 네트워크 호출을 원천 차단합니다.
- 후속 레이어와의 결합도 분리 (
currentUserProvider)
- 이슈: Auth 외의 feature들은 로그인 진행 상태(
AsyncLoading)에는 관심이 없고 오직 "현재 사용자(User?)" 데이터만 필요합니다.- 설계 방식:
다음과 같은 편의 Provider를 파일 하단에 선언함으로써, 후속 ViewModel들이 복잡한 AsyncValue 매핑 없이ref.watch(currentUserProvider)만으로 현재 로그인 사용자를 반응형으로 구독할 수 있게 설계합니다.final currentUserProvider = Provider<User?>((ref) { return ref.watch(authNotifierProvider).value; });
1번의 경우 Riverpod이 아닌 기본 Provider를 사용할 경우 lib/utils/command.dart 를 통해 한다던 처리인데, 분명 들은 게 있었음에도 구현하다 보니 생각을 못 했다. 나머지는 그냥 고려하지 못했던 부분들이다.
여기선 여러 개의 State가 존재한다. 이런 걸 구현하는 방식은 여러 가지가 있다.
- 방법 1: 각각의 View가 하나의 State를 관리하도록 화면을 여러 개의 View로 쪼갠다.
- 방법 2: 모든 State를
ChangeNotifier를 extends 하는 class에 넣고 변경될 때마다notifyListeners()한다.- 방법 3: 모든 State를
AsyncNotifier<T>를 extends 하는 class에 넣고 변경될 때마다state = AsyncData(state.value.copyWith(...))한다.
두 번째 방법은 Provider를 사용할 때 흔히 하는 방식이고 세 번째 방법은 Riverpod을 사용할 때 흔히 하는 방식이다. 세 번째 방식을 사용하면 AsyncValue 가 로딩/에러/성공을 자동 래핑해주며 코드 안정성이 높아진다. 따라서 이 방식으로 구현하도록 하겠다.
하다 보니 놓친 부분, 누락된 내용이 꽤 있다는 걸 인지했다. 발견되는 대로 issue로 등록하고 반영해 가며 적절한 commit을 남기면 좋을 텐데 마음이 급하니 그게 잘 안 된다.