DAY 34 | 개인 프로젝트 — viewmodel & riverpod (1)

작업 일정

  • #16 [state] AuthNotifier 및 인증 상태 관리 구현
  • #17 [state] HomeNotifier 및 홈 대시보드 상태 관리 구현

issue 번호가 아주 엉망이다. 처음 스케줄 잡은 게 아키텍처 이해도가 많이 떨어졌던 시점이라 중간에 좀 엎었기 때문이다. 아무렴 어때.


#16 [state] AuthNotifier 및 인증 상태 관리 구현

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는 쉽지 않다. 주요 이슈는 다음과 같다. 이것도 몇 개 작성하다 보면 어떤 것들을 고려해야 하는지 요령이 생기려나.

  1. 레이스 컨디션 및 광클 방지 (if (state.isLoading))
  • 이슈:
    네트워크 지연 상황에서 사용자가 로그인/가입/탈퇴 버튼을 빠르게 연타할 때, 동일한 요청이 백엔드/Firebase로 중복 전송되어 email-already-in-use 에러나 불필요한 트랜잭션이 폭증하는 현상.
  • 설계 방식:
    모든 상태 변경 액션 진입부에 if (state.isLoading) return const Result.error(ValidationException('이미 요청이 진행 중입니다.')); 가드를 배치하여 첫 번째 요청이 끝날 때까지 후속 입력을 즉시 차단합니다.
  1. Provider 해제 후 메모리 누수 및 크래시 방지 (ref.mounted)
  • 이슈:
    사용자가 로그인/가입/프로필 조회 비동기 처리를 기다리는 도중 화면을 이탈하거나 앱이 백그라운드로 전환될 때, 비동기 처리가 끝난 뒤 이미 해제된 Notifier의 상태를 바꾸려다 발생하는 Bad state: Cannot use ref/state after dispose 치명적 크래시.
  • 설계 방식: Riverpod 3.x 프레임워크 내장 표준인 ref.mounted 를 모든 await 직후에 적용하여 Provider가 안전하게 살아있을 때만 상태를 변경하도록 보장합니다.
  1. 앱 기동 시 불필요한 Firestore 중복 쿼리 방지 (_listenAuthState)
  • 이슈:
    Firebase Auth의 authStateChanges 는 구독 직후 즉시 1회 초기 이벤트를 방출합니다. 이때 build() 에서 프로필을 조회하고 있는데 리스너도 동시에 조회를 시작하여 앱 켤 때마다 Firestore 읽기 비용이 2배로 청구되는 낭비.
  • 설계 방식:
    if (state.isLoading) return;으로 build() 초기화 중 발생하는 초기 스트림 이벤트를 무시.
    state.value?.uid != uid 조건으로 계정이 실제로 바뀌었을 때만 프로필 조회를 트리거하여 불필요한 네트워크 호출을 원천 차단합니다.
  1. 후속 레이어와의 결합도 분리 (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 를 통해 한다던 처리인데, 분명 들은 게 있었음에도 구현하다 보니 생각을 못 했다. 나머지는 그냥 고려하지 못했던 부분들이다.

#17 [state] HomeNotifier 및 홈 대시보드 상태 관리 구현

여기선 여러 개의 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을 남기면 좋을 텐데 마음이 급하니 그게 잘 안 된다.

>>> GitHub Repository at this point (78e3fcd)

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다 (지금은 학생 때 하던 거 아무거나 공부하고 있고요, 취업시켜 주시면 그 분야로 공부할게요)

0개의 댓글