Jetpack Compose로 앱을 개발하다 보면 이런 생각이 들 수 있다. "이제 화면을 모두 Composable 함수로 만드니까, Activity나 Fragment를 쓸 때처럼 Intent에 데이터를 담아 보낼 일도 없겠지? 그럼 Parcelable도 더는 필요 없는 거 아닐까?"
결론부터 말하면, 그렇지 않다.
Jetpack Compose의 Navigation 라이브러리를 사용해 Composable 화면(Screen) 간에 커스텀 객체를 전달해야 한다면, Parcelable은 여전히 가장 중요하고 효율적인 방법이다. 이 글에서는 그 이유를 Compose Navigation의 내부 동작과 함께 파헤쳐 보고자 한다.
Compose Navigation에서는 NavController를 사용해 경로(route)를 기반으로 화면을 이동한다.
navController.navigate("detail/12345")
String, Int 같은 간단한 원시 타입 데이터는 위 예시처럼 경로 문자열에 직접 포함시켜 간단하게 전달할 수 있다. 하지만 User나 Product 같은 복잡한 커스텀 객체를 전달해야 할 때는 어떻게 해야 할까? 객체를 통째로 JSON 문자열로 변환해서 경로에 꾸역꾸역 집어넣는 것은 보기에도 좋지 않고, 데이터가 크면 URL 길이 제한으로 앱이 비정상 종료될 수도 있다.
바로 이 지점에서 안드로이드의 전통적인 데이터 꾸러미, Bundle이 등장한다.
Jetpack Compose Navigation 라이브러리는 화면의 상태를 저장하고 복원하기 위해, 내부적으로 안드로이드 프레임워크의 Bundle
을 사용한다.
Bundle은 원래 Activity나 Fragment의 상태를 저장하기 위해 설계된 핵심 컴포넌트다. 사용자가 화면을 회전시키거나, 잠시 앱을 나갔다 돌아오거나, OS가 메모리 확보를 위해 앱 프로세스를 종료했다가 다시 시작하는 등 UI가 예기치 않게 파괴되었다가 재생성될 때, Bundle에 저장된 데이터를 사용해 이전 상태를 그대로 복원한다.
Compose Navigation 역시 이 강력한 메커니즘을 그대로 활용한다. 우리가 navController를 통해 화면을 이동할 때 전달하는 인자(arguments)들은 NavBackStackEntry 내의 Bundle에 안전하게 보관된다.
그렇다면 Bundle에 우리가 만든 커스텀 객체를 어떻게 넣을 수 있을까? Bundle은 아무 객체나 받아주지 않는다. Bundle에 담기려면 객체를 바이트 스트림으로 변환(직렬화)할 수 있어야 한다.
그리고 안드로이드에서 객체를 직렬화하는 가장 효율적이고 빠른 방법이 바로 Parcelable
이다.
Serializable
: 자바 표준이지만, 리플렉션을 사용해 안드로이드에서 매우 느리다.Parcelable
: 안드로이드 SDK에 포함되어 있으며, IPC(프로세스 간 통신)와 상태 저장을 위해 특별히 설계되어 성능이 월등히 빠르다.결국, 이런 논리적 흐름이 완성된다.
@Parcelize
적용가장 먼저, 전달할 데이터 클래스가 Parcelable을 구현하도록 수정한다.
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize // @Parcelize 어노테이션 추가
data class User(val id: String, val name: String) : Parcelable
Compose Navigation은 Parcelable 타입을 직접 지원하므로, 네비게이션 그래프를 만들 때 인자 타입을 지정해주기만 하면 된다. (이를 위해선 navigation-compose 라이브러리 의존성이 추가되어 있어야 한다)
// NavHost를 설정하는 부분
NavHost(navController = navController, startDestination = "profile") {
composable("profile") {
// 프로필 화면 UI
}
// User 객체를 인자로 받는 detail 화면 정의
composable(
route = "detail",
// arguments 리스트에 User 타입을 추가
arguments = listOf(navArgument("user") { type = NavType.ParcelableType(User::class.java) })
) { backStackEntry ->
// Bundle에서 Parcelable 객체 꺼내기
val user = backStackEntry.arguments?.getParcelable<User>("user")
if (user != null) {
DetailScreen(user = user)
}
}
}
객체를 전달할 때는 currentBackStackEntry의 arguments에 Bundle처럼 값을 넣고, 경로로 이동하면 된다.
// 버튼 클릭 등 이벤트 핸들러 내부
val user = User(id = "user-123", name = "홍길동")
// 현재 백스택의 arguments에 user 객체를 넣음
navController.currentBackStackEntry?.arguments?.putParcelable("user", user)
navController.navigate("detail")
"Activity를 안 쓴다"는 생각에 Parcelable을 외면해서는 안 된다. 우리가 작성하는 Composable 코드는 결국 안드로이드라는 큰 운영체제 위에서 실행되며, Compose 라이브러리 역시 안정성과 성능을 위해 안드로이드의 검증된 시스템(Bundle)을 적극적으로 활용하고 있다.
따라서 Jetpack Compose로 앱을 만들 때도, 화면 간에 커스텀 객체를 전달해야 한다면 Parcelable
을 사용하는 것이 성능, 안정성, 그리고 안드로이드 생태계의 표준을 모두 지키는 최선의 방법이라고 할 수 있다.