Circuit Navigation 사용 시 feature 모듈간의 참조는 어떻게 해결했을까?

seoyoon·2025년 7월 22일
1

Circuit을 사용하면서, Circuit Navigation 사용 시 feature 모듈간의 참조 문제가 있었는데, 팀원인 지훈님이 해결한 코드를 보고 어떻게 해결했는지 분석해보고자 한다!

Circuit에 대한 분석, 지훈님 블로그 참고
https://velog.io/@mraz3068/Circuit-Try-Out

자세한 내용 및 코드는 아래 PR 참고
https://github.com/YAPP-Github/Reed-Android/pull/39

개요

Circuit에서 Screen은 해당 화면을 식별하는 key 역할을 하며, 다음과 같은 역할을 한다

  • 식별자: @Parcelize된 객체로서 화면을 구분
  • 의존성 연결 고리: @CircuitInject 어노테이션으로 해당 Screen에 맞는 Presenter와 UI가 자동 연결됨
  • 내비게이션 대상: Navigator.goTo(Screen) 방식으로 이동

Compose Navigation과는 달리, Circuit에서는 Screen 객체가 하나의 진입점이며 이에 맞춰 Presenter, UI, 상태(State), 이벤트(Event)가 연결된다

문제: feature 모듈 간 순환참조의 위험

변경 전 구조

📦 feature
 ├── 📂 home
 │    ├── HomePresenter.kt
 │    └── HomeScreen.kt  // 내부에 UiState + Event 중첩 정의됨

feature:home 모듈의 HomeScreen.kt파일 내 HomeScreen 정의는 다음과 같이 구현되어 있었다

// 식별자 역할을 하는 Screen
@Parcelize
data object HomeScreen : Screen {
	// Screen 내부에 State와 Event가 중첩
    data class State(
        val eventSink: (Event) -> Unit,
    ) : CircuitUiState

    sealed interface Event : CircuitUiEvent
}

// Composable UI
@CircuitInject(HomeScreen::class, ActivityRetainedComponent::class)
@Composable
internal fun Home(
    state: HomeScreen.State,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        HomeContent(
            state = state,
            modifier = modifier,
        )
    }
}
  • HomeScreen은 식별자를 넘어 해당 화면이 가질 수 있는 UiState와 Event에 대한 정의까지 포함하고 있다
  • 즉 Screen 자체가 "이 화면이 어떻게 생겼고(상태), 무엇을 할 수 있는지(이벤트)"를 모두 정의하는 역할을 한다
  • Screen이 feature 모듈 내에 정의되어 있기 때문에, 다른 Presenter에서
    navigator.goTo(HomeScreen)과 같은 코드를 호출할 경우 직접적인 참조를 갖게 된다

해결: Screen을 공통 모듈로 분리

변경 후 구조

📦 feature
 ├── 📂 home
 │    ├── HomePresenter.kt
 │    ├── HomeUiState.kt
 │    └── HomeScreen.kt 
 └── 📂 screens
     └── ReedScreen.kt

1. feature:screens 모듈에 추상화된 ReedScreen 정의

abstract class ReedScreen(val name: String) : Screen {
    override fun toString(): String = name
}

@Parcelize
data object HomeScreen : ReedScreen(name = "Home()") // HomeScreen이 ReedScreen을 상속
  • 모든 Screen은 공통 모듈 screens에 정의한다
  • feature 모듈은 screens 모듈만 참조하고 서로 직접 참조하지 않는다

2. feature:home의 HomeUiState.kt에서 UiState/UiEvent 정의

data class HomeUiState(
    val eventSink: (HomeUiEvent) -> Unit,
) : CircuitUiState

sealed interface HomeUiEvent : CircuitUiEvent {
    data object OnOcrButtonClicked : HomeUiEvent
}
  • UiState와 UiEvent를 Screen 내부가 아닌 외부에 별도로 정의한다

3.HomePresenter.kt에서는 screens 모듈만 참조한다

import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import com.ninecraft.booket.screens.HomeScreen
import com.ninecraft.booket.screens.OcrScreen // screens 모듈 참조
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.components.ActivityRetainedComponent

@Suppress("unused")
class HomePresenter @AssistedInject constructor(
    @Assisted private val navigator: Navigator,
) : Presenter<HomeUiState> {

    @Composable
    override fun present(): HomeUiState {
        fun handleEvent(event: HomeUiEvent) {
            when (event) {
                is HomeUiEvent.OnOcrButtonClicked -> {
                    navigator.goTo(OcrScreen)
                }
            }
        }
        return HomeUiState(
            eventSink = ::handleEvent,
        )
    }

    @CircuitInject(HomeScreen::class, ActivityRetainedComponent::class)
    @AssistedFactory
    fun interface Factory {
        fun create(navigator: Navigator): HomePresenter
    }
}
  • Presenter에서는 screen모듈을 참조하여 다른 Screen을 가져와 네비게이팅 시킬 수 있다

결과

  • 기존 Screen의 역할을 단순한 식별자 및 내비게이션 단위인 ReedScreen으로 표준화하고 별도 모듈로 구성한다
  • UiState/UiEvent는 Screen 내부에 중첩하지 않고 feature 내에 별도로 정의한다
  • 네비게이션 대상으로 feature를 직접 참조하는 것이 아닌 screens 모듈만 참조한다
profile
seoyoon's development blog

0개의 댓글