Android

UI도 두들겨 보고 건너라 - modifier 알아보기

hyeumm0514 2024. 8. 7. 20:44

안녕하세요, 한끼족보의 Android 개발자 송혜음입니다

 

한끼족보 Android는 100% compose로 작성되었습니다

 

여러분은 Compose를 잘 알고 계신가요?

 

사실 Compose는 등장한지 오래되지 않았기때문에 아직 모르거나 잘알지 못하는 분들이 많이 계실 것 같아요

저와 같은 경우도 한끼족보 프로젝트에 들어가기전에는 XML만 사용했기때문에 초반에는 Compose에 익숙해지는데 시간이 필요했었습니다

 

 

Compose의 특징은 여러가지 있습니다

한끼족보 Android Lead 개발자인 동민님은 지난 아티클에서 특징으로 선언형 UI,컴포넌트화를 뽑아주셨는데

관련된 글은 아래 링크에서 확인실 수 있답니다 :)

 

한끼족보 Android 팀이 도입한 기술과 근거 모음.zip

안녕하세요 한끼족보 Android Lead 개발자 박동민입니다.오늘은 제가 한끼족보 프로젝트를 기획하며 도입하기로 결정한 기술들과, 그 기저에 깔려있는 근거들을 소개합니다.  저는 코드 한줄에도

teamhankki.tistory.com

 

기존의 XML 방식대로 코드를 작성하다보니 Compose는 어떻게 UI를 작성하는건지 헷갈렸던 부분들이 많았습니다

그래서 지난 트러블 슈팅을 바탕으로 오늘은 저와같은 컴린이(컴포즈 어린이)들을 위해 관련하여 아티클을 작성해보고자 합니다 :)

 


먼저 트러블 슈팅이 있었던 컴포넌트를 보여드리겠습니다

족보 컴포넌트

해당 컴포넌트에는 어떤 속성들이 들어갈까요?

족보 제목 및 족보 이미지가 들어가며 부가적인 속성으로는 배경과 둥근 모서리 정도가 있겠네요

 

 

 

그래서 저는 이러한 속성들을 생각나는대로 선언했습니다

음~ 배경색 주고~ 모서리 둥글게 하고~

 

그런데 웬걸 제가 머리속으로 그렸던 족보 컴포넌트와는 다른 컴포넌트가 생성되었습니다

분명 저는 모서리 둥글기를 12dp를 주었는데 프리뷰를 확인해보니 모서리 둥글기가 적용되지 않은 것입니다

120dp를 줘도 1200dp를 줘도 모서리는 여전히 직각이었습니다

 

 

 

무엇이 문제였을까요?

 

 

 

다음은 제가 초기에 작성한 코드입니다

 

@Composable
fun JogboItem(
    id :Long,
    title: String,
    image: Int,
    modifier: Modifier = Modifier,
    isEditMode: Boolean = false,
    isSelected: Boolean = false,
    editJogbo: () -> Unit,
    navigateToJogboDetail: (Long) -> Unit
) {
    Box(
        modifier = modifier
            .background(Gray100)
            .clip(RoundedCornerShape(12.dp))
            .run {
                if (!isEditMode) noRippleClickable(onClick = {navigateToJogboDetail(id)})
                else noRippleClickable(onClick = editJogbo)
            }
    ) {
        Column(
            modifier = Modifier
                .wrapContentSize(),

            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.SpaceBetween
        ) {
            Box {
                Column {
                    JogboItemText(
                        text = "\n\n",
                        color = Color.Transparent
                    )
                }
                JogboItemText(
                    text = title,
                    color = Gray800
                )
            }
            AsyncImage(
                model = image,
                contentDescription = "jogbo image",
                modifier = modifier.wrapContentSize(),
                contentScale = ContentScale.Crop
            )
        }
        if (isSelected) {
            Box(
                modifier = Modifier
                    .matchParentSize()
                    .border(
                        width = 2.dp,
                        color = Red,
                        shape = RoundedCornerShape(12.dp)
                    )
            ) {
                Image(
                    painter = painterResource(id = com.hankki.core.designsystem.R.drawable.ic_check_btn),
                    contentDescription = "check button",
                    modifier = Modifier
                        .wrapContentSize()
                        .align(Alignment.TopEnd)
                        .padding(
                            top = 14.dp,
                            end = 13.dp
                        )
                )
            }
        }
    }
}

 

 

여러분은 어떤 부분이 문제인지 감이 잡히시나요?

 

 

 

정답은 바로 선언의 순서에 있습니다

 

 

 

현재 문제가 되는 부분은 가장 상위 Box의 모양이므로 Box의 modifier 부분입니다

본격적으로 문제점과 해결방법을 말씀드리기전, modifier가 무엇인지 먼저 알아보도록 하겠습니다

 

공식문서에서는 modifier를 다음과 같이 설명하고 있습니다

modifier는 컴포저블을 장식하거나 강화할 수 있는 표준 Kotlin 객체입니다.
수정자를 통해 다음과 같은 종류의 작업을 실행할 수 있습니다.

- 컴포저블의 크기, 레이아웃, 동작 및 모양 변경
- 접근성 라벨과 같은 정보 추가
- 사용자 입력 처리요소를 클릭 가능, 스크롤 가능, 드래그 가능 또는 확대/축소 가능하게 만드는 높은 수준의 상호작용 추가

 

간단하게 요약하자면 컴포저블을 장식하는 하나의 decorator라고 생각하시면 될 것 같아요

modifier를 통해 다양한 속성들을 변경할 수 있답니다

 

여기서 주목할 점은 modifier는 먼저 나타나는 요소가 먼저 적용됩니다.

 

 

그래서 이게 무슨말이냐구요?

코드를 통해 두 컴포넌트의 모양을 비교해보도록 하겠습니다,modifier의 순서에 주목해주세요!

 

수정 전

    Box(
        modifier = modifier
            .background(Gray100)
            .clip(RoundedCornerShape(12.dp))
            .run {
                if (!isEditMode) noRippleClickable(onClick = {navigateToJogboDetail(id)})
                else noRippleClickable(onClick = editJogbo)
            }
    )

 

 

수정 후

    Box(
        modifier = modifier
            .clip(RoundedCornerShape(12.dp))
            .background(Gray100)
            .run {
                if (!isEditMode) noRippleClickable(onClick = {navigateToJogboDetail(id)})
                else noRippleClickable(onClick = editJogbo)
            }
    )

 

의도한 디자인을 구현하려면 modifiier의 background의 clip 속성의 순서를 반대로 설정해주어야합니다

 

 

기존 코드의 경우

  • background를 통해 컴포넌트의 배경영역만큼 색상을 칠한 뒤
  • clip을 통해 컴포넌트의 모양이 정의됩니다

 

수정된 코드의 경우

  • clip을 통해 컴포넌트의 모양을 정의한 뒤
  • background를 통해 컴포넌트의 배경영역에 색상이 칠해집니다

 

기존 코드는 clip이 나중에 선언되었으므로 이전의 선언한 배경색상에는 clip속성이 적용되지 않은 것입니다. 그러므로 개발자가 의도한 둥근 모서리 모양의 컴포넌트가 아닌 직각 모서리 모양의 컴포넌트로 보이게 되는 것이죠

 

반대로 수정된 코드는 clip을 처음에 선언했으므로 배경영역이 clip된 부분만큼 정의되어 둥근 모서리 모양의 컴포넌트로 보이게 되는 것이랍니다

 

 

동일한 속성을 선언했어도 어떤 속성 코드가 먼저 오느냐 에 따라 UI가 다르게 보일 수 있다는 이야기입니다

참 신기하지 않나요?

 

 

생각해보면 아주 당연하고 기본적인 개념인데

초반에는 이런 부분들을 생각하지않고 그저 생각나는대로,XML에서 했던 방식대로 UI를 제작했습니다

그래서인지 초반에는 컴포넌트를 만들때마다 순서를 다르게 선언하여 어떨땐 원하는 모양이 나오고 어떨땐 원하는 모양이 나오지 않아 무엇이 정답인지 헷갈려 매번 구글링을 했던 기억이 있네요 

 

 

.

.

.

오늘 작성한 내용은 정말 기초적이고 사소한 부분일 수 있지만

이런 부분부터 차근차근 하나씩 정리할 수 있는 개발자가 되길 바라며

그리고 자그만한 글이 여러분들에 도움이 되셨길 바라며 글을 마치도록 하겠습니다

 

 

읽어주셔서 감사합니다 더 좋은 글로 찾아오겠습니다 :)

 

야 너도 compose 할 수 있어