popBackStack vs navigateUp
Compose Navigation의 뒤로가기
Compose Navigation을 사용하여 화면 전환을 관리한다면, 백스택(back stack)에 대해 알고 계실 것입니다. 백스택은 유저가 화면을 탐색할 때 쌓이는 스택을 의미하며, 유저가 화면을 이탈한 후 이전 화면으로 돌아가고자 할 때 활용됩니다. 백스택은 앱 내에 여러 개가 존재할 수 있으며, 뒤로가기 동작을 프로그래밍 방식으로 구현하기 위해 NavHostController의 popBackStack() 함수나 navigateUp() 함수를 사용할 수 있습니다. 이 두 함수는 현재 백스택의 화면을 pop하여 뒤로 가기를 수행한다는 점에서 유사하지만, 특정 상황에서는 서로 다른 결과를 초래할 수 있습니다.
Issue
예를 들어, B 화면에서 popBackStack을 호출하는 버튼이 있다고 가정해보겠습니다. 유저가 A 화면에서 B 화면으로 이동한 뒤, B 화면에서 해당 버튼을 빠르게 두 번 이상 누른다면, 유저는 아무것도 표시되지 않는 흰 화면을 보게 될 수 있습니다. 이는 B 화면에서 A 화면으로 전환될 때 트랜지션 애니메이션이 실행되는데, 이 과정에서 popBackStack이 두 번 이상 호출되면서 백스택의 엔트리 전체가 pop되기 때문입니다. 이렇게 되면 백스택을 유지할 방법이 없어집니다.
이 문제를 해결하려면 navigateUp을 대신 사용하거나, popBackStack을 호출하는 버튼 이벤트에 delay를 추가하는 등의 대응이 필요합니다.
Why?
이 문제는 popBackStack 함수의 동작 원리에 의해 발생합니다. popBackStack과 navigateUp은 각각 아래와 같은 특징을 가지고 있습니다.
popBackStack: 백스택 큐를 확인한 뒤, 비어 있지 않다면 내부적으로 해당 백스택의 pop 동작을 수행합니다. 하지만 현재 백스택 상태를 인지하지 못하기 때문에, 위와 같은 상황에서 불안정한 동작을 보일 수 있습니다.
@MainThread
public open fun popBackStack(): Boolean {
return if (backQueue.isEmpty()) {
// 백스택 큐를 확인
false
} else {
// 내부적으로 pop 동작 수행
popBackStack(currentDestination!!.id, true)
}
}navigateUp:popBackStack과 달리 현재 백스택을 인지하고, 스택의 맨 위에 있는 화면만 pop합니다. 이 함수는 빠르게 두 번 호출되더라도 동일한 화면이 스택의 맨 위에 존재하기 때문에 안정적인 동작을 보장합니다. 또한, 현재 화면이 딥링크로 열린 경우navigateUp을 호출하면 이전 앱으로 돌아가지만popBackStack을 호출하면 앱의 백스택으로만 이동하는 차이도 존재합니다.
@MainThread
public open fun navigateUp(): Boolean {
// 백스택이 하나만 존재하는 경우, 딥링크로 인해 실행된 작업인지 체크하고 돌아가려고 시도
if (destinationCountOnBackStack == 1) {
val extras = activity?.intent?.extras
if (extras?.getIntArray(KEY_DEEP_LINK_IDS) != null) {
return tryRelaunchUpToExplicitStack()
} else {
return tryRelaunchUpToGeneratedStack()
}
} else {
// 그렇지 않다면 popBackStack 호출
return popBackStack()
}
}공식 문서에서는 popBackStack이 기기 하단의 네비게이션 영역에 있는 뒤로 가기 버튼과 대응하며, navigateUp은 앱 바의 “<-” 버튼과 대응한다고 설명하고 있습니다. 따라서 상황에 따라 적절한 API를 선택해서 의도치 않은 동작을 방지하는 것이 좋겠습니다.
추가로 Compose Navigation의 내부 동작이 궁금하시다면 아래 링크에서 확인해보실 수 있습니다.

