한글 같은 Jetpack Compose

Jetpack Compose는 API가 설계된 과정에 대한 글이나 코드를 읽어보면 읽어볼수록 아름답게 느껴진다. 미술관에서 한 그림앞에 팔짱 끼고 가만히 오랜 시간 감상하는 사람들이 이런 기분일까? 이 기분을 다른 사람들과 공유하기 위해서 이 글에서는 Compose가 기존 안드로이드 View의 문제를 해결하기 위해서 어떤 우아한 방법을 사용했는지 우리가 익숙하게 사용하고 있는 한글에 비유해서 써보려고 한다. 이 글이 Compose의 아름다움을 사람들에게 전도해서 조금이라도 점유율을 높여주면 좋겠다 🥰

본론

한글의 자모는 자음, 모음, 받침으로 이루어져 있고 우리들은 이 요소들을 조합해서 아주 많은 종류의 표기와 발음을 만들어 낸다. 일본의 가나에서는 이런 특징이 나타나지 않는데, 더 이상 분해할 수 없는 문자인 "が"가 "가"라는 발음을 낸다. 또한 한글은 자음 "ㄱ"에 "ㅗ"를 합성해서 "고"를 만들 수 있지만, 일본 가나는 아예 별개의 문자인 "ご"로 표기해야 한다. 이렇게 볼 수 있듯 한글은 24개라는 적은 개수의 자모만을 이용해서 만 개가 넘는 표기와 발음을 만들어낼 수 있다는 것이 장점이다. 이런 특징을 가진 문자를 표음 문자 중에서도 음소 문자라고 한다.

한글 창제에서 신기한 점은, 세종대왕이 우리가 내는 소리를 자음과 모음으로 나눌  수 있다는 사실을 알았다는 것이다. 이 사실을 몰랐다면 "가", "고""거"처럼 우리가 자음 "ㄱ"을 사용해서 만드는 모든 표기에 대응하는 문자를 따로따로 만들었어야 했을 것이다. 이 의미를 좀 더 조명하기 위해서 지금 우리가 쓰는 현대 한글에서 새로운 표기를 추가해보자. 옛한글의 반시옷(ㅿ)은 어떨까? 자음은 겨우 한 개밖에 추가하지 않았지만, "ᅀᅡ""ᅀᅥ" 같이 모음과 받침을 조합하면 꽤나 많은 수의 표기가 추가된다. 엄청난 확장성이 느껴지지 않는가?

Jetpack Compose도 UI를 구성함에 있어서 한글과 비슷한 전략을 취한다. UI를 구성할 때는 크게 1)배치와 2)모양이라는 두 가지 요소를 고려한다. 기존의 안드로이드 View 시스템에서는 1)onMeasureonLayout 두 가지 메서드를 오버라이딩 해서 어떻게 배치할지를 정의하고, 2)onDraw를 오버라이딩 하거나 ViewGroup에 자식을 추가해서 모양을 만든다. 이 모든 것들을 하나의 클래스에서 관리하는 점에서 기존 안드로이드 View 시스템은 하나의 문자가 발음을 완전히 결정하는 일본의 가나 문자와 많이 닮아있다. 한 클래스에 들어가버린 것들은 분리하거나 재사용하기 어려워진다. 하지만 Jetpack Compose는 레이아웃의 속성을 부여하는 Modifier와 레이아웃 노드를 생성하는 부분이 분리됨에 따라 모든 노드에 공통적으로 적용할 수 있는 레이아웃 속성을 손쉽게 특정한 뷰 코드에서 분리할 수 있게 되었다.

예를 들어보자면 안드로이드 기존 View 시스템에서 margin 속성은 ViewGroup.MarginLayoutParams에서 정의하고, 여기서 얻을 수 있는 정보를 이용해서 onMeasure 단계에서 마진 값을 계산한다. 즉, ViewGroup이 마진의 존재를 알고 있어야 다른 모든 ViewGroup을 상속받는 레이아웃 클래스가 마진을 사용할 수 있다는 것이다.

이 문제 때문에 겪을 수 있는 문제의 예시로는 maxWidth 속성이 있다. 직관적으로 생각했을 때 너비는 모든 뷰가 가지고 있는 속성이기 때문에 레이아웃 XML에  android:maxWidth="100dp"를 붙이면 최대 너비가 100dp가 될 것이라고 생각할 수 있다. 하지만 틀렸다. maxWidthView에 정의되어 있지 않고 TextView 같은 일부 자식 클래스에서만 정의되어 있기 때문에, LinearLayout은 물론 FrameLayout에서도 사용할 수 없다. 우리가 직접 커스텀 뷰로 정의해야만 사용할 수 있다. 당연하게도 커스텀 뷰를 사용하는 방식에는 한계가 존재하는데, 기존에 이미 존재하는 LinearLayout에서는 이 속성을 여전히 사용할 수 없다는 것이다. 이 문제를 근본적으로 해결할 수 있는 방법은 구글이 모든 뷰가 상속 받는 View 클래스에 모든 노드가 공통적으로 사용할 수 있는 모든 속성을 구현해두는 것이지만, View 클래스가 이미 더 이상 관리하기 어려울 수준의 방대한 코드(> 30000줄)를 가져버리게 된 것에서 볼 수 있듯 현실적으로는 어려운 방안이다.

반면 Jetpack Compose는 레이아웃의 속성을 모든 노드에서 동일한 조건으로 적용할 수 있도록 모델링했다. 다시 말해서 Text composable 함수 + Modifier.padding을 하면 패딩을 가진 Text 노드가 나온다는 것이다.

@Composable
fun TextWithPadding() {
    Text(
        text = "Hello Compose!",
        modifier = Modifier.padding(8.dp)
    )
}

기존 안드로이드 뷰에서 사용하기 어려웠던 최대 너비 속성은 어떻게 적용할까? Text는 그대로 사용하고 다른 Modifier를 갖다 붙이면 되지 않을까? 정답이다.

@Composable
fun TextWithMaxWidth() {
    Text(
        text = "Hello maxWidth!",
        modifier = Modifier.widthIn(max = 100.dp)
    )
}

마치 한글이 "가"를 만들 때 "ㄱ" + "ㅏ"를 하고 "고"를 만들 때 "ㄱ" + "ㅗ"를 사용하듯이 Jetpack Compose에서는 Text + Modifier.padding을 하거나 Text + Modifier.widthIn을 사용할 수 있다.

이제 Modifier들은 그대로 두고, 위에서 반시옷(ㅿ)을 추가한 것 같이 Text 대신 TextField를 추가해보면 어떨까?

@Composable
fun TextFieldWithPadding() {
    TextField(
        modifier = Modifier.padding(8.dp),
        ...
    )
}

@Composable
fun TextFieldWithMaxWidth() {
    TextField(
        modifier = Modifier.widthIn(max = 100.dp),
        ...
    )
}

여기서 추가된 것은 TextField 하나밖에 없지만 표현 가능한 UI는 훨씬 많다. 기존의 안드로이드 View 시스템에서는 View에 정의되어 있지 않은 속성은 사용할 수 없었지만 Jetpack Compose에서는 어떤 Modifier를 추가하든 모든 노드에서 사용할 수 있게 되는 것이다.

구글이 Jetpack Compose 프로젝트를 시작한 이유가 너무 방대해진 View의 유지보수가 어려웠기 때문임을 고려하면 매우 합리적인 결정이라고 생각한다. (기존 View 시스템을 유지보수 하는 팀이 Jetpack Compose를 만들고 있다!)

지금까지 배치에 관한 것만을 중점적으로 다뤘지만 Modifier.drawBehindModifier.background 같은 걸 보면 알 수 있듯 Modifier는 배치만을 위한 건 아니다. 여기서 말하고자 하는 건 Jetpack Compose는 Modifier를 통해서 모든 레이아웃 노드에 공통적으로 적용할 수 있는 속성과 특정한 노드를 정의하기 위한 부분이 분리되었다는 사실이다. 예를 들어 모종의 이유로 캔버스로 뷰의 테두리를 특정한 색으로 그려야 하는 뷰가 많아졌을 때 BaseBorderedLayout 같은 걸 만들지 않고 Modifier.bordered()만 만들어 붙이면 원하는 동작을 이끌어낼 수 있다. 전자는 부모 클래스를 바꿔야 하는 반면에 후자는 Modifier 체인에 단 한 줄만 추가해주면 되기 때문에 훨씬 큰 유연성을 가진다.

결론

트리가 사용된다면 UI가 아닌 다른 것에도 활용될 수 있는 compose-runtime 모듈, 여러 쓰레드에서 UI를 업데이트 하기 위한 MVCC의 활용, 멀티플랫폼 사용성 등 Jetpack Compose는 무궁무진한 가능성을 가지고 있습니다. 유일한 단점이라고 하면 코틀린 컴파일러 플러그인을 사용한다는 것 때문에 컴파일러 버전에 제약을 받는다는 것인데, 이 점은 이후에 코틀린 IR 컴파일러 API가 안정화 되면 이 단점마저도 사라질 것입니다. 5년 정도 뒤에는 지금의 코틀린이 그렇듯 모바일 앱을 개발하는 모든 회사의 채용 공고에서 Compose의 이름이 보이게 되는 걸 기대하고 있습니다.

이걸 보시고 Jetpack Compose가 아름답게 보이신다면 제 라이브러리에 스타 하나만 달아주고 가시면 감사하겠습니다 ㅎㅎ

GitHub - onebone/compose-collapsing-toolbar: A simple implementation of collapsing toolbar for Jetpack Compose
A simple implementation of collapsing toolbar for Jetpack Compose - GitHub - onebone/compose-collapsing-toolbar: A simple implementation of collapsing toolbar for Jetpack Compose