# go_router 사용법 go_router는 Flutter 팀이 공식적으로 지원하는 라우팅 패키지로, Navigator 2.0의 기능을 더 쉽게 사용할 수 있게 해줍니다. 복잡한 라우팅 시나리오를 처리하면서도 간결한 API를 제공하여 개발자 경험을 크게 향상시킵니다. ## go_router의 소개 go_router는 다음과 같은 목표로 개발되었습니다: 1. **간결한 API**: Navigator 2.0의 복잡성을 줄이고 더 직관적인 API 제공 2. **선언적 라우팅**: 앱의 모든 라우트를 한 곳에서 선언적으로 정의 3. **딥 링크 지원**: 모바일 앱의 딥 링크와 웹 URL 지원 4. **중첩 라우팅**: 중첩된 네비게이션 시나리오 지원 5. **페이지 전환 애니메이션**: 커스텀 페이지 전환 효과 지원 ## go_router 설치하기 pubspec.yaml 파일에 go_router 패키지를 추가합니다: ```yaml dependencies: flutter: sdk: flutter go_router: ^10.0.0 # 최신 버전을 확인하세요 ``` 그리고 패키지를 설치합니다: ```bash flutter pub get ``` ## go_router의 기본 개념 ### 1. GoRouter 설정 앱의 라우팅을 설정하는 GoRouter 인스턴스를 생성합니다: ```dart final GoRouter _router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), GoRoute( path: '/details/:id', builder: (context, state) => DetailsScreen( id: state.pathParameters['id']!, ), ), ], ); // 앱에 라우터 적용 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _router, title: 'GoRouter Example', ); } } ``` ### 2. 경로 정의와 매개변수 go_router에서는 URL 경로에 매개변수를 포함할 수 있습니다: - **경로 매개변수**: `/user/:id`와 같이 콜론으로 시작하는 세그먼트 - **쿼리 매개변수**: `/search?query=flutter`와 같이 URL에 추가되는 키-값 쌍 ```dart GoRoute( path: '/user/:userId/post/:postId', builder: (context, state) { // 경로 매개변수 추출 final userId = state.pathParameters['userId']!; final postId = state.pathParameters['postId']!; // 쿼리 매개변수 추출 final filter = state.queryParameters['filter']; return PostScreen(userId: userId, postId: postId, filter: filter); }, ), ``` ### 3. 화면 이동 go_router는 다양한 방법으로 화면 간 이동을 지원합니다: ```dart // 명시적 경로로 이동 context.go('/details/123'); // 현재 스택에 새 화면 추가 context.push('/details/123'); // 스택을 모두 비우고 새 화면으로 대체 context.pushReplacement('/details/123'); // 해당 경로까지 모든 화면 제거 후 새 화면 추가 context.pushAndRemoveUntil('/details/123', predicate); // 이전 화면으로 돌아가기 context.pop(); ``` ## go_router 고급 기능 ### 1. 중첩 라우팅 go_router는 StatefulShellRoute를 통해 중첩 라우팅을 지원합니다: ```dart final GoRouter _router = GoRouter( routes: [ StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) { // 바텀 네비게이션 바 또는 탭 컨트롤러와 함께 사용 return ScaffoldWithNavBar(navigationShell: navigationShell); }, branches: [ // 첫 번째 탭 (Home) StatefulShellBranch( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), routes: [ GoRoute( path: 'details/:id', builder: (context, state) => DetailsScreen( id: state.pathParameters['id']!, ), ), ], ), ], ), // 두 번째 탭 (Profile) StatefulShellBranch( routes: [ GoRoute( path: '/profile', builder: (context, state) => ProfileScreen(), routes: [ GoRoute( path: 'edit', builder: (context, state) => EditProfileScreen(), ), ], ), ], ), // 세 번째 탭 (Settings) StatefulShellBranch( routes: [ GoRoute( path: '/settings', builder: (context, state) => SettingsScreen(), ), ], ), ], ), ], ); // 바텀 네비게이션 바 위젯 class ScaffoldWithNavBar extends StatelessWidget { final StatefulNavigationShell navigationShell; const ScaffoldWithNavBar({required this.navigationShell}); @override Widget build(BuildContext context) { return Scaffold( body: navigationShell, bottomNavigationBar: BottomNavigationBar( currentIndex: navigationShell.currentIndex, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: '홈'), BottomNavigationBarItem(icon: Icon(Icons.person), label: '프로필'), BottomNavigationBarItem(icon: Icon(Icons.settings), label: '설정'), ], onTap: (index) => navigationShell.goBranch(index), ), ); } } ``` ### 2. 리다이렉트 리다이렉트를 사용하여 인증이 필요한 페이지나 다른 경로로 자동 리다이렉션할 수 있습니다: ```dart final GoRouter _router = GoRouter( initialLocation: '/', routes: [...], // 전역 리다이렉트 (모든 라우트에 적용) redirect: (context, state) { final isLoggedIn = AuthService.isLoggedIn; final isGoingToLogin = state.matchedLocation == '/login'; // 로그인되지 않았고 로그인 페이지로 가는 중이 아니면 로그인 페이지로 리다이렉트 if (!isLoggedIn && !isGoingToLogin) { return '/login?redirect=${state.matchedLocation}'; } // 이미 로그인되었고 로그인 페이지로 가려고 한다면 홈으로 리다이렉트 if (isLoggedIn && isGoingToLogin) { return '/'; } // 리다이렉트 없음 return null; }, ); // 특정 라우트에 대한 리다이렉트 GoRoute( path: '/admin', redirect: (context, state) { final isAdmin = AuthService.hasAdminRole; if (!isAdmin) { return '/access-denied'; } return null; }, builder: (context, state) => AdminPanel(), ), ``` ### 3. 오류 처리 go_router는 존재하지 않는 경로에 대한 오류 처리를 지원합니다: ```dart final GoRouter _router = GoRouter( initialLocation: '/', routes: [...], // 경로가 매치되지 않을 때 표시할 화면 errorBuilder: (context, state) => NotFoundScreen(), ); ``` ### 4. 페이지 전환 애니메이션 go_router를 사용하여 화면 전환 애니메이션을 커스터마이징할 수 있습니다: ```dart GoRoute( path: '/details/:id', pageBuilder: (context, state) { return CustomTransitionPage( key: state.pageKey, child: DetailsScreen(id: state.pathParameters['id']!), transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); var offsetAnimation = animation.drive(tween); return SlideTransition( position: offsetAnimation, child: child, ); }, ); }, ), ``` ## go_router 활용 예제 다음은 go_router를 활용한 전체 샘플 앱입니다: ```dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(MyApp()); } // 앱 상태 (인증 상태) class AppState extends ChangeNotifier { bool _isLoggedIn = false; bool get isLoggedIn => _isLoggedIn; void login() { _isLoggedIn = true; notifyListeners(); } void logout() { _isLoggedIn = false; notifyListeners(); } } // 라우터 설정 final appState = AppState(); final GoRouter _router = GoRouter( initialLocation: '/', refreshListenable: appState, // 인증 상태가 변경될 때 라우터 갱신 redirect: (context, state) { // 인증이 필요한 경로 목록 final protectedRoutes = ['/profile', '/settings']; // 현재 경로가 보호된 경로인지 확인 final isProtectedRoute = protectedRoutes.any( (route) => state.matchedLocation.startsWith(route), ); // 로그인 되지 않았지만 보호된 경로로 접근하려고 할 때 if (!appState.isLoggedIn && isProtectedRoute) { return '/login?redirect=${state.matchedLocation}'; } // 로그인 되어 있고 로그인 페이지로 가려고 할 때 if (appState.isLoggedIn && state.matchedLocation == '/login') { return '/'; } // 리다이렉트 없음 return null; }, routes: [ // 홈 화면 GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), // 로그인 화면 GoRoute( path: '/login', builder: (context, state) { // 로그인 후 리다이렉트할 경로 final redirectUrl = state.queryParameters['redirect'] ?? '/'; return LoginScreen(redirectUrl: redirectUrl); }, ), // 상품 상세 화면 GoRoute( path: '/product/:id', builder: (context, state) { final productId = state.pathParameters['id']!; return ProductDetailScreen(productId: productId); }, ), // 프로필 섹션 (중첩 라우트) GoRoute( path: '/profile', builder: (context, state) => ProfileScreen(), routes: [ GoRoute( path: 'edit', builder: (context, state) => EditProfileScreen(), ), GoRoute( path: 'orders', builder: (context, state) => OrderHistoryScreen(), ), ], ), // 설정 화면 GoRoute( path: '/settings', builder: (context, state) => SettingsScreen(), ), ], // 경로를 찾을 수 없을 때의 오류 화면 errorBuilder: (context, state) => NotFoundScreen(), ); // 메인 앱 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( title: 'GoRouter Example', theme: ThemeData( primarySwatch: Colors.blue, ), routerConfig: _router, ); } } // 홈 화면 class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('홈')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('홈 화면'), SizedBox(height: 20), // 상품 목록 Expanded( child: ListView.builder( itemCount: 10, itemBuilder: (context, index) { final productId = index + 1; return ListTile( title: Text('상품 $productId'), onTap: () => context.go('/product/$productId'), ); }, ), ), ], ), ), drawer: AppDrawer(), ); } } // 앱 드로어 class AppDrawer extends StatelessWidget { @override Widget build(BuildContext context) { return Drawer( child: ListView( padding: EdgeInsets.zero, children: [ DrawerHeader( decoration: BoxDecoration(color: Colors.blue), child: Text( 'GoRouter 예제', style: TextStyle(color: Colors.white, fontSize: 24), ), ), ListTile( leading: Icon(Icons.home), title: Text('홈'), onTap: () { context.go('/'); Navigator.pop(context); // 드로어 닫기 }, ), ListTile( leading: Icon(Icons.person), title: Text('프로필'), onTap: () { context.go('/profile'); Navigator.pop(context); }, ), ListTile( leading: Icon(Icons.settings), title: Text('설정'), onTap: () { context.go('/settings'); Navigator.pop(context); }, ), Divider(), if (appState.isLoggedIn) ListTile( leading: Icon(Icons.logout), title: Text('로그아웃'), onTap: () { appState.logout(); Navigator.pop(context); }, ) else ListTile( leading: Icon(Icons.login), title: Text('로그인'), onTap: () { context.go('/login'); Navigator.pop(context); }, ), ], ), ); } } // 로그인 화면 class LoginScreen extends StatelessWidget { final String redirectUrl; LoginScreen({required this.redirectUrl}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('로그인')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('로그인 화면'), SizedBox(height: 20), ElevatedButton( onPressed: () { appState.login(); context.go(redirectUrl); }, child: Text('로그인'), ), ], ), ), ); } } // 상품 상세 화면 class ProductDetailScreen extends StatelessWidget { final String productId; ProductDetailScreen({required this.productId}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('상품 상세')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('상품 ID: $productId의 상세 정보'), SizedBox(height: 20), ElevatedButton( onPressed: () => context.go('/'), child: Text('홈으로 돌아가기'), ), ], ), ), ); } } // 프로필 화면 class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('프로필')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('프로필 화면'), SizedBox(height: 20), ElevatedButton( onPressed: () => context.go('/profile/edit'), child: Text('프로필 수정'), ), SizedBox(height: 10), ElevatedButton( onPressed: () => context.go('/profile/orders'), child: Text('주문 내역'), ), ], ), ), ); } } // 프로필 수정 화면 class EditProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('프로필 수정')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('프로필 수정 화면'), SizedBox(height: 20), ElevatedButton( onPressed: () => context.pop(), child: Text('뒤로 가기'), ), ], ), ), ); } } // 주문 내역 화면 class OrderHistoryScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('주문 내역')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('주문 내역 화면'), SizedBox(height: 20), ElevatedButton( onPressed: () => context.pop(), child: Text('뒤로 가기'), ), ], ), ), ); } } // 설정 화면 class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('설정')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('설정 화면'), SizedBox(height: 20), ElevatedButton( onPressed: () => context.go('/'), child: Text('홈으로 돌아가기'), ), ], ), ), ); } } // 404 화면 class NotFoundScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('페이지를 찾을 수 없음')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('404 - 페이지를 찾을 수 없습니다'), SizedBox(height: 20), ElevatedButton( onPressed: () => context.go('/'), child: Text('홈으로 돌아가기'), ), ], ), ), ); } } ``` ## go_router 베스트 프랙티스 ### 1. 라우터 설정 분리 라우터 설정을 별도의 파일로 분리하여 코드를 구조화하세요: ```dart // router_config.dart final GoRouter router = GoRouter( routes: [ // 라우트 정의 ], ); // main.dart import 'router_config.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: router, // ... ); } } ``` ### 2. 경로 상수 사용 문자열 경로 대신 상수를 사용하여 오타를 방지하세요: ```dart // route_paths.dart abstract class RoutePaths { static const home = '/'; static const login = '/login'; static const product = '/product/:id'; static const productDetails = '/product/'; static const profile = '/profile'; static const settings = '/settings'; } // 사용 예시 context.go(RoutePaths.productDetails + productId); ``` ### 3. 매개변수 타입 검증 경로 매개변수의 타입을 검증하여 잘못된 데이터로 인한 오류를 방지하세요: ```dart GoRoute( path: '/product/:id', builder: (context, state) { // 숫자 ID 검증 final idStr = state.pathParameters['id']!; final id = int.tryParse(idStr); if (id == null) { // 잘못된 ID 형식 return InvalidProductScreen(id: idStr); } return ProductDetailScreen(id: id); }, ), ``` ### 4. 로깅 및 디버깅 go_router의 디버그 모드를 활성화하여 라우팅 문제를 디버깅하세요: ```dart final GoRouter _router = GoRouter( debugLogDiagnostics: true, // 라우팅 디버그 로그 활성화 routes: [...], ); ``` ## go_router vs 다른 라우팅 라이브러리 go_router는 다른 Flutter 라우팅 라이브러리에 비해 몇 가지 장점이 있습니다: ### 1. go_router vs Navigator 2.0 직접 사용 - **go_router**: 간결한 API, 적은 보일러플레이트 코드, 더 직관적인 사용법 - **Navigator 2.0**: 더 많은 유연성, 더 많은 보일러플레이트 코드 필요 ### 2. go_router vs auto_route - **go_router**: 공식 지원, 간단한 설정, 코드 생성 불필요 - **auto_route**: 코드 생성 기반, 타입 안전성, 더 많은 설정 필요 ### 3. go_router vs get - **go_router**: 공식 지원, Navigator 2.0 기반, URL 동기화 지원 강력 - **get**: 더 넓은 기능 세트 (상태 관리, 종속성 주입 등), 더 단순한 API ## 요약 - **go_router**는 Flutter 팀이 공식 지원하는 네비게이션 라이브러리로, Navigator 2.0의 기능을 더 쉽게 사용할 수 있게 해줍니다. - **선언적 라우팅**을 통해 앱의 모든 경로를 한 곳에서 정의할 수 있습니다. - **중첩 라우팅**, **리다이렉트**, **오류 처리**, **애니메이션** 등 고급 기능을 제공합니다. - **경로 매개변수**와 **쿼리 매개변수**를 통해 데이터를 쉽게 전달할 수 있습니다. - **context.go()**, **context.push()** 등의 직관적인 메서드로 화면 간 이동이 가능합니다. - **StatefulShellRoute**를 사용하여 바텀 네비게이션 바와 같은 탭 기반 UI를 쉽게 구현할 수 있습니다. go_router는 대부분의 Flutter 앱에서 권장되는 라우팅 솔루션으로, 간단한 앱부터 복잡한 앱까지 효과적으로 네비게이션 관리를 할 수 있게 해줍니다. 다음 섹션에서는 go_router의 고급 기능인 라우트 가드, ShellRoute, 딥 링크 처리에 대해 더 자세히 알아보겠습니다.