Flutter에서 내비게이션은 어떻게 처리하나요?

질문

Flutter 앱에서 화면 간 내비게이션을 어떻게 구현하나요? 다양한 내비게이션 방법과 패턴에 대해 설명해주세요.

답변

Flutter에서 화면 간 내비게이션(라우팅)은 모바일 앱의 핵심 기능입니다. Flutter는 단순한 화면 전환부터 복잡한 내비게이션 패턴까지 다양한 방법을 제공합니다.

1. 기본 내비게이션

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. 내비게이션 패턴 및 모범 사례

일반적인 내비게이션 패턴

  1. 계층적 내비게이션: 홈 화면에서 상세 화면으로 이동하는 기본 패턴
  2. 탭 기반 내비게이션: 앱의 주요 섹션을 탭으로 구분
  3. 드로어 내비게이션: 사이드 메뉴를 통한 내비게이션
  4. 스택 내비게이션: 화면들이 스택으로 쌓이는 구조

내비게이션 모범 사례

  1. 중앙 집중식 라우팅 구성: 애플리케이션의 모든 라우트를 한 곳에서 관리
  2. 인수 유효성 검사: 라우트로 전달되는 인수의 유효성을 확인
  3. 딥 링크 지원: 앱 외부에서 특정 화면으로 직접 이동할 수 있도록 구성
  4. 불필요한 화면 스택 방지: 사용자가 길게 내비게이트한 후 홈으로 돌아갈 쉬운 방법 제공
  5. 화면 전환 애니메이션의 일관성: 일관된 애니메이션으로 사용자 경험 향상

요약

Flutter에서 내비게이션을 처리하는 주요 방법은 다음과 같습니다:

  1. 기본 내비게이션: Navigator.push(), Navigator.pop() 등의 기본 메서드
  2. 명명된 라우트: routes 맵에 정의하고 Navigator.pushNamed()로 호출
  3. 중첩 내비게이션: 여러 Navigator 인스턴스를 사용한 계층적 내비게이션
  4. 동적 라우팅: onGenerateRoute를 사용한 동적 라우트 처리
  5. 패키지 활용: auto_route, go_router 등의 패키지로 고급 내비게이션 구현
  6. Navigator 2.0: 선언적 API를 사용한 복잡한 내비게이션 패턴 지원

내비게이션 방식을 선택할 때는 앱의 복잡성, 화면 간 관계, 웹 지원 여부 등을 고려하세요. 간단한 앱은 기본 내비게이션이나 명명된 라우트로 충분하지만, 복잡한 앱은 go_router나 Navigator 2.0과 같은 고급 접근 방식이 더 적합할 수 있습니다.

results matching ""

    No results matching ""