영어 데일리

컴포즈로 효율적인 UI 컴포넌트 디자인하기 (1)

현욱 정리장 2025. 2. 10. 11:37

https://medium.com/proandroiddev/designing-effective-ui-components-in-jetpack-compose-cb8d18f7f888

 

Designing Effective UI Components in Jetpack Compose

Since Google announced the Jetpack Compose 1.0 stable release, many companies have adopted Jetpack Compose to leverage its numerous…

proandroiddev.com

 

구글에서 컴포즈 1.0 스테이블 버전을 릴리즈한 이후로, 많은 회사들은 컴포즈의 많은 강점들을 활용하기 위해 도입했습니다. 컴포즈는 

컴포즈는 안드로이드 생태계에 널리받아들여졌고, 컴포즈는 라이브러리들과 sdk들의 통합 지원을 시작합니다. 

전통적으로 xml 기반에 프로젝트에서 UI 구성요소는 속성을 통해 사용자 정의할 수 있는 옵션과 함께 맞춤형 뷰로 제공되었습니다. 이 접근방식은 xml layout에서 컴포넌트들을 통합하기 쉽게 만들었지만, 이것은 여러 구성요소에서 테마 시스템을 일관되게 적용하는 것에 어려움을 느끼게 했으며, 기본 뷰 클래스에서 노출된 메서드로 인해 API 오용문제가 발생했다 

 

컴포즈는 기존의 커스텀 뷰와 비교했을때 근본적으로 다른 전략을 제공합니다. 이것은 선언적인 구조를 제공하며 좀 더 직관적이고 플렉시블한 API 디자인입니다. 이러한 변화는 라이브러리 및 sdk 개발자 뿐만 아니라, UI컴포넌트를 공유하는 대규머 팀에게도 이점을 제공하며, 그들에게 더 나은 개발 관행을 강제하도록 돕고, API의 오용을 줄이며 전반적으로 개발자 경험을 향상시킬수 있습니다.

 

이 기사에서는 컴포즈로 UI컴포넌트를 디자인할때에 효율적인 전략에 대해서 소개하며, 좋은 예제는 Stream Video SDK에 있습니다. 

 

Modifier 좋은 예제 

Modifier 은 컴포즈에서 강력한 API로, UI 요소를 체이닝 방식과 컴포저블 방시긍로 꾸미고 확장할 수 있도록 해줍니다.  하지만 이것은 사려깊게 사용해야합니다. 또한 Modifier의 속성은 다른 컴포저블 함수로 전파될 수 있고, 적절한 관리를 하지 않으면 의도하지 않은 효과를 초래할 수도 있습니다. 

 

특히 Modifier 함수의 순서는 중요합니다. 각 함수는 이전 함수가 반환한 modifier 또는 컴포저블 외부에서 전달된 modifier를 수정합니다. 이 순서는 최종 출력에 직접적인 영향을 미칩니다.  이 섹션에서는 우리는 3개의 원칙을 설명하고, 컴포즈에서 효과적이고 예측 가능한 UI 컴포넌트 API를 설계하는데에 도움되는 가이드를 제공하겠습니다. 

 

1. Modifier는 컴포넌트의 최상위 레이아웃에 적용하라 

컴포즈의 Modifier들은 레이아웃 계층을 통해 아래로 전달됩니다. 하지만 Modifier들은 이상적으로 컴포저블 함수에서 최상위 레이아웃 노드에만 적용하는 것이 좋습니다. 계층 구조 내에서 임의의 레벨에서 modifier들을 적용하게 되면 예기치못한 행동이 계층에서 이뤄질수도 있고, 사용자가 이를 잘못 사용할 가능성을 높이고,  컴포넌트를 예측가능하기 어렵게 만들고, 효율적인 사용을 어렵게 합니다. 

 

예를들어, 하단 예제를 보며 라운드 모양인 버튼을 나타내는 컴포넌트가 있다고 가정해보겠습니다.

@Composable
fun RoundedButton(
  modifier: Modifier = Modifier,
  onClick: () -> Unit
) {
  Button(
    modifier = modifier.clip(RoundedCornerShape(32.dp)),
    onClick = onClick
  ) {
    Text(
      modifier = Modifier.padding(10.dp),
      text = "Rounded"
    )
  }
}

 

그러나 Modifier를 Button 대신에 텍스트에 적용해서는 안됩니다. 아래처럼 레이아웃 계층에 최상단 컴포저블 함수에 넣어야합니다.

@Composable
fun RoundedButton(
  modifier: Modifier = Modifier,
  onClick: () -> Unit
) {
  Button(
    modifier = Modifier.clip(RoundedCornerShape(32.dp)),
    onClick = onClick
  ) {
    Text(
      modifier = modifier.padding(10.dp), // Don't do this
      text = "Rounded"
    )
  }
}

 

이 커스텀 컴포저블 함수의 주요 목적은 라운드버튼이고, 이는 버튼을 나타내는거지, 텍스트를 나타내는것이 아닙니다. 그러므로  주요 컴포넌트를 생성할떄에 초점이나 목적이 변하지 않도록해야합니다. 

 

추가로 만약 레이아웃 계층이 복잡하게 되거나, Modifier 를 컴포저블 함수 내에 중간 레벨에서 적용하게 될 경우 사용자가 최종적으로 어떤 Modifier에 컴포ㅓㄴ트에 적용되는지 예측하기 어려워질 수 있습니다. 이 명확성 부족은 혼란과 오용을 초래할 수 있습니다. 

 

만약 너가 유저에게 버튼애 내부 콘텐츠를 수정할 수 있게 유연성을 제공하려면, 너는 슬롯을 사용함으로써 달성할 수 있습니다. 하단에 설명으로 시연해보겠습니다. 

@Composable
fun RoundedButton(
  modifier: Modifier = Modifier,
  onClick: () -> Unit,
  content: @Composable RowScope.() -> Unit
) {
  Button(
    modifier = modifier.clip(RoundedCornerShape(32.dp)),
    onClick = onClick
  ) {
    content()
  }
}

 

2. Modifier에 하나의 파라메터만 사용

컴포넌트의 구조를 제한하면서 레이아웃 계층 내에 특정 구성 요소에 적용할 여러 Modifier의 매개변수를 받을 수 있는게 가능한건지 궁금할 수도 있다. 아래 예제 처럼 

@Composable
fun RoundedButton(
  modifier: Modifier = Modifier,
  textModifier: Modifier = Modifier,
  onClick: () -> Unit,
) {
  Button(
    modifier = modifier.clip(RoundedCornerShape(32.dp)),
    onClick = onClick
  ) {
    Text(
      modifier = textModifier.padding(10.dp),
      text = "Rounded"
    )
  }
}

 

그러나 Modifier는 본질적으로 . 단일 체이닝 가능한 매게변수 설계되어있어, 사용자가 컴포저블 함수의 외부동작과 외관을 정의할 수 있도록 한다. 

컴포저블에서 여러 Modifier 매개변수를 도입하면 불필요한 복잡성이 증가되고, 오용의 위험이 증가하며, 컴포즈의 직관적이고 예측가능

한 API 원칙에서 벗어나게 된다. 

 

슬롯베이스 접근방식을 사용하는 것은 유저에게 유연성을 주고 외부 컨텐츠에서 커스터마이즈 할 수 있게 한다. 

여러 Modifier 매개변수를 추가하는 것 대신, 슬롯을 정의하여 사용자가 커스텀 콘텐츠를 제공할 수 있도록 하면서도, 외부 커스텀화를 위한 단일 Modifier는 여전히 유지할 수 있습니다.

 

@Composable
fun RoundedButton(
  modifier: Modifier = Modifier,
  onClick: () -> Unit,
  content: @Composable RowScope.() -> Unit
) {
  Button(
    modifier = modifier.clip(RoundedCornerShape(32.dp)),
    onClick = onClick
  ) {
    content()
  }
}

 

3. 컴포넌트 간의 Modifier들 재사용 피하기 

다른 중요한 고려사항은 컴포넌트들을 설계할때 제공된 Modifier 인스턴스를 재사용하지 않는 것입니다. 

몇몇의 개발자들은 매번 컴포넌트마다 Modifier객체를 생성하는 것을 메모리 사용 증가와, 부정적인 영향을 끼칠까봐 걱정할 수도 있습니다. 특히 복잡한 레이아웃 계층에서 많은 modifiers를 사용할때요.

 

하지만 이 걱정은 일반적으로 근거가 없습니다. 컴포즈에서 Modifier가 최적화된 방식으로 구현되어있기 때문에요.

Modifiers는 컴포즈 함수 내에 단일 레이아웃 노드에 적용되도록 의도되어있습니다. 이를 통해 명확하고 예측 가능한 동작을 보장합니다.

 

만약 같은 Modifier가 레이아웃 계층 내에 여러 컴포저블 내에 사용되면 의도하지 않은 부작용과 예측 불가능한 동작이 발생하여 컴포넌트의 일관성과 사용성이 저하될 수 있습니다. 

 

예제로 시나리오를 고려해보자. 같은 Modifier 파라메터를 전체 레이아웃 계층에서 재사용할 경우, 하단은 예제입니다. 

@Composable
fun MyButtons(
  modifier: Modifier = Modifier,
  onClick: () -> Unit,
) {
  Column(modifier = modifier) {
    Button(
      modifier = modifier,
      onClick = onClick
    ) {
      Text(
        modifier = modifier.padding(10.dp),
        text = "Rounded"
      )
    }

    Button(
      modifier = modifier,
      onClick = onClick
    ) {
      Text(
        modifier = modifier.padding(10.dp),
        text = "Not Rounded"
      )
    }
  }
}

 

언뜻보기에, 이 코드는 정확하게 동작하는 것처럼 보입니다. 그러나 호출하는 위치에서 Modifier를 수정하면 전체 레이아웃이 의도되지 않게 변경되는 예기치 않은 동작이 발생할 수 있습니다. 

MyButtons(
  modifier = Modifier
    .clip(RoundedCornerShape(32.dp))
    .background(Color.Blue)
) {}
view raw

 

적절한 행동 보장과 예기치 못한 이슈를 피하기 위해, 여러 컴포넌트들 사이에 Modifier 재사용을 자제해야합니다. 이 섹션에서는 

컴포즈 컴포넌트를 설계할 때에 Modifier들을 관리하는 좋은 예제에 대해 인사이트를 얻고, 다음번에는 테마를 구현할떄에 일관된 UI스타일을 제공하는 방법에 대해 알아보겠습니다. 

 

 

 

일관된 UI 테마 

이제  일관된 스타일을 공유하며  다양한 컴포넌트를 제공할 수 있다고 상상해보겠습니다.

이러한 컴포넌트들이 개별적으로 제공된다면, 컴포넌트간에 일관된 스타일을 유지하는 책임은 전적으로 사용자에게 맡겨집니다. 

이것은 상당히 어려울수 있는데. 각 컴포넌트가  스타일을 커스터마이징 하기 위해 서로 다른 API를 노출할 수도 있기에 이를 동기화 작업이 번거롭고 오류가 발생하기 쉬워집니다. 

 

이 니사리오에서는 컴포즈 머터리얼 라이브러리에서 제공하는 MaterialTheme API에서 영감을 얻을 수 있습니다. 

핵심 요점은 일관된 컴포넌트 스타일을 유지하면서도 사용자가 원활하게 커스터마이징을 할 수 있고, 다양한 컴포넌트에서 스타일을 지속적으로 유지할 수 있도록 하는 것을 보장하는 것입니다. 

 

컴포즈의 Stream Video SDK는 VideoTheme이라는 전용 테마 API를 제공하여 모범사례를 시연합니다. 

VideoTheme API는 SDK에서 제공하는 모든 COmpose 컴포넌트에 대해 칼라, 디멘션, 타이포그래피, 모양, ripple effect, 그외가 포함되어있는 sdk를 제공하고 보장합니다. 

setContent {
    VideoTheme(
        colors = StreamColors.defaultColors().copy(appBackground = Color.Black),
        dimens = StreamDimens.defaultDimens().copy(callAvatarSize = 72.dp),
        shapes = StreamShapes.defaultShapes().copy(
            avatar = RoundedCornerShape(8.dp),
            callButton = RoundedCornerShape(16.dp),
            callControls = RectangleShape,
            callControlsButton = RoundedCornerShape(8.dp)
        )
    ) {
        CallContent(
            modifier = Modifier.fillMaxSize(),
            call = call,
            onBackPressed = { finish() },
        )
    }
}
view raw

 

위에 예제처럼 Stream SDK에서 제공하는 컴포넌트를 VideoTheme으로 감싸면 사용자 정의 스타일들이 자동으로 모든 컴포넌트에 일관되게 적용됩니다. 

 

이 접근방식은 사용자가 UI의 일관성을 어려움 없이 유지하고 동시에 테마를 조정하면서도 애플리케이션 디자인 요구사항에 맞게 조정할 수 있도록 해줍니다. 

 

기타

leverage 활용하다

widespread 널리 받아들여진 

ecosystem 생태계 

introduced challenges 문제를 발생시키다. 

the misues if ~의 오용 

fundamentally 근본적으로 

intuitive 직관적인 

this shift 이러한 변화 

enforce 시행하다 

augment 확장 

manner 방식 

thoughtfully 사려깊게 

 propagate 전파 

unintended 의도하지 않은 

crucial 결정적인 

ideally 이상적으로 

arbitrary 임의의

likelihood 가능성

ultimately 최종적으로 

lack 부족

restricting 제한하다 

inherently 본질적으로 

deviate 일탈 

compromising 저하 되다

consistency 일관성

at first galnce, 언뜻보기에

potentially 잠재적으로

refrain 자제하다 

quite challenging 상당히 어렵다 

cumbersome 성가신 

inspiration 영감을 얻다 

dedicate 전용 

uniformity 균일성 

effortlessly 어려움 없이