Thinking In Compose (Compose 이해) 안드로이드 공식 문서

선언형 프로그래밍 패러다임

기존의 Android View계층구조

  • UI위젯트리로 표현가능
  • 데이터가 변화시 UI계층구조를 수동으로 업데이트 ex) findViewById()→button.setText(String)
  • 위와같은 방법으로 뷰를 조작시 오류 발생가능성 커짐

지난 몇년의 패러다임

  • 위와 같은 문제점을 해결하기 위해 선언형 UI모델로 전환
  • 이에 따라 UI빌드 및 업데이트와 관련된 작업이 간소화

선언형 UI, Compose

  • 처음부터 화면 저체를 개념적으로 재생성
  • 이후 필요한 변경사항만 적용
  • compose : 선언형 UI 프레임워크

간단한 Composable함수

  • @Composable주석을 통해 함수가 데이터를 UI로 변환하기 위한 함수라는 것을 Compose컴파일러에 알림
  • Composable함수는 매개변수로 데이터를 받아 앱 로직을 통해 UI를 생성
  • 함수는 아무것도 반환하지 않음.
  • 함수는 상태나 값을 변경하지 않으므로 여러번 호출 해도 결과는 항상같음(멱등원) → 따라서 side effect없음 (함수 선언시 이 부분을 염두해야함!)

선언형 패러다임 전환

명령형 객체 지향 UI tool kit : XML Layout

  • 위젯 트리를 인스턴스화 하여 UI 초기화
  • 각 위젯은 자체의 내부 상태를 유지함
  • 각 위젯은 앱 로직이 위젯과 상호작용할 수 있도록 하는 getter, setter메서드를 노출

선언형 UI tool kit : Compose

  • stateless상태로 setter,getter함수를 노출하지 않음 → 위젯이 객체로 노출되지 않음
  • 동일한 Composable함수를 다른 인수로 호출하여 UI를 업데이트
  • 따라서 ViewModel과 같은 아키텍처 패턴에 상태를 쉽게 제공가능
  • Composable은 식별가능한 데이터가 업데이트 될때마다 현재 앱 상태를 UI로 변환

  • 사용자가 UI와 상호작용할때 이벤트를 앱로직에 전달해 앱상태를 변경하고 상태가 변경되면 Composable함수가 새 데이터와 함께 다시 호출되어 다시 그려짐(재구성)

동적 콘텐츠

  • Composable함수는 kotlin으로 작성되기 때문에 동적으로 동작가능( 조건문 ,루프 등 코틀린 기능 사용가능)

재구성

  • 함수의 입력이 되면 Composable함수를 다시 호출하고 함수가 재구성됨
  • 필요한 경우 함수에서 내보낸 위젯이 새 데이터로 다시 그려짐
  • Compose프레임워크는 변경된 구성요소만 재구성

재구성 과정

  1. 함수의 입력이 변경됨
  2. Compose가 새 입력을 기반으로 변경되었을 수 있는 함수나 람다만 다시 호출(매개변수가 변경되지 않은 함수와 람다는 건너뜀)

주의사항

  • 매개변수가 변경되지 않은 함수는 재구성하지 않으므로 함수에 사이드 이펙트를 넣어서 이 사이드 이펙트를 의지하면 안됨 → side effect 사용시에는 콜백에서 트리거 하는 방식으로 사용해야함
  • 위험한 side effect 예시
    • 공유 객체의 속성에 쓰기
    • viewModel에서 식별 가능한 요소 업데이트
    • 공유환경설정 업데이트 ex) sharedPreference

공유 환경 설정 업데이트

  • 애니메이션이 렌더링 될때같이 모든 프레임에서와 같은 빈도로 재실행 될 수 있음 (애니메이션도중 버벅임을 방지하기 위해서)
  • 따라서 비용이 많이드는 작업(공유 환경설정에서 읽기 등)을 실행하려면 백그라운드에서 작업을 실행 후 Composable함수에 매개변수로 전달
@Composable
fun SharedPrefsToggle( // 컴포저블을 생성해서 SharedPreferences값을 업데이트
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit //컴포저블은 SharedPreferences에 읽거나 쓰지 말아야 하므로 백그라운드 코루틴의 ViewModel로 읽기 및 쓰기를 이동
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)//콜백으로 현재 값을 전달해 업데이트를 트리거(업데이트는 컴포저블이 아닌 코루틴의 ViewModel에서 이루어짐)
    }
}

Composable함수 특징

  • 순서와 관계없이 실행가능
    • Compose에는 일부 UI요소가 다른 요소보다 우선순위가 높다는 것을 인식하고 그 요소를 먼저 그리는 옵션이 있음
    • 따라서 어떤 함수의 side effect를 다른 함수에서 활용할수 없음
    • 따라서 각 함수는 독립적이어야함
  • 동시에 실행가능
    • 재구성을 최적화 할수 있음
    • Compose는 다중코어를 활용하여 화면에 없는 Coposable함수를 낮은 우선순위로 실행하여 재구성최적화 가능 → Composable함수는 백그라운드 스레드 풀에서 실행될 수 있음
    • 따라서 composable함수가 ViewModel에서 함수를 호출하면 Compose는 동시에 여러 스레드에서 이 함수를 호출 가능
    • composable함수가 호출될때 다른 스레드에서 호출이 발생할 수 있으므로 composable함수의 변수를 수정하는 코드는 스레드로부터 안전하지 않고 허용되지 않는 side effect가 있을 수 있으므로 피해야한다.

        @Composable
        fun ListComposable(myList: List<String>) {
            Row(horizontalArrangement = Arrangement.SpaceBetween) {
                Column {
                    for (item in myList) {//매개변수인 myList를 직접 사용하므로 thread safe하지 않음. 
                        Text("Item: $item")
                    }
                }
                Text("Count: ${myList.size}")
            }
        }
      
        @Composable
        @Deprecated("Example with bug")
        fun ListWithBug(myList: List<String>) {
            var items = 0
              
            Row(horizontalArrangement = Arrangement.SpaceBetween) {
                Column {
                    for (item in myList) {
                        Text("Item: $item")
                        items++ // Avoid! Side-effect of the column recomposing.
                    }// side effect로 재구성이 이루어질때마다 item이 수정됨!
                }
                Text("Count: $items")
            }
        }
      
  • 재구성은 최대한 많은 수의 컴포저블 함수와 람다를 건너뜀
    • Compose는 UI트리에 있는 전체 컴포저블이 아닌 단일 컴포저블을 재구성 할 수 있다.
      @Composable
      fun NamePicker(
          header: String,
          names: List<String>,
          onNameClicked: (String) -> Unit
      ) {
          Column {
              // header가 변경될때 recompose됨, name이 변경될때는 recompose안됌
              Text(header, style = MaterialTheme.typography.h5)
              Divider()
      				//LazyColumn은 리사이클러뷰의 컴포즈 버전느낌
              LazyColumn {
      						//items()에 전달된 람다는 리사이클러뷰의 뷰홀더와 유사
                  items(names) { name ->
                      // item의 name이 변경될때, item의 어댑터도 recompose됨
                      // header가 변경될때는 recompse되지 않음
                      NamePickerItem(name, onNameClicked)
                  }
              }
          }
      }
        
    
  • 재구성은 낙관적이며 취소될 수 있음
    • Compose는 Composable의 매개변수가 변경되었을 수 있다고 생각할때마다 재구성함.
    • 재구성은 낙관적임 : Compose는 매개 변수가 다시 변경되기 전에 재구성을 완료할것으로 예상
    • 재구성은 취소될 수 있음 : 재구성이 완료되기 전에 매개변수가 변경되면 Compose는 재구성을 취소 후 새 매개변수로 다시 재구성 할 수 있음
  • 매우 자주 실행 될 수 있음
    • 경우에 따라 Composable함수는 UI 애니메이션의 모든 프레임에서 실행될 수 있음
    • 위에 언급했다시피 비용이 많이 드는 작업을 하면 UI버벅임이 발생할 수 있고 앱 성능에 치명적 영향을 줄 수 있음
    • 비용이 많이 드는 작업은 백그라운드 스레드에서 진행 후 mutableStateOf, LiveData를 통해 Compose로 전달하는게 좋음.

❗위 특징을 잘 활용하기 위해 Composable함수는 항상 멱등원이어야하며 side effect가 없어야 한다!!

댓글남기기