# 라우트 가드, ShellRoute, DeepLink 이 장에서는 go_router를 활용한 고급 라우팅 기법에 대해 알아보겠습니다. 라우트 가드를 통한 접근 제어, ShellRoute를 이용한 중첩 네비게이션, 그리고 딥 링크를 통한 앱 외부에서의 접근 방법을 살펴보겠습니다. ## 라우트 가드 (Route Guards) 라우트 가드는 특정 경로에 대한 접근을 제어하는 메커니즘으로, 사용자 인증이나 권한 검사 등에 활용됩니다. ### redirect를 활용한 기본 라우트 가드 go_router의 `redirect` 기능을 사용하여 라우트 가드를 구현할 수 있습니다: ```dart final GoRouter router = GoRouter( routes: [...], // 전역 리다이렉트 (모든 라우트에 적용) redirect: (context, state) { // 현재 인증 상태 확인 final isLoggedIn = AuthService.isLoggedIn; // 로그인이 필요한 경로 목록 final protectedRoutes = ['/profile', '/settings', '/cart']; final isProtectedRoute = protectedRoutes.any( (route) => state.matchedLocation.startsWith(route), ); // 로그인 페이지 여부 확인 final isLoginRoute = state.matchedLocation == '/login'; // 로그인 되지 않았고 보호된 경로로 접근 시도 if (!isLoggedIn && isProtectedRoute) { return '/login?redirect=${state.matchedLocation}'; } // 이미 로그인된 상태에서 로그인 페이지 접근 시도 if (isLoggedIn && isLoginRoute) { return '/'; } // 조건에 해당하지 않으면 리다이렉트 없음 (원래 경로 유지) return null; }, ); ``` ### 특정 라우트에 대한 가드 개별 라우트에 대해서도 리다이렉트를 설정할 수 있습니다: ```dart GoRoute( path: '/admin', redirect: (context, state) { final user = AuthService.currentUser; // 관리자 권한 확인 if (user == null || !user.hasAdminRole) { return '/access-denied'; } // 권한이 있으면 원래 경로로 진행 return null; }, builder: (context, state) => AdminDashboard(), ), ``` ### 상태 변화에 따른 리다이렉트 갱신 인증 상태가 변경될 때 라우트를 재평가하기 위해 `refreshListenable`을 사용합니다: ```dart // 인증 상태 관리를 위한 ChangeNotifier class AuthNotifier extends ChangeNotifier { bool _isLoggedIn = false; bool get isLoggedIn => _isLoggedIn; Future login() async { _isLoggedIn = true; notifyListeners(); // 상태 변경 알림 (라우터가 이를 감지) } Future logout() async { _isLoggedIn = false; notifyListeners(); } } // 인증 상태 변경을 감지하는 라우터 final GoRouter router = GoRouter( refreshListenable: authNotifier, // 인증 상태 변경 감지 redirect: (context, state) { // 리다이렉트 로직... }, routes: [...], ); ``` ## ShellRoute를 이용한 네비게이션 ShellRoute는 중첩 네비게이션을 구현하는 데 사용되며, 특히 바텀 네비게이션 바나 탭 바와 같은 영구적인 UI 요소가 있는 앱에 유용합니다. ### StatefulShellRoute 기본 개념 StatefulShellRoute는 여러 브랜치(branch)를 관리하는 라우트로, 각 브랜치는 자체 네비게이션 상태와 히스토리를 가집니다: ```mermaid graph TD A[StatefulShellRoute] --> B[브랜치 1
네비게이션 스택] A --> C[브랜치 2
네비게이션 스택] A --> D[브랜치 3
네비게이션 스택] B --> B1[화면 1.1] B --> B2[화면 1.2] C --> C1[화면 2.1] D --> D1[화면 3.1] D --> D2[화면 3.2] D --> D3[화면 3.3] ``` ### 바텀 네비게이션 바 구현 StatefulShellRoute를 사용하여 바텀 네비게이션 바를 구현해 보겠습니다: ```dart final GoRouter router = GoRouter( initialLocation: '/', routes: [ // StatefulShellRoute 정의 (indexedStack 방식 사용) StatefulShellRoute.indexedStack( // 쉘 UI 구성 builder: (context, state, navigationShell) { return ScaffoldWithNavBar(navigationShell: navigationShell); }, // 브랜치 정의 branches: [ // 홈 탭 StatefulShellBranch( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), routes: [ // 홈 탭 내부의 중첩 라우트 GoRoute( path: 'details/:id', builder: (context, state) { final id = state.pathParameters['id']!; return DetailsScreen(id: id); }, ), ], ), ], ), // 검색 탭 StatefulShellBranch( routes: [ GoRoute( path: '/search', builder: (context, state) => SearchScreen(), ), ], ), // 프로필 탭 StatefulShellBranch( routes: [ GoRoute( path: '/profile', builder: (context, state) => ProfileScreen(), ), ], ), ], ), ], ); // 바텀 네비게이션 바가 있는 스캐폴드 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.search), label: '검색'), BottomNavigationBarItem(icon: Icon(Icons.person), label: '프로필'), ], onTap: (index) { // 탭 인덱스에 해당하는 브랜치로 이동 navigationShell.goBranch(index); }, ), ); } } ``` ### 브랜치 간 이동과 상태 유지 StatefulShellRoute의 강점은 각 브랜치 내의 네비게이션 상태가 유지된다는 점입니다: ```dart // 검색 화면에서 사용자가 검색 결과로 이동한 후 // 다른 탭으로 이동했다가 다시 검색 탭으로 돌아오면 // 검색 결과 화면이 그대로 유지됩니다. // 홈 탭에서 상세 화면으로 이동 context.go('/details/123'); // 프로필 탭으로 이동 (홈 탭의 상태는 유지됨) context.go('/profile'); // 다시 홈 탭으로 이동하면 상세 화면이 표시됨 context.go('/'); ``` ## 딥 링크 (Deep Linking) 딥 링크는 앱의 특정 화면으로 직접 접근할 수 있는 외부 링크로, 웹 URL, 푸시 알림 등에서 활용됩니다. ### 안드로이드 딥 링크 설정 1. **AndroidManifest.xml 설정**: ```xml ``` ### iOS 딥 링크 설정 1. **Info.plist 설정**: ```xml CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName com.example.app CFBundleURLSchemes myapp com.apple.developer.associated-domains applinks:example.com ``` ### Flutter에서 딥 링크 처리 go_router를 사용하면 딥 링크 처리가 매우 간단합니다: ```dart // 딥 링크를 자동으로 처리하는 설정 void main() { // 앱 초기화 WidgetsFlutterBinding.ensureInitialized(); // 라우터 설정 final router = GoRouter( // 라우트 설정 routes: [...], ); runApp(MyApp(router: router)); } class MyApp extends StatelessWidget { final GoRouter router; const MyApp({required this.router}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: router, title: '딥 링크 예제', // ... ); } } ``` ### 딥 링크 테스트 딥 링크를 테스트하기 위해 터미널에서 다음 명령어를 실행할 수 있습니다: **안드로이드**: ```bash adb shell am start -a android.intent.action.VIEW -d "https://example.com/product/123" com.example.app ``` **iOS 시뮬레이터**: ```bash xcrun simctl openurl booted "https://example.com/product/123" ``` ### 푸시 알림에서 딥 링크 처리 푸시 알림을 통한 딥 링크를 처리하는 방법: ```dart // firebase_messaging 패키지 사용 예제 FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { final deepLink = message.data['deepLink'] as String?; if (deepLink != null) { // 앱이 실행 중일 때 푸시 알림을 탭하면 해당 경로로 이동 router.go(deepLink); } }); // 앱이 종료된 상태에서 푸시 알림을 탭한 경우 Future setupInteractedMessage() async { RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { final deepLink = initialMessage.data['deepLink'] as String?; if (deepLink != null) { // 딥 링크로 이동 router.go(deepLink); } } } ``` ## 고급 라우팅 테크닉 ### 1. 동적 라우트 생성 API에서 가져온 데이터를 기반으로 동적으로 라우트를 생성할 수 있습니다: ```dart // 동적 라우트 생성 예제 Future> buildDynamicRoutes() async { // API에서 카테고리 목록 가져오기 final categories = await apiService.getCategories(); // 각 카테고리에 대한 라우트 생성 return categories.map((category) { return GoRoute( path: '/category/${category.slug}', builder: (context, state) => CategoryScreen(category: category), ); }).toList(); } ``` ### 2. 라우트 전환 애니메이션 커스터마이징 라우트 간 전환 애니메이션을 세밀하게 제어할 수 있습니다: ```dart GoRoute( path: '/details/:id', pageBuilder: (context, state) { final id = state.pathParameters['id']!; // 히어로 애니메이션을 위한 전환 페이지 return CustomTransitionPage( key: state.pageKey, child: ProductDetailsScreen(id: id), transitionsBuilder: (context, animation, secondaryAnimation, child) { // 페이드 트랜지션 return FadeTransition( opacity: CurveTween(curve: Curves.easeInOut).animate(animation), child: child, ); }, ); }, ), ``` ## 요약 - **라우트 가드**를 사용하여 인증 상태에 따라 접근을 제어할 수 있습니다. - **StatefulShellRoute**는 바텀 네비게이션 바와 같은 중첩 네비게이션을 효과적으로 구현할 수 있게 해줍니다. - **딥 링크**를 통해 앱 외부에서 특정 화면으로 직접 접근할 수 있습니다. - **안드로이드와 iOS** 모두에서 딥 링크를 설정하는 방법이 다릅니다. - **푸시 알림**에서 딥 링크를 처리하여 특정 화면으로 이동할 수 있습니다. - **동적 라우트 생성**, **커스텀 전환 애니메이션** 등 고급 라우팅 기법을 활용할 수 있습니다. 다음 섹션에서는 이러한 고급 라우팅 기법을 실제 앱에 적용하는 복수 화면 전환 실습을 진행하겠습니다.