본문 바로가기

[Flutter] - go_router로 화면 이동 구현하기 (로그인 가드 구현 포함)

ironwhale 2023. 3. 23.

go_router는 페이지 이동을 위한 플러터 패키지

고라우터(go_router)는 플러터(flutter)의 화면 전환을 위한 패키지 입니다. 예를 들어 초기 화면에서 어떤 버튼을 누르면 글쓰기 페이지로 이동하는 기능을 구현하기 위해 쓰는 패키지 입니다. 쉽게 설명하면 HTML의 링크(a태그)의 기능을 구현하는 것입니다. 
 
아래 공식 문서의 설명에도 나와 있듯이 url 기반으로 화면의 주소를 지정하고 버튼에 경로를 입력하여 해당 되는 화면으로 전환됩니다. 또한 로그인이 안되어 있으면 다른 화면에 접근할 수 없도록 하는 beamer로 치면 가드 기능인 redirect 기능도 함께 알아보도록 하겠습니다. 

공식문서의 설명

A declarative routing package for Flutter that uses the Router API to provide a convenient, url-based API for navigating between different screens. You can define URL patterns, navigate using a URL, handle deep links, and a number of other navigation-related scenarios.

초기 설정: GoRouter 만들기

go_router를 사용하기 위해서는 GoRouter 인스턴스를 만들어야 합니다. GoRouter 인스턴스를 만들때 경로와 그 경로에 맞는 위젯들을 연결시켜 주면 됩니다. 인스턴스 생성시 아래와 같은 프로퍼티를 입력해주어야 합니다. 
 

  • observers: 이것은 애널리틱스를 사용하여 어떤 페이지에 들어 갔는지 보는 기능을 합니다.
  • routes: [GoRoute, GoRoute, GoRoute...] 이런식으로 GoRoute 자료형(?)을 주어 경로와 위젯을 연결하는 부분입니다.
  • refreshListenable: 로그인 여부를 확인하기 위한 프로바이더 객체를 넣어 줍니다. 
  • redirect: 로그인 여부를 확인해 로그아웃 상태이면 로그인 페이지로 보내는 가드 기능을 합니다. 

 routes로 페이지 이동하기

go_router에 GoRouter 클래스에 routes가 제일 중요한 부분으로 경로에 따른 화면 이동을 위한 핵심 부분입니다. GoRouter에는 아래와 같은 프로퍼티가 필요합니다. 

  • path: 필수 설정으로 해당 위젯(페이지)의 경로를 나타내며 context.go("/write") 또는 context.push("/write") 를 사용해서 페이지를 이동합니다. go와 push의 큰 차이점은 push는 이전 페이지로 돌아가는 버튼이 자동으로 생기는 반면 go는 아니라는 점입니다. 이것은 push는 스택 형태로 페이지를 쌓아올리기 때문인데 그냥 단순하게 뒤로가기 버튼이 필요하시면 push를 사용하시면 됩니다. 
  • builder: (context,state)=>WriteScreen() 과 같이 사용하며 리턴값에 경로에 맞는 위젯을 넣으시면 위에서 설정한 경로를 go, push 하면 설정한 위젯을 보여줍니다. 
  • name: 필수는 아닌데 URL이 복잡해지면 각각의 경로를 편하게 즐겨찾기 처럼 이름을 붙여 접근할 수 있게 해주는 설정합니다.  context.pushName(경로이름) 또는  context.goName(경로이름) 으로 사용하고 위에서 여기서 설정한 이름은 애널리틱스에서 어떤 페이지를 많이 사람들이 찾는지 확인하는데 사용됩니다. 다만 GoRoute에 설정되지 않은 페이지는 애널리틱스에 자동으로 등록되지 않으므로 not set으로 표시 되는데 이것은 별도로 설정해주어야 합니다.    

 로그인 여부에 따라 화면 접근 제어(redirect 기능 구현)

여기까지 하셨으면 go_router 패키지를 사용하기 위한 기본적인 설정은 90% 끝났습니다. 

이제 로그인 되었는지를 확인하고 안되어 있으면 로그인 페이지로 보내는 beamer로 치면 guard 기능을 구현해보도록 하겠습니다. 

 

로그인 상태를 파악하기 위한 코드 구현 부분

refreshListenable에서 로그인 상태를 계속 감시하기위해 로그인 상태를 구현한 객체를 입력합니다. 주로 ChangeNotifier를 상속 받아 아래 코드와 같이 구성합니다. 

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:motto_train/repo/user_repo.dart';
import '../domain/member.dart';


class UserNotifier extends ChangeNotifier {
  User? _user;
  
  User? get user => _user;

  UserNotifier() {
    initStatus();
  }
  
  void initStatus() {
    FirebaseAuth.instance.authStateChanges().listen((User? user) async {
      await _setUser(user);
      notifyListeners();
    });
  } 
}

 

redirect 로그인 여부에 따라 로그인 페이지 외 다른 페이지는 접근 할 수 없게 하기

beamer에 guard 기능은 go_router에서 redirect로 구현합니다.  여러 차례 코드를 이리저리 수정해보면서 왜 아래와 같은 코드가 나왔는지 알아본 결과 다음과 같은 순서로 코드가 작동하는것을 알았습니다. 

null이 리턴 될때 까지 리다이렉트 구문이 작동하고 이거은 페이지 전환 될 때 마다 한다

 최종적으로 null 이 나와야 리다이렉트 구문은 끝납니다. 그래서 맨 마지막에 null을 리턴 하는 구조로 되어 있습니다. userStatus.user가 null 이라는 것은 로그인이 안된 상태를 뜻하므로 우선 첫번째 조건문에서 로그인 여부를 확인하여 로그인이 안된 상태라면 "/auth" 즉 로그인 페이지로 가게 합니다. 

 

 두번째 if문에 도착했다는 것은 로그인이 되었다는 뜻이고 해당 조건은 현재 페이지의 위치가 로그인 페이지라면 홈페이지로 가라는 뜻입니다. 여기까지만 하면 구지 return null 이 필요한 이유에 대한 의문이 생기 실수 있습니다.  저 역시 그랬구요 그래서 return null 없이 해보니 동작이 제대로 되지 않는 것이었습니다. 

 

 그 이유는 로그인 된 상태에서 로그인 페이지가 아닌 홈의 경로로 페이지가 전환 되었을 때 리다이렉트 구문이 돌아가는데 이 때 null이 리턴이 될 때까지 이 리다이렉트 구문이 돌아가기 때문입니다. 따라서 모든 조건이 패스 되는 마지막에 꼭 null 이 들어 가야 합니다.  

redirect: (context, state) {
          bool _isAuthpage = state.subloc == "/auth";

          if (userStatus.user == null) {
            return "/auth";
          }
          if (_isAuthpage) {
            return "/";
          }
          return null;          
        }

이렇게 해서 GoRouter 패키지를 사용하는 방법 설명이 끝났습니다. 저의 경우 아래 코드 처럼 GoRouterClass를 만들어서 생성자에서 Gouter 객체를 초기화 하였고, 구글 애널리 틱스 사용을 위해 FirebaseAnalytics 객체를 외부에서 주입받아 observers에 넣어 주었습니다.  

 

FirebaseAnalytics.instance.setCurrentScreen(screenName:"name")

참고로 go_router로 경로를 표시 하지 않은 페이지를 애널리틱스에서 탐지하기 위해서는  FirebaseAnalytics.instance.setCurrentScreen(screenName:"name") 를 위젯 어딘가에 넣어 주시면 됩니다. 

 

go_router 패키지 사용 전체 코드

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:go_router/go_router.dart';
import 'package:motto_train/screen/auth_screen.dart';
import 'package:motto_train/screen/motto_app.dart';
import 'package:motto_train/screen/tag/tag_screen.dart';
import 'package:motto_train/screen/write_update/write_screen.dart';

import '../notifier/user_status.dart';

class GoRouterClass {
  final FirebaseAnalytics analytics;
  final UserNotifier userStatus;
  late GoRouter router;

  GoRouterClass(this.analytics, this.userStatus) {
    router = GoRouter(
        observers: [
          FirebaseAnalyticsObserver(analytics: analytics)
        ],
        routes: [
          GoRoute(
              path: "/", builder: (context, state) => MottoApp(), name: "home"),
          GoRoute(
              path: "/write",
              builder: (context, state) => WriteScreen(),
              name: "write"),
          GoRoute(
              name: "auth",
              path: "/auth",
              builder: (context, state) => AuthScreen()),
          GoRoute(
              name: "tags",
              path: "/tags/:tag",
              builder: (context, state) =>
                  TagScreen(tag: state.params["tag"]!)),
          GoRoute(
              name: "update",
              path: "/update/:reference",
              builder: (context, state) {
                String id = state.params["reference"]!;

                return WriteScreen(id: id);
              })
        ],
        refreshListenable: userStatus,
        redirect: (context, state) {
          bool _isAuthpage = state.subloc == "/auth";

          if (userStatus.user == null) {
            return "/auth";
          }
          if (_isAuthpage) {
            return "/";
          }

          return null;          
        });
  }
}

 


Main.dart에 생성한 go_router 넣기

이제 마지막으로 방금 전 까지 만든 go_router의 GoRouter 객체를 main.dart 파일에 MaterialApp.router에 넣겠습니다. 

 

MaterialApp.router 

go_router가 버전업이 되면서 아래 부분이 변경 되었습니다. 

  • routeInformationProvider: _router.routeInformationProvider,
  • routeInformationParser: _router.routeInformationParser,
  • routerDelegate: _router.routerDelegate

이전 포스팅에서는 위에 3개가 들어가 있는데 현재 버전에서는 아래와 같이 routerConfig에 앞서 만든 GoRouter 객체를 넣어 주기만 하면 됩니다. 

       ... 중략 .....

MaterialApp.router(
        routerConfig: router.router,

       ... 중략 .....

 

전체코드

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await MobileAds.instance.initialize();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final UserNotifier userStatus = UserNotifier();
  final UserRepository userRepository = UserRepository();
  final MottoRepository mottoRepository = MottoRepository();
  final AnalyticsSingle analyticsSingle = AnalyticsSingle();

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    GoRouterClass router =
        GoRouterClass(analyticsSingle.firebaseAnalytics, userStatus);

    return MultiProvider(
      providers: [
        ChangeNotifierProvider<UserNotifier>.value(value: userStatus),
        ChangeNotifierProvider<BottomNotifier>(create: (_) => BottomNotifier()),
        Provider<UserService>(
            create: (_) => UserService(userRepository, mottoRepository)),
        Provider<MottoService>(create: (_) => MottoService(mottoRepository)),
        Provider<AnalyticsSingle>.value(value:  analyticsSingle)
      ],
      child: MaterialApp.router(
        routerConfig: router.router,
        title: "Motto App",
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
            fontFamily: "Happy",
            primarySwatch: Colors.green,
            appBarTheme: const AppBarTheme(
                backgroundColor: Colors.white,
                foregroundColor: Colors.black87,
                centerTitle: true),
            textButtonTheme: TextButtonThemeData(
                style: TextButton.styleFrom(foregroundColor: Colors.black))),
      ),
    );
  }
}

 

마치며

플러터의 버전업 속도가 빠르듯이 관련 패키지의 업데이트 속도도 빨라져서 잠깐만 트렌드를 놓치면 따라 잡기 어려운거 같습니다. 오랜마네 쓰는 패키지라면 우선 패키지의 공식 홈페이지나 pub.dev의 공식 예제를 참고 하시는걸 추천드립니다. 


참고자료

 

2022.08.14 - [flutter] 코딩파파 당근마켓 클론 코딩 강좌 공부 - Beamer에서 Go_Router로 전환

 

[flutter] 코딩파파 당근마켓 클론 코딩 강좌 공부 - Beamer에서 Go_Router로 전환

Beamer Stackoverflow 에러 발생 코딩파파 당근마켓 클론코딩을 공부하다 보면 대부분의 사람들이 막히는 부분이 있습니다. 바로 beamer를 사용하는 부분일것입니다. 1.2버전까지는 코딩파파님의 강의

jh-industry.tistory.com

 

2022.07.28 - [flutter,beamer] beamer 대신 go_router로 guard pages 구현

 

[flutter,beamer] beamer 대신 go_router로 guard pages 구현

1. beamer 1.3 이후 버전 사용시 stackoverflow 에러발생 아마도 이 에러가 나시는 분은 1.3 이상의 최신 beamer를 사용하시면서 beamGuard의 showpage를 사용하시는 분일거 같습니다. flutter에는 navigator 2.0을 쉽

jh-industry.tistory.com

 

댓글