개요
Flutter 애플리케이션에서 화면 간 전환과 데이터 전달은 핵심적인 기능입니다. 이 가이드에서는 Navigator의 기본 개념부터 Named Routes를 활용한 고급 패턴까지, 실무에서 바로 활용할 수 있는 체계적인 화면 관리 방법을 다룹니다.
목차
- Navigator 기본 개념과 작동 원리
- ModalRoute를 통한 데이터 접근 메커니즘
- Named Routes 구현과 설정 방법
- arguments를 활용한 타입 안전한 데이터 전달
- 실무 적용 사례와 구현 패턴
- 문제 해결과 성능 최적화
Navigator 기본 개념과 작동 원리
Navigator의 스택 구조
Flutter의 Navigator는 스택(Stack) 자료구조를 기반으로 화면을 관리합니다. 새로운 화면은 스택의 최상단에 추가되고, 뒤로가기 동작 시 최상단 화면이 제거됩니다.
// 기본 화면 전환 메서드
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => TargetScreen(),
));
// 현재 화면 제거 (뒤로가기)
Navigator.of(context).pop();
// 현재 화면을 새 화면으로 교체
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => ReplacementScreen(),
));
주요 Navigator 메서드 비교
메서드용도스택 변화사용 사례
push() | 새 화면 추가 | [A] → [A, B] | 상세 페이지 이동 |
pop() | 현재 화면 제거 | [A, B] → [A] | 뒤로가기 |
pushReplacement() | 현재 화면 교체 | [A] → [B] | 로그인 후 홈 이동 |
pushNamedAndRemoveUntil() | 조건부 스택 정리 | [A, B, C] → [D] | 로그아웃 |
ModalRoute를 통한 데이터 접근 메커니즘
ModalRoute의 역할과 중요성
ModalRoute는 현재 화면의 라우트 정보를 담고 있는 클래스로, Named Routes를 통해 전달된 arguments에 접근할 수 있는 유일한 통로입니다.
// ModalRoute를 통한 arguments 접근
final RouteSettings? settings = ModalRoute.of(context)?.settings;
final Object? arguments = settings?.arguments;
// 타입 안전성을 위한 캐스팅
final Map<String, dynamic>? args = arguments as Map<String, dynamic>?;
데이터 접근 시 주의사항
- Null Safety 준수: ?. 연산자를 활용한 null 체크
- 타입 캐스팅: 명시적 타입 변환으로 런타임 오류 방지
- 기본값 설정: ?? 연산자를 활용한 fallback 값 제공
// 안전한 데이터 접근 패턴
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
final String title = arguments?['title'] ?? '기본 제목';
final List<dynamic> items = arguments?['items'] ?? [];
final Function? callback = arguments?['callback'];
Named Routes 구현과 설정 방법
MaterialApp에서의 Routes 설정
Named Routes를 사용하면 라우트 관리가 중앙집중화되어 유지보수성이 크게 향상됩니다.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Navigator Demo',
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/todo': (context) => _buildTodoScreen(context),
'/profile': (context) => _buildProfileScreen(context),
'/settings': (context) => SettingsScreen(),
},
);
}
Widget _buildTodoScreen(BuildContext context) {
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
return TodoScreen(
todos: arguments?['todos'] ?? <Todo>[],
onTodoAdded: arguments?['onTodoAdded'] ?? (_) {},
userId: arguments?['userId'] ?? 'anonymous',
);
}
}
동적 라우트 생성
복잡한 애플리케이션에서는 onGenerateRoute를 활용한 동적 라우트 생성이 유용합니다.
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/todo':
return _createRoute(
_buildTodoScreen(settings.arguments),
settings,
);
case '/profile':
return _createRoute(
_buildProfileScreen(settings.arguments),
settings,
);
default:
return _createRoute(UnknownScreen(), settings);
}
},
)
Route<dynamic> _createRoute(Widget screen, RouteSettings settings) {
return PageRouteBuilder(
settings: settings,
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: animation.drive(
Tween(begin: Offset(1.0, 0.0), end: Offset.zero),
),
child: child,
);
},
);
}
arguments를 활용한 타입 안전한 데이터 전달
기본 데이터 타입 전달
// 화면 이동 시 데이터 전달
Navigator.pushNamed(
context,
'/todo',
arguments: {
'title': 'My Todo List',
'priority': Priority.high,
'createdAt': DateTime.now(),
'tags': ['work', 'urgent'],
},
);
복잡한 객체와 콜백 함수 전달
실무에서는 커스텀 객체나 콜백 함수도 전달해야 하는 경우가 많습니다.
// 복잡한 데이터 구조 전달
Navigator.pushNamed(
context,
'/profile',
arguments: {
'user': User(
id: 'user123',
name: 'John Doe',
email: 'john@example.com',
),
'onProfileUpdated': (User updatedUser) {
setState(() {
currentUser = updatedUser;
});
},
'settings': {
'theme': ThemeMode.dark,
'notifications': true,
'language': 'ko',
},
},
);
타입 안전성 보장을 위한 헬퍼 클래스
class ArgumentsHelper {
static T? getValue<T>(Map<String, dynamic>? arguments, String key, [T? defaultValue]) {
if (arguments == null || !arguments.containsKey(key)) {
return defaultValue;
}
final value = arguments[key];
return value is T ? value : defaultValue;
}
static String getString(Map<String, dynamic>? arguments, String key, [String defaultValue = '']) {
return getValue<String>(arguments, key, defaultValue) ?? defaultValue;
}
static int getInt(Map<String, dynamic>? arguments, String key, [int defaultValue = 0]) {
final value = arguments?[key];
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? defaultValue;
return defaultValue;
}
}
// 사용 예시
Widget _buildProfileScreen(BuildContext context) {
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
return ProfileScreen(
userId: ArgumentsHelper.getString(arguments, 'userId', 'anonymous'),
displayName: ArgumentsHelper.getString(arguments, 'displayName', 'User'),
age: ArgumentsHelper.getInt(arguments, 'age', 0),
onUpdated: ArgumentsHelper.getValue<Function?>(arguments, 'onUpdated'),
);
}
실무 적용 사례와 구현 패턴
사례 1: Todo 애플리케이션의 화면 간 데이터 동기화
실제 Todo 애플리케이션에서 목록 화면과 상세 화면 간의 데이터 동기화를 구현해보겠습니다.
// Todo 모델 클래스
class Todo {
final String id;
final String title;
final String description;
final Priority priority;
final bool isCompleted;
Todo({
required this.id,
required this.title,
this.description = '',
this.priority = Priority.medium,
this.isCompleted = false,
});
Todo copyWith({
String? title,
String? description,
Priority? priority,
bool? isCompleted,
}) {
return Todo(
id: id,
title: title ?? this.title,
description: description ?? this.description,
priority: priority ?? this.priority,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
// 메인 화면에서 Todo 목록 화면으로 이동
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<Todo> todos = [];
void _navigateToTodoList() async {
final updatedTodos = await Navigator.pushNamed(
context,
'/todoList',
arguments: {
'todos': List<Todo>.from(todos),
'onTodoAdded': _addTodo,
'onTodoUpdated': _updateTodo,
'onTodoDeleted': _deleteTodo,
},
);
if (updatedTodos != null && updatedTodos is List<Todo>) {
setState(() {
todos = updatedTodos;
});
}
}
void _addTodo(Todo todo) {
setState(() {
todos.add(todo);
});
}
void _updateTodo(Todo updatedTodo) {
setState(() {
final index = todos.indexWhere((t) => t.id == updatedTodo.id);
if (index != -1) {
todos[index] = updatedTodo;
}
});
}
void _deleteTodo(String todoId) {
setState(() {
todos.removeWhere((todo) => todo.id == todoId);
});
}
}
사례 2: 사용자 프로필 관리 시스템
복잡한 사용자 프로필 데이터를 안전하게 전달하고 수정하는 패턴입니다.
// 프로필 데이터 모델
class UserProfile {
final String id;
final String name;
final String email;
final String phone;
final int age;
final String bio;
final String profileImageUrl;
final Map<String, dynamic> preferences;
UserProfile({
required this.id,
required this.name,
required this.email,
this.phone = '',
this.age = 0,
this.bio = '',
this.profileImageUrl = '',
this.preferences = const {},
});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'email': email,
'phone': phone,
'age': age,
'bio': bio,
'profileImageUrl': profileImageUrl,
'preferences': preferences,
};
}
factory UserProfile.fromMap(Map<String, dynamic> map) {
return UserProfile(
id: map['id'] ?? '',
name: map['name'] ?? '',
email: map['email'] ?? '',
phone: map['phone'] ?? '',
age: map['age'] ?? 0,
bio: map['bio'] ?? '',
profileImageUrl: map['profileImageUrl'] ?? '',
preferences: Map<String, dynamic>.from(map['preferences'] ?? {}),
);
}
}
// 프로필 화면으로 안전한 데이터 전달
void _navigateToProfile(UserProfile profile) {
Navigator.pushNamed(
context,
'/profile',
arguments: {
'mode': ProfileMode.edit,
'profileData': profile.toMap(),
'onProfileSaved': (Map<String, dynamic> updatedData) {
final updatedProfile = UserProfile.fromMap(updatedData);
_updateUserProfile(updatedProfile);
},
'validationRules': {
'name': (String value) => value.length >= 2,
'email': (String value) => RegExp(r'^[\w-\.]+@[\w-\.]+\.\w+$').hasMatch(value),
'age': (int value) => value >= 0 && value <= 150,
},
},
);
}
문제 해결과 성능 최적화
일반적인 문제와 해결책
1. Arguments가 null인 경우
// 문제: Hot Restart 후 직접 접근 시 null 발생
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>?;
// 해결책: 적절한 기본값과 null 체크
final arguments = ModalRoute.of(context)?.settings.arguments
as Map<String, dynamic>? ?? <String, dynamic>{};
// 또는 조건부 렌더링
if (arguments.isEmpty) {
return Scaffold(
body: Center(
child: Text('잘못된 접근입니다.'),
),
);
}
2. 타입 캐스팅 오류
// 문제: 타입이 맞지 않아 런타임 오류 발생
final List<Todo> todos = arguments['todos']; // 위험
// 해결책: 안전한 타입 체크와 변환
final dynamic todosData = arguments['todos'];
final List<Todo> todos = todosData is List<Todo>
? todosData
: <Todo>[];
// 또는 헬퍼 함수 사용
List<T> safeListCast<T>(dynamic value) {
if (value is List<T>) return value;
if (value is List) return value.cast<T>();
return <T>[];
}
3. 메모리 누수 방지
// 문제: 큰 객체나 콜백 함수가 메모리에 남아있음
class TodoListScreen extends StatefulWidget {
final Function(Todo) onTodoAdded;
@override
void dispose() {
// 콜백 함수나 컨트롤러 정리
super.dispose();
}
}
// 해결책: WeakReference 사용하거나 적절한 dispose 처리
성능 최적화 기법
1. 라우트 지연 로딩
// 큰 화면은 지연 로딩으로 초기 로딩 시간 단축
routes: {
'/heavy-screen': (context) {
return FutureBuilder<Widget>(
future: _loadHeavyScreen(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
}
return LoadingScreen();
},
);
},
}
Future<Widget> _loadHeavyScreen() async {
// 필요한 데이터 로딩
await Future.delayed(Duration(milliseconds: 100));
return HeavyScreen();
}
2. Arguments 직렬화 최적화
// 큰 데이터는 직렬화하여 전달
class LargeDataTransfer {
static String serialize(Map<String, dynamic> data) {
return jsonEncode(data);
}
static Map<String, dynamic> deserialize(String data) {
return jsonDecode(data);
}
}
// 사용
Navigator.pushNamed(
context,
'/target',
arguments: {
'serializedData': LargeDataTransfer.serialize(largeData),
},
);
결론 및 다음 단계
Flutter Navigator와 Named Routes를 활용한 화면 관리는 애플리케이션의 사용자 경험과 코드 품질에 직접적인 영향을 미칩니다.
핵심 포인트 요약
- 타입 안전성: 항상 null 체크와 타입 캐스팅을 수행
- 중앙집중식 관리: Named Routes로 라우트 관리를 체계화
- 데이터 무결성: arguments를 통한 안전한 데이터 전달
- 성능 고려: 메모리 관리와 지연 로딩 활용
다음 학습 단계
- 고급 내비게이션 패턴: PopScope, WillPopScope 활용
- 상태 관리 통합: Provider, Bloc과의 연동
- 딥 링킹: URL 기반 내비게이션 구현
- 애니메이션: 커스텀 페이지 전환 효과
질문과 답변
이 포스트가 도움이 되셨나요?
기술 토론 참여
댓글로 의견을 나누어주세요:
- 이 구현 방식에 대한 개선점이나 대안이 있으시면 공유해주세요
- 실제 프로덕션 환경에서 적용해보신 경험담을 들려주세요
- 성능이나 메모리 관련 추가 고려사항이 있다면 알려주세요
지식 공유
커뮤니티와 함께 성장해요:
- Navigator 2.0과의 비교 경험이나 마이그레이션 팁
- 다른 플랫폼(React Native, Native)과의 차이점
- 대규모 프로젝트에서의 라우팅 아키텍처 설계 방법
다음 주제 제안
어떤 Flutter 기술 주제를 다뤄드릴까요?
- Provider와 Bloc을 활용한 상태 관리 심화
- Flutter 애니메이션 완전 가이드: 기초부터 고급 기법까지
- 로컬 데이터베이스 완전 정복: SQLite, Hive, Isar 비교 분석
- Flutter 성능 최적화: 메모리 관리와 렌더링 최적화
가장 궁금한 주제를 댓글로 알려주세요!
참고 자료
관련 포스트
- Flutter 상태 관리 패턴 비교 분석
- Widget 생명주기와 성능 최적화
- Flutter 앱 아키텍처 설계 가이드
태그: Flutter, Navigator, Named Routes, 화면전환, 모바일앱개발, 타입안전성, 에러처리, 성능최적화, 모바일개발, 실무패턴
'📚 학습 기록 > Dart & Flutter 기초' 카테고리의 다른 글
Flutter 핵심 위젯 선택 가이드: Padding vs Container, Navigator 활용법, ScaffoldMessenger 실무 적용법 (2) | 2025.07.10 |
---|---|
Flutter TodoList 앱 완전 가이드: 편집 기능 구현을 통한 5가지 핵심 개념 마스터 (3) | 2025.07.10 |
Flutter SharedPreferences 완전 정복: 초보자도 쉽게 따라하는 로컬 저장소 사용법 (4) | 2025.07.07 |
Flutter 데이터 그룹핑과 동적 위젯 생성: Map 자료구조를 활용한 확장 가능한 아키텍처 설계 (5) | 2025.07.05 |
🎲 코드팩토리 강의로 Flutter 프로젝트 만들기! (손코딩 후기) (3) | 2025.06.24 |