INEEDACHACHA

Pub Workspace에서 go_router 기반 네비게이션 시스템 구현 본문

Flutter/전환실록

Pub Workspace에서 go_router 기반 네비게이션 시스템 구현

INEEDACHACHA 2025. 7. 15. 10:48

개요

  • 현재 전환 중인 앱에서 Clean Architecture를 유지하면서 go_router를 사용한 네비게이션 시스템을 구현.
  • 멀티 패키지 구조에서 상호참조 없이 안전한 네비게이션을 제공한다.

스토리

  • 막상 Bottom Navigation Bar 구현 후, 이 것을 어떻게 멀티 패키지에서 쓰지? 하는 고민이 뒤늦게 생겼다.
  • Pub Workspace에서 go_router를 어떻게 쓰는지 예시가 나온 블로그는 없었던 것 같아서 글을 남긴다.

아키텍처 구조

의존성 방향

Apps Layer (****)
├── AppRouter (go_router 설정)
├── NavigationServiceImpl (구현체)
└── DI Container 등록

↓ (DI를 통한 주입)

Feature Layer (home_feature, feed_feature, etc.)
├── NavigationService 인터페이스 사용
└── GetIt.instance<NavigationService>()로 접근

Shared Layer (navigation)
└── NavigationService 추상 인터페이스 정의

파일 구조

📁 apps/****/
├── lib/router/
│   ├── app_router.dart                    # GoRouter 설정
│   └── app_route_path.dart               # 라우트 경로 정의
├── lib/services/
│   └── navigation_service_impl.dart      # NavigationService 구현체
└── lib/di/
    └── app_di.dart                       # DI 설정

📁 packages/shared/navigation/
└── lib/navigation_service.dart           # 추상 인터페이스

📁 packages/feature/home_feature/
└── lib/home_page.dart                    # NavigationService 사용

주요 구현 코드

1. NavigationService 인터페이스

// packages/shared/navigation/lib/navigation_service.dart
abstract interface class NavigationService {
  void goBack();
  void goToHome();
  void goToHealthFeed();
  void goToRecord();
  void goToMyPage();
  void goToHomeDetail();
}

2. 라우트 경로 정의

// apps/****/lib/router/app_route_path.dart
enum RoutePaths {
  home('/home'),
  healthFeed('/health-feed'),
  record('/record'),
  myPage('/my-page'),
  homeDetail('detail');

  final String path;
  const RoutePaths(this.path);
}

3. AppRouter 구현

// apps/****/lib/router/app_router.dart
@singleton
class AppRouter {
  static GoRouter? _router;

  GoRouter get router {
    _router ??= GoRouter(
      initialLocation: RoutePaths.home.path,
      routes: [
        StatefulShellRoute.indexedStack(
          builder: (context, state, shell) => MainTabView(
            currentIndex: shell.currentIndex,
            onTap: (int index) => shell.goBranch(index),
            child: shell,
          ),
          branches: [
            StatefulShellBranch(
              routes: [
                GoRoute(
                  path: RoutePaths.home.path,
                  name: RoutePaths.home.name,
                  builder: (context, state) => const HomePage(),
                  routes: [
                    GoRoute(
                      path: RoutePaths.homeDetail.path,
                      name: RoutePaths.homeDetail.name,
                      builder: (context, state) => const HomeDetailPage(),
                    ),
                  ],
                ),
              ],
            ),
            // mypage, record, feed 탭들 
          ],
        ),
      ],
    );
    return _router!;
  }
}

4. NavigationService 구현체

// apps/****/lib/services/navigation_service_impl.dart
@Singleton(as: NavigationService)
class NavigationServiceImpl implements NavigationService {
  final AppRouter _appRouter;

  NavigationServiceImpl(this._appRouter);

  GoRouter get _router => _appRouter.router;

  @override
  void goToHomeDetail() {
    _router.goNamed(RoutePaths.homeDetail.name);
  }

  @override
  void goToHome() {
    _router.go(RoutePaths.home.path);
  }
}

5. Feature에서의 사용

// packages/feature/home_feature/lib/home_page.dart
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ElevatedButton(
        onPressed: () {
          GetIt.instance<NavigationService>().goToHomeDetail();
        },
        child: Text('Go to Detail'),
      ),
    );
  }
}

주요 특징

장점

  1. 상호참조 방지: Feature 패키지들이 Apps를 직접 참조하지 않음
  2. Clean Architecture 준수: 의존성 역전 원칙 적용
  3. Injectable 활용: 일관된 DI 패턴 사용
  4. 타입 안전성: 컴파일 타임에 의존성 검증
  5. 테스트 용이성: Mock 객체로 쉽게 교체 가능
  6. 라우터 랩핑: 라우터를 구현체로 랩핑해 Router를 구체적으로 모르게 만듦

핵심 원리

  • Service Locator 패턴: GetIt을 통한 전역 서비스 접근
  • Dependency Inversion: Feature → Interface ← Implementation
  • StatefulShellRoute: 탭 상태 유지하면서 네비게이션

설정 방법

1. 의존성 추가

# pubspec.yaml (root)
workspace:
  - packages/shared/navigation  # 추가 필요

# apps/****/pubspec.yaml
dependencies:
  go_router: ^16.0.0
  navigation:

2. DI 설정

// apps/****/lib/main.dart
void runAppWithFlavor(Flavor flavor) async {
  WidgetsFlutterBinding.ensureInitialized();
  await initEnv(flavor);
  await configureDependencies(); // NavigationService 등록
  runApp(const MyApp());
}

3. 코드 생성

flutter packages pub run build_runner build --delete-conflicting-outputs

주의사항

  1. 하위 라우트 경로: 상대 경로로 설정 ('detail' not '/home/detail')
  2. Workspace 등록: 루트 pubspec.yaml에 navigation 패키지 추가 필요
  3. Build Runner: DI 설정 변경 시 반드시 실행
  4. Named Route: enum의 기본 name 속성 활용

확장 방법

새로운 라우트 추가

  1. RoutePaths enum에 경로 추가
  2. AppRouter에 GoRoute 설정
  3. NavigationService에 메서드 추가
  4. NavigationServiceImpl에 구현
  5. Build runner 실행

예시: 설정 페이지 추가

// 1. 경로 추가
enum RoutePaths {
  // 기존 경로들...
  myPageSettings('settings');
}

// 2. 라우트 설정
GoRoute(
  path: RoutePaths.myPageSettings.path,
  name: RoutePaths.myPageSettings.name,
  builder: (context, state) => const MyPageSettingsPage(),
),

// 3. 인터페이스 추가
abstract interface class NavigationService {
  void goToMyPageSettings();
}

// 4. 구현 추가
@override
void goToMyPageSettings() {
  _router.goNamed(RoutePaths.myPageSettings.name);
}

결과

  • 멀티 패키지 환경에서 안전한 네비게이션 구현
  • Clean Architecture 원칙 준수
  • 상호참조 없는 의존성 구조
  • 확장 가능한 네비게이션 시스템