GoRouter — 선언적 라우팅과 딥링크 처리

GoRouter는 Flutter 공식 추천 라우팅 패키지입니다. Navigator 2.0의 복잡함을 숨기고, 선언적으로 라우트를 정의할 수 있게 해줍니다.


설치

YAML
dependencies:
  go_router: ^14.0.0

기본 설정

DART
final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/detail/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return DetailScreen(id: id);
      },
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
);

// MaterialApp.router에 전달
MaterialApp.router(
  routerConfig: _router,
)

네비게이션

DART
// 이동
context.go('/detail/42');

// push (스택에 추가)
context.push('/detail/42');

// 뒤로 가기
context.pop();

// 교체
context.pushReplacement('/home');

// 쿼리 파라미터
context.go('/search?q=flutter&page=1');

// 쿼리 파라미터 받기
GoRoute(
  path: '/search',
  builder: (context, state) {
    final query = state.uri.queryParameters['q'] ?? '';
    final page = state.uri.queryParameters['page'] ?? '1';
    return SearchScreen(query: query, page: int.parse(page));
  },
)

go vs push

메서드동작뒤로가기
go()해당 경로로 이동 (스택 교체)부모 경로로
push()현재 스택 위에 추가이전 화면으로

면접 포인트: go()는 URL 기반 네비게이션(웹 스타일), push()는 스택 기반 네비게이션(앱 스타일)입니다. 웹에서는 주로 go(), 모바일에서는 상황에 따라 선택합니다.


중첩 라우팅 (ShellRoute)

하단 탭 네비게이션처럼 공통 UI를 유지하면서 내부 화면만 전환하는 패턴입니다.

DART
final _router = GoRouter(
  routes: [
    ShellRoute(
      builder: (context, state, child) {
        return ScaffoldWithNavBar(child: child);
      },
      routes: [
        GoRoute(
          path: '/home',
          builder: (context, state) => const HomeTab(),
        ),
        GoRoute(
          path: '/search',
          builder: (context, state) => const SearchTab(),
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileTab(),
        ),
      ],
    ),
    // ShellRoute 밖의 라우트 (전체 화면)
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
  ],
);

// 하단 네비게이션 바 위젯
class ScaffoldWithNavBar extends StatelessWidget {
  final Widget child;
  const ScaffoldWithNavBar({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        selectedIndex: _calculateIndex(GoRouterState.of(context).uri.path),
        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(String path) {
    if (path.startsWith('/search')) return 1;
    if (path.startsWith('/profile')) return 2;
    return 0;
  }
}

리다이렉트 (인증 가드)

DART
final _router = GoRouter(
  redirect: (context, state) {
    final isLoggedIn = authService.isLoggedIn;
    final isLoginRoute = state.matchedLocation == '/login';

    // 로그인 안 했으면 로그인 화면으로
    if (!isLoggedIn && !isLoginRoute) {
      return '/login?redirect=${state.matchedLocation}';
    }

    // 로그인했는데 로그인 화면이면 홈으로
    if (isLoggedIn && isLoginRoute) {
      return '/home';
    }

    return null;  // 리다이렉트 하지 않음
  },
  routes: [...],
);

특정 라우트에만 리다이렉트

DART
GoRoute(
  path: '/admin',
  redirect: (context, state) {
    if (!authService.isAdmin) {
      return '/unauthorized';
    }
    return null;
  },
  builder: (context, state) => const AdminScreen(),
)

에러 페이지 (404)

DART
final _router = GoRouter(
  errorBuilder: (context, state) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              '404',
              style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            Text('페이지를 찾을 수 없습니다: ${state.uri.path}'),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('홈으로'),
            ),
          ],
        ),
      ),
    );
  },
  routes: [...],
);

페이지 전환 애니메이션

DART
GoRoute(
  path: '/detail/:id',
  pageBuilder: (context, state) {
    return CustomTransitionPage(
      key: state.pageKey,
      child: DetailScreen(id: state.pathParameters['id']!),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return FadeTransition(
          opacity: animation,
          child: child,
        );
      },
    );
  },
)

Extra 데이터 전달

DART
// 객체 전달
context.go('/detail', extra: product);

// 받기
GoRoute(
  path: '/detail',
  builder: (context, state) {
    final product = state.extra as Product;
    return DetailScreen(product: product);
  },
)

주의: extra는 딥링크나 URL 기반 복원이 안 됩니다. 가능하면 경로 파라미터를 사용하세요.


정리

  • GoRouter는 Flutter 공식 추천 선언적 라우팅 패키지입니다
  • go()는 URL 기반 이동, push()는 스택 기반 이동입니다
  • ShellRoute로 하단 탭 같은 중첩 네비게이션을 구현합니다
  • redirect로 인증 가드 등 접근 제어를 처리합니다
  • 딥링크와 웹 URL을 자연스럽게 지원하는 것이 가장 큰 장점입니다
댓글 로딩 중...