GoRouter 기반 Flutter 라우팅 관리하기

이은지·2026년 1월 4일

flutter-development

목록 보기
7/8

이닝로그 프로젝트에 합류하면서 flutter를 다시 쓰게 되었다.
GoRouter를 활용하는 기존 프로젝트 구조도 이해할 겸 앞으로 리팩토링 기준도 잡아볼겸 Flutter Routing 방식에 대해 전체적으로 공부하고, 어떤식으로 활용할지에 대해 정리해보았다.

Flutter의 Routing

사용자가 화면을 이동할 때, 어떤 화면을 보여주고(전환), 뒤로가기를 어떻게 처리할지(히스토리) 관리하는 것

Flutter는 웹처럼 “페이지 전환”이 아니라, 기본적으로 화면(Route)들을 Navigator 스택에 쌓고(push), 빼는(pop) 방식으로 이동이 이루어진다.

  • 현재 화면은 Navigator 스택의 맨 위
  • push: 새 화면을 위에 올림
  • pop: 위 화면을 제거하고 이전 화면으로 돌아감
  • 라우팅 구현 = “이 스택을 어떤 규칙으로 운영할지”를 설계하는 것

Navigator는 Flutter의 기본 네비게이션 엔진이다.

Route(화면) 스택을 관리하는 위젯/시스템

  • push / pop : 화면 전환
  • “뒤로가기” 처리 (Android back)
  • 스택 기반 히스토리 관리
  • 앱 전체에 하나만 있는 게 아니라, 중첩된 Navigator(네비게이터 스택 분리) 구조를 만들 수 있다.

이걸 활용해서 “하단 탭이 유지되는 영역”과 “탭 없이 전체 화면으로 덮는 영역”을 서로 다른 스택으로 분리할 수 있다.


GoRouter

Navigator를 더 편하게 쓰기 위한 라우팅 라이브러리이다.

GoRouter는 내부적으로 Navigator를 사용하며, 우리는 Navigator를 직접 push/pop 하기보다 URL(경로) 기반 규칙으로 화면 전환을 구성한다.

즉, 경로 기반으로 라우트를 선언하고, 이동/파라미터/중첩/리다이렉트를 체계적으로 관리하게 해준다.

GoRouter 장점

  • 라우트 정의를 트리 구조로 표현 가능 (중첩 라우트)
  • /boards/LG/posts/10 처럼 경로 기반 상태를 만들기 쉬움
  • redirect, 인증 가드 같은 흐름 제어가 편함

GoRouter 사용 방법

  1. 라우트 선언하기
GoRoute(
  path: '/boards/:code',
  builder: (context, state) => TeamBoardPage(...),
);
  1. 중첩 라우트 구성하기: Nested Routes
GoRoute(
  path: '/boards/:code',
  routes: [
    GoRoute(path: 'post/new', builder: ...),
    GoRoute(path: 'posts/:postId', builder: ...),
  ],
);
  1. 라우팅 하는 방법 2가지: go vs push
context.go('/path');   // 해당 경로로 이동(메인 이동/탭 전환에 적합)
context.push('/path'); // 현재 화면 위에 새 화면 push(상세/작성처럼 “다녀오는” 흐름에 적합)
  1. BottomNavigationBar 구성하기: ShellRoute
  • BottomNavigationBar(=GNB)를 구현하려면, ShellRoute로 “공통 레이아웃”을 고정하는 방식이 유용하다.
  • ShellRoute는 GoRouter에서 제공하는 “공통 레이아웃을 유지하고, 안쪽 화면(child)만 교체하는” 라우트이다.
  • 하단탭(GNB), 공통 AppBar 같은 항상 유지되는 UI를 감싸는 컨테이너 역할을 한다.
ShellRoute(
  builder: (context, state, child) => MainNavigation(child: child),
  routes: [
    GoRoute(path: '/home', builder: ...),
    GoRoute(path: '/community', builder: ...),
  ],
);

ShellRoute를 사용하면 탭 UI는 고정되고, 라우트에 따라 child 화면만 바뀌므로 탭 기반 UX가 안정적으로 유지된다.


RouteObserver / RouteAware

Flutter에서 화면 이동은 기본적으로 Navigator 스택(Route 스택)에 의해 일어난다.

RouteObserver / RouteAware는 그 스택의 “화면 전환 이벤트”를 관찰/구독하기 위한 도구다.

RouteObserver / RouteAware는 GoRouter 기능이 아니라 Flutter(Navigator)의 기능이며, GoRouter는 자신이 생성한 Navigator에 observer를 “주입”할 수 있도록 설정(observers)을 제공한다.

RouteObserver

  • Navigator에 붙는 관찰자(Observer)
  • push / pop / replace 같은 Route 변경 이벤트를 감지하고, 구독자에게 알려준다.
  • 보통 RouteObserver<PageRoute> 형태로 사용한다.

RouteAware

  • 화면(위젯)이 RouteObserver를 “구독(subscribe)”하면,
  • 아래 생명주기 콜백을 받을 수 있다.
  • didPush() : 내가 화면에 새로 올라옴
  • didPop() : 내가 pop되어 사라짐
  • didPushNext() : 내 위로 다른 화면이 올라옴
  • didPopNext() : 내 위 화면이 pop되어 내가 다시 보이게 됨 (복귀 감지)

즉, RouteAware는 “화면이 다시 활성화되는 타이밍(복귀 시점)”을 잡는 데 특화되어 있다.

예: 글쓰기/수정 페이지 다녀온 뒤 목록 페이지 refresh


프로젝트 라우팅 구조

앱의 화면을 “GNB 없는 전체 플로우”와 “GNB 유지 탭 영역”으로 나누어, 각각 다른 Navigator 스택에서 관리한다.

Root vs Shell로 화면을 분리

  1. Root Navigator에 둔 화면들 (GNB 없음)
  • /splash/onboarding

  1. ShellRoute 내부에 둔 화면들 (GNB 유지)
  • /home/diary/seat/community/mypage
  • /seat_detail 같은 상세
  • /boards/:code 게시판 흐름 전체

(구조 예시) 라우트 트리

Root Navigator
- /splash, /onboarding...

Shell Navigator (ShellRoute)
- /home, /diary, /seat, /community, /mypage
- /boards/:code

RouteObserver + RouteAware 화면 복귀 시점 최신 데이터 갱신

  • 아래처럼 RouteObserver + RouteAware를 붙여, 글쓰기 등에서 되돌아올 때 목록을 새로 페칭하게 했다.
  • 관찰자 분리: 루트/셸용으로 별도 객체를 두고 등록
/// RouteObserver for the root navigator (outside ShellRoute).
final RouteObserver<ModalRoute<void>> rootRouteObserver =
    RouteObserver<ModalRoute<void>>();

/// RouteObserver for the shell navigator (inside ShellRoute).
final RouteObserver<ModalRoute<void>> shellRouteObserver =
    RouteObserver<ModalRoute<void>>();
  • 자유게시판 탭에서 RouteAware 구현: 탭이 보여질 때 사용 중인 네비게이터의 RouteObserver에 구독하고, didPopNext에서 PostListViewModel.refresh()를 호출해 복귀 시 최신 데이터를 다시 불러오는 구조
class _FreeBoardTabState extends State<FreeBoardTab> with RouteAware {
  bool _initialized = false;
  NavigatorObserver? _subscribedObserver;

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (!_initialized) {
      _initialized = true;
      context.read<PostListViewModel>().ensureLoaded();
    }
    _subscribeRouteObserver();
  }

  
  void dispose() {
    final route = ModalRoute.of(context);
    if (route != null && _subscribedObserver != null) {
      if (_subscribedObserver == shellRouteObserver) {
        shellRouteObserver.unsubscribe(this);
      } else if (_subscribedObserver == rootRouteObserver) {
        rootRouteObserver.unsubscribe(this);
      }
    }
    super.dispose();
  }

  
  void didPopNext() {
    if (!mounted) return;
    context.read<PostListViewModel>().refresh();
  }

  void _subscribeRouteObserver() {
    if (_subscribedObserver != null) return;
    final route = ModalRoute.of(context);
    if (route == null) return;

    final observers = route.navigator?.widget.observers ?? const <NavigatorObserver>[];

    if (observers.contains(shellRouteObserver)) {
      shellRouteObserver.subscribe(this, route);
      _subscribedObserver = shellRouteObserver;
    } else if (observers.contains(rootRouteObserver)) {
      rootRouteObserver.subscribe(this, route);
      _subscribedObserver = rootRouteObserver;
    }
  }
}

리팩토링 TODO: 경로 하드코딩을 AppRoutePaths로 통일

  • 지금은 경로가 변경 되면 해당 경로를 이용하는 컴포넌트 코드까지 함께 수정해야 한다. 이 부분에서 "경로가 어디에서 활용되고 있는지 하나하나 찾아야함 + 변경되는 파일 많아짐" 문제를 발견했다.
  • AppRoutePaths에서 “경로 생성 규칙”을 한 곳에서 관리한다.
  • 문자열 오타/불일치 감소 + 이동 코드가 읽기 쉬워진다.

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

0개의 댓글