코딩

Compose Navigation & 아키텍처 컴포넌트 연동

Eastpark 2025. 1. 6. 22:53
728x90
반응형

1. 왜 Navigation이 중요한가?

1.1 여러 화면 전환의 필수 요소

실제 앱은 단일 화면으로만 구성되지 않는다. 회원가입 화면, 메인 화면, 설정 화면 등 다양한 화면을 이동하며 데이터를 전달할 필요가 있다.

기존에는 Fragment Activity 간 전환을 위해 Intent, Bundle, FragmentManager 등을 주로 사용했다.

Jetpack Compose에서는 Navigation Compose 라이브러리를 통해 선언형(Declarative) 방식으로 화면 전환과 인자 전달을 처리할 수 있다.

 

1.2 Navigation Compose의 장점

코드의 간결화: NavHost, NavController 등 Compose 전용 API를 통해 화면 전환 로직을 직관적으로 작성 가능.

인자 전달 간편화: 화면 간 데이터 전달을 navController.navigate("detail/${itemId}") 식으로 처리하면, 별도의 Intent 코드가 불필요.

Back Stack 자동 관리: 뒤로 가기(Back) 동작이나 중첩 Navigation 같은 복잡도도 Compose가 내부적으로 처리해준다.


2. Navigation Compose 시작하기

2.1 Gradle 의존성 추가

프로젝트의 module-level build.gradle 파일에 아래와 같이 의존성을 추가한다.

dependencies {
    // Jetpack Navigation Compose
    implementation "androidx.navigation:navigation-compose:2.6.0"
    // (Compose Core, Material 등은 이미 포함되어 있다고 가정)
}

2.2 NavHost & NavController

@Composable
fun MyAppNavHost() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { itemId ->
                    navController.navigate("detail/$itemId")
                }
            )
        }
        composable(
            route = "detail/{itemId}",
            arguments = listOf(navArgument("itemId") { type = NavType.StringType })
        ) {
            val itemId = it.arguments?.getString("itemId") ?: ""
            DetailScreen(itemId)
        }
    }
}

1. rememberNavController(): Composable에서 Navigation 상태를 관리하는 NavController 객체를 생성한다.

2. NavHost: Navigation 경로를 정의해주는 컨테이너. startDestination으로 앱이 시작될 화면을 지정한다.

3. composable("home") { ... } composable("detail/{itemId}") { ... }: 라우트 경로(route)마다 다른 Composable 함수를 호출한다.

4. 인자 전달: navController.navigate("detail/$itemId")로 화면을 전환하고, detail/{itemId}와 같이 인자를 받을 수도 있다.


3. 아키텍처 컴포넌트와의 연동

3.1 ViewModel과 Composable

Compose에서 ViewModel을 활용해 화면 상태를 관리하면, 상태 보존 데이터 로직 분리를 동시에 실현할 수 있다.

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = viewModel(),
    onNavigateToDetail: (String) -> Unit
) {
    val uiState by viewModel.uiState.collectAsState()

    Column {
        Text(text = "현재 상태: ${uiState.status}")
        
        uiState.items.forEach { item ->
            Button(onClick = { onNavigateToDetail(item.id) }) {
                Text("Go to ${item.name}")
            }
        }
    }
}

viewModel(): lifecycle-viewmodel-compose 라이브러리를 사용하면, Composable 함수 안에서 쉽게 ViewModel을 호출할 수 있다.

collectAsState(): StateFlow LiveData 등을 Compose의 State로 변환해, 상태가 바뀔 때마다 화면이 자동으로 갱신되도록 한다.

3.2 LiveData or StateFlow?

LiveData: 안드로이드에서 오래전부터 지원해온 관찰 가능한 데이터 홀더. 기존 MVVM 구조에 익숙하다면 사용하기 편리.

StateFlow: 코틀린 코루틴(Flow) 기반으로, 비동기 처리와 결합하기 쉽다. Compose와도 자연스럽게 연동된다.

프로젝트 성격이나 팀의 합의에 따라 선택하면 되며, Compose에서는 둘 다 잘 호환된다.


4. Clean Architecture & Compose

4.1 Clean Architecture 개념 간단 정리

Clean Architecture는 의존성 방향을 “도메인(비즈니스 로직) → 데이터 → UI” 순으로 제한함으로써, 유지보수성과 테스트 용이성을 높이는 구조다.

Domain Layer: UseCase, Entity 등 순수 비즈니스 로직

Data Layer: Repository 구현, 네트워크/DB 접근

UI Layer: ViewModel, Composable UI

4.2 Compose에서 Clean Architecture 적용 방법

1. UI(View) - ViewModel 분리

Composable 함수는 UI 렌더링에 집중하고, 핵심 로직은 ViewModel이 담당한다.

2. ViewModel - UseCase 연결

ViewModel에서 UseCase를 호출해 비즈니스 로직을 수행한다.

3. UseCase - Repository 연결

Repository가 실제 네트워크/DB로부터 데이터를 가져온다.

4. UI 업데이트

ViewModel이 받은 결과 데이터를 StateFlow or LiveData로 발행하면, Composable 함수가 자동으로 재구성(Recompose).

 

아래는 HomeViewModel HomeUseCase가 분리된 예시를 간단히 보자.

class HomeViewModel(private val homeUseCase: HomeUseCase) : ViewModel() {
    private val _uiState = MutableStateFlow(HomeUiState())
    val uiState: StateFlow<HomeUiState> = _uiState

    init {
        fetchItems()
    }

    private fun fetchItems() {
        viewModelScope.launch {
            val items = homeUseCase.getItems()
            _uiState.value = HomeUiState(items = items)
        }
    }
}

class HomeUseCase(private val repository: HomeRepository) {
    suspend fun getItems(): List<Item> {
        // 복잡한 비즈니스 로직 또는 여러 Repository 결합
        return repository.fetchItems()
    }
}

UI는 HomeViewModel uiState를 관찰하고, 화면을 갱신한다.

ViewModel UseCase를 통해 데이터를 가져오고, UseCase Repository에 실제 로직을 위임한다.


5. 실제 예제로 합쳐보기

MainActivity에서 MyAppNavHost()를 호출해 전체 네비게이션과 화면 구성을 관리한다고 해보자.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppNavHost()
        }
    }
}

HomeScreen: HomeViewModel을 주입받아 아이템 목록을 표시

DetailScreen: 전달받은 itemId로 상세 데이터를 조회하고 보여주기

ViewModel & UseCase & Repository: Clean Architecture 형태로 분리해 유지보수성을 높인다.

 

이런 구조로 구성하면, Compose UI와 Navigation, 그리고 아키텍처 컴포넌트(ViewModel, LiveData/StateFlow)들이 유기적으로 연결돼, 프로젝트가 커져도 가독성을 유지하기 쉽다.


6. 마무리

이번에는 Compose Navigation을 통한 화면 전환과 아키텍처 컴포넌트 연동 방법을 다뤄보았다.

Navigation Compose로 라우트(Route)를 정의하고, NavController를 통해 화면 이동을 직관적으로 처리할 수 있다.

ViewModel + StateFlow(LiveData) 조합은 선언형 UI와 찰떡궁합을 이루며, 비즈니스 로직을 깔끔하게 분리할 수 있다.

Clean Architecture를 Compose에 적용하면, 대규모 프로젝트에서도 유지보수성과 테스트 용이성을 높일 수 있다.

728x90
반응형