Flutter 개발

GoRouter ��전: ShellRoute와 인증 가드

하단 탭 유지 + 로그인 리다이렉트 한 번에 잡기

John Doe
2025년 7월 31일
7분 읽기
56

GoRouter를 선택한 이유

Flutter의 Navigator 2.0은 강력하지만 직접 쓰기에는 복잡합니다. GoRouter는 이를 선언적으로 감싸서 웹 라우팅처럼 쓸 수 있게 해줍니다.

특히 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 조합이면 대부분의 앱 네비게이션을 깔끔하게 처리할 수 있어요.
#Flutter
#GoRouter
#ShellRoute
#인증
#Navigation