Skip to content

go_router 사용법

go_router는 Flutter 팀이 공식적으로 지원하는 라우팅 패키지로, Navigator 2.0의 기능을 더 쉽게 사용할 수 있게 해줍니다. 복잡한 라우팅 시나리오를 처리하면서도 간결한 API를 제공하여 개발자 경험을 크게 향상시킵니다.

go_router는 다음과 같은 목표로 개발되었습니다:

  1. 간결한 API: Navigator 2.0의 복잡성을 줄이고 더 직관적인 API 제공
  2. 선언적 라우팅: 앱의 모든 라우트를 한 곳에서 선언적으로 정의
  3. 딥 링크 지원: 모바일 앱의 딥 링크와 웹 URL 지원
  4. 중첩 라우팅: 중첩된 네비게이션 시나리오 지원
  5. 페이지 전환 애니메이션: 커스텀 페이지 전환 효과 지원

pubspec.yaml 파일에 go_router 패키지를 추가합니다:

dependencies:
flutter:
sdk: flutter
go_router: ^10.0.0 # 최신 버전을 확인하세요

그리고 패키지를 설치합니다:

Terminal window
flutter pub get

앱의 라우팅을 설정하는 GoRouter 인스턴스를 생성합니다:

final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) => DetailsScreen(
id: state.pathParameters['id']!,
),
),
],
);
// 앱에 라우터 적용
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
title: 'GoRouter Example',
);
}
}

go_router에서는 URL 경로에 매개변수를 포함할 수 있습니다:

  • 경로 매개변수: /user/:id와 같이 콜론으로 시작하는 세그먼트
  • 쿼리 매개변수: /search?query=flutter와 같이 URL에 추가되는 키-값 쌍
GoRoute(
path: '/user/:userId/post/:postId',
builder: (context, state) {
// 경로 매개변수 추출
final userId = state.pathParameters['userId']!;
final postId = state.pathParameters['postId']!;
// 쿼리 매개변수 추출
final filter = state.queryParameters['filter'];
return PostScreen(userId: userId, postId: postId, filter: filter);
},
),

go_router는 다양한 방법으로 화면 간 이동을 지원합니다:

// 명시적 경로로 이동
context.go('/details/123');
// 현재 스택에 새 화면 추가
context.push('/details/123');
// 스택을 모두 비우고 새 화면으로 대체
context.pushReplacement('/details/123');
// 해당 경로까지 모든 화면 제거 후 새 화면 추가
context.pushAndRemoveUntil('/details/123', predicate);
// 이전 화면으로 돌아가기
context.pop();

go_router는 StatefulShellRoute를 통해 중첩 라우팅을 지원합니다:

final GoRouter _router = GoRouter(
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
// 바텀 네비게이션 바 또는 탭 컨트롤러와 함께 사용
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
// 첫 번째 탭 (Home)
StatefulShellBranch(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) => DetailsScreen(
id: state.pathParameters['id']!,
),
),
],
),
],
),
// 두 번째 탭 (Profile)
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
routes: [
GoRoute(
path: 'edit',
builder: (context, state) => EditProfileScreen(),
),
],
),
],
),
// 세 번째 탭 (Settings)
StatefulShellBranch(
routes: [
GoRoute(
path: '/settings',
builder: (context, state) => SettingsScreen(),
),
],
),
],
),
],
);
// 바텀 네비게이션 바 위젯
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.person), label: '프로필'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: '설정'),
],
onTap: (index) => navigationShell.goBranch(index),
),
);
}
}

리다이렉트를 사용하여 인증이 필요한 페이지나 다른 경로로 자동 리다이렉션할 수 있습니다:

final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [...],
// 전역 리다이렉트 (모든 라우트에 적용)
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isGoingToLogin = state.matchedLocation == '/login';
// 로그인되지 않았고 로그인 페이지로 가는 중이 아니면 로그인 페이지로 리다이렉트
if (!isLoggedIn && !isGoingToLogin) {
return '/login?redirect=${state.matchedLocation}';
}
// 이미 로그인되었고 로그인 페이지로 가려고 한다면 홈으로 리다이렉트
if (isLoggedIn && isGoingToLogin) {
return '/';
}
// 리다이렉트 없음
return null;
},
);
// 특정 라우트에 대한 리다이렉트
GoRoute(
path: '/admin',
redirect: (context, state) {
final isAdmin = AuthService.hasAdminRole;
if (!isAdmin) {
return '/access-denied';
}
return null;
},
builder: (context, state) => AdminPanel(),
),

go_router는 존재하지 않는 경로에 대한 오류 처리를 지원합니다:

final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [...],
// 경로가 매치되지 않을 때 표시할 화면
errorBuilder: (context, state) => NotFoundScreen(),
);

go_router를 사용하여 화면 전환 애니메이션을 커스터마이징할 수 있습니다:

GoRoute(
path: '/details/:id',
pageBuilder: (context, state) {
return CustomTransitionPage(
key: state.pageKey,
child: DetailsScreen(id: state.pathParameters['id']!),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
);
},
),

다음은 go_router를 활용한 전체 샘플 앱입니다:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(MyApp());
}
// 앱 상태 (인증 상태)
class AppState extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners();
}
void logout() {
_isLoggedIn = false;
notifyListeners();
}
}
// 라우터 설정
final appState = AppState();
final GoRouter _router = GoRouter(
initialLocation: '/',
refreshListenable: appState, // 인증 상태가 변경될 때 라우터 갱신
redirect: (context, state) {
// 인증이 필요한 경로 목록
final protectedRoutes = ['/profile', '/settings'];
// 현재 경로가 보호된 경로인지 확인
final isProtectedRoute = protectedRoutes.any(
(route) => state.matchedLocation.startsWith(route),
);
// 로그인 되지 않았지만 보호된 경로로 접근하려고 할 때
if (!appState.isLoggedIn && isProtectedRoute) {
return '/login?redirect=${state.matchedLocation}';
}
// 로그인 되어 있고 로그인 페이지로 가려고 할 때
if (appState.isLoggedIn && state.matchedLocation == '/login') {
return '/';
}
// 리다이렉트 없음
return null;
},
routes: [
// 홈 화면
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
// 로그인 화면
GoRoute(
path: '/login',
builder: (context, state) {
// 로그인 후 리다이렉트할 경로
final redirectUrl = state.queryParameters['redirect'] ?? '/';
return LoginScreen(redirectUrl: redirectUrl);
},
),
// 상품 상세 화면
GoRoute(
path: '/product/:id',
builder: (context, state) {
final productId = state.pathParameters['id']!;
return ProductDetailScreen(productId: productId);
},
),
// 프로필 섹션 (중첩 라우트)
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
routes: [
GoRoute(
path: 'edit',
builder: (context, state) => EditProfileScreen(),
),
GoRoute(
path: 'orders',
builder: (context, state) => OrderHistoryScreen(),
),
],
),
// 설정 화면
GoRoute(
path: '/settings',
builder: (context, state) => SettingsScreen(),
),
],
// 경로를 찾을 수 없을 때의 오류 화면
errorBuilder: (context, state) => NotFoundScreen(),
);
// 메인 앱
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'GoRouter Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerConfig: _router,
);
}
}
// 홈 화면
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('홈')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('홈 화면'),
SizedBox(height: 20),
// 상품 목록
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
final productId = index + 1;
return ListTile(
title: Text('상품 $productId'),
onTap: () => context.go('/product/$productId'),
);
},
),
),
],
),
),
drawer: AppDrawer(),
);
}
}
// 앱 드로어
class AppDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text(
'GoRouter 예제',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
ListTile(
leading: Icon(Icons.home),
title: Text('홈'),
onTap: () {
context.go('/');
Navigator.pop(context); // 드로어 닫기
},
),
ListTile(
leading: Icon(Icons.person),
title: Text('프로필'),
onTap: () {
context.go('/profile');
Navigator.pop(context);
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('설정'),
onTap: () {
context.go('/settings');
Navigator.pop(context);
},
),
Divider(),
if (appState.isLoggedIn)
ListTile(
leading: Icon(Icons.logout),
title: Text('로그아웃'),
onTap: () {
appState.logout();
Navigator.pop(context);
},
)
else
ListTile(
leading: Icon(Icons.login),
title: Text('로그인'),
onTap: () {
context.go('/login');
Navigator.pop(context);
},
),
],
),
);
}
}
// 로그인 화면
class LoginScreen extends StatelessWidget {
final String redirectUrl;
LoginScreen({required this.redirectUrl});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('로그인')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('로그인 화면'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
appState.login();
context.go(redirectUrl);
},
child: Text('로그인'),
),
],
),
),
);
}
}
// 상품 상세 화면
class ProductDetailScreen extends StatelessWidget {
final String productId;
ProductDetailScreen({required this.productId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('상품 상세')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('상품 ID: $productId의 상세 정보'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.go('/'),
child: Text('홈으로 돌아가기'),
),
],
),
),
);
}
}
// 프로필 화면
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('프로필')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('프로필 화면'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.go('/profile/edit'),
child: Text('프로필 수정'),
),
SizedBox(height: 10),
ElevatedButton(
onPressed: () => context.go('/profile/orders'),
child: Text('주문 내역'),
),
],
),
),
);
}
}
// 프로필 수정 화면
class EditProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('프로필 수정')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('프로필 수정 화면'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.pop(),
child: Text('뒤로 가기'),
),
],
),
),
);
}
}
// 주문 내역 화면
class OrderHistoryScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('주문 내역')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('주문 내역 화면'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.pop(),
child: Text('뒤로 가기'),
),
],
),
),
);
}
}
// 설정 화면
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('설정')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('설정 화면'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.go('/'),
child: Text('홈으로 돌아가기'),
),
],
),
),
);
}
}
// 404 화면
class NotFoundScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('페이지를 찾을 수 없음')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('404 - 페이지를 찾을 수 없습니다'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.go('/'),
child: Text('홈으로 돌아가기'),
),
],
),
),
);
}
}

라우터 설정을 별도의 파일로 분리하여 코드를 구조화하세요:

router_config.dart
final GoRouter router = GoRouter(
routes: [
// 라우트 정의
],
);
// main.dart
import 'router_config.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
// ...
);
}
}

문자열 경로 대신 상수를 사용하여 오타를 방지하세요:

route_paths.dart
abstract class RoutePaths {
static const home = '/';
static const login = '/login';
static const product = '/product/:id';
static const productDetails = '/product/';
static const profile = '/profile';
static const settings = '/settings';
}
// 사용 예시
context.go(RoutePaths.productDetails + productId);

경로 매개변수의 타입을 검증하여 잘못된 데이터로 인한 오류를 방지하세요:

GoRoute(
path: '/product/:id',
builder: (context, state) {
// 숫자 ID 검증
final idStr = state.pathParameters['id']!;
final id = int.tryParse(idStr);
if (id == null) {
// 잘못된 ID 형식
return InvalidProductScreen(id: idStr);
}
return ProductDetailScreen(id: id);
},
),

go_router의 디버그 모드를 활성화하여 라우팅 문제를 디버깅하세요:

final GoRouter _router = GoRouter(
debugLogDiagnostics: true, // 라우팅 디버그 로그 활성화
routes: [...],
);

go_router vs 다른 라우팅 라이브러리

Section titled “go_router vs 다른 라우팅 라이브러리”

go_router는 다른 Flutter 라우팅 라이브러리에 비해 몇 가지 장점이 있습니다:

1. go_router vs Navigator 2.0 직접 사용

Section titled “1. go_router vs Navigator 2.0 직접 사용”
  • go_router: 간결한 API, 적은 보일러플레이트 코드, 더 직관적인 사용법
  • Navigator 2.0: 더 많은 유연성, 더 많은 보일러플레이트 코드 필요
  • go_router: 공식 지원, 간단한 설정, 코드 생성 불필요
  • auto_route: 코드 생성 기반, 타입 안전성, 더 많은 설정 필요
  • go_router는 Flutter 팀이 공식 지원하는 네비게이션 라이브러리로, Navigator 2.0의 기능을 더 쉽게 사용할 수 있게 해줍니다.
  • 선언적 라우팅을 통해 앱의 모든 경로를 한 곳에서 정의할 수 있습니다.
  • 중첩 라우팅, 리다이렉트, 오류 처리, 애니메이션 등 고급 기능을 제공합니다.
  • 경로 매개변수쿼리 매개변수를 통해 데이터를 쉽게 전달할 수 있습니다.
  • context.go(), context.push() 등의 직관적인 메서드로 화면 간 이동이 가능합니다.
  • StatefulShellRoute를 사용하여 바텀 네비게이션 바와 같은 탭 기반 UI를 쉽게 구현할 수 있습니다.

go_router는 대부분의 Flutter 앱에서 권장되는 라우팅 솔루션으로, 간단한 앱부터 복잡한 앱까지 효과적으로 네비게이션 관리를 할 수 있게 해줍니다. 다음 섹션에서는 go_router의 고급 기능인 라우트 가드, ShellRoute, 딥 링크 처리에 대해 더 자세히 알아보겠습니다.