ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Modifier composed{} 이대로 괜찮은가
    Android 2025. 4. 30. 16:36

    .

    안녕하세요! 매시업 13 기부터 활동하고 있는 안드로이드 팀 서정우 라고 합니다. 오늘은 Modifier composed에 대해 살펴 보도록하겠습니다.

    저는 보통 Modifier를 커스텀하게 만들때 Modifier 확장함수로 선언하여 사용하고는 했습니다.

    예를 들면 clickable 같은 경우 Modifier.composed{….}을 하여 많이 사용하고는 했는데요. 구글 공식 홈페이지에서는 composed{} 를 권장하지 않는다고 나와 있어서 그 이유가 궁금하고 내부는 어떻게 되어있을지 한 번 살펴보겠습니다.

    composed로 체이닝할때 일어나는 일

    fun Modifier.rippleClick(): Modifier {
        return this.composed {
            clickable(
                onClick = { /*TODO*/ },
                indication = ripple(...),
                interactionSource = remember { MutableInteractionSource() }
            )
        }
    }

    다들 잘 아시다시피 해당 코드는 clickable을 커스텀 할때 사용하는 Modifier의 확장함수를 의미합니다. 이러한 방식은 보기에는 간단하고 편리하지만, 실제 내부 동작을 이해하게 되면 반드시 이 방식이 '좋은 코드' 라고 할 수는 없다는 것을 깨닫게 됩니다.

    먼저 결론부터 말씀 드리자면 composed는 다음 2가지의 단점이 있습니다.

    • 스킵 불가능 (Non-Skippable): composed는 람다이고 람다안에서 Modifier라는 반환 값이 있기 때문에 생성된 Modifier는 Compose의 스마트 리컴포지션 대상에서 제외되어 다시 구성됩니다.
    • materialize 호출 증가: composed는 Composition 단계마다 materialize 과정을 통해 Modifier 체인을 변환해야 하는데 이 과정은 불필요한 리소스를 소비 합니다.

    skip에 관련한 내용들은 전부 이해하셨을 것이라고 생각됩니다! skip이 안된다는 것이 정말 큰 문제점입니다. 꼭 기억해주세요!

    하지만 materialize 라는 내용에 대해서는 생소하신 분들이 계실텐데요. 저 역시 이번에 공부하게 되면서 처음으로 알게 된 내용들 이었습니다. 내부 코드를 보며 materialize 과정까지 한번 살펴보겠습니다.

    composed{} 의 내부

    Modifer.composed 내부 코드

    composed 함수의 구현을 보면 then을 통해 현재 Modifier 체인에 ComposedModifier 객체를 추가하는 것을 확인할 수 있습니다.

    여기서 ComposedModifier는 다음 두 가지 인자를 받습니다:

    • inspectorInfo: 디버깅 도구에서 Modifier를 추적할 수 있도록 도와주는 정보입니다. (이 글에서는 다루지 않겠습니다)
    • factory: Modifier.() -> Modifier 타입의 함수로, composed 블록 안에 우리가 정의한 Modifier 체인입니다.

    타입을 보시면 아시겠지만 Modifier 타입을 받아서 Modifier를 반환하는 함수로 볼 수 있습니다. 이후 해당 factory 인자를 ComposedModifier 라는 클래스에 넣어주게 됩니다.

    ComposedModifier는 실제 Modifier가 아니다?

    당연히 ComposedModifier라는 클래스에서 어떤 작업을 할 것으로 기대가 되겠죠? 하지만 이 클래스를 가지고 작업하는 부분은 Compose가 친절하게 내부적으로 알아서 해주고 있답니다.

    여기서 제일 중요한 점은 ComposedModifier는 아직 Modifier가 아니라는 것입니다. 그래서 실제 UI 트리에 ComposedModifier가 추가 되는 것이 아니라 어떤 일련의 작업(materialize)을 한 이후 Modifier 로서 추가가 되어 UI에 반영이 되게 됩니다.

    즉, ComposedModifier는 단지 Modifier에 걸린 체인을 계산하기 위한 하나의 Wrapper 라고 생각해주시면 될 것 같습니다. 우리가 composed{} 내부에 작성된 코드와 Modifier는 아직 반영되지 않은 상태라는 것 입니다.

     

    ComposedModfier 코드

    ComposedModifier 내부를 들어가게 되면 다음과 같이 나오게 됩니다. 말씀 드렸던 것처럼 아무것도 작업하지 않고 단지 인자들을 받아 가지고 있는 상태로 보여집니다.

    그래서 컴포즈는 Composition 단계때 이 객체를 가지고 materialize라는 작업을 거쳐 실제 노드 트리에 붙을 수 있는 Modifier를 반환하고 UI에 반영 되게 됩니다.

    materialize란?

    내부 코드를 먼저 살펴 보기 전에 materialize가 무엇인지에 대해 먼저 말씀드리겠습니다.

    materialize를 번역하면 ‘구체화되다(실현하다)’ 라는 뜻이 있습니다. 이 이름처럼 materialize를 한 문장으로 정리하자면 “ComposedModifier의 람다(factory)를 전부 풀어서 실제 Modifier로 반환 하는 과정 ” 이라고 정리하겠습니다.

    materilize는 컴포즈의 구성 단계에서 Composition 단계에서 실행되는 함수입니다. 즉, 최종 UI 노드 트리에 붙게 하기 위한 ComposedModifier를 Modifier로 반화하는 함수 라고 생각해주시면 좋을 것 같습니다. 이제 내부 코드를 살펴 보겠습니다.

    materialize 코드

    materialize()의 내부 코드를 들어가게 되면 제일 먼저 나오는 코드입니다. 특이하게 제일 중요한 result 값은 Impl로 따로 구현해 놓은 것을 볼 수 있습니다.

    먼저 처음 코드와 마지막 코드에 ~~ReplaceGroup이라는 것을 볼 수 있는데, 이 부분은 Slot 테이블 구조에 대해 말하는 것이기 때문에 해당 포스트에서는 다루지 않겠습니다. 간단히 UI 상태를 저장하기 위한 어떤 구조로 생각해주시면 될 것 같습니다.

    materializeImpl 코드

    다음은 제일 중요한 materializeImpl에 대해 자세히 들여다 보겠습니다. Impl은 다음 코드 처럼 구성되어 있고 코드가 각각 무슨 역할을 하는지 하나씩 살펴 보도록 하겠습니다. 제일 먼저 긴 주석이 심상치 않아 보입니다. 먼저 살펴 봐야겠죠?

    // This is a fake composable function that invokes the compose runtime directly so that it
    // can call the element factory functions from the non-@Composable lambda of Modifier.foldIn.
    // It would be more efficient to redefine the Modifier type hierarchy such that the fold
    // operations could be inlined or otherwise made cheaper, which could make this unnecessary.
    --------------------
    // 이 코드는 @Composable이 아닌 위치에서 Compose 런타임을 직접 호출하여 composed Modifier의 factory 람다를 실행할 수 있도록 만든 가짜 composable 함수입니다.
    // 현재는 Modifier.foldIn 안에서 @Composable 함수를 호출할 수 없기 때문에 이런 방식이 필요하지만,
    // Modifier 타입 구조를 재설계해서 fold 연산을 인라인 처리하거나 더 저렴하게 만들 수 있다면, 이런 꼼수는 필요 없을 수도 있습니다.

    해당 주석은 composable context에서 composed에 관련한 람다(factory)를 실행해야 하기 때문에 컴포즈 런타임을 직접 우회해서 실행하는 구조를 말하고 있습니다.

    결국은 나중에는 더 효율적인 구조로 바꾸자~ 라는 성격의 주석입니다. materializeImpl은 컴포저블로 선언되지 않았지만, 내부적으로는 Composable 처럼 행동하게 로직을 구성했다는 의미입니다. slot 처럼 메모리에 저장하게 하려는 것도 remember처럼 구현을 하고 싶어했다는 것을 볼 수 있었습니다.

     if (modifier.all { it !is ComposedModifier }) {
            return modifier
        }

    제일 먼저 나오는 코드입니다. 기본적으로 Modifier 타입이라면 내장 메소드들이 선언되어있는데 그중 all이라는 메소드를 사용하여 검사합니다.

    우리가 알고 있는 all 메소드 처럼 Modifier의 element중 ComposedModifier가 없다면 밑에 로직을 실행하지 않고 바로 Modifier를 반환하는 코드입니다. 하나의 최적화 코드와 더불어 재귀를 탈출하는 코드로도 볼 수 있습니다.

    val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
            acc.then(
                if (element is ComposedModifier) {
                    @Suppress("UNCHECKED_CAST")
                    val factory = element.factory as Modifier.(Composer, Int) -> Modifier
                    val composedMod = factory(Modifier, this, 0)
                    materializeImpl(composedMod)
                } else {
                    element
                }
            )
        }

    가장 핵심이 되는 코드 입니다. 중요한 만큼 자세히 살펴보겠습니다.

    큰 맥락을 먼저 말씀드리면 Modifier의 체인을 하나씩 살펴보면서 ComposedModifier라면 거기에 걸려있는 체인을 전부 살펴보면서 element로 풀고, Modifier에 element를 추가하여 최종적으로 계산되 Modifier를 반환하는 과정입니다.

    if문을 통해서 ComposedModifier가 아니라면 element로 단순 반환하기 때문에 ComposedModifier일 경우 로직을 중심으로 살펴보겠습니다.

    val result = modifier.foldIn(Modifier) { acc, element →

    • Modifier에 내장 되어 있는 함수 입니다. Modifier의 체인을 왼쪽 → 오른쪽 방향으로 탐색하면서 각 element들을 누적하며 작업을 합니다.
    • 만약 Modifier.paading().background()가 있다면 padding이 먼저 적용이되고 이후 background가 적용되는 느낌이라고 생각하시면 될 것 같습니다.
    • acc는 modifier를 의미합니다.
    • element는 이름에 나온 것 처럼 modifier의 element를 의미합니다.

    factory = element.factory as Modifier.(Composer, Int) -> Modifier

    • ComposedModifier의 factory를 꺼내서 ****Modifier.(Composer, Int) -> Modifier 함수 타입으로 변환합니다.

    val composedMod = factory(Modifier, this, 0)

    • 이후 변환된 factory에 인자로 Modifier, 현재 Composer, key 값을 넣습니다.
    • key 같은 경우 slot에 저장하기 위해 키값을 넣지만 0으로 하드 코딩한 만큼 여기서는 아무의미가 없고 주석에 써있던 것 처럼 최대한 컴포저블 형태로 만들고 싶었기 때문에 default로 선언한 것으로 보입니다.

    materializeImpl(composedMod)

    • 이후 materializeImpl 함수를 재귀 호출을 하는 코드입니다. 왜냐하면 composed 내부에 composed가 중첩되어 있을 수 있기 때문입니다.

    acc.then

    • 재귀 호출이나 element들로 누적되어진 체인들을 최종적으로 modifier에 추가하는 과정입니다. 이후 만들어진 modifier를 result 변수에 담아 materialzeImpl 함수에 반환 값으로 사용됩니다.

    이렇게 Composition 단계에서 최종적으로 materialize 과정을 거친 ComposedModifier를 모든 체인을 검사하고 Modifier로 변환하여 리턴 합니다. 이후 최종적으로 UI노드 트리에 붙게 됩니다.

    체이닝 할때는 어떤 것을 사용해야 될까?

    공식 문서에 권장된 것 처럼 단순히 체인을 추가하고 싶다고 한다면 Node API를 사용해서 커스텀 하거나 then을 이용해서 일반 Modifier에 체인을 추가하는 방식으로 하는 것을 추천드립니다. 왜냐하면 앞서 말한 방식을 거치지 않고 바로 element로서 Modifier에 반영되게 되기 때문입니다.

    다만, 커스텀시 remember가 필요하고 여러 상태가 필요하다면 composed{}를 사용하는 방법도 고려해보는게 좋을 것 같습니다. then은 remember를 사용할 수 없기에 당연한 거겠죠?

    결론

    composed{}를 사용하게 되면 이렇게 모든 체인들을 검사하고, materialize과정을 거쳐야 합니다. composed{}가 많으면 많을 수록 materialize가 많이 일어나게 되고, 내부적으로 많은 변환이 필요할 것 입니다. 그래서 웬만하면 우리는 Modifier를 커스텀 한다면 공식문서에 적힌대로 Node API나 then을 이용하여 커스텀 해보는 것을 어떨까요? ㅎ_ㅎ

    이상 안드로이드 서정우 였습니다. 감사합니다.

    Reference

    https://developer.android.com/develop/ui/compose/mental-model?

     

    Compose 이해  |  Jetpack Compose  |  Android Developers

    이 페이지는 Cloud Translation API를 통해 번역되었습니다. Compose 이해 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose는 Android를 위한 현대적인 선언

    developer.android.com

    https://developer.android.com/develop/ui/compose/lifecycle?hl=ko

     

    컴포저블 수명 주기  |  Jetpack Compose  |  Android Developers

    이 페이지는 Cloud Translation API를 통해 번역되었습니다. 컴포저블 수명 주기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 컴포저블의 수명

    developer.android.com

    https://www.youtube.com/watch?v=BjGX2RftXsU

    modifier deep dive

    https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt

     

    https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt

     

    cs.android.com

     

    본 내용은 저의 주관적인 생각이 많이 들어가 있는 글입니다. 틀린 내용이 있다면 언제든 말씀해주시고 참고만 부탁드립니다!

     

Designed by Tistory.