복잡한 UI를 표시하는 stateless 컴포저블에는 많은 매개변수가 포함될 수 있다. 매개변수가 많지 않고, 매개변수가 직접적으로 컴포저블을 설정하는 경우, 이건 괜찮다. 그러나 때로는 하위 컴포저블을 설정하기 위해 매개변수를 전달해야 한다.

우리의 네오-모던 인터렉티브 디자인에서는 디자이너가 Add 버튼을 상단에 유지하고, 인라인 에디터에는 두개의 이모지 버튼으로 교체하기를 원한다. 이 경우를 처리하기 위해 TodoItemInput에 더 많은 매개변수를 추가할 수 있지만, 이것이 실제로 TodoItemInput의 책임인지는 확실하지 않다.

우리에게 필요한 것은 컴포저블이 미리 구성된 버튼 섹션을 가져오는 방법이다. 이렇게 하면 호출자가 버튼을 구성할 수 있지만 TodoItemInput으로 구성하는 데 필요한 모든 state를 공유하지 않고도 버튼을 구성할 수 있다.

이렇게 하면 상태 stateless 컴포저블에 전달되는 매개변수의 수를 줄이고 재사용 가능성을 높일 수 있다.

미리 구성된 섹션을 전달하는 패턴으로 slot이 있다. slot은 컴포저블에 대한 매개변수로, 호출자가 화면의 한 섹션을 형성할 수 있도록 한다. 내장된 컴포저블 API를 통해 slot 예제를 찾을 수 있다. 가장 일반적으로 사용되는 예제중 하나는 Scaffold다.

Scaffold는 화면의 topBar, bottomBar, body와 같은 Material 디자인의 전체 화면을 설명하기 위한 컴포저블이다.

화면의 각 섹션을 구성하기 위해 수백 개의 매개변수를 제공하는 대신 Scaffold는 원하는 컴포저블로 채울 수 있는 slot을 노출시킨다. 이는 Scaffold에 대한 매개변수의 수를 줄이고, 재사용성을 높인다. 커스텀 topBar를 만들고 싶다면 Scaffold가 기꺼이 표현해준다.

@Composable
fun Scaffold(
   // ..
   topBar: @Composable (() -> Unit)? = null,
   bottomBar: @Composable (() -> Unit)? = null,
   // ..
   bodyContent: @Composable (PaddingValues) -> Unit
) {

Slot은 호출자가 화면의 한 섹션을 표현하도록 하는 컴포저블 함수 매개변수들이다.

slot은 매개변수 타입이 @Composable() -> Unit 과 함께 선언한다.

TodoItemInput 에 slot 정의하기

// TodoScreen.kt
@Composable
fun TodoItemInput(
   text: String,
   onTextChange: (String) -> Unit,
   icon: TodoIcon,
   onIconChange: (TodoIcon) -> Unit,
   submit: () -> Unit,
   iconsVisible: Boolean,
   buttonSlot: @Composable () -> Unit
) {
  // ... 

이것은 호출자가 원하는 버튼으로 채울 수 있는 일반적인 slot이다. 헤더 및 인라인 에디터에서 다른 버튼을 지정하는데 사용할 것이다.

buttonSlot의 내용 보여주기

TodoEditButton에 대한 호출을 slot의 내용으로 교체한다.

// TodoScreen.kt
@Composable
fun TodoItemInput(
   text: String,
   onTextChange: (String) -> Unit,
   icon: TodoIcon,
   onIconChange: (TodoIcon) -> Unit,
   submit: () -> Unit,
   iconsVisible: Boolean,
   buttonSlot: @Composable() () -> Unit,
) {
   Column {
       Row(
           Modifier
               .padding(horizontal = 16.dp)
               .padding(top = 16.dp)
       ) {
           TodoInputText(
               text,
               onTextChange,
               Modifier
                   .weight(1f)
                   .padding(end = 8.dp),
               submit
           )

           // 새로운 코드: TodoEditButton에 대한 호출을 slot의 내용으로 교체한다

           Spacer(modifier = Modifier.width(8.dp))
           Box(Modifier.align(Alignment.CenterVertically)) { buttonSlot() }

           
           // 새로운 코드의 끝
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, onIconChange, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

buttonSlot()을 직접적으로 호출할 수도 있지만, align을 중앙에 정렬을 유지해야 하기 때문에 Box내에 slot을 배치한다.

Slot을 사용하기 위해 stateful한 TodoItemEntryInput 수정하기

이제 buttonSlot을 사용하도록 호출하는 쪽을 수정해야 한다. 먼저 TodoItemEntryInput을 수정하도록 하자

// TodoScreen.kt
@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, onTextChange) = remember { mutableStateOf("") }
   val (icon, onIconChange) = remember { mutableStateOf(TodoIcon.Default)}
   
   val submit = {
        if (text.isNotBlank()) {
            onItemComplete(TodoItem(text, icon))
            onTextChange("")
            onIconChange(TodoIcon.Default)
        }
   }
   TodoItemInput(
       text = text,
       onTextChange = onTextChange,
       icon = icon,
       onIconChange = onIconChange,
       submit = submit,
       iconsVisible = text.isNotBlank()
   ) {
       TodoEditButton(onClick = submit, text = "Add", enabled = text.isNotBlank())
   }
}

buttonSlot은 TodoItemInput의 마지막 매개변수이므로 후행 람다 구문을 사용할 수 있다. 그런 다음 람다에서 이전처럼 TodoEditButton을 호출한다.

Slot을 사용하기 위해 TodoItemInlineEditor 수정하기

리팩터링을 완료하려면 TodoItemInlineEditor도 slot을 사용하도록 변경해야한다.

// TodoScreen.kt
@Composable
fun TodoItemInlineEditor(
   item: TodoItem,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit,
   onRemoveItem: () -> Unit
) = TodoItemInput(
   text = item.task,
   onTextChange = { onEditItemChange(item.copy(task = it)) },
   icon = item.icon,
   onIconChange = { onEditItemChange(item.copy(icon = it)) },
   submit = onEditDone,
   iconsVisible = true,
   buttonSlot = {
       Row {
           val shrinkButtons = Modifier.widthIn(20.dp)
           TextButton(onClick = onEditDone, modifier = shrinkButtons) {
               Text(
                   text = "\uD83D\uDCBE", // floppy disk
                   textAlign = TextAlign.End,
                   modifier = Modifier.width(30.dp)
               )
           }
           TextButton(onClick = onRemoveItem, modifier = shrinkButtons) {
               Text(
                   text = "❌",
                   textAlign = TextAlign.End,
                   modifier = Modifier.width(30.dp)
               )
           }
       }
   }
)

여기서는 buttonSlot을 이름이 있는 매개변수로 전달했다. 그런 다음 buttonSlot에서 인라인 편집기 디자인을 위한 두 개의 Button을 포함하는 Row를 만든다.

앱 다시 시작하기

앱을 다시 실행하고 인라인 편집기를 사용해보자.

자세히 보면 편집기에 들어가고 나갈 때 아이콘 투명도 색상이 변경되는 것을 알 수 있다. 이는 투명도 색상이 TodoRow에 기억되어 편집기를 열 때 제거되었다가 컴포지션에 다시 추가되기 때문이다.

이 색상이 변경되지 않도록 하려면 ViewModel로 state를 hoisting하면된다.

이 섹션에서는 호출자가 화면의 섹션을 제어할 수 있는 slot을 사용하여 stateless 컴포저블을 커스터마이징 했다. slot을 사용하여, 추후에 추가될 수 있는 모든 다른 디자인과 TodoItemInput가 함께 결합(coupling)되는 것을 방지했다.

stateless 컴포저블에 매개변수를 추가하여 하위요소를 커스터마이징하는 경우 slot이 더 나은 디자인인지 평가해보자. slot은 매개변수의 수를 관리 가능한 상태로 유지하면서 컴포저블을 더 재사용할 수 있게 만드는 경향이 있다.

카테고리: Compose

2개의 댓글

이상효 · 2021년 12월 6일 6:33 오후

덕분에 Compose 공부 좀 더 수월하게 하고 있습니다 감사합니다

    Charlezz · 2021년 12월 7일 9:07 오전

    도움이 되서 기쁩니다 🙂 컴포즈 공부 화이팅!

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.