Flutter ShellRoute pop, RouteObserver/RouteAware 트러블슈팅

이은지·2026년 1월 13일

flutter-development

목록 보기
8/8

문제 상황

아래 케이스에서 자유게시판 탭 최신 데이터가 갱신되지 않는 문제가 발생했다.

  • 게시글 작성 완료 후 자유게시판 탭 복귀
  • 게시글 목록 카드 클릭 ➡️ 상세 페이지에서 좋아요/스크랩 후 뒤로가기

원인 분석

현재 라우트 구조는 바텀 네비게이션 영역(Shell)과 네비게이션 위 오버레이(Root)로 분리되어 있다.

ShellRoute (바텀 네비게이션 영역)
 └ /boards/:code                    (TeamBoardPage)

RootNavigator (바텀 네비게이션 위)
 └ /boards/:code/post/new
 └ /boards/:code/posts/:postId
  • 자유게시판 탭: ShellRoute 내부
  • 게시글 상세/작성 페이지: Root Navigator

자유게시판 탭은 ShellRoute 내부에 있는데, 상세/작성 화면은 Root Navigator에 올라가므로:

  • Shell(탭) 입장에서는 “내 스택 위에 올라갔다가 pop되는” 이벤트가 발생하지 않음
  • 즉, 복귀(pop) 이벤트를 Shell에서 구조적으로 감지하기 어려움

결과적으로 RouteObserver/RouteAware 기반의 자동 감지 접근이 깨진 것이다.


해결 방향 검토

1️⃣ Shell/Root 분리 유지 + Event-driven 패턴을 적용한 수동 Refresh

먼저 라우트 계층을 그대로 두고, 상세/작성 복귀 시점에 refresh 신호를 명시적으로 전달하는 방식을 생각했다.

1. 글쓰기 복귀 시 refresh signal 발행

글쓰기 화면으로 이동한 뒤 돌아왔을 때 tick을 증가시켜 “복귀 이벤트”를 신호로 만든다.

onPressed: () async {
  await context.push(
    AppRoutePaths.boardPostWriteLocation(widget.teamCode),
  );
  _refreshTick.value += 1;
}

2. FreeBoardTab에서 signal 구독 → refresh 실행

FreeBoardTab은 ValueListenable<int> 형태의 신호를 받아, tick 변화가 감지되면 refresh를 실행한다.

class FreeBoardTab extends StatefulWidget {
  final ValueListenable<int>? refreshSignal;
}
void _handleRefreshSignal() {
  final signal = widget.refreshSignal;
  if (signal == null) return;
  if (signal.value == _lastRefreshTick) return;

  _lastRefreshTick = signal.value;
  _vm.refresh();
}
  • 장점: 구조 변경이 거의 없어 빠르게 적용 가능
  • 단점: 복귀 지점마다 트리거를 넣어야 해서 화면이 늘수록 브릿지 로직이 누적됨

라우트 구조 변경이 없어 빨리 해결할 수 있다는 장점이 있지만,
게시판 플로우가 확장될수록 수동 refresh 로직을 하나하나 추가해줘야 하기 때문에 유지보수 비용이 커질 가능성이 있다고 판단했다.


2️⃣ Shell 내부로 통합 + 바텀 네비 숨김

게시글 상세/작성 화면을 ShellRoute 내부로 편입하고, 해당 라우트에서는 바텀 네비를 숨기는(fullscreen 전환) 방식으로 전환한다.

  • Shell 스택에서 push/pop이 발생하므로 복귀 이벤트가 자연스럽게 연결
  • RouteAware.didPopNext() 기반 refresh가 정상 동작
  • 수동 refresh signal 의존을 제거

1. 게시글 상세/작성 화면을 ShellRoute 내부로 편입

GoRoute(
  path: AppRoutePaths.board,
  builder: (_, state) {
    final code = state.pathParameters['code']!;
    final tabParam = state.uri.queryParameters['tab'] ?? 'onlywan';
    return TeamBoardPage(
      teamCode: code,
      activeTab: boardTabFromPath(tabParam),
    );
  },
  routes: [
    GoRoute(
      path: AppRoutePaths.boardPostWrite,
      name: AppRouteNames.writingPost,
      builder: (_, state) {
        final code = state.pathParameters['code']!;
        return WritingPostPage(teamCode: code);
      },
    ),
    GoRoute(
      path: AppRoutePaths.boardPostDetail,
      name: AppRouteNames.postDetail,
      builder: (context, state) {
        final teamCode = state.pathParameters['code']!;
        final postId = int.parse(state.pathParameters['postId']!);
        return PostDetailPage(teamCode: teamCode, postId: postId);
      },
    ),
  ],
),

2. bottom nav 숨김 로직 추가

bool _shouldHideBottomNav(GoRouter router) {
  final config = router.routerDelegate.currentConfiguration;
  if (config.matches.isEmpty) return false;
  // 중첩 라우트에서 실제로 활성화된 GoRoute name을 찾는다.
  final name = _findLastGoRouteName(config.matches);
  return name != null && hideBottomNavRouteNames.contains(name);
}

String? _findLastGoRouteName(List<RouteMatchBase> matches) {
  // ShellRoute 등을 통과하므로, 가장 안쪽 GoRoute를 찾기 위해 역순으로 탐색한다.
  for (final match in matches.reversed) {
    final route = match.route;
    if (route is GoRoute) {
      return route.name;
    }
    final nested = match is ShellRouteMatch ? match.matches : null;
    if (nested != null && nested.isNotEmpty) {
      // ShellRoute 내부 매칭이 있으면 재귀적으로 더 내려간다.
      final nestedName = _findLastGoRouteName(nested);
      if (nestedName != null) return nestedName;
    }
  }
  return null;
}

3. 탭 복귀 시 refresh

@override
void didPopNext() {
  if (!mounted) return;
  // 상세/작성 화면에서 뒤로가기(pop)로 돌아오면 목록을 새로고침한다.
  _vm.refresh();
}

왜 Shell 내부로 통합했는지!!

Shell/Root 분리

장점

  • 바텀 네비게이션 유무에 따라, 동일한 방식으로 쉽게 화면을 띄울 수 있음

단점

  • Navigator 스택이 분리돼 복귀 이벤트/상태 동기화가 자연스럽지 않음. RouteAware / RouteObserver는 “구독한 스택” 기준이라, Shell(탭)에서 Root pop을 자동 감지하기 어려움
  • 같은 도메인이더라도 Root/Shell로 분리가 되면서, 뒤로가기 UX가 복잡해질 수 있음

➡️ 로그인/설정 같이 앱 전역 기능에는 적합하지만, 게시판 상세/작성처럼 “커뮤니티 도메인 화면”은 동기화 비용이 커짐.

Shell 내부 통합

장점

  • 같은 Navigator 스택에서 push/pop이 일어나 복귀 시점 처리(didPopNextpush result)가 자연스러움
  • 목록 → 상세 → 뒤로가기 같은 기본 UX가 예측 가능하고 일관됨
  • “게시판 기능은 community board 소유”처럼 도메인 응집도가 높아짐

단점

  • 바텀 네비 숨김 조건/로직 추가 설계 필요

➡️ 커뮤니티 게시판 내 화면 등 특정 도메인에 귀속된 화면에는 적합하다고 생각, 전역적인 온보딩 같은 화면은 Root로 분리하는 혼합 전략이 맞다고 판단


profile
소통하는 개발자가 꿈입니다!

0개의 댓글