Flutter Navigation 2.0과 GoRouter 2026 완벽 가이드: 딥링크부터 면접 대비까지
GoRouter 17.5를 활용한 Flutter Navigation 2.0 구현 방법을 상세히 다룹니다. 딥링크 설정, ShellRoute, StatefulShellRoute, 인증 가드, 타입 안전 라우팅까지 프로덕션 앱에 필요한 모든 내용을 포함합니다.

Flutter 내비게이션은 Navigation 2.0 도입 이후 크게 발전했습니다. Flutter 팀 공식 패키지인 GoRouter 17.5는 Router API의 복잡성을 추상화하고, 선언적 라우팅, 자동 딥링크, 중첩 내비게이션을 기본으로 제공합니다.
Flutter 팀은 2026년 기준으로 GoRouter를 기능 완비 상태로 간주합니다. 경로 매개변수, 쿼리 매개변수, 리다이렉트, 영구 UI를 위한 ShellRoute, 탭 상태 유지를 위한 StatefulShellRoute를 지원합니다. 프로덕션 환경에서 raw Navigator 2.0을 직접 구현하는 경우는 거의 없습니다.
Navigator 2.0 아키텍처 이해
Navigator 2.0은 선언적이고 URL 기반의 접근 방식을 Flutter 내비게이션에 도입했습니다. 명령형 push와 pop 호출 대신, 앱 상태가 내비게이션 스택에 표시될 내용을 결정합니다. 이 아키텍처는 세 가지 핵심 클래스에 의존합니다.
- Router: 내비게이션을 조정하는 최상위 위젯
- RouteInformationParser: URL을 앱 상태로 변환
- RouterDelegate: 해당 상태를 기반으로 위젯 트리 구축
OS에서 딥링크가 도착하거나 웹 브라우저에서 URL이 변경되면, 프레임워크가 상태를 업데이트하고 UI가 반응합니다. 문제점: 커스텀 RouterDelegate와 RouteInformationParser를 직접 작성하려면 상당한 보일러플레이트가 필요합니다.
// Raw Navigator 2.0 approach - verbose and rarely used directly
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
AppRoutePath? _currentPath;
AppRoutePath? get currentConfiguration => _currentPath;
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
const MaterialPage(child: HomeScreen()),
if (_currentPath?.isProductPage == true)
MaterialPage(child: ProductScreen(id: _currentPath!.productId!)),
],
onDidRemovePage: (page) {
// Handle page removal
},
);
}
Future<void> setNewRoutePath(AppRoutePath path) async {
_currentPath = path;
notifyListeners();
}
}이러한 장황함이 GoRouter가 존재하는 이유입니다. Navigator 2.0을 래핑하고 보일러플레이트를 처리합니다.
GoRouter 17.5 설정
GoRouter 17.5에서는 라우트 메타데이터 지원과 경로 매개변수에 대한 정규식 제약 조건이 도입되었습니다. 최소 SDK 요구 사항은 Flutter 3.32와 Dart 3.8입니다.
import 'package:go_router/go_router.dart';
final goRouter = GoRouter(
initialLocation: '/',
debugLogDiagnostics: true, // Useful during development
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'products/:id', // Path parameter
name: 'product',
builder: (context, state) {
final productId = state.pathParameters['id']!;
return ProductScreen(productId: productId);
},
),
GoRoute(
path: 'search',
name: 'search',
builder: (context, state) {
// Query parameters: /search?q=flutter&category=books
final query = state.uri.queryParameters['q'] ?? '';
final category = state.uri.queryParameters['category'];
return SearchScreen(query: query, category: category);
},
),
],
),
],
);라우터는 MaterialApp.router와 통합됩니다.
import 'package:flutter/material.dart';
import 'router_config.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: goRouter,
title: 'GoRouter Demo',
);
}
}Android와 iOS 딥링크 설정
딥링크를 통해 외부 URL이 앱 내 특정 화면을 열 수 있습니다. 플랫폼 설정이 완료되면 GoRouter가 자동으로 라우팅을 처리합니다.
Android App Links
Android에서는 AndroidManifest.xml에 intent-filter를 설정하고, 도메인에 Digital Asset Links 파일을 호스팅해야 합니다.
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/products" />
</intent-filter>
</activity>assetlinks.json 파일은 https://example.com/.well-known/assetlinks.json에서 제공되어야 합니다.
iOS Universal Links
iOS의 경우 Xcode에서 Associated Domains capability를 활성화하고, apple-app-site-association 파일을 호스팅해야 합니다.
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.app",
"paths": ["/products/*", "/search"]
}
]
}
}GoRouter 17.5에서는 빈 경로가 있는 Android 콜드 스타트 딥링크가 스키마와 authority를 잃어버리는 중요한 버그가 수정되었습니다. 이로 인해 앱이 실행 중이 아닐 때도 딥링크가 안정적으로 작동합니다.
Android 딥링크는 adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123"로 테스트할 수 있습니다. iOS의 경우 xcrun simctl openurl booted "https://example.com/products/123"을 사용합니다.
영구 내비게이션 UI를 위한 ShellRoute
ShellRoute는 BottomNavigationBar나 Drawer와 같은 영구적인 UI 요소로 자식 라우트를 래핑합니다. 셸은 자식 라우트 간 내비게이션 중에도 계속 표시됩니다.
final goRouter = GoRouter(
initialLocation: '/home',
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: '/home',
name: 'home',
builder: (context, state) => const HomeTab(),
),
GoRoute(
path: '/explore',
name: 'explore',
builder: (context, state) => const ExploreTab(),
),
GoRoute(
path: '/profile',
name: 'profile',
builder: (context, state) => const ProfileTab(),
),
],
),
],
);
// scaffold_with_nav_bar.dart
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _calculateSelectedIndex(context),
onTap: (index) => _onItemTapped(index, context),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.toString();
if (location.startsWith('/explore')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0: context.go('/home');
case 1: context.go('/explore');
case 2: context.go('/profile');
}
}
}GoRouter 17.5에서는 iOS 뒤로 가기 제스처가 활성 서브라우트 대신 전체 ShellRoute를 팝하는 문제가 수정되었습니다.
Flutter 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
탭 상태를 유지하는 StatefulShellRoute
사용자가 탭을 전환하면 ShellRoute는 자식을 다시 빌드합니다. StatefulShellRoute는 각 브랜치의 내비게이션 스택을 유지합니다.
final goRouter = GoRouter(
initialLocation: '/home',
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeTab(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) => DetailsScreen(
id: state.pathParameters['id']!,
),
),
],
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/explore',
builder: (context, state) => const ExploreTab(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileTab(),
),
],
),
],
),
],
);이 설정을 사용하면, 사용자가 /home/details/42로 이동하고 Explore 탭으로 전환한 후 Home으로 돌아와도 여전히 details 화면에 있게 됩니다.
리다이렉트를 통한 라우트 가드
리다이렉트는 인증 검사와 조건부 내비게이션을 처리합니다. redirect 콜백은 각 내비게이션 전에 실행됩니다.
final goRouter = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = AuthService.instance.isLoggedIn;
final isLoggingIn = state.matchedLocation == '/login';
// Redirect to login if not authenticated
if (!isLoggedIn && !isLoggingIn) {
return '/login?redirect=${state.uri}';
}
// Redirect away from login if already authenticated
if (isLoggedIn && isLoggingIn) {
final redirect = state.uri.queryParameters['redirect'];
return redirect ?? '/';
}
return null; // No redirect
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/admin',
redirect: (context, state) {
// Route-level redirect for admin-only access
if (!AuthService.instance.isAdmin) {
return '/';
}
return null;
},
builder: (context, state) => const AdminScreen(),
),
],
);GoRouter 17.5에서는 리다이렉트 콜백 내에서 context.namedLocation()과 같은 컨텍스트 확장 메서드를 사용할 수 있게 되었습니다.
Flutter 내비게이션 면접에서 자주 나오는 질문
면접관은 내비게이션이 아키텍처, 상태 관리, 플랫폼 통합과 관련되기 때문에 이 지식을 자주 테스트합니다. 시니어 후보자를 구별하는 질문들을 소개합니다.
Q: Navigator 2.0이 Navigator 1.0에서 해결하지 못하는 어떤 문제를 해결합니까?
Navigator 1.0은 명령형 내비게이션을 사용합니다: Navigator.push()와 Navigator.pop(). 앱은 내비게이션 상태를 URL로 표현할 수 없어 딥링크와 웹 지원이 어렵습니다. Navigator 2.0은 선언적입니다: 앱 상태가 내비게이션 스택을 결정하고, URL은 일급 시민이며, 딥링크가 자동으로 작동합니다.
Q: context.go()와 context.push()는 언제 사용하나요?
context.go('/path')는 매칭된 라우트까지 현재 내비게이션 스택을 교체합니다. context.push('/path')는 기존 스택 위에 새 라우트를 추가합니다. 최상위 내비게이션(탭 전환, 홈으로 이동)에는 go()를 사용하고, 뒤로 내비게이션을 유지하면서 상세 화면으로 이동할 때는 push()를 사용합니다.
Q: 탭 전환 시 상태를 유지하려면 어떻게 하나요?
StatefulShellRoute.indexedStack()을 사용합니다. 각 브랜치에 대해 별도의 내비게이션 스택을 유지하고 탭 전환 시에도 이를 보존합니다. 이것 없이는 탭을 전환할 때 전체 서브트리가 다시 빌드됩니다.
Q: 라우트 간에 복잡한 객체를 전달하려면 어떻게 하나요?
세 가지 접근 방식이 있습니다. 첫째, 경로 매개변수로 ID를 전달하고 목적지 화면에서 객체를 가져옵니다. 둘째, GoRouter의 extra 매개변수를 사용하여 객체를 직접 전달합니다. 셋째, 상태 관리 솔루션(Riverpod, Bloc)에 객체를 저장하고 목적지에서 접근합니다. 첫 번째 접근 방식은 URL이 공유 가능하게 유지되므로 딥링크에 가장 적합합니다.
// Passing via extra (not deep-linkable)
context.push('/product', extra: productObject);
// Receiving via extra
GoRoute(
path: '/product',
builder: (context, state) {
final product = state.extra as Product?;
return ProductScreen(product: product);
},
),Q: GoRouter 17.5의 onEnter 콜백이란 무엇입니까?
onEnter 콜백은 라우트에 진입할 때 실행되며 현재 라우트 상태와 다음 라우트 상태 모두에 접근할 수 있습니다. 라우트가 완전히 로드되기 전에 로직을 실행할 수 있어 분석이나 조건부 프리로딩에 유용합니다.
코드 생성을 통한 타입 안전 라우팅
GoRouter는 코드 생성을 통한 타입 안전 라우팅을 지원합니다. 라우트 데이터 클래스를 정의하고 build_runner가 내비게이션 코드를 생성하도록 합니다.
import 'package:go_router/go_router.dart';
part 'routes.g.dart';
<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
const HomeRoute();
Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}
<ProductRoute>(path: '/product/:id')
class ProductRoute extends GoRouteData {
final String id;
const ProductRoute({required this.id});
Widget build(BuildContext context, GoRouterState state) =>
ProductScreen(productId: id);
}dart run build_runner build 실행 후 컴파일 타임 안전성으로 내비게이션할 수 있습니다.
// Type-safe navigation
ProductRoute(id: '123').go(context);이를 통해 라우트 경로의 오타가 제거되고 매개변수가 항상 제공됨이 보장됩니다.
프로덕션 앱을 위한 Flutter 내비게이션 마스터하기
- GoRouter 17.5는 딥링크, 중첩 내비게이션 또는 웹 지원이 필요한 Flutter 앱에 권장되는 내비게이션 솔루션입니다
- 영구적인 하단 내비게이션 바에는
ShellRoute를 사용하고, 각 탭의 내비게이션 스택을 유지하려면StatefulShellRoute를 사용합니다 - 플랫폼별 딥링크(Android App Links, iOS Universal Links)는 GoRouter와 별도로 구성합니다. GoRouter는 라우팅 로직을 처리합니다
- 인증 가드는 라우터 수준 또는 개별 라우트 수준에서
redirect콜백을 사용하여 구현합니다 - 면접 시나리오에서는 선언적 모델 설명에 집중합니다: 앱 상태가 UI를 구동하고, URL은 상태에서 파생되며, GoRouter가 변환을 처리합니다
- 코드 생성을 통한 타입 안전 라우트는 오타로 인한 경로 런타임 오류를 제거합니다
- 버전별 기능에 대해서는 Flutter 내비게이션 문서와 GoRouter 변경 로그를 확인하세요
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Flutter 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 23일 업데이트
태그
공유
관련 기사

2026년 Flutter 성능 최적화: Impeller, 리빌드, 모범 사례
2026년에 Flutter 앱을 60 또는 120fps로 안정적으로 유지하는 방법. Impeller, 절제된 위젯 리빌드, RepaintBoundary, DevTools 프로파일링을 다룹니다.

Flutter vs React Native 2026년 비교: 아키텍처와 성능 완벽 분석
Flutter 3.44와 React Native 0.86의 렌더링 아키텍처, 성능 벤치마크, 개발자 경험, 채용 전략을 상세히 비교합니다. 2026년 크로스플랫폼 모바일 개발에서 프레임워크 선택을 위한 가이드를 제공합니다.

Flutter 테스트 완벽 가이드 2026: 위젯 테스트, 통합 테스트 및 면접 베스트 프랙티스
Flutter의 위젯 테스트, 통합 테스트, 골든 테스트, 모킹 전략을 실전 코드와 함께 해설합니다. 2026년 기술 면접에서 자주 출제되는 테스트 패턴과 모범 답안을 제공합니다.