Deferred Navigation(지연 네비게이션) — 정의, 원리 및 구현

저자: IT Sectr 게시일: 2026-06-10 읽는 시간: 9 분

Deferred Navigation(지연 네비게이션)은 비동기 작업이 완료된 후에 다음 화면으로의 전환이 발생하는 패턴으로, 사용자 작업 순간에 직접 이루어지지 않습니다. Android Developers (2024)에 따르면, 지연 네비게이션은 네비게이션과 데이터 로딩 간의 경합 상태를 방지하고 푸시 알림 및 딥링크에서의 전환 처리를 단순화합니다. 핵심 차이점은 필요한 모든 데이터를 사용할 수 있게 된 후에 경로가 계산된다는 것입니다.

핵심 포인트

  • Deferred Navigation — 비동기 작업 완료 후 전환이 실행되는 지연 네비게이션
  • 직접 네비게이션은 전환을 즉시 처리하고, 지연 네비게이션은 데이터를 기다림
  • 일반적인 시나리오: 인증, 딥링크, 푸시 알림, 설정 로딩
  • Android에서는 Navigation Component와 콜백, StateFlow로 구현
  • iOS에서는 Combine 또는 async/await와 네비게이션 코디네이터 사용

Deferred Navigation이란

Deferred Navigation(지연 네비게이션)은 필요한 모든 데이터를 사용할 수 있을 때까지 네비게이션 결정을 연기하는 아키텍처 패턴입니다. 사용자가 버튼을 누르고 즉시 새 화면으로 이동하는 직접 전환과 달리, 지연 네비게이션은 트리거 이벤트와 실제 전환을 분리하고 그 사이에 비동기 작업을 배치합니다.

아키텍처적으로 Deferred Navigation은 상태 변경에 기반합니다. 버튼 누름이 비동기 프로세스를 시작하고, 그 결과에 대한 구독이 네비게이션을 트리거합니다. 이는 MVVM 또는 MVI 아키텍처의 애플리케이션에서 특히 중요하며, ViewModel이 상태를 관리하고 View(Activity, Fragment, SwiftUI View)가 변경 사항을 구독하여 전환으로 반응합니다. 이 접근 방식은 UI와 네비게이션 로직 간의 직접적인 의존성을 제거합니다.

Google I/O 2023에 따르면, 지연 네비게이션은 네트워크 요청, 인증 확인, 설정 로딩 또는 권한의 결과에 네비게이션이 의존하는 모든 시나리오에서 권장됩니다. 이 패턴은 딥링크를 처리할 때도 필수적이며, 앱이 먼저 실행되고 루트 화면을 로드한 후에야 대상 경로로 이동해야 합니다.

지연 네비게이션이 필요한 경우

Deferred Navigation은 직접 네비게이션이 잘못된 화면 상태나 로딩 오류로 이어지는 시나리오에서 사용됩니다. 지연 네비게이션이 필요한 네 가지 주요 사례를 살펴보겠습니다.

권한 부여 및 인증

사용자가 보호된 콘텐츠를 탭하면 애플리케이션이 먼저 액세스 토큰을 확인해야 합니다. 토큰이 만료된 경우 직접 네비게이션으로 콘텐츠 화면으로 이동하면 빈 화면이나 401 오류가 발생합니다. 지연 네비게이션은 토큰을 확인하고 성공한 경우에만 대상 화면으로 이동합니다. 실패한 경우 로그인 화면으로 리디렉션합니다.

딥링크 처리

외부 링크를 통해 애플리케이션이 열리면 먼저 루트 화면을 로드하고 네비게이션 상태를 복원한 후에야 딥링크 전환을 실행해야 합니다. 루트 컨텍스트 없이 대상 화면으로 직접 네비게이션하면 빈 네비게이션 스택이나 깨진 백 스택과 같은 이상 현상이 발생합니다.

콘텐츠가 포함된 푸시 알림

푸시 알림을 탭할 때 애플리케이션은 닫힘, 백그라운드 또는 활성의 세 가지 상태 중 하나일 수 있습니다. Deferred Navigation은 애플리케이션 상태를 확인하고 필요한 콘텐츠를 로드한 후에만 대상 화면을 표시합니다. iOS에서는 UNNotificationContentExtension을 통해 이 시나리오를 처리할 수 있습니다.

동적 기능 플래그

화면의 기능이 서버의 기능 플래그로 제어되는 경우, 지연 네비게이션은 먼저 설정을 요청하고 그 후에만 화면을 표시할 수 있게 합니다. 기능이 비활성화된 경우 사용자는 빈 화면 대신 대체 콘텐츠 또는 플레이스홀더를 봅니다.

시나리오직접 네비게이션Deferred Navigation
인증만료된 토큰으로 빈 화면로그인으로 리디렉션
딥링크깨진 백 스택올바른 네비게이션 스택
푸시컨텍스트 없이 로딩전환 전 데이터 준비 완료
기능 플래그사용 불가 기능 표시플레이스홀더 또는 대체

Deferred Navigation vs 직접 네비게이션

직접 네비게이션은 이벤트에 대한 응답으로 즉시 전환이 수행되는 전통적인 접근 방식입니다. 사용자가 버튼을 누르면 UI 라우터가 즉시 화면을 전환합니다. 이 접근 방식은 간단하고 예측 가능하지만 서버의 데이터나 조건 확인이 필요한 시나리오에서는 제한적입니다.

Deferred Navigation은 비동기 상태 형태의 레이어를 추가합니다. 사용자 이벤트가 작업을 시작하고, 결과에 대한 구독이 네비게이션을 제어합니다. 이는 코드 복잡성을 증가시키지만 유연성을 제공합니다. 동일한 트리거가 로드된 데이터에 따라 다른 화면으로 이어질 수 있습니다.

두 접근 방식 중 선택은 요구 사항에 따라 다릅니다. 화면 표시에 비동기 데이터가 필요하지 않은 경우 직접 네비게이션을 사용합니다. 화면이 요청, 인증 또는 외부 조건의 결과에 의존하는 경우 지연 네비게이션이 필요합니다. 일부 전환은 직접, 일부는 지연으로 하는 하이브리드 접근 방식이 산업용 애플리케이션에서 가장 일반적인 관행입니다.

Android 구현: Navigation Component와 ViewModel

Android Jetpack은 아키텍처 수준에서 Deferred Navigation을 구현하는 메커니즘을 제공합니다. 핵심 아이디어는 ViewModel이 상태를 관리하고 Activity 또는 Fragment가 변경 사항을 구독하여 NavController를 통해 네비게이션을 트리거하는 것입니다.

StateFlow를 사용한 Deferred Navigation

StateFlow는 Kotlin 코루틴에서 지연 네비게이션에 이상적인 도구입니다. ViewModel이 네비게이션 이벤트로 StateFlow를 업데이트하고 Activity가 이를 관찰하여 전환을 수행합니다. 이벤트가 처리되면 StateFlow가 지워져 반복 네비게이션을 방지합니다.

kotlin
class MainViewModel : ViewModel() {
    private val _navigation = MutableSharedFlow<NavigationEvent>()
    val navigation: SharedFlow<NavigationEvent> = _navigation

    fun onDeepLinkReceived(link: String) {
        viewModelScope.launch {
            val data = resolveDeepLink(link)
            _navigation.emit(NavigationEvent.GoToScreen(data))
        }
    }
}

NavController를 사용한 Deferred Navigation

Activity에서 네비게이션 구독이 ViewModel의 경로로 NavController를 트리거합니다. 화면 회전 시 반복 네비게이션을 방지하기 위해 이벤트를 한 번만 처리하는 NavigationEventWrapper 래퍼가 사용됩니다. Jetpack Navigation 2.7+는 타입 안전 인수 전달을 위해 Safe Args를 지원합니다.

kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val vm: MainViewModel by viewModels()
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            vm.navigation.collect { event ->
                when (event) {
                    is NavigationEvent.GoToScreen ->
                        findNavController(R.id.nav_host)
                            .navigate(event.route)
                }
            }
        }
    }
}

iOS 구현: Coordinator Pattern과 Combine

iOS에는 Android Jetpack과 같은 내장 Navigation Component가 없으므로 개발자는 Combine 또는 async/await와 함께 Coordinator Pattern을 통해 Deferred Navigation을 구현합니다. Coordinator는 화면 스택을 관리하고 로드된 데이터를 기반으로 네비게이션 결정을 내립니다.

Combine을 사용한 Coordinator

Coordinator는 ViewController 간의 네비게이션을 관리하는 객체입니다. Combine과 함께 ViewModel이 PassthroughSubject를 통해 이벤트를 게시하고 Coordinator가 이를 구독하여 전환을 수행합니다. 이 접근 방식은 UI를 네비게이션 로직에서 완전히 분리하고 Apple의 애플리케이션 아키텍처 권장 사항을 따릅니다.

swift
final class AppCoordinator {
    private var cancellables = Set<AnyCancellable>()

    func start(viewModel: MainViewModel) {
        viewModel.$navigationDestination
            .compactMap { $0 }
            .sink { [weak self] destination in
                self?.navigateTo(destination)
            }
            .store(in: &cancellables)
    }
}

async/await를 사용한 Deferred Navigation

Swift 5.5는 구조적 동시성을 도입하여 Combine 없이 async/await를 통한 지연 네비게이션 구현을 가능하게 했습니다. ViewModel이 데이터 로드 후 경로를 반환하는 async 함수를 제공합니다. Coordinator가 Task에서 이 함수를 호출하고 수신된 경로를 기반으로 전환을 수행합니다.

swift
class AuthViewModel: ObservableObject {
    func resolveDeeplink(_ url: URL) async -> AppRoute? {
        guard let token = await AuthService.shared.getValidToken() else { return .login }
        return await DeeplinkRouter.resolve(url, token: token)
    }
}

// In Coordinator:
Task {
    if let route = await viewModel.resolveDeeplink(url) {
        navigateTo(route)
    }
}

일반적인 실수와 모범 사례

Deferred Navigation은 비동기 시나리오 처리를 단순화하지만 상태 관리에 대한 체계적인 접근 방식이 필요합니다. 개발자가 지연 네비게이션을 구현할 때 저지르는 주요 실수를 살펴보겠습니다.

실수: 초기화 완료 전 네비게이션

가장 흔한 실수는 루트 화면이 완전히 초기화되고 NavController 또는 Coordinator가 전환 준비가 되기 전에 Deferred Navigation을 시도하는 것입니다. Android에서는 IllegalStateException이 발생하고 iOS에서는 정의되지 않은 UI 상태가 됩니다. 해결책은 네비게이션을 트리거하기 전에 컴포넌트의 라이프사이클이 STARTED 또는 RESUMED 상태인지 확인하는 것입니다.

실수: 화면으로 돌아갈 때 반복 네비게이션

StateFlow 또는 Subject가 처리 후 이벤트를 지우지 않으면 이전 화면으로 돌아갈 때 사용자가 자동으로 동일한 화면으로 리디렉션될 수 있습니다. Android에서는 replay=0으로 SharedFlow를, iOS에서는 처리 후 CurrentValueSubject를 nil로 사용하여 네비게이션 이벤트가 한 번만 발생하도록 합니다.

모범 사례: 단일 NavController 또는 Coordinator

애플리케이션의 모든 네비게이션에 하나의 중앙 컴포넌트를 사용합니다. 각 Activity, Fragment 또는 ViewController가 자체 네비게이션 컨트롤러를 가지면 애플리케이션의 다른 부분 간의 지연 네비게이션이 혼란스러워집니다. 단일 Coordinator는 네비게이션 시나리오의 디버깅과 테스트를 단순화합니다.

모범 사례: 지연 네비게이션 시나리오 테스트

Deferred Navigation은 비동기 작업이 시간 요소를 도입하기 때문에 직접 네비게이션보다 테스트하기 어렵습니다. Android에서는 TestDispatcher(kotlinx-coroutines-test)를, iOS에서는 XCTestExpectation을 사용하여 데이터 로딩을 시뮬레이션하고 네비게이션이 예상 경로를 따르는지 확인합니다. 각 시나리오의 격리된 테스트를 위해 인증 및 딥링크 서비스를 모킹합니다.

자주 묻는 질문

Deferred Navigation과 Deep Link의 차이점은 무엇인가요?

Deferred Navigation은 모든 비동기 시나리오에 적용할 수 있는 지연 전환 패턴입니다. Deep Link는 지연 네비게이션의 트리거 중 하나이지만 유일한 것은 아닙니다. 인증 및 기능 플래그도 지연 네비게이션을 사용합니다.

지연 네비게이션과 직접 네비게이션을 결합할 수 있나요?

, 대부분의 애플리케이션은 하이브리드 접근 방식을 사용합니다. 비동기 의존성이 없는 제품 목록 화면은 직접 네비게이션을, 데이터 로딩이 있는 상세 화면은 지연 네비게이션을 사용할 수 있습니다. 구분은 각 특정 화면의 아키텍처에 따라 결정됩니다.

지연 네비게이션에서 경합 상태를 피하려면?

사용하세요 Android에서는 replay 없는 SharedFlow를, iOS에서는 버퍼링 없는 combineLatest를. 새 트리거에서 이전 구독을 취소하세요. 이렇게 하면 최신 네비게이션 이벤트만 처리됩니다.

Jetpack Compose는 지연 네비게이션을 지원하나요?

, Compose에서는 ViewModel StateFlow 구독과 LaunchedEffect에서 NavController.navigate 호출을 통해 지연 네비게이션을 구현합니다. Google은 지연 시나리오에 이벤트 기반 모델의 Navigation Compose를 권장합니다.

앱이 최소화된 경우 지연 네비게이션을 어떻게 처리하나요?

연기하세요 앱이 다시 활성화될 때까지 네비게이션을. Android에서는 Lifecycle.State.STARTED를 사용하여 이벤트를 필터링합니다. iOS에서는 Combine 또는 async/await 블록에서 UIApplication.State를 확인합니다.

요약

  • Deferred Navigation — 비동기 작업 완료 후 전환이 발생하는 패턴
  • 주요 시나리오: 인증, 딥링크, 푸시 알림, 기능 플래그
  • StateFlow/SharedFlow(Android)와 Combine/async-await(iOS)가 구현 도구
  • Deferred Navigation은 직접 네비게이션과 달리 비동기 로딩 중 경합 상태를 일으키지 않음
  • Navigation Component(Android)와 Coordinator Pattern(iOS)이 기본 아키텍처
  • 단일 NavController/Coordinator가 일관된 네비게이션에 필수
  • 테스트에는 TestDispatcher와 비동기 서비스 모킹이 필요

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기