ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [안드로이드] Compose architectural layering
    안드로이드 2021. 9. 16. 02:57
    반응형

    개요

    이번에는 Compose의 아키텍처 레이어링에 대해 알아보자. 내용은 안드로이드 개발자 사이트를 기반으로 작성하였다.

     

    Jetpack Compose 아키텍처 레이어링

    Jetpack Compose는 모놀리식 프로젝트가 아닌 완전한 스택을 만들기 위한 여러가지 모듈의 조합으로 만들어졌다.

    Jetpack Compose를 구성하는 모듈을 이해한다면 아래와 같은 기능을 개발하는데 용이하다.

    • 적절한 수준의 추상화를 사용하여 앱 및 라이브러리를 빌드 할 수 있다.
    • 보다 세부적인 제어나 맞춤 설정을 위한 낮은 수준으로 드롭다운 할 수 있는 경우를 파악할 수 있다.
    • 종속 항목을 최소화 할 수 있다.

    레이어

    Jetpack Compose의 기본 레이어

    각 레이어는 하위 수준 기반으로 상위수준의 구성요소를 만들기 위해 기능을 결합한다. 각 레이어는 하위 레이어의 공개 API를 기반으로하여 모듈의 경계를 확인하고 필요한 경우 이를 대체할 수 있다. 아래는 이러한 레이어에 대한 설명이다.

    Runtime

    이 모듈은 remember, mutableStateOf, @Compoable, SideEffect와 같은 Compose 런타임 기초를 제공한다. UI가 아닌 Compose의 트리 관리 기능만 필요한 경우 런타임 레이어에 바로 빌드하는 것이 좋다.

    UI

    UI레이어는 여러개의 모듈(ui-text, ui-graphics, ui-tooling 등)로 구성된다. 이런한 모듈은 LayoutNode, Modifier, 입력 핸들로(input handler), 커스텀 레이아웃(custom layout), 그리기(drawing)와 같은 UI 툴킷의 기본사항을 구현한다. UI툴킷의 기본 개념만 필요한 경우 이 레이어를 기반으로 빌드하는 것을 고려해야한다.

    Foundation

    이 모듈은 Compose UI에 Row, Column, LazyColumn, 특정 동작 인식 등과 같이 디자인 시스템에 구속되지 않는 컴포넌트를 제공한다. 자체 디자인 시스템을 만들 경우에는 Foundation 레이어를 기반으로 빌드하는 것이 좋다.

    Material

    이 모듈은 Compose UI에 Material 디자인 시스템을 제공하고 테마 설정 시스템, 스타일된 컴포넌트, 물결표시, 아이콘을 제공한다. 앱에서 Material 디자일을 사용할 때 이 레이어를 기반으로 빌드한다. 

     

    디자인 원칙

    Jetpack Compose의 기본 원칙은 조립하거나 구성할 수 있는 작고 집중된 기능을 제공하는 것이다. 이 접근은 몇가지 이점을 가지고 있다.

    Control

    높은 수준의 컴포넌트들은 많은 것을 하지만 직접 컨트롤하는 것을 제한하고 있다. 만약 더 많은 컨트롤이 필요하다면 하위 수준의 컴포넌트를 사용도록 드롭다운 하면 된다.

     

    예를 들어 컴포넌트의 색상을 변경하는데 애니메이션을 주고 싶다면 animateColorAsState API를 사용하면 된다.

    val color = animateColorAsState(if (condition) Color.Green else Color.Red)

    그러나 만약 시작 색상을 회색으로 지정하고 싶다면, 이 API로는 할 수 없다. 대신에 하위 수준의 Animatable API로 드롭다운 할 수 있다.

    val color = remember { Animatable(Color.Gray) }
    LaunchedEffect(condition) {
        color.animateTo(if (condition) Color.Green else Color.Red)
    }

    상위 수준의 API인 animateColorAsState는 하위 수준의 API인 Animatable을 기반으로 만들어졌다. 하위수준의 API를 사용하는 것을 더 복잡하지만 더 많은 제어를 할 수 있도록 제공한다. 그렇기 때문에 필요한 정도의 수준의 추상화를 선택해서 사용하는게 좋다.

     

    Customization

    작은 블록으로 조립된 상위 수준의 컴포넌트는 커스터마징하기 훨씬 더 쉽게 해준다. 

    예를 들어 Material 레이어에서 제공하는 Button의 구현을 살펴보자.

    @Composable
    fun Button(
        // …
        content: @Composable RowScope.() -> Unit
    ) {
        Surface(/* … */) {
            CompositionLocalProvider(/* … */) { // set LocalContentAlpha
                ProvideTextStyle(MaterialTheme.typography.button) {
                    Row(
                        // …
                        content = content
                    )
                }
            }
        }
    }

    Button은 4개의 컴포넌트로 조합되어 있다.

    1. Surface
      • 배경, 모양, 클릭 핸들링 등을 제공한다. 
    2. CompositionLocalProvider
      • 컴포넌트가 enable, disable 될 때 내용의 alpha 값을 변경한다.
    3. ProvidedTextStyle
      • Default 텍스트 스타일을 설정한다.
    4. Row
      • 버튼 내용의 기본 레이아웃 정책을 제공한다.

    위 코드는 구조를 더 명확하게 보기 위해 몇 파라메터와 주석을 생략했다. 하지만 전체 컴포넌트는 Button을 구현하기 위해서 구성하는 4개의 컴포넌트로 단순하게 구성되어 있으며 code는 약 40줄밖에 되지 않는다. Button과 같은 컴포넌트들은 노출되는 파라메터가 편향적이어서 컴포넌트를 사용하기 어렵게 만들 수 있는 커스터마이징으로 인한 파라메터 증가의 균형을 맞춘다. 예를 들어 Material 컴포넌트는 Material 디자인 시스템에서 지정된 커스터마이징을 제공하여 Meterial 디자인 시스템의 원칙을 따르기 쉽게 만들어준다. 

     

    그러나 만약 파라메터 이외의 커스터마이징이 필요하다면 수준을 드롭다운하고 컴포넌트를 fork하면 된다. 예를 들어 Material 디자인에서 제공하는 Button은 단색의 background 색상만 지정이 가능하다. 하지만 그라데이션의 background를 넣고 싶다면 Material에서 제공하는 Button 컴포넌트는 이를 지원하지 않는다. 이런 경우 Button의 구현을 참조하여 자신의 컴포넌트를 만들 수 있다.

    @Composable
    fun GradientButton(
        // …
        background: List<Color>,
        content: @Composable RowScope.() -> Unit
    ) {
        Row(
            // …
            modifier = modifier
                .clickable(/* … */)
                .background(
                    Brush.horizontalGradient(background)
                )
        ) {
            CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
                ProvideTextStyle(MaterialTheme.typography.button) {
                    content()
                }
            }
        }
    }

    위 구현은 Material의 현재 내용 alpha와 텍스트 스타일의 material 개념과 같은 Material 레이어의 컴포넌트를 계속 사용하고 있다. 그러나 Surface를 Row로 변경하고 스타일을 지정하여 원하는 모양을 만들 수 있다.

     

    만약 Material의 모든 개념을 사용하지 않고 자신의 맞춤 디자인 시스템을 만들고 싶다면 Foundation 레이어의 컴포넌트만 순수하게 사용하여 드롭다운 할 수 있다.

    @Composable
    fun BespokeButton(
        // …
        content: @Composable RowScope.() -> Unit
    ) {
        Row(
            // …
            modifier = modifier
                .clickable(/* … */)
                .background(/* … */)
        ) {
            // No Material components used
            content()
        }
    }

    Jetpack Compose에서는 최상위 수준의 컴포넌트는 가장 단순한 이름을 예약하고 있다. 예를 들어 androidx.comopse.material.Text는 androidx.compose.material.BasicText를 기반으로 만들어 졌다. 이렇게 하면 상위 수준의 컴포넌트를 대체하려고 할 때 자체 구현에 찾기 쉬운 이름을 제공할 수 있다.

     

    정확한 추상화 선택

    재사용가능한  계층화된 컴포넌트를 빌드한다는 Compose의 철학의 의미는 항상 하위 수준의 컴포넌트만 추구해서는 안된다는 뜻을 담고 있다. 대다수의 최상위 컴포넌트들은 많은 기능을 제공하고 있고 접근성 지원과 같은 권장사항도 구현하는 경우도 많기 때문이다.

     

    예를 들어 Modifier.pointerInput의 하위 수준의 컴포넌트를 사용하여 스크롤, 드래그, 스와이프를 구현하는 것 보다 이미 구현되어 있는 최상위 컴포넌트인 Modifier.scrollable, Modifer.draggable, Modifier.swipeable을 사용하는게 구현하는데 시간도 절약하고 생각지도 못한 버그를 생성하지 않게 된다.

     

    일반적으로 권장사항이 구현되어 이 이점을 누리기 위해 최상위 수준의 컴포넌트를 사용하는게 좋다. 

     

    마치며 

    회사 일이 바빠 시간을 쪼개 Compose의 아키텍처 레이어링을 살펴보았다. Compose의 철학과 개념을 더 확실하게 알 수 있게 되었고 커스터마징하는데 꼭 하위 수준의 컴포넌트를 사용하지 않아도 된다는 것도 알게 되었다. 

    반응형

    댓글

Designed by Tistory.