12 KiB
라우트 가드, ShellRoute, DeepLink
이 장에서는 go_router를 활용한 고급 라우팅 기법에 대해 알아보겠습니다. 라우트 가드를 통한 접근 제어, ShellRoute를 이용한 중첩 네비게이션, 그리고 딥 링크를 통한 앱 외부에서의 접근 방법을 살펴보겠습니다.
라우트 가드 (Route Guards)
라우트 가드는 특정 경로에 대한 접근을 제어하는 메커니즘으로, 사용자 인증이나 권한 검사 등에 활용됩니다.
redirect를 활용한 기본 라우트 가드
go_router의 redirect
기능을 사용하여 라우트 가드를 구현할 수 있습니다:
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;
},
);
특정 라우트에 대한 가드
개별 라우트에 대해서도 리다이렉트를 설정할 수 있습니다:
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
을 사용합니다:
// 인증 상태 관리를 위한 ChangeNotifier
class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
Future<void> login() async {
_isLoggedIn = true;
notifyListeners(); // 상태 변경 알림 (라우터가 이를 감지)
}
Future<void> logout() async {
_isLoggedIn = false;
notifyListeners();
}
}
// 인증 상태 변경을 감지하는 라우터
final GoRouter router = GoRouter(
refreshListenable: authNotifier, // 인증 상태 변경 감지
redirect: (context, state) {
// 리다이렉트 로직...
},
routes: [...],
);
ShellRoute를 이용한 네비게이션
ShellRoute는 중첩 네비게이션을 구현하는 데 사용되며, 특히 바텀 네비게이션 바나 탭 바와 같은 영구적인 UI 요소가 있는 앱에 유용합니다.
StatefulShellRoute 기본 개념
StatefulShellRoute는 여러 브랜치(branch)를 관리하는 라우트로, 각 브랜치는 자체 네비게이션 상태와 히스토리를 가집니다:
graph TD
A[StatefulShellRoute] --> B[브랜치 1<br>네비게이션 스택]
A --> C[브랜치 2<br>네비게이션 스택]
A --> D[브랜치 3<br>네비게이션 스택]
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를 사용하여 바텀 네비게이션 바를 구현해 보겠습니다:
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의 강점은 각 브랜치 내의 네비게이션 상태가 유지된다는 점입니다:
// 검색 화면에서 사용자가 검색 결과로 이동한 후
// 다른 탭으로 이동했다가 다시 검색 탭으로 돌아오면
// 검색 결과 화면이 그대로 유지됩니다.
// 홈 탭에서 상세 화면으로 이동
context.go('/details/123');
// 프로필 탭으로 이동 (홈 탭의 상태는 유지됨)
context.go('/profile');
// 다시 홈 탭으로 이동하면 상세 화면이 표시됨
context.go('/');
딥 링크 (Deep Linking)
딥 링크는 앱의 특정 화면으로 직접 접근할 수 있는 외부 링크로, 웹 URL, 푸시 알림 등에서 활용됩니다.
안드로이드 딥 링크 설정
- AndroidManifest.xml 설정:
<manifest ...>
<application ...>
<activity ...>
<!-- 기존 인텐트 필터 -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- 딥 링크 인텐트 필터 추가 -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- 딥 링크 스킴 및 호스트 설정 -->
<data
android:scheme="https"
android:host="example.com" />
</intent-filter>
</activity>
</application>
</manifest>
iOS 딥 링크 설정
- Info.plist 설정:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.example.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
<!-- Universal Links 설정 -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:example.com</string>
</array>
Flutter에서 딥 링크 처리
go_router를 사용하면 딥 링크 처리가 매우 간단합니다:
// 딥 링크를 자동으로 처리하는 설정
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: '딥 링크 예제',
// ...
);
}
}
딥 링크 테스트
딥 링크를 테스트하기 위해 터미널에서 다음 명령어를 실행할 수 있습니다:
안드로이드:
adb shell am start -a android.intent.action.VIEW -d "https://example.com/product/123" com.example.app
iOS 시뮬레이터:
xcrun simctl openurl booted "https://example.com/product/123"
푸시 알림에서 딥 링크 처리
푸시 알림을 통한 딥 링크를 처리하는 방법:
// firebase_messaging 패키지 사용 예제
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
final deepLink = message.data['deepLink'] as String?;
if (deepLink != null) {
// 앱이 실행 중일 때 푸시 알림을 탭하면 해당 경로로 이동
router.go(deepLink);
}
});
// 앱이 종료된 상태에서 푸시 알림을 탭한 경우
Future<void> 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에서 가져온 데이터를 기반으로 동적으로 라우트를 생성할 수 있습니다:
// 동적 라우트 생성 예제
Future<List<GoRoute>> 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. 라우트 전환 애니메이션 커스터마이징
라우트 간 전환 애니메이션을 세밀하게 제어할 수 있습니다:
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 모두에서 딥 링크를 설정하는 방법이 다릅니다.
- 푸시 알림에서 딥 링크를 처리하여 특정 화면으로 이동할 수 있습니다.
- 동적 라우트 생성, 커스텀 전환 애니메이션 등 고급 라우팅 기법을 활용할 수 있습니다.
다음 섹션에서는 이러한 고급 라우팅 기법을 실제 앱에 적용하는 복수 화면 전환 실습을 진행하겠습니다.