Navigation 코드를 줄여보세요 (feat: Compose Destinations)

2025. 6. 11. 21:06· Skils/Android
목차
  1. 개요
  2. Compose Destinations
  3. 설명
  4. 프로젝트에 적용하기
  5. 기존 Navigation과 비교
  6. 참고 자료

개요

compose navigation을 활용해서 프로젝트를 진행 중인데요, 생각보다 보일러 플레이트 코드가 많이 발생하더라고요.

그래서 어떻게 구조를 바꿀 수 있을까 하다가 KSP를 활용해서 Navigation 코드를 자동화하는 라이브러리를 찾게 되었고, 적용해 봤습니다.

Compose Destinations

설명

Compose Destinations는 Jetpack Compose를 위한 Type-Safe Navigation을 제공하는 라이브러리입니다.

해당 라이브러리에서 강조하는 점은 Compose Navigation의 복잡함과 보일러플레이트 코드를 크게 줄여준다는 점입니다.

 

 

첫 Stable 한 버전은 1.9.50으로, 23년 10월 자 라이브러리이다.

https://github.com/raamcosta/compose-destinations/releases/tag/1.9.50

 

Release 1.9.50 No longer beta! 🙌 No more accompanist-navigation! 🎉 · raamcosta/compose-destinations

Removed accompanist navigation since animations were imported to official compose navigation! 🎉 Because of this, some minor changes had to be made: DestinationStyleAnimated -> DestinationStyle.Ani...

github.com

최근 릴리즈가 5월 3일인데, 꽤 활발하게 운영되고 있는 라이브러리인 것 같다...

(따끈따끈하다)

 

소개하고 있는 가장 큰 특징은 다음과 같다.

  • @Destination 어노테이션의 navArgs에 대응되는 클래스를 선언하면 Type Safe한 네비게이션 인자를 지원한다.
  • 다양한 어노테이션을 통해 그래프를 쉽게 정의할 수 있다. (+ 중첩 그래프)
  • 화면 간 이동 후 결과가 Type-Safe하다!
  • SavedStateHandle / BackStackEntry Type Safe 지원
    • viewModel에서 네비게이션 인자를 처리할 수 있다.
  • 애니메이션, 바텀시트, 딥링크 등 쉽게 구현할 수 있다.

공식 Compose Navigation의 모든 기능을 사용할 수 있다.

프로젝트에 적용하기

의존성 설정

gradle [project]

plugins {
alias(libs.plugins.ksp) apply false
}

 

gradle [app]

plugins {
alias(libs.plugins.ksp)
}
dependencies {
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.ksp)
implementation(libs.compose.destinations.bottom.sheet)
}

 

여기서 version은 프로젝트에서 사용되는 compose의 버전에 따라 바뀝니다.

 

compose의 버전은 compose-bom 버전을 통해서 확인할 수 있는데요,

https://developer.android.com/develop/ui/compose/bom/bom-mapping?hl=ko

 

BOM과 라이브러리 버전 매핑  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. BOM과 라이브러리 버전 매핑 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 그룹 출시 노트 열의 URL은

developer.android.com

해당 페이지에서 자신의 프로젝트 compose-bom 버전 -> compose 버전을 확인해서 version을 선택하면 될 것 같습니다.

네비게이션 그래프 정의

RootGraph

@NavHostGraph
annotation class RootGraph

해당 어노테이션을 붙이면 navigation graph를 형성할 수 있습니다.

만약 nested navigation이라면 해당 어노테이션을 활용해서 그래프 클래스를 선언해 주면 됩니다.

 

만약 중첩 네비게이션 구조라면,

 

위 코드를 활용해서 customGraph 어노테이션을 만들어 주시면 됩니다.

@NavGraph<RootGraph>(start = true)
annotation class HomeGraph

 

HomeGraph는 RootGraph 타입을 가지는 다른 그래프 중 시작 그래프라는 뜻입니다.

만약 아니라면,

@NavGraph<RootGraph>
annotation class TestGraph

 

만들기 전, sample 코드의 화면 구성은 Home -> Detail 간단한 구성입니다.

Home은 첫 번째 진입점이라는 표시를 해주기 위해서 start 값을 true로 설정하면 됩니다.

@Destination<RootGraph>(start = true)
fun HomeScreen()
@Destination<RootGraph>
fun DetailScreen()

 

위에서 NavGraph 코드를 살펴본 것처럼 start의 기본값이 false이기 때문에 생략해도 됩니다.

Compose Destinations는 NavHost가 아닌 DestinationNavHost를 사용합니다.

 

주된 파라미터는 다음과 같습니다.

  1. navGraph : 시작 네비게이션 그래프
  2. start : 런타임에 시작 목적지
  3. defaultTransitions : 화면 전환 애니메이션 style
  4. dependenciesContainerBuilder : 의존성 주입을 위한 ContainerBuilder

예시 코드는 아래와 같습니다.

DestinationsNavHost(navGraph = NavGraphs.root)

 

NavGraphs.root는 프로젝트를 빌드하면 생성됩니다.

처음에 kdoc를 활용해서 제가 어노테이션을 생성할 수 있을 줄 알았는데 빌드 후 생성된 NavGraphs 코드를 타고 들어가면 생성되네요!

화면 전환과 인자 전달

Detail 화면에선 owner와 repositoryName이 필요합니다.

Compose Destinations에서는 navArgs를 정의 후, 넘겨주면 됩니다.

data class DetailArgs(
val owner: String,
val repositoryName: String,
)
@Destination<RootGraph>(navArgs = DetailArgs::class)
@Composable
fun DetailRoute()

 

위와 같이 간단한 DetailArgs를 생성 후, @Destination 어노테이션 navArgs에 정의해 주면 됩니다.

그리고 해당 코드를 빌드하면 다음과 같은 코드가 생성됩니다.

/**
* Generated from [DetailRoute]
*/
public data object DetailRouteDestination : BaseRoute(), TypedDestinationSpec<DetailArgs> {
override fun invoke(navArgs: DetailArgs): Direction = with(navArgs) {
invoke(owner, repositoryName)
}
public operator fun invoke(
owner: String,
repositoryName: String,
): Direction {
return Direction(
route = "$baseRoute" +
"/${stringNavType.serializeValue("owner", owner)}" +
"/${stringNavType.serializeValue("repositoryName", repositoryName)}"
)
}
override val baseRoute: String = "detail_route"
override val route: String = "$baseRoute/{owner}/{repositoryName}"
override val arguments: List<NamedNavArgument>
get() = listOf(
navArgument("owner") {
type = stringNavType
},
navArgument("repositoryName") {
type = stringNavType
}
)
@Composable
override fun DestinationScope<DetailArgs>.Content() {
DetailRoute()
}
override fun argsFrom(bundle: Bundle?): DetailArgs {
return DetailArgs(
owner = stringNavType.safeGet(bundle, "owner")
?: throw RuntimeException("'owner' argument is mandatory, but was not present!"),
repositoryName = stringNavType.safeGet(bundle, "repositoryName")
?: throw RuntimeException("'repositoryName' argument is mandatory, but was not present!"),
)
}
override fun argsFrom(savedStateHandle: SavedStateHandle): DetailArgs {
return DetailArgs(
owner = stringNavType.get(savedStateHandle, "owner")
?: throw RuntimeException("'owner' argument is mandatory, but was not present!"),
repositoryName = stringNavType.get(savedStateHandle, "repositoryName")
?: throw RuntimeException("'repositoryName' argument is mandatory, but was not present!"),
)
}
}

내부적으로 전달한 클래스를 분해해서 type-safe 한 인자 전달을 지원하고 있네요!

만약 navArgs를 정의 안 했다면 아래와 같이 간단한 코드가 생성됩니다.

그럼 이제 생성된 DetailRouteDestination을 활용해서 화면 전환 코드를 작성해 볼 수 있습니다.

destinationsNavigator.navigate(
DetailRouteDestination(
DetailArgs(
owner = owner,
repositoryName = repositoryName
)
)
)

 

여기서 사용되는 navigator의 타입은 DestinationsNavigator인데요,

Compose Destinations 내부 코드에선 NavHostController -> DestinationsNavController로 변환해 주는 확장함수를 지원합니다.

 

DestinationScope의 구현체인 Impl에서 navController를 DestinationNavigtor로 바꿔주고 있음을 확인할 수 있네요!

 

이제 실제 복잡한 화면을 예로 들어 설명

📦 navGraph
├── 📁 HomeBaseGraph
│ ├── HomeFirstScreen
│ ├── HomeSecondScreen
│ └── 📁 MenuGraph
│
├── 📁 SearchGraph
│ ├── SearchScreen
│ └── SearchSecondScreen
│
├── 📄 MyScreen
│
├── 📁 MenuGraph
│ ├── MenuScreen
│ ├── 📁 KoreanFoodGraph
│ │ ├── KimchiScreen
│ │ └── RamenScreen
│ │
│ ├── 📁 AmericanFoodGraph
│ │ ├── HambugerScreen
│ │ └── ...
│ │
│ └── 📁 MoreFoodGraph
│ └── ...

만약 Compose Navigation을 사용했다면 어떻게 구현되었을까요?


기존 Navigation과 비교

우선 Route와 Graph를 정의해 줘야겠죠?

Compose Navigation

@Serializable
sealed interface Route {
@Serializable
data object HomeBase : Route
@Serializable
data object HomeFirst : Route
@Serializable
data object HomeSecond : Route
@Serializable
data object SearchBase : Route
@Serializable
data object Search : Route
@Serializable
data class Menu(
val type: FoodType,
val foodTitle: String,
) : Route
sealed interface Food : Route {
val foodDetailType: FoodDetailType
@Serializable
data class KoreaFood(
override val foodDetailType: FoodDetailType
) : FoodRoute
@Serializable
data class AmericanFood(
override val foodDetailType: FoodDetailType
) : FoodRoute
}
@Serializable
data object My : Route
}

추후, Food에 대한 Type이 추가될 때마다 FoodRoute를 상속받는 data class가 추가될 것이다.

 

HomeGraph

fun NavGraphBuilder.homeGraph(
navController: NavHostController,
) {
navigation<Route.HomeBase>(startDestination = Route.HomeFirst) {
composable<Route.HomeFirst> {
HomeFirstScreen()
}
composable<Route.HomeSecond> {
HomeSecondScreen()
}
MoreGraph()
}
}

 

HomeGraph를 구현함에 있어서 발생되는 코드는 크게 적지 않다고 생각할 수 있다.

하지만 해당 구현은 최대한 담백한 구현으로 navArgs, animation, 화면 이동에 대한 코드를 적용하지 않은 스켈레톤 코드이다.

하지만 HomeGraph보다 MoreGraph에서 개발자가 작성할 코드의 영역이 훨씬 길고 장황하다.

 

MenuGraph

fun NavGraphBuilder.MenuGraph(
navController: NavHostController,
) {
composable<Route.Menu> {
MenuScreen()
}
KoreanFoodGraph()
AmericanFoodGraph()
...
MoreFoodGraph()
}

여기서 음식이 추가될 때마다 MenuGraph가 비대해질 것이다.

그리고 음식의 타입이 추가될 때마다 우리는 추가적인 FoodGraph를 또 생성해야 할 것이다.

 

실제 애플리케이션에서는 nested navigation을 사용하는 복잡한 구조를 채택하고 있고, 이를 구현하기 위해 많은 양의 코드가 발생된다.

비슷한 형식으로 구현되는 보일러 플레이트 코드는 작성자로 하여금 피곤을 느끼게 하고, 읽는 사람으로 하여금 가독성을 저해하는 원인이 되기도 합니다.

 

이제 Compose Destinations를 적용해 보겠습니다.

작업은 간단합니다.

  1. @NavGraph를 사용해 그래프를 정의
  2. 실제 destination인 Screen에 @Destination 추가
  3. (만약 전달할 argument가 있다면 argumentClass 정의)
import com.ramcosta.composedestinations.annotation.NavGraph
import com.ramcosta.composedestinations.annotation.RootGraph
@NavGraph<RootGraph>(start = true)
annotation class HomeGraph(val start: Boolean = false)
@NavGraph<HomeGraph>
annotation class HomeSubGraph(val start: Boolean = false)
@NavGraph<RootGraph>
annotation class SearchGraph(val start: Boolean = false)
@NavGraph<RootGraph>
annotation class MyGraph(val start: Boolean = false)
@NavGraph<HomeSubGraph>
annotation class MenuGraph(val start : Boolean = false)
@NavGraph<MenuGraph>
annotation class KoreanFoodGraph(val start : Boolean = false)
@NavGraph<MenuGraph>
annotation class AmericanFoodGraph(val start : Boolean = false)
...
//MoreFoodGraph

 

세웠던 내비게이션 구조대로 그래프를 정의해 봤습니다.

여기서 특징은 RootGraph 타입과 아닌 것이 있는데요, RootGraph 타입은 Top-Level-Destination이라고 생각하시면 됩니다.

그중에서 start가 true인 HomeGraph가 Top-Level-Destination의 기본 시작점입니다.

 

HomeSub는 HomeGraph를 타입으로 가지는데, 해당 그래프의 하위 그래프가 됩니다.

 

이제 스크린의 @Destination을 붙여주세요.

@Composable
@Destination<HomeGraph>(start=true)
fun HomeFirstScreen()
@Composable
@Destination<HomeGraph>
fun HomeSecondScreen()
@Composable
@Destination<SearchGraph>(start=true)
fun SearchScreen()
@Composable
@Destination<MyGraph>(start=true)
fun MyScreen()
// MyScreen은 one-depth라 MyGraph가 아닌 RootGraph를 타입으로 가져도 되지만
// 확장 가능성을 고려해 graph로 선언했습니다.
@Composable
@Destination<MenuGraph>(start = true)
fun MenuScreen()
@Composable
@Destination<KoreanFoodGraph>(start = true)
fun KimchiScreen()

 

이렇게 쭉쭉 붙여주면 됩니다!

 

만약 화면 전환 간 인자가 필요하다면 위에서 구현한 것처럼 NavArgs를 구현하고 파라미터에 정의해 주세요.

 

모든 준비를 마쳤다면, 그동안 작성했던 Navigation 코드를 전부 지워주세요(NavHost 포함)

그리고 Compose Destination에서 사용하는 navHost를 적용해 주시면 됩니다.

 

DestinationsNavHost(
modifier = Modifier
navController = navController,
navGraph = NavGraphs.root,
)

여기서 정의했던 NavGraphs.root는 위 어노테이션에서 RootGraph를 타입으로 가진 Graph 중 start가 true인 어노테이션입니다.

 

Compose Destination은 Compose Navigation과 다르게 화면 전환 애니메이션이 굉장히 밋밋한 편입니다.

DestinationNavHost의 파라미터인 defaultTransitions를 커스텀해서 구현해 줍니다.

 


 

Compose Destinations를 적용해 보니, 네비게이션 관련 코드가 훨씬 간결해지고, 타입 세이프하게 인자를 전달할 수 있어 실수도 줄일 수 있었습니다. 특히 복잡한 네비게이션 구조를 관리할 때 반복적인 코드 작성에서 오는 피로감이 많이 줄어든 점이 인상적이었습니다. Jetpack Compose로 앱을 개발하면서 네비게이션 구조가 복잡해질수록 Compose Destinations의 진가가 드러납니다. 타입 세이프티, 자동 코드 생성, 쉬운 그래프 정의 등 다양한 장점을 직접 경험해 보시길 추천합니다!

 

 

참고 자료

https://github.com/raamcosta/compose-destinations/?tab=readme-ov-file

 

GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose navigation with no boilerpl

Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate. - raamcosta/compose-destinations

github.com

https://composedestinations.rafaelcosta.xyz/v2/defining-navgraphs/

 

Defining Navigation Graphs | Compose Destinations

Defining navigation graphs

composedestinations.rafaelcosta.xyz

 

저작자표시 (새창열림)

'Skils > Android' 카테고리의 다른 글

복잡한 레이아웃을 아름답게 해결할 수 있는 방법(feat: SubComposeLayout)  (0) 2025.06.23
Compose Layout  (2) 2025.05.18
[Android] - Interceptor를 이용한 Retrofit 에러 핸들링  (1) 2024.10.19
[Android] - Authenticator를 활용해 JwtToken 갱신하기  (2) 2024.10.12
[Android] - Test 코드를 작성해보자(Compose UI Test)  (3) 2024.09.17
  1. 개요
  2. Compose Destinations
  3. 설명
  4. 프로젝트에 적용하기
  5. 기존 Navigation과 비교
  6. 참고 자료
'Skils/Android' 카테고리의 다른 글
  • 복잡한 레이아웃을 아름답게 해결할 수 있는 방법(feat: SubComposeLayout)
  • Compose Layout
  • [Android] - Interceptor를 이용한 Retrofit 에러 핸들링
  • [Android] - Authenticator를 활용해 JwtToken 갱신하기
재한
재한
안녕하세요 💻
재한
짜이한
전체
오늘
어제
  • 분류 전체보기 (504)
    • Skils (118)
      • Android (52)
      • C++ (5)
      • Kotlin (36)
      • Algorithm (24)
      • Server (1)
    • CodingTest (228)
      • Programmers (45)
      • Baekjoon (183)
    • Experience (8)
      • 후기(코딩테스트,프로그램,프로젝트) (8)
    • Computer Science (70)
      • Design Pattern (2)
      • OOP (2)
      • Computer Architecture (14)
      • OS (2)
      • Software Engineering (3)
      • DataBase (8)
      • Network (39)
    • 학교 (75)
      • R프로그래밍 (26)
      • 회계와 사회생활 (17)
      • 컴퓨터학개론 (20)
      • it기술경영개론 (12)

블로그 메뉴

  • 홈
  • 태그
  • 카테고리
  • 글쓰기
  • 설정

인기 글

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
재한
Navigation 코드를 줄여보세요 (feat: Compose Destinations)
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.