Where who wants to meet someone

[Flutter] Firebase 인증 상태에 따른 GoRouter redirect 구현하기 본문

Flutter

[Flutter] Firebase 인증 상태에 따른 GoRouter redirect 구현하기

Lust3r 2024. 9. 24. 12:00
728x90
 

go_router | Flutter package

A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more

pub.dev


로그인 기능을 구현하다보니, 로그인 / 로그아웃을 할 때 context.go 메서드를 통해 화면을 전환하는 것이 아니라 자동으로 상태 반영이 되면 어떨까 하는 생각이 들게 되었다.

 

 

Flutter에서 Firebase 인증 시작하기  |  Firebase 문서

Google I/O 2022에서 Firebase의 새로운 기능을 확인하세요. 자세히 알아보기 의견 보내기 Flutter에서 Firebase 인증 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류

firebase.google.com

 

현재 인증 상태 확인을 위해서는 다음의 3가지 메서드를 사용할 수 있다.

 

1. authStateChanges()

- 리스너가 등록된 직후

- 사용자가 로그인한 경우

- 현재 사용자가 로그아웃한 경우

 

2. idTokenChanges()

- 리스너가 등록된 직후

- 사용자가 로그인한 경우

- 현재 사용자가 로그아웃한 경우

- 현재 사용자의 토큰이 변경된 경우

 

3. userChanges()

- 리스너가 등록된 직후

- 사용자가 로그인한 경우

- 현재 사용자가 로그아웃한 경우

- 현재 사용자의 토큰이 변경된 경우

- FirebaseAuth.instance.currentUser에서 제공하는 다음 메서드가 호출되는 경우:
  - reload()

  - unlink()

  - updateEmail()

  - updatePassword()

  - updatePhoneNumber()

  - updateProfile()

 

지금 프로젝트의 상황에서는 토큰을 사용하거나 currentUser의 메서드 감지 목적이 아닌 로그인/로그아웃에 의한 상태 변화만 원하기 때문에 authStateChanges를 사용하기로 했다.

 

처음에는 authStateChanges()가 Stream<User?>이기 때문에 RootPage를 만들고, StreamBuilder로 해당 메서드에 따라 Auth() 혹은 Home() 위젯을 반환하면 되지 않을까 했지만..

 

실제로 구현을 해보니 첫 순간에만 화면이 변하고(Auth -> Home || Home -> Auth) 두 번째 부터는 변화가 없었다.

중단점을 찍어본 결과, 로그인/로그아웃 시 해당 코드가 실행되긴 하지만 화면이 변하지 않아 Hot Reload를 하거나 Restart를 해야 변한 것을 확인할 수 있었다.

stateless -> state, WidgetsBinding.instance.addPostFrameCallback(이는 혹시 몰라 router로 이동해볼까 했던 것인데, 위젯이 그려지기 전 router를 호출하는 이슈가 있어 위젯이 그려진 이후 메서드를 사용하고자) 등 다양한 방법을 써보았으나 원하는 방식으로 동작하지 않았다.

 

그러던 중, 라우팅에 사용했던 GoRouterredirect 기능이 있다는 것을 알게 되었고 이를 사용해 보기로 했다.

 

결과는? 실패.

redirect에 똑같이 조건문과 분기를 넣어도, 로그인과 로그아웃 시 트리거가 되지 않았다.

 

 

go_router

Multiple redirections It's possible to redirect multiple times w/ a single navigation, e.g. / => /foo => /bar. This is handy because it allows you to build up a list of routes over time and not to worry so much about attempting to trim each of them to thei

docs.page

 

go_router의 redirection 문서를 보니, go_router는 앱의 상태 변화를 모르기 때문에 redirect만으로는 변화를 줄 수 없어 수동으로 라우팅을 해야 했다.

 

대신, 앱의 상태에 따라 go_router가 자동으로 redirect를 하기 위해서는 refreshListenable을 사용할 수 있다고 한다.

If you'd like to have the app's state cause go_router to automatically redirect, you can use the refreshListenable
 argument of the GoRouter constructor:

 

해당 생성자에는 Listenable 타입이 들어가야 하는데, 이건 무엇인가 하니

 

 

Listenable class - foundation library - Dart API

An object that maintains a list of listeners. The listeners are typically used to notify clients that the object has been updated. There are two variants of this interface: Many classes in the Flutter API use or implement these interfaces. The following su

api.flutter.dev

 

공식문서에 따르면 객체가 업데이트되었음을 클라이언트에 알리는데 사용하는 리스너 목록을 관리하는 객체라고 한다.

 

ChangeNotifier와 ValueNotifer를 통해 이 인터페이스를 사용할 수 있는데 User 값을 사용할 것이 아니기 때문에 ChangeNotifier를 써보기로 했다.

 

Since the loginInfo is a ChangeNotifier, it will notify listeners when it changes. By passing it to the GoRouter constructor, go_router will automatically refresh the route when the login info changes. This allows you to simplify the login logic in your app:

 

final class _AuthChangeNotifier extends ChangeNotifier {
  late final StreamSubscription<User?> _authSubscription;

  _AuthChangeNotifier() {
    _authSubscription = FirebaseAuth.instance.authStateChanges().listen((_) {
      notifyListeners();
    });
  }

  @override
  void dispose() {
    _authSubscription.cancel();
    super.dispose();
  }
}

 

StreamSubscription<User?> 에 FirebaseAuth.instance.authStateChanges().listen을 할당하고, 상태 변경이 일어날 때마다 notifiyListeners를 사용하여 변경이 일어났음을 알려주도록 했다.

 

또한 상태 변경에 사용한 _authSubscription이 StreamSubscription 타입이기 때문에 dispose시 cancel로 구독을 취소하도록 했다.

 

이제 트리거를 해줄 refreshListenable과 redirect가 구성되었으므로 실제로 테스트만 해보면 된다.

 

final GoRouter router = GoRouter(
  redirect: (context, state) {
    final user = FirebaseAuth.instance.currentUser;

    if (user == null && state.fullPath == '/home') {
      return '/auth';
    } else if (user != null && state.fullPath == '/auth') {
      return '/home';
    }
    return null;
  },
  refreshListenable: _authChangeNotifier,
  routes: <RouteBase>[
    GoRoute(
      path: '/auth',
      builder: (context, state) => const Auth(),
    ),
    GoRoute(
      path: '/home',
      builder: (context, state) => const Home(),
    ),
  ]
)

 

아. refreshListenable에서 _AuthChangeNotifier()로 인스턴스화 해서 넣었었는데, router가 리빌드되는 경우를 생각하면 매번 새로운 인스턴스가 생성되고 불필요한 구독이 생길 수 있으므로 다른 방법이 필요했다.

 

Riverpod을 사용했기 때문에 ChangeNotiferProvider를 써야하나 했지만 router가 ConsumerWidget 내부에 있는 것이 아니라 전역적으로 생성 후 MaterialApp.router에 전달되는 구조기 때문에 ref를 사용할 수 없었다.

(이는 좀 더 생각해보고자 한다)

 

이에 _AuthChangeNotifier를 인스턴스화 한 것을 router에 전달하는 방식을 사용했다.

 

final _AuthChangeNotifier _authChangeNotifier = _AuthChangeNotifier();

final class _AuthChangeNotifier extends ChangeNotifier {
  late final StreamSubscription<User?> _authSubscription;

  _AuthChangeNotifier() {
    _authSubscription = FirebaseAuth.instance.authStateChanges().listen((_) {
      notifyListeners();
    });
  }

  @override
  void dispose() {
    _authSubscription.cancel();
    super.dispose();
  }
}

 

이렇게 하면 로그인 버튼에서는 signIn 메서드만, 로그아웃 버튼에서는 signOut 메서드만 호출하면 바뀐 사용자 상태에 따라 router가 자동으로 트리거되어 라우팅해주게 된다.

'Flutter' 카테고리의 다른 글

[Flutter] Firebase에 사용할 SHA1, SHA-256 확인하기  (2) 2024.09.26