GoRouter를 선택한 이유
Flutter의 Navigator 2.0은 강력하지만 직접 쓰기에는 복잡합니다. GoRouter는 이를 선언적으로 감싸서 웹 라우팅처럼 쓸 수 있게 해줍니다.
특히 ShellRoute와 redirect 기능이 실전에서 유용합니다. 하단 탭을 유지하면서 내부 페이지를 전환하고, 인증 상태에 따라 자동 리다이렉트하는 패턴을 정리해보겠습니다.
특히 ShellRoute와 redirect 기능이 실전에서 유용합니다. 하단 탭을 유지하면서 내부 페이지를 전환하고, 인증 상태에 따라 자동 리다이렉트하는 패턴을 정리해보겠습니다.
ShellRoute로 하단 탭 구현
ShellRoute는 하위 라우트가 전환되어도 상위 레이아웃(하단 탭바)을 유지합니다. 일반 route로는 페이지 전환 시 하단 탭이 매번 리빌드되겠죠.
Dart
final router = GoRouter(
initialLocation: '/home',
routes: [
// ShellRoute: 하단 탭을 감싸는 껍데기
ShellRoute(
builder: (context, state, child) {
return MainShell(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/search',
builder: (context, state) => const SearchScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
// ShellRoute 밖: 탭바 없이 풀스크린
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/post/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
],
);MainShell: 하단 탭 위젯
Dart
class MainShell extends StatelessWidget {
final Widget child;
const MainShell({required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _calculateIndex(context),
onDestinationSelected: (index) {
switch (index) {
case 0: context.go('/home');
case 1: context.go('/search');
case 2: context.go('/profile');
}
},
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: '홈'),
NavigationDestination(icon: Icon(Icons.search), label: '검색'),
NavigationDestination(icon: Icon(Icons.person), label: '프로필'),
],
),
);
}
int _calculateIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
if (location.startsWith('/search')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
}context.go()와 context.push()의 차이를 알아야 합니다. go()는 스택을 교체하고, push()는 스택 위에 쌓어요. 탭 전환은 go(), 상세 페이지는 push()를 쓰는 게 자연스럽습니다.
인증 가드: redirect
Dart
final router = GoRouter(
initialLocation: '/home',
// 인증 상태 변경 시 라우터 재평가
refreshListenable: authNotifier,
redirect: (context, state) {
final isLoggedIn = authNotifier.isLoggedIn;
final isOnLoginPage = state.uri.path == '/login';
// 미로그인 + 로그인 페이지가 아님 → 로그인으로
if (!isLoggedIn && !isOnLoginPage) {
// 원래 가려던 경로를 쿼리로 저장
return '/login?redirect=${state.uri.path}';
}
// 로그인됨 + 로그인 페이지에 있음 → 홈으로
if (isLoggedIn && isOnLoginPage) {
// 저장된 경로가 있으면 그쪽으로
final redirect = state.uri.queryParameters['redirect'];
return redirect ?? '/home';
}
return null; // 리다이렉트 없음
},
routes: [...],
);AuthNotifier 구현
Dart
class AuthNotifier extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
AuthNotifier() {
// Firebase Auth 상태 감지
FirebaseAuth.instance.authStateChanges().listen((user) {
_isLoggedIn = user != null;
notifyListeners(); // GoRouter가 redirect를 재평가
});
}
}refreshListenable에 AuthNotifier를 넣으면 인증 상태가 바뀔 때마다 redirect가 자동으로 실행됩니다. 로그아웃하면 어떤 화면에 있든 로그인 페이지로 이동해요.
StatefulShellRoute로 탭 상태 유지
Dart
// 각 탭의 네비게이션 상태를 독립적으로 유지
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return MainShell(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'detail/:id',
builder: (context, state) => DetailScreen(
id: state.pathParameters['id']!,
),
),
],
),
],
),
// ... 다른 탭 branches
],
)StatefulShellRoute를 쓰면 탭 A에서 깊이 들어간 상태를 유지한 채 탭 B로 갔다가 다시 돌아올 수 있죠. 처음에 좀 헷갈리는데 인스타그램이나 유튜브 같은 앱의 탭 동작을 재현하는 데 필수적인 기능이죠.
GoRouter + ShellRoute + redirect 조합이면 대부분의 앱 네비게이션을 깔끔하게 처리할 수 있어요.
GoRouter + ShellRoute + redirect 조합이면 대부분의 앱 네비게이션을 깔끔하게 처리할 수 있어요.