Jetpack Navigation 제대로 사용하기

kkosang·2025년 3월 1일

들어가며

서론

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

이번 글에서는 XML에서 Compose로 넘어오면서 변화된 Navigation 방식을 정리하였습니다.

이 문서를 읽으면

  • Navigation의 개념과 컴포넌트를 이해할 수 있습니다.
  • XML 기반 Jetpack Navigation과 Compose Navigation의 차이를 비교할 수 있습니다.

Navigation이 무엇인가요?

위의 사진에서 볼 수 있듯이, 내비게이션은 우리에게 너무나 친숙합니다.

그렇다면 안드로이드에서 내비게이션은 무엇일까요??

안드로이드에서도 우리가 알고 있는 내비게이션과 유사한 역할을 합니다. 사용자가 앱에서 특정 화면(A)에서 다른 화면(B)로 이동할 수 있도록 도와주는 시스템입니다. 이러한 내비게이션을 쉽게 사용하기 위해서 안드로이드에서는 Jetpack Navigation을 지원하고 있습니다.

Jetpack Navigation의 핵심 컴포넌트

Navigation 라이브러리에서 소개하는 컴포넌트는 아래와 같습니다.

  • Host : 내비게이션의 출발점이 되는 UI 컨테이너로, 목적지들을 포함하고 있습니다. XML에서는 NavHostFragment를 Compose에서는 NavHost 클래스를 사용하고 있습니다.
  • Graph : 앱 내의 모든 목적지(화면)가 서로 연결되는 방식을 정의하는 데이터 구조입니다. NavGraph 클래스를 통해 구현되어 있습니다.
  • Controller : 화면의 상태를 관리하고 새로운 화면으로 이동하는 역할을 합니다. 화면 간 이동, 딥 링크, 백 스택 관리 등의 작업을 할 수 있습니다.
  • Destination : NavGraph 내에서 각 화면을 의미하는 노드입니다. 사용자가 특정 노드로 이동하면 호스트가 해당 화면을 표시합니다.
  • Route : 목적지와 목적지에 필요한 데이터를 고유하게 식별하며 Route를 따라 목적지로 이동합니다.

이제 XML 기반과 Compose 기반의 Navigation 방식을 비교해 보겠습니다.

Compose 이전의 Navigation (XML : 대과거)

Compose 이전의 Navigation을 사용하는 방식은 XML을 사용하여 내비게이션 그래프(NavGraph)를 구축했습니다. 해당 방식은 res/navigation/nav_graph.xml 파일을 생성하여 앱의 화면 구조를 정의했습니다.

미리 정의한 XML로 동작하기 때문에 정적이고 Type Safe 했습니다.

Type Safe의 의미
코드에서 내비게이션의 경로를 지정할 때, XML에 정의된 경로를 사용할 수 있고, NavController가 잘못된 경로를 지정하면 컴파일 에러를 발생시킨다는 의미입니다.
즉, NavController.navigate(R.id.detailFragment) 처럼 정확하게 리소스 ID를 사용하여 처리할 수 있습니다.

사용 방법

  1. 내비게이션 그래프 구축하기

먼저 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>
  1. NavHostFragment

내비게이션의 출발점이 되는 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" />
  1. NavController 사용하기

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 Navigation (ver 2.8.0 이전 : 과거)

Compose에서는 내비게이션 방식이 XML에서 Kotlin DSL을 사용하도록 전환되었습니다.NavHost와 NavController를 사용하여 화면을 정의하고, 컴포저블을 통해 개별 화면을 연결합니다.

아래는 Compose Navigation 2.8.0 이전에서 내비게이션을 사용하는 방법입니다.

사용 방법

  1. NavController와 NavHost를 설정하기
@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() 
	      }
    }
}      
            

정리하면,

  • NavController는 앱 전반적인 화면의 상태를 관리하는 객체이므로 상태 호이스팅을 적용한다.
  • 리컴포지션 시, 상태를 기억하기 위해 rememberNavController()를 사용하여 객체를 생성한다.
  1. 화면 이동 구현

HomeScreen에서 상세 화면으로 이동할 때, NavController를 사용하여 이동합니다.

@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = { navController.navigate("detail") }) {
        Text("Go to Detail")
    }
}

Navigation 2.8.0 이전에서는 기존 방식(XML)이 정적으로 그래프를 그리는 방식과 달리, 런타임 방식으로 생성하여 그래프를 동적으로 그릴 수 있게되었습니다.

하지만, String을 사용하여 화면을 식별하고 있습니다. 이러한 방식은 타입 안정성이 부족하고, 경로를 잘못 입력하는 경우 런타임 오류가 발생할 수 있다는 단점이 있습니다.

Compose Navigation (ver 2.8.0 이후 : 현재)

Compose Navigation 2.8.0 이후에는 Type Safety를 보장하여 컴파일 타임에도 신뢰할 수 있게 되었습니다.

그렇다면 기존의 String으로 화면을 식별하던 방식이 어떻게 변경되었을까요?

바로 Kotlin Serialization을 도입하여 탐색 경로를 Type Safety하게 정의할 수 있게되었습니다.

사용방법

  1. Serializable을 사용한 목적지를 정의
// 인수를 받지 않는 목적지를 정의함
@Serializable
object Home

// id값을 인수로 갖는 목적지를 정의함
@Serializable
data class Detail(val id: String)

2.8.0 이전에서는 화면을 식별하기 위해 String을 사용했지만, 2.8.0 이후부터는 Serializable객체를 활용하여 컴파일 타입에 오류를 잡아줄 수 있습니다.

  1. NavHost 설정하기
@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


0개의 댓글