
Flutter에서 go_router를 이용한 웹앱 URL 전략
Flutter에서 웹 애플리케이션을 개발할 때, URL 전략은 사용자 경험에 큰 영향을 미칩니다. 특히 웹 환경에서는 사용자가 URL을 통해 직접 페이지에 접근하거나, 뒤로 가기 버튼을 사용하는 등 네비게이션이 매우 중요합니다. 이번 글에서는 Flutter의 강력한 라우팅 라이브러리인 go_router
를 소개하고, 이를 활용한 효과적인 라우팅 전략을 알아보겠습니다.
go_router 소개
go_router
는 Flutter 팀에서 공식적으로 권장하는 라우팅 솔루션입니다. 기존의 Navigator 2.0
의 복잡성을 해결하면서도, 선언적이고 강력한 라우팅 기능을 제공합니다.
주요 특징
- 선언적 라우트 정의
- 중첩 네비게이션 지원
- URL 파라미터 및 쿼리 파라미터 지원
- 딥링크 지원
- 리다이렉션 규칙 설정
- 에러 핸들링
- 웹 플랫폼 최적화
설치하기
pubspec.yaml
파일에 다음과 같이 의존성을 추가합니다:
dependencies:
go_router: ^14.3.0 # 2024-10 기준 최신 버전입니다.
1. 해시태그 제거하기
Flutter 웹 애플리케이션의 기본 URL 전략은 해시 기반입니다(예: example.com/#/home
). 이는 SEO에 좋지 않고 사용자 경험도 저하시킵니다. usePathUrlStrategy()
를 사용하면 이를 개선할 수 있습니다.
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy(); // 해시태그 제거
runApp(MyApp());
}
2. go_router 기본 설정
2.1 기본 라우터 설정
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
],
);
2.2 Riverpod과 통합
Riverpod과 go_router를 통합하면 앱의 상태에 따른 동적 라우팅을 효과적으로 구현할 수 있습니다. 특히 인증 상태나 사용자 권한에 따른 라우팅 처리가 매우 용이해집니다.
기본 설정
먼저 필요한 provider들을 정의합니다:
// 인증 상태를 관리하는 provider
final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier();
});
// 라우터 provider
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
navigatorKey: rootNavKey,
refreshListenable: authState as Listenable,
initialLocation: '/',
routes: [
// 라우트 설정
],
redirect: (context, state) {
// 리다이렉트 로직
return null;
},
);
});
인증 상태 클래스 구현
class AuthState extends ChangeNotifier {
AuthStatus _status = AuthStatus.initial;
User? _user;
AuthState() {
// 초기 인증 상태 체크
checkAuthStatus();
}
AuthStatus get status => _status;
User? get user => _user;
void setAuthenticated(User user) {
_user = user;
_status = AuthStatus.authenticated;
notifyListeners();
}
void setUnauthenticated() {
_user = null;
_status = AuthStatus.unauthenticated;
notifyListeners();
}
}
라우터 설정과 리다이렉트 로직
final routerProvider = Provider<GoRouter>((ref) {
final authState = ref.watch(authStateProvider);
return GoRouter(
navigatorKey: rootNavKey,
refreshListenable: authState as Listenable,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardScreen(),
),
],
redirect: (context, state) {
final authStatus = authState.status;
final isLoggingIn = state.matchedLocation == '/login';
// 초기 상태일 경우 리다이렉트하지 않음
if (authStatus == AuthStatus.initial) return null;
// 인증되지 않은 사용자가 보호된 경로에 접근할 경우
if (authStatus == AuthStatus.unauthenticated) {
return isLoggingIn ? null : '/login';
}
// 인증된 사용자가 로그인 페이지에 접근할 경우
if (authStatus == AuthStatus.authenticated && isLoggingIn) {
return '/dashboard';
}
return null;
},
);
});
실제 사용 예시
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
routerConfig: router,
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
// 로그인 처리 예시
class LoginScreen extends ConsumerWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
// 로그인 로직 수행
final user = await performLogin();
// 인증 상태 업데이트
ref.read(authStateProvider.notifier).setAuthenticated(user);
// go_router가 자동으로 리다이렉트 처리
},
child: const Text('로그인'),
),
),
);
}
}
이렇게 Riverpod과 통합하면 다음과 같은 이점이 있습니다:
- 상태 기반 라우팅: 앱의 상태 변화에 따라 자동으로 적절한 페이지로 리다이렉트됩니다.
- 코드 구조화: 라우팅 로직을 provider로 분리하여 관리할 수 있습니다.
- 테스트 용이성: provider를 통한 의존성 주입으로 테스트가 쉬워집니다.
- 유지보수성: 인증 관련 로직이 한 곳에서 관리되어 유지보수가 용이합니다.
2.3 URL 파라미터 활용
GoRoute(
path: '/user/:userId/posts/:postId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
final postId = state.pathParameters['postId']!;
return PostDetailScreen(userId: userId, postId: postId);
},
),
2.4 쿼리 파라미터 처리
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'];
final filter = state.uri.queryParameters['filter'];
return SearchScreen(query: query, filter: filter);
},
),
3. 고급 라우팅 기능
3.1 중첩 네비게이션
GoRoute(
path: '/dashboard',
builder: (context, state) => const DashboardScreen(),
routes: [
GoRoute(
path: 'profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsScreen(),
),
],
),
3.2 리다이렉트 설정
redirect: (context, state) {
final authState = ref.read(authStateProvider);
final isLoginPage = state.matchedLocation == '/login';
// 인증이 필요한 경로들
final protectedPaths = ['/dashboard', '/profile', '/settings'];
// 현재 경로가 보호된 경로인지 확인
final isProtectedRoute = protectedPaths.any(
(path) => state.matchedLocation.startsWith(path),
);
if (authState == AuthState.unauthenticated && isProtectedRoute) {
// 로그인 페이지로 리다이렉트하면서 원래 가려던 경로를 쿼리 파라미터로 저장
return '/login?redirect=${state.matchedLocation}';
}
if (authState == AuthState.authenticated && isLoginPage) {
// 이미 인증된 사용자가 로그인 페이지에 접근하면 대시보드로 이동
return '/dashboard';
}
return null;
},
3.3 에러 처리
errorBuilder: (context, state) {
return MaterialPage(
key: state.pageKey,
child: Scaffold(
appBar: AppBar(
title: const Text('오류가 발생했습니다'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
const SizedBox(height: 16),
Text(
'요청하신 페이지를 찾을 수 없습니다:\n${state.error}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.go('/'),
child: const Text('홈으로 돌아가기'),
),
],
),
),
),
);
},
4. 실전 활용 예제
4.1 페이지 전환 애니메이션
CustomTransitionPage<void>(
key: state.pageKey,
child: const DetailsScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
4.2 페이지 히스토리 관리
// 뒤로 가기
context.pop();
// 특정 경로로 이동 (히스토리 추가)
context.push('/details/123');
// 특정 경로로 이동 (히스토리 교체)
context.replace('/new-route');
// 홈으로 이동하고 히스토리 초기화
context.go('/');
5. 성능 최적화
5.1 라우트 캐싱
final router = GoRouter(
routes: routes,
// 페이지 캐싱 설정
restorationScopeId: 'router',
// 캐시할 페이지 수 제한
observers: [
GoRouterObserver(
navigatorObservers: () => [
HeroController(),
],
),
],
);
6. 결론
go_router
는 Flutter 웹 애플리케이션에서 강력하고 유연한 라우팅 솔루션을 제공합니다. URL 전략, 인증 처리, 중첩 네비게이션 등 다양한 시나리오를 효과적으로 처리할 수 있으며, Riverpod과의 통합을 통해 상태 관리도 깔끔하게 할 수 있습니다.
특히 웹 환경에서는 SEO 최적화, 딥링킹, 브라우저 히스토리 관리 등이 중요한데, go_router
를 활용하면 이러한 요구사항들을 효과적으로 구현할 수 있습니다.
더 자세한 정보와 예제는 go_router 공식 문서에서 확인할 수 있습니다.