Skip to content

Navigator 2.0

Flutter 1.22 버전에서 도입된 Navigator 2.0은 이전의 명령형(imperative) 스타일의 Navigator 1.0을 보완하는 선언적(declarative) 접근 방식의 네비게이션 API입니다. 이 새로운 API는 복잡한 라우팅 시나리오와 웹 애플리케이션에서의 딥 링크 처리를 더 효과적으로 관리할 수 있도록 설계되었습니다.

Navigator 1.0은 간단한 앱에서는 충분했지만, 복잡한 네비게이션 시나리오나 웹 애플리케이션에서는 한계가 있었습니다:

  1. 앱 상태와 URL 동기화 어려움: 웹에서 URL을 기반으로 화면을 관리하기 어려움
  2. 딥 링크 처리의 복잡성: 특정 화면으로 직접 이동하는 딥 링크 구현이 복잡함
  3. 선언적 UI와의 불일치: Flutter의 나머지 부분은 선언적인데 네비게이션만 명령형
  4. 전체 라우팅 상태 관리 부재: 앱의 전체 네비게이션 상태를 한 곳에서 관리하기 어려움

이러한 문제를 해결하기 위해 Navigator 2.0이 도입되었습니다.

Navigator 2.0은 다음과 같은 세 가지 핵심 컴포넌트로 구성됩니다:

Router 위젯은 Navigator 2.0의 최상위 위젯으로, 라우팅 정보를 처리하고 적절한 화면을 표시합니다. 주로 MaterialApp.router 생성자를 통해 사용됩니다.

MaterialApp.router(
routeInformationParser: MyRouteInformationParser(),
routerDelegate: MyRouterDelegate(),
);

RouteInformationParser는 URL 경로와 같은 라우트 정보를 앱의 상태 객체로 변환하는 역할을 합니다. 예를 들어, ‘/users/123’이라는 URL을 UserDetailsState(userId: 123)와 같은 앱 상태 객체로 변환합니다.

class MyRouteInformationParser extends RouteInformationParser<AppState> {
@override
Future<AppState> parseRouteInformation(RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location!);
// 홈 화면
if (uri.pathSegments.isEmpty) {
return HomeState();
}
// 사용자 세부 정보 화면
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'users') {
return UserDetailsState(userId: uri.pathSegments[1]);
}
// 알 수 없는 경로는 Not Found 상태로 처리
return NotFoundState();
}
@override
RouteInformation? restoreRouteInformation(AppState state) {
if (state is HomeState) {
return RouteInformation(location: '/');
}
if (state is UserDetailsState) {
return RouteInformation(location: '/users/${state.userId}');
}
if (state is NotFoundState) {
return RouteInformation(location: '/not-found');
}
return null;
}
}

RouterDelegate는 앱의 상태를 기반으로 Navigator 위젯과 그 안의 페이지 스택을 구성합니다. 앱의 상태가 변경되면 이를 감지하고 화면을 업데이트합니다.

class MyRouterDelegate extends RouterDelegate<AppState>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppState> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
AppState _appState = HomeState();
AppState get appState => _appState;
set appState(AppState value) {
if (_appState == value) return;
_appState = value;
notifyListeners();
}
@override
AppState? get currentConfiguration => _appState;
@override
Widget build(BuildContext context) {
List<Page> pages = [];
// 앱 상태에 따라 페이지 스택 구성
if (_appState is HomeState) {
pages.add(MaterialPage(
key: ValueKey('home'),
child: HomeScreen(
onUserTap: (String userId) {
appState = UserDetailsState(userId: userId);
},
),
));
}
if (_appState is UserDetailsState) {
final userState = _appState as UserDetailsState;
pages.add(MaterialPage(
key: ValueKey('home'),
child: HomeScreen(
onUserTap: (String userId) {
appState = UserDetailsState(userId: userId);
},
),
));
pages.add(MaterialPage(
key: ValueKey('user-${userState.userId}'),
child: UserDetailsScreen(
userId: userState.userId,
onBack: () {
appState = HomeState();
},
),
));
}
if (_appState is NotFoundState) {
pages.add(MaterialPage(
key: ValueKey('not-found'),
child: NotFoundScreen(
onBack: () {
appState = HomeState();
},
),
));
}
return Navigator(
key: navigatorKey,
pages: pages,
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// 뒤로 가기 처리
if (pages.length > 1) {
appState = HomeState();
}
return true;
},
);
}
@override
Future<void> setNewRoutePath(AppState configuration) async {
appState = configuration;
}
}

Navigator 2.0에서는 화면을 Page 위젯으로 표현합니다. Page는 화면에 표시될 내용과 전환 애니메이션을 정의하는 불변(immutable) 객체입니다.

Navigator(
pages: [
MaterialPage(
key: ValueKey('home'),
child: HomeScreen(),
),
MaterialPage(
key: ValueKey('details'),
child: DetailsScreen(),
),
],
onPopPage: (route, result) {
// 뒤로 가기 처리
return route.didPop(result);
},
);

주요 Page 구현체로는 다음과 같은 것들이 있습니다:

  • MaterialPage: Material Design 스타일의 화면 전환
  • CupertinoPage: iOS 스타일의 화면 전환
  • CustomPage: 커스텀 애니메이션을 정의한 화면 전환

2. 선언적 방식의 페이지 스택 관리

Section titled “2. 선언적 방식의 페이지 스택 관리”

Navigator 2.0의 가장 큰 특징은 페이지 스택을 선언적으로 관리한다는 점입니다. 화면의 추가/제거를 명령형 메서드(push, pop)로 처리하는 대신, 전체 페이지 스택을 한 번에 정의합니다.

// 명령형 방식 (Navigator 1.0)
Navigator.push(context, MaterialPageRoute(builder: (_) => DetailsScreen()));
// 선언적 방식 (Navigator 2.0)
// 페이지 스택 전체를 다시 정의
Navigator(
pages: [
MaterialPage(child: HomeScreen()),
if (showDetails) MaterialPage(child: DetailsScreen()),
],
onPopPage: (route, result) => route.didPop(result),
);

선언적 방식의 장점은 앱의 상태와 UI가 항상 동기화된다는 점입니다. 라우팅 상태가 변경되면 전체 페이지 스택이 자동으로 업데이트됩니다.

Navigator 2.0은 브라우저의 히스토리 스택과도 통합됩니다. 이는 웹 앱에서 특히 중요하며, 사용자가 브라우저의 뒤로 가기/앞으로 가기 버튼을 사용할 때 앱의 상태가 적절히 변경되도록 합니다.

// RouterDelegate 내부에서 페이지 스택 관리
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(child: HomeScreen()),
if (_showUserList) MaterialPage(child: UserListScreen()),
if (_selectedUserId != null) MaterialPage(
child: UserDetailsScreen(userId: _selectedUserId!),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
// 뒤로 가기 로직
if (_selectedUserId != null) {
_selectedUserId = null;
return true;
}
if (_showUserList) {
_showUserList = false;
return true;
}
return false;
},
);
}

Navigator 2.0을 사용한 쇼핑 앱의 구현 예제를 살펴보겠습니다:

// 앱 상태 정의
abstract class AppState {}
class HomeState extends AppState {}
class CategoryState extends AppState {
final String categoryId;
CategoryState({required this.categoryId});
}
class ProductDetailsState extends AppState {
final String productId;
ProductDetailsState({required this.productId});
}
class CartState extends AppState {}
class CheckoutState extends AppState {}
// RouteInformationParser 구현
class ShopRouteInformationParser extends RouteInformationParser<AppState> {
@override
Future<AppState> parseRouteInformation(RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location!);
// 홈 화면
if (uri.pathSegments.isEmpty) {
return HomeState();
}
// 카테고리 화면
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'category') {
return CategoryState(categoryId: uri.pathSegments[1]);
}
// 상품 상세 화면
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'product') {
return ProductDetailsState(productId: uri.pathSegments[1]);
}
// 장바구니 화면
if (uri.pathSegments.length == 1 && uri.pathSegments[0] == 'cart') {
return CartState();
}
// 결제 화면
if (uri.pathSegments.length == 1 && uri.pathSegments[0] == 'checkout') {
return CheckoutState();
}
// 알 수 없는 경로는 홈으로
return HomeState();
}
@override
RouteInformation? restoreRouteInformation(AppState state) {
if (state is HomeState) {
return RouteInformation(location: '/');
}
if (state is CategoryState) {
return RouteInformation(location: '/category/${state.categoryId}');
}
if (state is ProductDetailsState) {
return RouteInformation(location: '/product/${state.productId}');
}
if (state is CartState) {
return RouteInformation(location: '/cart');
}
if (state is CheckoutState) {
return RouteInformation(location: '/checkout');
}
return null;
}
}
// RouterDelegate 구현
class ShopRouterDelegate extends RouterDelegate<AppState>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppState> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
AppState _appState = HomeState();
List<AppState> _history = [HomeState()];
AppState get appState => _appState;
void navigateTo(AppState state) {
_appState = state;
_history.add(state);
notifyListeners();
}
bool goBack() {
if (_history.length > 1) {
_history.removeLast();
_appState = _history.last;
notifyListeners();
return true;
}
return false;
}
@override
AppState? get currentConfiguration => _appState;
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('home'),
child: HomeScreen(
onCategorySelected: (categoryId) {
navigateTo(CategoryState(categoryId: categoryId));
},
onProductSelected: (productId) {
navigateTo(ProductDetailsState(productId: productId));
},
onCartTap: () {
navigateTo(CartState());
},
),
),
if (_appState is CategoryState)
MaterialPage(
key: ValueKey('category-${(_appState as CategoryState).categoryId}'),
child: CategoryScreen(
categoryId: (_appState as CategoryState).categoryId,
onProductSelected: (productId) {
navigateTo(ProductDetailsState(productId: productId));
},
),
),
if (_appState is ProductDetailsState)
MaterialPage(
key: ValueKey('product-${(_appState as ProductDetailsState).productId}'),
child: ProductDetailsScreen(
productId: (_appState as ProductDetailsState).productId,
onAddToCart: () {
// 장바구니에 추가 로직
},
onBuyNow: () {
navigateTo(CheckoutState());
},
),
),
if (_appState is CartState)
MaterialPage(
key: ValueKey('cart'),
child: CartScreen(
onCheckout: () {
navigateTo(CheckoutState());
},
),
),
if (_appState is CheckoutState)
MaterialPage(
key: ValueKey('checkout'),
child: CheckoutScreen(
onOrderComplete: () {
// 주문 완료 로직
navigateTo(HomeState());
},
),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
goBack();
return true;
},
);
}
@override
Future<void> setNewRoutePath(AppState configuration) async {
_appState = configuration;
_history = [..._history, configuration];
}
}
// MaterialApp에 Router 통합
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: '쇼핑 앱',
theme: ThemeData(primarySwatch: Colors.blue),
routerDelegate: ShopRouterDelegate(),
routeInformationParser: ShopRouteInformationParser(),
);
}
}

Navigator 2.0은 앱의 상태 관리 시스템과 통합하기 쉽습니다. 예를 들어, Provider나 Riverpod와 같은 상태 관리 라이브러리와 함께 사용할 수 있습니다:

// 라우팅 상태 관리를 위한 ChangeNotifier
class AppRouterState extends ChangeNotifier {
AppState _currentState = HomeState();
AppState get currentState => _currentState;
void navigateTo(AppState state) {
_currentState = state;
notifyListeners();
}
}
// Provider와 RouterDelegate 통합
class MyRouterDelegate extends RouterDelegate<AppState>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppState> {
final AppRouterState appRouterState;
MyRouterDelegate(this.appRouterState) {
appRouterState.addListener(notifyListeners);
}
@override
void dispose() {
appRouterState.removeListener(notifyListeners);
super.dispose();
}
@override
AppState? get currentConfiguration => appRouterState.currentState;
@override
Widget build(BuildContext context) {
// 앱 상태에 따라 페이지 스택 구성
// ...
}
@override
Future<void> setNewRoutePath(AppState configuration) async {
appRouterState.navigateTo(configuration);
}
}
// 메인 앱에서 Provider 사용
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => AppRouterState(),
child: Builder(
builder: (context) {
final appRouterState = Provider.of<AppRouterState>(context);
return MaterialApp.router(
routerDelegate: MyRouterDelegate(appRouterState),
routeInformationParser: MyRouteInformationParser(),
);
},
),
),
);
}
  1. 선언적 API: 앱의 상태와 UI가 항상 동기화됨
  2. 딥 링크 지원 개선: 복잡한 딥 링크 시나리오를 더 쉽게 처리할 수 있음
  3. 웹 통합: 브라우저 히스토리와 URL을 앱 상태와 쉽게 동기화할 수 있음
  4. 상태 기반 라우팅: 전체 앱 상태를 기반으로 라우팅을 관리할 수 있음
  5. 테스트 용이성: 선언적 API는 테스트하기 더 쉬움
  1. 복잡성: Navigator 1.0보다 구현이 더 복잡함
  2. 학습 곡선: 새로운 개념을 학습해야 함
  3. 코드량 증가: 간단한 앱에서는 오히려 코드가 더 복잡해질 수 있음
  4. 라이브러리 성숙도: 아직 관련 생태계가 Navigator 1.0만큼 성숙하지 않음

언제 Navigator 2.0을 사용해야 할까?

Section titled “언제 Navigator 2.0을 사용해야 할까?”

다음과 같은 경우에 Navigator 2.0을 고려해볼 만합니다:

  1. 웹 지원 필요: Flutter 웹 앱에서 URL 관리가 중요한 경우
  2. 복잡한 딥 링크: 다양한 딥 링크 시나리오를 처리해야 하는 경우
  3. 상태 기반 라우팅: 앱 상태와 라우팅이 밀접하게 연결된 경우
  4. 중첩 네비게이션: 복잡한 중첩 네비게이션 구조가 필요한 경우

단순한 앱에서는 Navigator 1.0이 더 적합할 수 있습니다.

Navigator 2.0의 복잡성을 줄이기 위해 Flutter 팀은 go_router 패키지를 개발했습니다. go_router는 Navigator 2.0의 기능을 활용하면서도 더 간단한 API를 제공합니다:

// go_router 사용 예제
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/category/:id',
builder: (context, state) {
final categoryId = state.pathParameters['id'];
return CategoryScreen(categoryId: categoryId!);
},
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final productId = state.pathParameters['id'];
return ProductDetailsScreen(productId: productId!);
},
),
],
);
// 메인 앱에서 사용
MaterialApp.router(
routerConfig: _router,
);
// 화면 전환
context.go('/category/electronics');

go_router는 다음 장에서 더 자세히 다루겠습니다.

  • Navigator 2.0은 Flutter의 선언적 네비게이션 API로, 복잡한 라우팅 시나리오와 웹 애플리케이션에 적합합니다.
  • 주요 구성 요소로는 Router, RouteInformationParser, RouterDelegate가 있습니다.
  • Page 위젯을 사용하여 화면을 정의하고, 전체 페이지 스택을 선언적으로 관리합니다.
  • 앱 상태와 라우팅이 밀접하게 통합되어, 상태 변경 시 UI가 자동으로 업데이트됩니다.
  • 브라우저 히스토리와 쉽게 통합되어 웹 애플리케이션에서 URL 관리가 용이합니다.
  • 복잡성을 줄이기 위해 go_router 같은 고수준 패키지를 사용할 수 있습니다.

Navigator 2.0은 복잡한 네비게이션 요구사항을 가진 앱에서 강력한 도구이지만, 모든 앱에 필요한 것은 아닙니다. 다음 장에서는 Navigator 2.0의 기능을 더 간단하게 사용할 수 있는 go_router 패키지에 대해 자세히 알아보겠습니다.