Recomposition이 일어나는 상황들에는 어떤게 있을까?

이윤설·2025년 3월 3일
0

안드로이드 연구소

목록 보기
33/33

1. 상태(State) 변경

가장 일반적이고 중요한 재구성 트리거는 상태 변경이다.
Compose에서는 다음과 같은 상태 객체들이 변경될 때 재구성이 발생한다.

// 기본 상태 객체
val count = mutableStateOf(0)

// remember를 사용한 상태 관리
val count = remember { mutableStateOf(0) }

// by 위임을 통한 간결한 사용
var count by remember { mutableStateOf(0) }

상태가 변경되면 Compose는 해당 상태를 읽는 모든 컴포저블을 재구성한다.

2. 컴포저블 함수의 매개변수 변경

컴포저블 함수에 전달되는 매개변수가 변경될 때 재구성이 발생한다.

@Composable
fun UserProfile(user: User) {  // user 객체가 변경되면 재구성 발생
    // 프로필 UI
}

// 사용 예시
UserProfile(viewModel.currentUser)  // currentUser가 변경될 때마다 재구성

3. 사용자 인터랙션

버튼 클릭, 텍스트 입력, 스와이프 등의 사용자 인터랙션으로 인한 상태 변화도 리컴포지션을 트리거한다.

Button(onClick = { count++ }) {  // 클릭하면 count 증가로 인한 재구성
    Text("클릭 수: $count")
}

4. ViewModel이나 다른 데이터 소스의 변경

ViewModel이나 다른 데이터 소스에서 관찰 가능한 데이터가 변경될 때에도 리컴포지션이 발생한다.

val viewModel: MainViewModel = viewModel()
val uiState by viewModel.uiState.collectAsState()  // 상태 변화 관찰

// uiState가 변경되면 이를 사용하는 컴포넌트 재구성
Text("현재 상태: ${uiState.status}")

5. 부모 컴포넌트 재구성

기본적으로 부모 컴포넌트가 재구성되면 모든 자식 컴포넌트도 재구성 대상이 된다.

@Composable
fun Parent(count: Int) {  // count 변경 시 Parent 재구성
    Child()  // Parent가 재구성되면 Child도 재구성 대상
}

6. 애니메이션

애니메이션 실행 중에는 각 프레임마다 재구성이 발생한다!

val alpha by animateFloatAsState(
    targetValue = if (isVisible) 1f else 0f
)
// alpha 값이 변할 때마다 재구성
Box(modifier = Modifier.alpha(alpha))

7. LaunchedEffect 및 부수 효과

LaunchedEffect API를 사용할 때 상태 변경이 발생하면 재구성된다.

LaunchedEffect(key1 = viewModel.dataLoaded) {
    if (viewModel.dataLoaded) {
        // 데이터 로드 완료 시 상태 변경으로 재구성 발생
        snackbarState.showSnackbar("데이터 로드 완료")
    }
}

8. Configuration 변경

화면 회전, 다크 모드 전환, 언어 변경 등의 구성 변경

// 현재 구성에 따라 다른 UI 표시
val isDarkTheme = isSystemInDarkTheme()  // 테마 변경 시 재구성

9. Window 크기 변경

앱 창 크기 조정이나 멀티윈도우 변경 시

val windowSizeClass = calculateWindowSizeClass()
// 창 크기에 따른 레이아웃 조정
when (windowSizeClass) {
    WindowSizeClass.COMPACT -> CompactLayout()
    WindowSizeClass.MEDIUM -> MediumLayout()
    WindowSizeClass.EXPANDED -> ExpandedLayout()
}

효율적인 재구성 관리 방법

Compose에서 재구성은 필연적이지만, 성능 최적화를 위해 몇 가지 전략을 사용할 수 있다.

  1. 상태 호이스팅: 상태를 필요한 최소한의 상위 컴포넌트로 올립니다.
  2. remember: 재구성 간에 객체를 보존하여 불필요한 재생성을 방지합니다.
  3. derivedStateOf: 계산 비용이 높은 상태 변환을 최적화합니다.
  4. 컴포넌트 분리: 변경되는 부분과 안정적인 부분을 분리합니다.
  5. key: 리스트 항목에 안정적인 키를 제공하여 효율적인 재구성을 지원합니다.

1. 상태 호이스팅(State Hoisting) – 상태를 상위로 올려서 관리하기

👉 "상태를 관리하는 곳과 UI를 그리는 곳을 분리한다."

🔹 왜 필요할까?

  • 하위 컴포넌트에서 직접 상태를 관리하면 여러 곳에서 같은 상태를 수정해야 할 수도 있다.
  • 상태를 최소한의 상위 컴포넌트로 올리면, 변경이 필요할 때 불필요한 재구성을 줄일 수 있다.

예제

@Composable
fun ParentScreen() {
    var text by remember { mutableStateOf("") } // 상태를 상위에서 관리

    ChildInput(text = text, onTextChange = { text = it }) // 상태를 전달
}

@Composable
fun ChildInput(text: String, onTextChange: (String) -> Unit) {
    TextField(value = text, onValueChange = onTextChange) // UI만 담당
}

이렇게 하면 ChildInput이 직접 상태를 관리하지 않고, ParentScreen이 상태를 변경할 때만 필요한 부분이 재구성된다.


2. remember – 불필요한 객체 재생성 방지하기

👉 "recomposition이 되어도 값이 초기화되지 않게 기억시킨다."

왜 필요할까?

  • remember를 사용하지 않으면 화면이 다시 그려질 때마다 변수가 초기화될 수 있다.
  • remember를 사용하면 변수의 값을 유지할 수 있다.

🔹 예제

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) } // 화면이 다시 그려져도 값 유지

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

이렇게 하면 버튼을 클릭해도 count 값이 유지되고, 불필요한 재구성이 방지된다.


3. derivedStateOf – 계산 비용이 높은 작업 최적화하기

👉 "값이 바뀔 때만 계산이 실행되도록 한다!"

왜 필요할까?

  • 리스트 개수를 계산하거나 필터링할 때, 데이터가 바뀌지 않아도 매번 계산하면 성능이 저하될 수 있다.

  • derivedStateOf를 사용하면 필요할 때만 계산이 실행되어 최적화할 수 있다.

    예제

@Composable
fun ExpensiveCalculation(items: List<String>) {
    val itemCount by remember { derivedStateOf { items.size } } // items가 바뀔 때만 계산

    Text("아이템 개수: $itemCount")
}

이렇게 하면 items가 바뀌지 않는 한 size를 계속 계산하지 않아 성능이 향상된다.


4. 컴포넌트 분리 – 변경되는 부분만 재구성되도록 만들기

👉 "안정적인 UI와 변경되는 UI를 분리하면 불필요한 재구성을 줄일 수 있다!"

왜 필요할까?

  • 한 컴포넌트에서 모든 걸 처리하면, 작은 변경에도 전체가 다시 그려질 수 있음
  • 변경되는 부분과 변하지 않는 부분을 분리하면, 필요한 부분만 다시 그려져서 성능이 향상됨

예제

@Composable
fun MainScreen() {
    Column {
        StaticHeader() // 변하지 않는 부분
        DynamicContent() // 변경될 가능성이 있는 부분
    }
}

@Composable
fun StaticHeader() {
    Text("안녕하세요! 👋", fontSize = 24.sp)
}

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

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

이렇게 하면 버튼을 눌러도 StaticHeader는 다시 그려지지 않고, DynamicContent만 재구성된다.


5. key – 리스트 아이템의 재구성을 최소화하기

👉 "리스트에서 특정 항목이 불필요하게 재구성되지 않도록 한다!"

왜 필요할까?

  • LazyColumn 같은 리스트를 사용할 때, 항목이 추가/삭제될 때 기존 항목이 재구성될 수 있음
  • key를 사용하면 항목의 변경이 최소화되어 성능이 최적화됨

예제

LazyColumn {
    items(items, key = { it.id }) { item ->
        Text("아이템: ${item.name}")
    }
}

이렇게 하면 id가 같은 항목은 재사용되고, **새로운 항목만 추가/삭제된다.

cf. key란?

Jetpack Compose의 리스트(LazyColumn, LazyRow 등)는 항목을 효율적으로 그리기 위해 자동으로 재사용(Recycling)한다.

하지만 항목을 구별할 기준이 없으면 기존 항목을 새 항목으로 잘못 인식하여 불필요한 재구성이 발생할 수 있다.

예를 들어 1학년 5반의 학생 목록 30명이 LazyColumn으로 저장되어있다고 가정하자.
이때 김철수가 삭제되면, key를 지정하지 않았다면 리컴포지션이 발생한다.

만약 key를 사용하여 각 항목의 고유한 값을 지정하면,
변경된 부분만 재구성되고, 나머지는 유지할 수 있어 성능이 향상된다.

@Composable
fun StudentList(students: List<Student>) {
    LazyColumn {
        items(students, key = { it.id }) { student -> 
            Text(text = student.name)
        }
    }
}

동적으로 변하는 리스트라면 key를 꼭 써야 최적화됨을 기억하자.

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글

관련 채용 정보