Flutter에서 내비게이션은 어떻게 처리하나요?
질문
Flutter 앱에서 화면 간 내비게이션을 어떻게 구현하나요? 다양한 내비게이션 방법과 패턴에 대해 설명해주세요.
답변
Flutter에서 화면 간 내비게이션(라우팅)은 모바일 앱의 핵심 기능입니다. Flutter는 단순한 화면 전환부터 복잡한 내비게이션 패턴까지 다양한 방법을 제공합니다.
1. 기본 내비게이션
Navigator 클래스
Flutter의 내비게이션 시스템은 Navigator
클래스를 중심으로 동작합니다. Navigator
는 화면들을 스택으로 관리하며, push(추가)와 pop(제거) 연산을 지원합니다.
// 새 화면으로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
// 현재 화면에서 이전 화면으로 돌아가기
Navigator.pop(context);
명명된 라우트 사용하기
애플리케이션의 메인 위젯에서 라우트를 미리 정의하고 이름으로 호출할 수 있습니다:
// MaterialApp에서 라우트 정의
MaterialApp(
// 초기 라우트
initialRoute: '/',
// 라우트 맵 정의
routes: {
'/': (context) => HomeScreen(),
'/second': (context) => SecondScreen(),
'/third': (context) => ThirdScreen(),
},
);
// 명명된 라우트로 이동
Navigator.pushNamed(context, '/second');
// 명명된 라우트로 이동하며 인자 전달
Navigator.pushNamed(
context,
'/second',
arguments: {'id': 123, 'name': '홍길동'},
);
// 인자 받기
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
라우트 내비게이션 메서드
// 새 화면으로 이동
Navigator.push(context, route);
// 이름으로 새 화면 이동
Navigator.pushNamed(context, routeName);
// 현재 화면을 제거하고 새 화면으로 대체
Navigator.pushReplacement(context, route);
// 이름으로 현재 화면 대체
Navigator.pushReplacementNamed(context, routeName);
// 모든 화면을 제거하고 새 화면으로 시작
Navigator.pushAndRemoveUntil(
context,
route,
(Route<dynamic> route) => false,
);
// 이름으로 모든 화면 제거하고 새 화면으로 시작
Navigator.pushNamedAndRemoveUntil(
context,
routeName,
(Route<dynamic> route) => false,
);
// 특정 화면까지 제거하고 새 화면 추가
Navigator.popUntil(context, ModalRoute.withName('/home'));
// 현재 화면에서 이전 화면으로 돌아가기
Navigator.pop(context);
// 결과값과 함께 이전 화면으로 돌아가기
Navigator.pop(context, '결과값');
2. 화면 간 데이터 전달하기
생성자를 통한 데이터 전달
가장 직접적인 방법은 화면 위젯의 생성자를 통해 데이터를 전달하는 것입니다:
// 데이터 전달
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(id: 1, title: '상품 상세'),
),
);
// 데이터 수신
class DetailScreen extends StatelessWidget {
final int id;
final String title;
DetailScreen({required this.id, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('ID: $id')),
);
}
}
라우트 인자를 통한 데이터 전달
명명된 라우트를 사용할 때 인자를 통해 데이터를 전달할 수 있습니다:
// 데이터 전달
Navigator.pushNamed(
context,
'/detail',
arguments: {'id': 1, 'title': '상품 상세'},
);
// 데이터 수신
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final id = args['id'];
final title = args['title'];
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('ID: $id')),
);
}
}
결과값 받기
다른 화면으로 이동 후 결과값을 받아올 수 있습니다:
// 결과값 요청
Future<void> _navigateAndGetResult() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('선택된 항목: $result')),
);
}
}
// 결과값 반환
class SelectionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('항목 선택')),
body: ListView(
children: [
ListTile(
title: Text('항목 1'),
onTap: () {
Navigator.pop(context, '항목 1');
},
),
ListTile(
title: Text('항목 2'),
onTap: () {
Navigator.pop(context, '항목 2');
},
),
],
),
);
}
}
3. 중첩 내비게이션
다중 Navigator 사용하기
앱에 여러 섹션이 있는 경우(예: 탭 바 또는 드로어), 각 섹션에 독립적인 내비게이션 스택을 가질 수 있습니다:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _selectedIndex = 0;
final List<Widget> _tabs = [
// 각 탭에 독립적인 Navigator 위젯 사용
Navigator(
onGenerateRoute: (settings) {
return MaterialPageRoute(
builder: (context) => HomeTab(),
);
},
),
Navigator(
onGenerateRoute: (settings) {
return MaterialPageRoute(
builder: (context) => ProfileTab(),
);
},
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _tabs[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '홈'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '프로필'),
],
),
);
}
}
4. 고급 라우팅
onGenerateRoute 사용하기
동적 라우트 생성이나 더 복잡한 라우트 로직이 필요할 때는 onGenerateRoute
콜백을 사용할 수 있습니다:
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/') {
return MaterialPageRoute(builder: (context) => HomeScreen());
}
// 동적 라우트 경로 처리 (예: /product/123)
var uri = Uri.parse(settings.name!);
if (uri.pathSegments.length == 2 &&
uri.pathSegments.first == 'product') {
var id = uri.pathSegments[1];
return MaterialPageRoute(
builder: (context) => ProductDetailScreen(id: id),
);
}
// 알 수 없는 라우트
return MaterialPageRoute(
builder: (context) => UnknownScreen(),
);
},
);
// 동적 경로로 이동
Navigator.pushNamed(context, '/product/123');
페이지 전환 애니메이션 커스터마이징
사용자 정의 페이지 전환 애니메이션을 구현할 수 있습니다:
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = Offset(1.0, 0.0);
var end = Offset.zero;
var curve = Curves.ease;
var tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
5. 패키지를 이용한 내비게이션
auto_route
auto_route
패키지는 코드 생성을 통해 타입 안전한 라우팅을 제공합니다:
// pubspec.yaml
// dependencies:
// auto_route: ^7.4.0
// dev_dependencies:
// auto_route_generator: ^7.3.1
// build_runner: ^2.4.6
// app_router.dart
import 'package:auto_route/auto_route.dart';
part 'app_router.gr.dart';
@AutoRouterConfig()
class AppRouter extends _$AppRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: HomeRoute.page, initial: true),
AutoRoute(page: ProfileRoute.page),
AutoRoute(page: SettingsRoute.page),
];
}
// main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _appRouter = AppRouter();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _appRouter.config(),
);
}
}
// 사용 예시
context.router.push(const ProfileRoute(userId: 1));
go_router
Flutter 팀에서 권장하는 go_router
패키지는 선언적 접근 방식과 URL 기반 내비게이션을 제공합니다:
// pubspec.yaml
// dependencies:
// go_router: ^10.1.2
// main.dart
import 'package:go_router/go_router.dart';
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
routes: [
GoRoute(
path: 'profile/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProfileScreen(id: id);
},
),
GoRoute(
path: 'settings',
builder: (context, state) => SettingsScreen(),
),
],
),
],
);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
// 사용 예시
context.go('/profile/123');
context.push('/settings');
6. Navigator 2.0 (페이지 기반 API)
Flutter 1.22부터 도입된 Navigator 2.0은 선언적 API를 제공하며, 복잡한 내비게이션 패턴과 웹 내비게이션을 더 잘 지원합니다:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final RouteDelegate _routeDelegate = MyRouteDelegate();
final RouteInformationParser _routeInformationParser = MyRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: _routeDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
// 경로 정보
class AppRoutePath {
final int? id;
final bool isUnknown;
AppRoutePath.home() : id = null, isUnknown = false;
AppRoutePath.details(this.id) : isUnknown = false;
AppRoutePath.unknown() : id = null, isUnknown = true;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
// 경로 정보 파서
class MyRouteInformationParser extends RouteInformationParser<AppRoutePath> {
@override
Future<AppRoutePath> parseRouteInformation(RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location!);
if (uri.pathSegments.isEmpty) {
return AppRoutePath.home();
}
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'details') {
var id = int.tryParse(uri.pathSegments[1]);
if (id != null) {
return AppRoutePath.details(id);
}
}
return AppRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(AppRoutePath configuration) {
if (configuration.isUnknown) {
return RouteInformation(location: '/404');
}
if (configuration.isHomePage) {
return RouteInformation(location: '/');
}
if (configuration.isDetailsPage) {
return RouteInformation(location: '/details/${configuration.id}');
}
return RouteInformation(location: '/');
}
}
// 라우트 델리게이트
class MyRouteDelegate extends RouterDelegate<AppRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
int? selectedId;
bool show404 = false;
@override
AppRoutePath get currentConfiguration {
if (show404) {
return AppRoutePath.unknown();
}
if (selectedId == null) {
return AppRoutePath.home();
}
return AppRoutePath.details(selectedId!);
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('HomePage'),
child: HomeScreen(
onItemTapped: (id) {
selectedId = id;
notifyListeners();
},
),
),
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else if (selectedId != null)
MaterialPage(
key: ValueKey('DetailsPage-$selectedId'),
child: DetailsScreen(
id: selectedId!,
),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
selectedId = null;
show404 = false;
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(AppRoutePath configuration) async {
if (configuration.isUnknown) {
selectedId = null;
show404 = true;
return;
}
if (configuration.isDetailsPage) {
selectedId = configuration.id;
show404 = false;
return;
}
selectedId = null;
show404 = false;
}
}
7. 내비게이션 패턴 및 모범 사례
일반적인 내비게이션 패턴
- 계층적 내비게이션: 홈 화면에서 상세 화면으로 이동하는 기본 패턴
- 탭 기반 내비게이션: 앱의 주요 섹션을 탭으로 구분
- 드로어 내비게이션: 사이드 메뉴를 통한 내비게이션
- 스택 내비게이션: 화면들이 스택으로 쌓이는 구조
내비게이션 모범 사례
- 중앙 집중식 라우팅 구성: 애플리케이션의 모든 라우트를 한 곳에서 관리
- 인수 유효성 검사: 라우트로 전달되는 인수의 유효성을 확인
- 딥 링크 지원: 앱 외부에서 특정 화면으로 직접 이동할 수 있도록 구성
- 불필요한 화면 스택 방지: 사용자가 길게 내비게이트한 후 홈으로 돌아갈 쉬운 방법 제공
- 화면 전환 애니메이션의 일관성: 일관된 애니메이션으로 사용자 경험 향상
요약
Flutter에서 내비게이션을 처리하는 주요 방법은 다음과 같습니다:
- 기본 내비게이션:
Navigator.push()
,Navigator.pop()
등의 기본 메서드 - 명명된 라우트:
routes
맵에 정의하고Navigator.pushNamed()
로 호출 - 중첩 내비게이션: 여러
Navigator
인스턴스를 사용한 계층적 내비게이션 - 동적 라우팅:
onGenerateRoute
를 사용한 동적 라우트 처리 - 패키지 활용:
auto_route
,go_router
등의 패키지로 고급 내비게이션 구현 - Navigator 2.0: 선언적 API를 사용한 복잡한 내비게이션 패턴 지원
내비게이션 방식을 선택할 때는 앱의 복잡성, 화면 간 관계, 웹 지원 여부 등을 고려하세요. 간단한 앱은 기본 내비게이션이나 명명된 라우트로 충분하지만, 복잡한 앱은 go_router
나 Navigator 2.0과 같은 고급 접근 방식이 더 적합할 수 있습니다.