Compose에서 상태관리 및 종속성 주입 방법에 대한 고민

JhoonP·2023년 7월 8일
0

기존 상태관리 및 의존성 주입법

Compose를 사용하지 않은 기존 Android 앱은 Activity, Fragment, 기타 수명주기에 따라 상태관리를 수행해야 합니다.
이를 위해 수명 주기에 맞춰서 생성되고 소멸되는 상태관리 매니저가 필요했고, Android Jetpack에서는 lifecycle-viewmodel을 통해서 수명 주기와 연결된 상태관리 매니저를 쉽게 사용할 수 있었습니다.
또한 이러한 수명주기를 고려한 의존성 주입법이 필요했고, Hilt를 사용해 이를 극복했습니다.

Compose를 도입하면서 생긴 변화

하지만 Compose를 도입하면 기존 생명주기와는 다른 Composable의 생명주기를 따라야 합니다.
따라서 만약 앱의 A부터 Z까지 Compose만을 사용해서 구현한다면 굳이 ViewModel을 사용하지 않고 임의의 클래스를 구현해 상태관리에 사용해도 됩니다.

여기서 말하는 ViewModellifecycle-viewmodel을 말하는 것이지 일반적인 MVVM의 ViewModel을 이야기 하는 것이 아닙니다.

Compose를 처음 접할 당시엔 ViewModel을 사용했는데 다음과 같은 이유였습니다.

  • 기존에 사용하던 ViewModel이 더 익숙한데다 Compose에서도 호환성 문제 없이 사용할 수 있었다.
  • 의존성 주입을 위해 Hilt를 도입하려 했는데 ViewModel이 Hilt와 호환되었다.

하지만 점차 Compose 앱을 구현하면서 불편함에 마주했는데 이는 특히 Hilt에서 가장 크게 느꼈습니다. Hilt가 지원하는 클래스가 아래와 같이 제한적이라 ViewModel에 필요한 의존성 및 필요한 생성자 매개변수를 주입하기 매우 까다로웠기 때문입니다.

Application(@HiltAndroidApp을 사용하여)
ViewModel(@HiltViewModel을 사용하여)
Activity
Fragment
View
Service
BroadcastReceiver

Hilt를 사용하면 의존성 주입에 필요한 boilerplate code를 줄일 수 있는 장점이 있긴 했지만, Hilt의 큰 장점 중 하나인 수명 주기 관리가 Compose에서는 (아직까지) 큰 의미를 가지지 못했고, 의존성 주입에 제한을 가지는 부분이 훨씬 큰 불편함으로 다가와서 Hilt를 제거하게 됐습니다.

Compose의 Provider Pattern

대신, Flutter와 거의 유사한 방식으로 전역 상태관리 및 의존성 주입에 유용하게 사용할 수 있는 CompositionLocal이 있어 이것을 도입했습니다.

CompositionLocal은 Composable의 하위 계층에 Provider Pattern을 사용해 데이터를 전달할 수 있는 방식입니다.
예시와 함께 CompositionLocal 사용법을 살펴보겠습니다.

CompositionLocal 생성

먼저 compositionLocalOf 함수로 CompositionLocal 객체를 생성합니다.
compositionLocalOfdefaultFactory로 의존성의 기본값을 주입할 수 있습니다.
일반적으로 CompositionLocal은 테스트 및 Preview 용이성을 위해 적절한 기본값을 제공하는 것이 좋다고 합니다.

// LocalElevations.kt file

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }

CompositionLocalProvider을 사용하여 CompositionLocal에 값 제공

앞에서 만든 CompositionLocalLocalElevation에 주입할 의존성인 elevationsprovides infix 함수를 사용해 연결하면(LocalElevations provides elevations) providedValue를 얻을 수 있습니다.
이렇게 얻은 providedValueCompositionLocalProvider Composable 함수에 매개변수로 전달합니다.
그리고 CompositionLocalProviderelevations를 사용하고자 하는 Composable들을 구성합니다.

// MyActivity.kt file

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Calculate elevations based on the system theme
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }
        }
    }
}

CompositionLocal 사용

CompositionLocalProvider 하위 계층의 Composables들은 LocalElevation를 사용해 elevations에 접근할 수 있게 됩니다.

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    Card(elevation = LocalElevations.current.card) {
        // Content
    }
}

결론

Provider 패턴은 Compose, Flutter와 같은 선언형 UI 도구에 적합한 방식이었고 CompositionLocal라는 도구로 사용할 수 있게 됐습니다.
하지만 암시적으로 의존성을 주입하기 떄문에 Composable의 동작 추론을 어렵게 할 수도 있어 Repository, Theme, Service와 같은 성격의 전역 상태가 아닌 경우에는 사용에 유의해야 합니다.

애초에 Compose에서는 Composable의 재사용성을 위해 가능한 필요한 상태만을 하위 계층에 전달하도록 권고하고 있기 때문에 ViewModel을 통째로 CompositionLocal으로 넘기는 등의 동작은 가능한 피해야 할 것입니다.

profile
배울게 끝이 없네 끝이 없어

0개의 댓글