
Navigation3 라이브러리가 새로 발표되었다. Compose를 위한 네비게이션 라이브러리는 이미 존재했는데, 왜 Type-Safe Args가 추가됐을 때처럼 버전업이 아닌 새로운 라이브러리로 발표가 되었을까?
바로 Jetpack Navigation 부터 이어져온 NavController를 통한 네비게이션이 송두리째 바뀌기 때문이다.
기존의 방식은 Host, Graph, Controller, Destination, Route의 다섯가지 핵심 구성요소로 구성되어 있다. Host라는 컨테이너 안에 Graph라는 지도를 구성해서 Controller를 사용해 특정 화면인 Destination으로 가기 위한 Route를 지정하는 방식이었다.
이 중에서 개발자에게 가장 중요한것은 Controller이다.
NavController
The central coordinator for managing navigation between destinations. The controller offers methods for navigating between destinations, handling deep links, managing the back stack, and more.
공식문서의 소개문구에서도 알 수 있다싶이, 화면 전환과 딥링크, 백스택 관리 등 네비게이션에 관련된 모든 것을 총괄하는 컨트롤 타워라고 할 수 있다.
그런데 개발자 입장에서 답답했던 것은, 이 컨트롤 타워에서 다루는 백스택이 '내 소유'가 아니라는 것이다. 개발자가 할 수 있는 것이라고는 NavController에게 어느 쪽으로 갈 것인지 계획을 넘겨주는 것이 전부다.

문제는 이 계획이 수행되는 것의 결과만 알 수 있고 백스택이 어떻게 변경되는지 투명하게 알기가 어려웠다.
내가 겪었던 문제로는 다이얼로그 선택에 따라 다른 화면으로 이동하는 중에 발생했었다.
Dialog Fragment가 setFragmentResult()를 통해 이전 Fragment A에 FragmentResult를 넘겨주면서 popBackStack()으로 닫히고, 받은 결과값에 따라 Fragment A에서 Fragment B로 navigate를 하는 방식이었다.
하지만 결과는 Dialog Fragment가 닫히기만 하고 Fragment B로 이동이 되지 않았다.

넘겨준 두개의 계획 중 하나가 누락된 것이었다. 정확히는 두 계획 수행이 동시에 되면서 백스택에 FragmentA가 올라가자마자 popBackStack에 의해 Dialog까지의 백스택이 모두 pop되어서 생긴 문제였다.
다이얼로그가 사라지는 것에 대한 이벤트를 받아서 처리하는 방식으로 해결을 하긴 했지만, "백스택에 Fragment A를 추가하고 Dialog를 제거한다" 라는 간단한 로직을 구현하기 위해 불필요한 것들이 추가될 수 밖에 없었다.
그리고 이러한 불편함을 나만 느낀 것은 아니었다.
새로운 네비게이션 라이브러리는 백스택을 NavController를 통해 간접적으로 컨트롤하던 Jeckpack Navigation 시절부터의 구조에서 벗어나, 개발자가 직접적으로 네비게이션 스택을 컨트롤할 수 있게 해준다.
val backStack = rememberNavBackStack(RouteA)
NavDisplay(
backStack = backStack,
entryProvider = { key ->
when (key) {
is RouteA -> NavEntry(key) {
DestinationA()
}
is RouteB -> NavEntry(key) {
DestinationB()
}
else -> {
...
}
}
},
...
)
NavHost도 사라지고 그 자리를 NavDisplay라는 새로운 친구가 차지했다.
그리고 무엇보다 인상적인 부분은, 바로 네비게이션 사용을 위해 항상 선언하던 NavController가 사라지고 그 자리를 backStack이라는 NavBackStack이 차지하고 있다는 것이다!
막상 백스택이 손안에 들어오긴 했지만, 어떻게 네비게이션을 수행해야하는 것일까?
사실은 굉장히 간단하다. NavBackStack이 이름은 거창하지만, 실제로는 mutableList와 동일하기 때문이다.
또한, NavDisplay가 백스택 상태를 확인하면서 변화를 반영해주기 때문에 백스택에 RouteA를 추가하면 DestinationA가 보이고, 백스택에서 RouteA를 제거하면 DestinationA가 사라진다.
// RouteA를 추가한다.
backStack.add(RouteA)
...
// 가장 마지막에 존재하는 Dialog를 제거한다.
backStack.remove(backStack.last{ key -> key is DialogRoute})
그렇다! 이젠 "백스택에 Fragment A를 추가하고 Dialog를 제거한다" 는 간단한 로직을 이렇게 간단하게 수행할 수 있다.
오늘은 Navigation 3의 가장 큰 변경점 중 하나인 백스택 관리에 대해 다루어보았다. 현재 Navigation 3 라이브러리는 알파 버전이다. 아직까지 많은 버그가 존재하고 당장 사용하기에는 무리가 있겠지만, 백스택을 이처럼 간편하게 관리할 수 있다는 것만으로도 기대를 하지 않을 수가 없다.
그리고 Compose만을 위한 라이브러리이기 때문에, Compose를 사용해야할 이유가 하나 더 늘었다고 볼 수 있다.
앞으로의 업데이트를 기다리며, Navigation 3의 정식 버전이 빠른 시일 내에 나오길 바란다.