나만의 안드로이드 앱 만들기/중급자

나만의 안드로이드 앱 만들기(중급자 편) - Compose 의 Closure 와 Recomposition(클로저, 리컴포지션)

Victorywskim 2023. 12. 27. 09:03
반응형

Compose를 사용할 때 클로저와 리컴포지션 개념은 매우 중요합니다.

 

아마 대부분 Compose 를 사용하시는 분들은 대부분 컴포넌트에 대한 매커니즘이나 문법 등에 더 관심이 많으실 것이라 생각합니다.

 

하지만 클로저와 리컴포지션은 Compose 퍼포먼스에 중대한 영향을 미치는 부분이기 때문에 Compose 를 어느정도 익힌 이후에는 반드시 알아야 하는 개념입니다.

 

클로저(Closure)는 함수형 프로그래밍에서 유용하게 활용되며, Compose는 함수형 UI 프레임워크로써 함수형 프로그래밍의 원리를 많이 채택하고 있습니다.

 

리컴포지션(Recomposition)은 함수형 UI 프레임워크에서 UI를 다시 그리는 프로세스를 의미합니다. Jetpack Compose에서는 상태가 변경될 때마다 해당 상태에 영향을 받는 컴포저블이 다시 실행되어 UI가 업데이트됩니다.

 

따라서 클로저를 이해하고 활용하는 것이 Compose를 효과적으로 사용하는 데 도움이 됩니다.

여러 측면에서 클로저의 중요성을 살펴보겠습니다.

클로저를 먼저 설명드릴려는 이유는 Compose 의 상태 관리 때문입니다. 상태 관리는 UI의 상태를 추적하고 변경하는 과정이며, Compose에서는 상태 관리를 위해 클로저를 사용합니다.

 

상태 보존 및 UI 업데이트


Compose에서는 remember 함수를 사용하여 상태를 보존합니다. 이때 클로저가 사용되어 현재의 Compose 컴포넌트 환경을 기억하고 상태를 보존합니다. 예를 들어, remember 내부에서 사용되는 변수에 대한 변경은 해당 컴포넌트를 리컴퍼징할 때에도 기억되어야 합니다.

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

 

 

이벤트 핸들러에서 클로저 활용

 

클로저는 이벤트 핸들러에서 외부 변수를 캡처하여 사용할 때 유용합니다. 예를 들어, 버튼 클릭 시 상태를 업데이트하거나 특정 동작을 수행하는 이벤트 핸들러에서 클로저를 활용할 수 있습니다.

@Composable
fun ClickableComponent() {
    var message by remember { mutableStateOf("Hello") }

    Button(onClick = {
        message = "Button Clicked"
    }) {
        Text(message)
    }
}

 

 

컴포저블 재사용

 

클로저를 이용하면 컴포저블 내부에서 외부 상태를 활용하여 재사용 가능한 컴포넌트를 만들 수 있습니다. 이는 코드의 모듈성을 높이고, UI 로직을 분리하여 관리하기 용이하게 만듭니다.

@Composable
fun GreetingComponent(name: String) {
    var message by remember { mutableStateOf("Hello, $name") }

    Button(onClick = {
        message = "Hello, $name Clicked"
    }) {
        Text(message)
    }
}

 

 

클로저를 기반으로 리컴포지션에 대해 설명드리겠습니다.

리컴포지션은 함수형 UI 프레임워크에서 UI를 다시 그리는 프로세스를 의미합니다. Jetpack Compose에서는 상태가 변경될 때마다 해당 상태에 영향을 받는 컴포저블이 다시 실행되어 UI가 업데이트됩니다.

 

리컴포지션은 실제 코드를 기반으로 설명드리고자 합니다.

 

GitHub - tmvlke/SimpleDutch: 심플더치 프로젝트 입니다.

심플더치 프로젝트 입니다. Contribute to tmvlke/SimpleDutch development by creating an account on GitHub.

github.com

 

간단한 테스트를 진행해보고 그 결과를 통해 리팩토링 진행해볼 예정입니다. MainScreen.kt 의 소스코드를 일부 수정해보려 합니다.

 

MainScreen.kt

 

 

 

위 3부분의 코드를 추가 및 수정합니다.

 

이는 리컴포지션이 어떻게 이루어지는지 확인하기 위한 예제 코드입니다.

 

코드 수정을 해주셨다면 다음과 같이 break point 를 설정해주시면 되겠습니다.

 

 

 

이제 준비가 끝났고 디버깅으로 앱을 실행해주시면 되겠습니다.

 

앱을 실행되면 저장소 탭 버튼을 클릭합니다.

 

 

그럼 위 순서대로 코드가 실행되는 것을 확인 할 수 있습니다.

 

하지만 다시 저장소 버튼을 클릭하면 text.value = "!234" 만 실행되고 TabFooterText 메소드는 실행되지 않는 것을 알 수 있습니다.

 

그 이유는 상태에 변화가 생기지 않았기 때문입니다.

 

이를 재 검증해보고 싶다면 다음과 같이 코드를 작성해보면 됩니다.

 

위 코드를 실행하면 TabFooterText 메소드가 지속적으로 호출되는 것을 확인 할 수 있습니다.

 

즉, 불필요하게 UI 가 갱신되는 것을 막는 것이 리컴포지션 최적화의 핵심입니다.

 

다시 한번 코드를 수정해보겠습니다.

 

이번에는 TabText 부분을 주석처리하고 Text 부분의 주석을 푼 뒤 다시 코드를 실행해보겠습니다.

 

탭을 변경할때마다 Text 가 계속 새로 그려지는 것을 확인 할 수 있습니다.

 

그럼 이제 불필요한 코드를 모두 제거 하고 리팩토링 의도에 맞게 코드를 다시 수정해보겠습니다.

 

  • Text를 직접 사용하는 경우
    • 각 Text 컴포저블은 독립적으로 평가되기 때문에, state.openTab.value.ordinal 값이 변경될 때마다 모든 Text 컴포저블이 리컴포지션됩니다.
    • 이는 불필요한 리컴포지션을 발생시킬 수 있으며, 특히 탭이 많아질수록 성능에 영향을 줄 수 있습니다.
  • TabText 함수로 감싸서 사용하는 경우
    • TabText 함수 내부에서 isSelected 값을 기준으로 리컴포지션 여부를 판단하기 때문에, state.openTab.value.ordinal 값이 변경되더라도 실제로 텍스트의 색상이나 굵기가 변경되어야 하는 TabText 컴포저블만 리컴포지션됩니다.
    • 즉, 불필요한 리컴포지션을 방지하여 성능을 향상시킬 수 있습니다.


결론적으로, TabText 함수를 사용하는 것이 리컴포지션을 최적화하고 리소스 비용을 절약할 수 있는 방법입니다.

 

  • 추가적인 고려사항
    • TabText 함수 내부에서 사용하는 if 문 또한 리컴포지션을 발생시킵니다. 만약 isSelected 값이 매우 자주 변경된다면, if 문 대신 remember 함수와 같은 다른 방법을 사용하여 리컴포지션을 더욱 최적화할 수 있습니다.
    • TabText 함수를 사용하는 것이 항상 가장 좋은 방법은 아닙니다. 만약 탭의 개수가 적고 성능에 크게 영향을 주지 않는다면, 가독성을 위해 Text 컴포저블을 직접 사용하는 것을 고려할 수 있습니다.

결국은 상황에 따라 가장 적절한 방법을 선택하는 것이 중요합니다.

 

감사합니다.

 

반응형