| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- 스택
- 클래스
- 상태 호이스팅
- Kotlin LinkedList
- 리컴포지션
- LinkedList
- Queue
- State Hoisting
- 라즈베리파이 피코
- Compoae
- Android
- 코틀린
- 아두이노
- 추상화함수
- 우송대학교
- 컴포즈
- 큐
- kotlin
- Class
- 우송대
- PICO4
- kotlinClass
- Kotlin자료구조
- 개발 보드
- 자료구조
- List
- 안드로이드
- 재정의함수
- Stack
- deque
- Today
- Total
개발자의 생활
리컴포지션(ReComposition) 본문
앱은 사용하는 과정에서 UI들의 상태값이 변경이 됩니다. 당연히 상태가 변경되면 화면은 UI를 새로 그려야 합니다.
기존 XML을 사용한 View 방식은 UI의 속성이 변경되면 자동으로 해당 UI만 새로 렌더링을 하여 보여주는 방식이었습니다.
리컴포지션 이란
Compose 방식은 UI 상태를 기준으로 함수를 재구성하는 방식을 사용합니다. 즉 상태가 변경되면 함수를 부분적으로만 다시 호출하게 됩니다. 이를 리컴포지션이라고 합니다.
함수를 다시 호출한다는 시점에서만 보면 너무 비효율이 아닌가 라는 생각이 들게 됩니다.
하나의 컴포저블 함수에 여러 버튼, 텍스트들이 존재하는데 그중 하나의 상태만 변경돼도 컴포저블 함수가 다시 호출이 되면 렌더링을 위한 GPU사용량이 많아지게 되니 성능이 안 좋은 거 아닌가 라는 생각이 듭니다.
하지만 Compose는 화면을 그릴 때 내부적으로 Composeable 함수의 UI 요소의 상태와 계층구조 위치정보 등을 Composition 트리를 만들어 두게 됩니다. 그래서 리컴포지션을 수행할 때 Composition트리의 정보와 비교하여 상태가 변경되지 않은 UI는 그대로 유지하고 변경된 UI 만 다시 렌더링 하기 때문에 성능에 문제가 없다고 합니다.
즉 성능적으로는 XML View 방식과 거의 동일하다고 합니다.
조금 더 자세한 구조는 이후에 코드와 함께 설명하겠습니다.
리컴포지션 이 발생하지 않는다면?
결과는 당연히 값이 변경되더라도 UI 상태를 변경되지 않게 됩니다.
const val LOG = "MainActivity"
@Composable
fun MyCount(){
Log.d(LOG, "Composable 생성")
Scaffold() {
var countValue: Int = 0
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
Button(
onClick = {
countValue++
Log.d(LOG, countValue.toString())
}
) {
Text(
text = countValue.toString()
)
}
}
}
}
버튼을 클릭하면 countValue 값이 1씩 증가하도록 하였습니다. 그리고 text 컴포저블에 출력하도록 하였고 log 에도 출력하게 만들었습니다.
Logcat 창에서 Log 을 보시면 정상적으로 카운트가 되는걸을 볼 수 있습니다. 하지만 UI 에는 반영이 되지 않고 있습니다.
분명히 위에서는 UI상태가 변경이 되면 리컴포지션을 통해 컴포저블 함수가 재구성되면서 변경된 UI도 다시 렌더링이 돼야 합니다.
리컴포지션이 발생하지 않는 이유는 상태변경을 감지하지 못하고 있기 때문입니다.
리컴포지션을 발생시키는 방법
그러면 어떻게 해야 UI상태가 변경됐는지를 compos runtime 시스템이 감지할 수 있을까요?
방법은 MutableState 를 사용하면 됩니다. MutableState 에는 옵저버 패턴이 구현되어 있습니다.
그래서 값이 변경이 된 것을 확인하고 compos runtime를 실행시키게 됩니다.
그러면 리컴포지션 을 수행하게 되고 MutableState 객체의 상태를 읽고있는 컴포저블 UI만 다시 렌더링을 수행하게 됩니다.
@Composable
fun MyCount(){
Scaffold() {
val countValue = mutableIntStateOf(0)
Log.d(LOG, countValue.intValue.toString())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
Button(
onClick = {
countValue.intValue++
}
) {
Text(
text = countValue.intValue.toString()
)
}
}
}
}
이 코드는 MutableState 를 사용한 코드입니다. Log 도 약간 변경하였습니다.
countValue 변수에 값을 담는 구조가 바뀌었습니다. 기존에는 단순히 정수를 담고 있었지만 이번에는 파라미터로 정수를 받는 mutableIntStateOf라는 메서드를 사용해서 객체를 countValue에
이 객체는 MutableState 타입의 객체를 반환하게 됩니다.
이제 countValue의 객체에 있는 값을 변경하면 상태변경을 감지하고 리컴포지션을 수행하게 됩니다.
실행해 보시면 이번에는 카운트 자체가 동작하지 않는 것처럼 보입니다.
하지만 실제로는 리컴포지션이 정상적으로 수행되고 있는 것입니다.
리컴포지션은 상태가 변경되는 부분을 재구성한다고 했습니다. 해당 코드에는 버튼을 클릭하면 countValue.intValue 에 1씩 증가하도록 되어 있습니다. 그리고 Text 컴포저블 함수의 text와 Log에서 countValue.intValue를 읽고 있습니다.
즉 컴포저블 함수가 재구성되면서 mutableIntStateOf 도 새로 만들어지기 때문에 계속 초기값만 담고 있는 것입니다.
추가설명
MutableState 은 mutableStateOf 메서드를 사용하여 객체를 생성합니다. 그리고 이는 제네릭 타입입니다.
하지만 코드에서는 mutableIntSataeOf를 사용하였습니다. 이는 정수타입이며 이 객체의 타입은 MutableIntState 가 됩니다.
물론 정수뿐 아니라 다양한 데이터 타입도 가능합니다.
제가 mutableStateOf 말고 mutableIntSataeOf를 사용한 이유는 박싱 때문입니다.
mutableStateOf를 사용하더라고 결국 Int를 담으면 mutableIntSataeOf으로 박싱이 일어나기 때문입니다.
사실 어떤 걸 사용해도 상관없지만 IDE에서 경고를 띄우네요
리컴포지션에서도 데이터 유지하기
이렇게 UI 상태를 변경하기 위해서는 리컴포지션을 통해서 재구성해야 한다고 했지만 문제는 객체 또한 재구성이 돼버려 초기값만 가지는 문제가 발행합니다.
그러면 이러한 문제를 해결하기 위해서는 어떻게 해야 할까요?
방법은 여러 가지가 있습니다.
1. 리컴포지션 영역에서 제외시키기(추천 X)
이 부분이 리컴포지션의 핵심 원리입니다.
제가 처음에 리컴포지션은 부분적으로만 다시 호출한다는 설명을 하였습니다. 그리고 중간중간에 재구성이라는 말을 하였습니다.
즉 리컴포지션 이란 정확히는 함수를 다시 실행하는 것이 아닌 변경된 부분만 새로 만든다고 보시면 됩니다.
지금까지 위에서 작성한 글에서는 저의 표현이 부족하여 잘 와닿지 않으셨을 거라 생각됩니다.
그래서 적절할지 모르겠지만 비유적으로 설명하면
글을 작성하고 나서 변경해야 할 부분이 생기면 글을 처음부터 다시 작성하지 않고 변경할 부분만 지우고 다시 작성을 하게 되죠 리컴포지션이 동작하는 방식도 동일합니다. 변경할 부분만 새로 변경하고 다른 부분은 그대로 유지하게 됩니다.
그러면 리컴포지션이 실행되는 영역은 어떻게 나뉘게 될까요?
이는 컴포저블함수가 구성되는 영역으로 나뉘게 됩니다.
@Composable
fun ComposableA(){
ComposableB()
ComposableC()
}
@Composable
fun ComposableB(){
Text(
text = ""
)
}
@Composable
fun ComposableC(){
Text(
text = ""
)
Button(
onClick = {}
) { }
}

위 컴포저블 함수를 트리구조로 표현한 것입니다.
만약 ComposableA에서 상태변경만 발생하고 ComposableB에서 이 상태를 읽고 있다면 ComposableB에서만 리컴포지션이 발생하게 됩니다.
즉 데이터를 유지해야 하는 곳에서는 상태를 읽지 않기만 하면 데이터는 유지된다는 것입니다.
그러면 실제로 동작하는지를 확인해 보겠습니다.
@Composable
fun MyCount(){
Scaffold() {
val countValue = mutableIntStateOf(0)
//Log.d(LOG, countValue.intValue.toString())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
Button(
onClick = {
countValue.intValue++
}
) {
Text(
text = countValue.intValue.toString()
)
}
}
}
}
Log 부분을 주석처리 하여 상위 컴포저블함수에서는 상태를 읽지 않도록 하였습니다.
실행시켜 보면 실제로 데이터가 정상적으로 카운트될 겁니다.
하지만 이 방법은 권장하지 않습니다.
단순히 리컴포지션이 어떻게 수행되는지 원리를 알아보기 위해 작성한 방법입니다.
실제로 앱을 개발할 때는 이런 방법은 절대 사용하지 않습니다.
2. remember 사용(권장 O)
이 방법이 가장 많이 사용되는 방법입니다.
remember 은 리컴포지션이 발생해도 데이터를 유지할 수 있습니다.
@Composable
fun MyCount(){
Scaffold() {
val countValue = remember { mutableIntStateOf(0) }
Log.d(LOG, countValue.intValue.toString())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
Button(
onClick = {
countValue.intValue++
}
) {
Text(
text = countValue.intValue.toString()
)
}
}
}
}
remember는 고차함수 형태로 받을 수 있습니다. 그래서 remember에 mutableStateOf를 넣으면 리컴포지션이 발생해도 데이터를 유지할 수 있습니다.
데이커를 유지할 수 있는 이유는 slot table에 데이터를 저장하고 있기 때문입니다.
조금 자세히 알아보겠습니다. 앱의 Activity 가 생성되면 Composition 객체가 생성이 되고 Activity 가 소멸되면 같이 사라지게 됩니다. 그리고 Composition 내부 slot table에 Compose 메서드의 정보들이 들어있습니다. 그리고 remember 상태를 관리하는 데이터도 저장이 되기 때문에 리컴포지션이 발생하더라도 slot table 에 데이터가 저장되어 있기 때문에 유지할 수 있게 됩니다.
3. rememberSaveable 사용
이거는 remember은 Activity 가 재생성이 되면 Composition 객체도 소멸되고 다시 만들어지게 되므로 데이터가 사라지게 됩니다.
예를 들어 화면을 회전을 하거나 다크모드 라이트모드로 변경하게 되면 Activity 가 새로 만들어지게 됩니다.
하지만 rememberSaveable를 사용하면 Composition 이 다시 만들어지게 돼도 데이터를 유지할 수 있습니다.
이 원리는 Activity 가 소멸되기 전에 Bundle에 저장되고 Activity 가 새로 만들어지면서 Composition 객체가 다시 생성이 되면 Bundle 에 저장된 Composition 데이터로 복원하기 때문입니다.
@Composable
fun MyCount(){
Scaffold() {
val countValue = rememberSaveable { mutableIntStateOf(0) }
Log.d(LOG, countValue.intValue.toString())
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
Button(
onClick = {
countValue.intValue++
}
) {
Text(
text = countValue.intValue.toString()
)
}
}
}
}
이렇게 만들면 회면을 회전하거나 테마를 변경하더라도 데이터가 유지될 수 있습니다.
3. viewModel 사용
이 방법은 여기서 설명하기에는 글이 너무 길어지기에 따로 다루도록 하겠습니다.
이번 글은 조금 많이 길었지만 끝까지 읽어 주셔서 감사합니다.
'안드로이드 앱 개발 > Compose' 카테고리의 다른 글
| 상태 호이스팅 (0) | 2025.11.18 |
|---|