서론
안드로이드 개발에서 화면 간 이동(Navigation)은 중요한 요소입니다. Jetpack Navigation은 화면 이동을 간편하게 구현할 수 있도록 도와주는 라이브러리이며, 기존의 XML 방식에서 Compose에서는 코드 기반으로 내비게이션을 구성하도록 변화했습니다.
이번 글에서는 XML에서 Compose로 넘어오면서 변화된 Navigation 방식을 정리하였습니다.
이 문서를 읽으면

위의 사진에서 볼 수 있듯이, 내비게이션은 우리에게 너무나 친숙합니다.
그렇다면 안드로이드에서 내비게이션은 무엇일까요??
안드로이드에서도 우리가 알고 있는 내비게이션과 유사한 역할을 합니다. 사용자가 앱에서 특정 화면(A)에서 다른 화면(B)로 이동할 수 있도록 도와주는 시스템입니다. 이러한 내비게이션을 쉽게 사용하기 위해서 안드로이드에서는 Jetpack Navigation을 지원하고 있습니다.
Navigation 라이브러리에서 소개하는 컴포넌트는 아래와 같습니다.
이제 XML 기반과 Compose 기반의 Navigation 방식을 비교해 보겠습니다.
Compose 이전의 Navigation을 사용하는 방식은 XML을 사용하여 내비게이션 그래프(NavGraph)를 구축했습니다. 해당 방식은 res/navigation/nav_graph.xml 파일을 생성하여 앱의 화면 구조를 정의했습니다.
미리 정의한 XML로 동작하기 때문에 정적이고 Type Safe 했습니다.
Type Safe의 의미
코드에서 내비게이션의 경로를 지정할 때, XML에 정의된 경로를 사용할 수 있고, NavController가 잘못된 경로를 지정하면 컴파일 에러를 발생시킨다는 의미입니다.
즉, NavController.navigate(R.id.detailFragment) 처럼 정확하게 리소스 ID를 사용하여 처리할 수 있습니다.
먼저 res/navigation/nav_graph.xml 파일을 생성하여 앱의 화면 구조를 정의합니다.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/homeFragment">
<!-- 홈 화면 -->
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.ui.HomeFragment"
android:label="Home" />
<!-- 상세 화면 -->
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.ui.DetailFragment"
android:label="Detail">
<!-- 홈 화면에서 상세 화면으로 이동하는 액션 -->
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment" />
</fragment>
</navigation>
내비게이션의 출발점이 되는 NavHostFramgent를 정의합니다.
activity_main.xml에서 NavHostFragment를 정의하여 내비게이션을 구성합니다.
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
HomeFragment에서 버튼 클릭 시 NavController를 사용하여 상세 화면으로 이동할 수 있습니다.
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController()
// 버튼 클릭 시 detailFragment로 이동
view.findViewById<Button>(R.id.button_navigate).setOnClickListener {
navController.navigate(R.id.action_home_to_detail)
}
}
}
Compose에서는 내비게이션 방식이 XML에서 Kotlin DSL을 사용하도록 전환되었습니다.NavHost와 NavController를 사용하여 화면을 정의하고, 컴포저블을 통해 개별 화면을 연결합니다.
아래는 Compose Navigation 2.8.0 이전에서 내비게이션을 사용하는 방법입니다.
@Composable
fun NavigationModule(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home"){
composable("home") {
HomeScreen(navController)
}
composable("detail") {
DetailScreen()
}
}
}
NavController는 앱 전반적인 화면의 상태를 관리하는 역할을 합니다.
따라서 각 화면 단위에서 Controller를 만드는 것이 아닌, 상태호이스팅 기법을 적용하여 NavigationModule 과 같은 최상위 컴포저블 함수에서 생성하여 각 화면으로 전달합니다. 또한 리컴포지션에서도 상태를 기억하기 위해 rememberNavController()를 통해서 생성합니다.
❌ 잘못된 예시 ( 각 스크린에서 컨트롤러 만들기 )
@Composable
fun HomeScreen() {
val navController = rememberNavController()
Button(onClick = { navController.navigate("detail") }) {
Text("Go to Detail")
}
}
❌ 잘못된 예시 ( rememberNavController 미사용 )
@Composable
fun NavigationModule() {
val navController = NavController(LocalContext.current)
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(navController)
}
composable("detail") {
DetailScreen()
}
}
}
정리하면,
rememberNavController()를 사용하여 객체를 생성한다.HomeScreen에서 상세 화면으로 이동할 때, NavController를 사용하여 이동합니다.
@Composable
fun HomeScreen(navController: NavController) {
Button(onClick = { navController.navigate("detail") }) {
Text("Go to Detail")
}
}
Navigation 2.8.0 이전에서는 기존 방식(XML)이 정적으로 그래프를 그리는 방식과 달리, 런타임 방식으로 생성하여 그래프를 동적으로 그릴 수 있게되었습니다.
하지만, String을 사용하여 화면을 식별하고 있습니다. 이러한 방식은 타입 안정성이 부족하고, 경로를 잘못 입력하는 경우 런타임 오류가 발생할 수 있다는 단점이 있습니다.
Compose Navigation 2.8.0 이후에는 Type Safety를 보장하여 컴파일 타임에도 신뢰할 수 있게 되었습니다.
그렇다면 기존의 String으로 화면을 식별하던 방식이 어떻게 변경되었을까요?
바로 Kotlin Serialization을 도입하여 탐색 경로를 Type Safety하게 정의할 수 있게되었습니다.
// 인수를 받지 않는 목적지를 정의함
@Serializable
object Home
// id값을 인수로 갖는 목적지를 정의함
@Serializable
data class Detail(val id: String)
2.8.0 이전에서는 화면을 식별하기 위해 String을 사용했지만, 2.8.0 이후부터는 Serializable객체를 활용하여 컴파일 타입에 오류를 잡아줄 수 있습니다.
@Composable
fun NavigationModule(navController: NavHostController) {
NavHost(navController = navController, startDestination = Home) {
composable<Home> { HomeScreen(navController) }
composable<Detail> { backStackEntry ->
val detail = backStackEntry.arguments?.getParcelable<Detail>("detail")
DetailScreen(navController, detail)
}
}
}
NavHost를 설정할 때도, composable <Home,Detail>과 같이 사용하여 안전하게 등록할 수 있습니다.
Jetpack Navigation은 화면을 이동하기 위해서 편리하게 관리할 수 있도록 도와주는 라이브러리입니다. 해당 라이브러리가 기존 XML에서는 컴파일 타임 방식으로 동작했습니다. 선언형 UI 방식인 Compose가 도입되면서 Kotlin DSL을 사용하여 런타임 방식으로 동작하도록 변경되었습니다. 하지만 Type Safety하지 않다는 문제점이 발견되어 버전 2.8.0 이후 Kotlin Serializable을 적용하여 해당 문제를 해결했습니다.
Navigation을 도입할 때, 프로젝트의 환경에 맞게 사용하는데 도움이 되었으면 좋겠습니다. 감사합니다 :)
https://developer.android.com/jetpack/androidx/releases/navigation?hl=ko#2.8.0
https://www.youtube.com/watch?v=AIC_OFQ1r3k