
Compose Navigation에서 nested graph는 화면이 많아졌을 때 route를 단순히 많이 나열하지 않고, 의미 있는 흐름 단위로 묶는 방법입니다. 로그인, 메인 탭, 상세 흐름처럼 성격이 다른 화면을 나눌 때 유용합니다.
핵심은 화면 묶음의 생명주기와 back stack 경계를 어디까지로 볼 것인가입니다. nested graph는 파일 정리가 아니라 앱 흐름을 나누는 설계 도구입니다.

nested graph가 필요한 신호
- 로그인 전후 화면 흐름이 명확히 다르다
- bottom navigation 탭마다 내부 화면 흐름이 있다
- 설정, 온보딩, 결제처럼 독립된 단계 흐름이 있다
- 특정 화면 묶음에서 ViewModel을 공유해야 한다
- route가 늘어나면서 NavHost가 읽기 어려워졌다
로그인 흐름을 나누는 예
로그인 전에는 splash, login, signup 화면이 있고 로그인 후에는 main, detail, settings 화면이 있다고 해봅니다. 이때 모든 route를 한 곳에 나열하면 인증 상태에 따라 어디로 보내야 하는지 흐름이 흐려집니다.
NavHost(navController, startDestination = "auth") {
navigation(startDestination = "login", route = "auth") {
composable("login") { LoginScreen() }
composable("signup") { SignupScreen() }
}
navigation(startDestination = "home", route = "main") {
composable("home") { HomeScreen() }
composable("detail/{id}") { DetailScreen() }
}
}탭마다 독립적인 흐름이 있으면 graph를 탭 단위로 나누는 편이 자연스럽습니다. 예를 들어 home 탭에는 feed와 detail이 있고, profile 탭에는 profile과 edit가 있을 수 있습니다.
route 이름을 어떻게 잡아야 할까
route는 화면 이름이면서 navigation graph의 계약입니다. 문자열을 아무렇게나 늘리면 나중에 argument, deep link, 테스트에서 흔들리기 쉽습니다. route는 기능 흐름과 화면 목적이 드러나게 잡는 편이 좋습니다.
object Routes {
const val AUTH = "auth"
const val LOGIN = "auth/login"
const val MAIN = "main"
const val HOME = "main/home"
const val FEED_DETAIL = "main/feed/{feedId}"
}실제 프로젝트에서는 sealed class나 type-safe route 접근을 쓸 수도 있습니다. 중요한 것은 route가 화면 이동의 공개 API처럼 관리되어야 한다는 점입니다.
ViewModel scope와 함께 봐야 한다
nested graph를 쓰는 이유 중 하나는 화면 묶음 안에서 공유되는 상태를 다루기 위해서입니다. 다만 ViewModel을 어디에 scope할지는 route 구조와 back stack entry를 함께 봐야 합니다.
로그인 graph에서 back stack을 정리하는 기준
로그인 성공 후 main graph로 이동할 때 auth 화면이 뒤로가기 stack에 남아 있으면 사용자가 뒤로가기로 로그인 화면에 돌아갈 수 있습니다. 그래서 인증 전후 경계에서는 popUpTo 같은 back stack 정리가 필요할 수 있습니다.
navController.navigate("main") {
popUpTo("auth") {
inclusive = true
}
launchSingleTop = true
}이 코드는 예시입니다. 실제 route 이름과 인증 흐름에 맞게 적용해야 합니다. 핵심은 graph를 나눈 뒤에도 back stack 정책을 함께 설계해야 한다는 점입니다.
상세 화면은 어느 graph에 둘까
상세 화면이 home 탭에서만 들어가는 화면인지, 여러 탭에서 공통으로 들어갈 수 있는 화면인지에 따라 위치가 달라질 수 있습니다. home 내부 흐름이면 home graph 안에 두고, 앱 전체 공통 목적지라면 main graph의 공통 destination으로 분리하는 편이 나을 수 있습니다.
- 특정 탭 안에서만 의미가 있다: 해당 tab graph 내부
- 여러 탭에서 공통으로 이동한다: main graph 공통 destination
- 로그인 전에도 접근 가능하다: auth와 main 바깥의 별도 흐름 검토
- deep link 진입이 중요하다: argument와 복원 경로까지 함께 설계
과하게 나누면 생기는 문제
모든 폴더나 기능마다 graph를 만들면 route 이동을 따라가기 어려워질 수 있습니다. nested graph는 화면 흐름이 독립적일 때 쓰는 것이 좋고, 단순히 파일을 나누기 위한 용도라면 별도 함수로 NavGraphBuilder를 정리하는 것만으로 충분할 수 있습니다.
실무 설계 체크리스트
- 이 화면 묶음에 명확한 시작 destination이 있는가
- 뒤로가기 정책이 다른 흐름과 분리되는가
- 공유 ViewModel scope가 필요한가
- deep link로 직접 들어왔을 때 복원 경로가 자연스러운가
- nested graph 없이 함수 분리만으로 충분하지 않은가
파일 분리와 graph 분리를 구분한다
NavHost 코드가 길어졌다고 해서 무조건 nested graph가 필요한 것은 아닙니다. 단순히 코드가 길어진 문제라면 NavGraphBuilder 확장 함수로 파일만 나눠도 됩니다. 흐름 자체의 시작점과 back stack 정책이 달라질 때 graph를 나누는 편이 더 타당합니다.
fun NavGraphBuilder.homeGraph(navController: NavController) {
composable("home") { HomeScreen() }
composable("home/detail/{id}") { DetailScreen() }
}즉, graph 분리는 사용자 흐름의 경계를 나누는 결정이고, 파일 분리는 유지보수를 위한 코드 정리입니다. 둘을 섞어 생각하면 작은 화면 묶음까지 과하게 graph가 늘어날 수 있습니다.
정리
Compose Navigation nested graph는 화면이 많아서 쓰는 기능이 아니라, 앱 흐름의 경계가 생겼을 때 쓰는 구조입니다. 로그인, 탭, 온보딩, 결제처럼 시작과 끝이 있는 흐름이라면 nested graph를 검토할 만합니다.
공식 사용법은 Android Developers Navigation with Compose 문서를 기준으로 확인할 수 있습니다. 상태 보존은 Compose Navigation 상태 보존 글과 함께 보면 좋습니다.