본문 바로가기

Android/Jetpack

Compose

 


안드로이드 Compose 공식 문서를 공부한다.

 

 

Jetpack Compose 시작하기  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Jetpack Compose 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose는 네이티브 Android UI

developer.android.com

 

 

Compose

 
 

1. Compose란 ?

컴포즈는 Jetpack에 포함된 도구로 네이티브 UI를 빌드하기 위한 최신 툴 키트이다. Kotlin API로 UI를 개발해 간단하고 빠르게 개발을 도와준다.

 2. Compose의 장점

  • 간단한 코드
    • Android View(.xml 파일)을 사용할때 보다 더 적은 코드로 구현이 가능하다. 기존엔 xml 파일을 생성 후 view와 연결해주는 과정이 필요했지만 컴포즈를 사용하게 된다면 @Compose 어노테이션을 이용해 fun으로 UI를 구성할 수 있다.
  • 직관적
    • 선언형 UI로 API 명칭이 직관적이기 때문에 UI를 구성할때 찾기와 사용, 구분이 모두 쉽다.
  • 빠른 개발 가능
    • 기존의 모든 코드와 호환이 가능하기 때문에 기존 코드에서 문제될 것이 없다. 또, 프리뷰 기능을 지원하기 때문에 오류 상태, 글꼴 크기와 같이 다양한 UI를 빠르게 확인이 가능하다.
  • 강력한 성능
    • 머티리얼 디자인, 다크 모드, 애니메이션 등 기본적으로 지원해주기 때문에 유연하게 다양한 디자인을 구현할 수 있다.

3. Compose 등장 배경

안드로이드 뷰는 기존에는 findViewById()와 같은 함수를 사용하거나 binding을 통해 view를 탐색하고 setText(), addChild()와 같이 함수를 호출해 위젯의 상태를 변경시켜주었다. 이런 방식은 수동으로 조작하기 때문에 업데이트가 필요한 여러뷰 중 하나가 누락되거나 업데이트 과정에서 충돌 등 유지보수가 어렵다는 문제가 있었다. 이런 문제를 해결하기 위해 등장한 것이 선언형 프로그래밍이다.

4. 컴포저블(Composable), 컴포지션(Composition)

컴포저블(Composable)이란?

@Composable 어노테이션이 붙은 함수로 UI 요소를 선언적으로 정의할때 사용된다. XML 레이아웃 대신 함수를 사용해 UI를 그릴 수 있게 해준다. 컴포저블 함수는 다른 컴포저블 함수를 호출할 수 있고 UI 계층을 함수 단위로 쪼개서 관리가 가능하다.

컴포저블은 하나의  @Composable 함수덩어리로 정의 되지만 하나의 인스턴스는 아니다 ?

 

각각 다른 색상은 서로 다른 인스턴스라는 것을 의미한다.

 

@Composable
fun MyComposable(name: String){
   Column{
      Text(text = "Hello")
      Text(text = "World")
   }
}

 

Jetpack Compose에서 컴포저블 개념은 단순히 @Composable 어노테이션이 달린 함수를 묶어서 의미하는게 아니라 트리 구조 안에 포함된 모든 UI 즉, 독립된 개별 컴포저블을 의미한다. (각각의 컴포저블 객체를 모아서 @Composable 어노테이션이 달린 함수형식의 새로운 컴포저블 객체를 만든다고 이해하면 쉬울듯하다.) MyComposable()안에 Column과 Text가 속한거 같아 보이지만 내부적으로는 Column과 Text 모두 각각 개별 컴포저블로 동작하기 때문에 Recomposition시 독립적으로 갱신이 가능하고 각자의 생명주기를 가지게 된다.

View(Column, Text)이 컴포즈에서 컴포저블이라는건 알겠어, 근데 MyComposable()은 왜 컴포저블이야 ?

 

컴포즈의 핵심 철학은 UI를 함수로 나누고 모듈화를 가능하게 하는 것이다. MyComposable()은 다른 컴포저블 함수를 호출하거나 UI(컴포저블)을 생성하는 역할을 하기 때문에 직접 View를 그리지 않아도 컴포저블이다. 여러 컴포저블을 하나의 모듈로 묶어주는 역할을 수행하는 최상위 함수이며 컴포지션 트리의 루트 노드가 된다.

컴포지션(Composition)이란?

컴포저블 함수들의 호출 트리 구조이다. 여러 컴포저블 함수가 호출되고 화면에 렌더링 되는 전체 UI 구조를 의미한다. 선언형 UI는 특성상 상태가 변할 때 어떤 부분만 다시 렌더링 할지 알아야하는데 컴포지션이 이 기능을 효율적으로 수행하도록 한다.

리컴포지션(Recomposition)이란?

컴포즈는 내부적으로 어떤 부분이 변경되었는지 파악하고 변경된 컴포저블 함수만 재구성하게 된다. 대표적인 리컴포지션 방법에는 State가 있다. State를 사용하게 된다면 화면 전체를 갱신하는 것이 아니라 Counter 컴포저블만 다시 그려지게 된다.

@Composable
fun Counter(){
   var count by remember { mutableStateOf(0) }
   
   Button(onClick = { count++ }){
      Text("Count: $count")
   }
}

 

 

 

생명 주기

 

 

1. 생명 주기

컴포즈의 생명주기는 UI 렌더링, UI 부분적 재구성, UI 제거 리소스 해제 3가지 단계가 있다. Activity, Fragment 보다 생명주기가 간단하기 때문에 복잡한 외부 리소스를 관리하거나 상호작용이 필요한 경우엔 Effect API를 사용해야한다.

 

 

 

  • Enter the Composition (컴포지션 시작)
  • Recompose 0 or more times (0회 이상의 재구성)
  • Leave the Composition (컴포지션 종료)

Enter the Composition (컴포지션 시작)

처음 화면에 진입하거나 컴포저블 함수가 호출될때 생성된다. 컴포저블 함수들이 트리 구조로 실행되며 UI를 구성한다. remember, mutableStateOf 같은 상태를 저장하는 함수가 있다면 초기화도 함께 이루어진다.

Recompose 0 or more times (0회 이상의 재구성)

리컴포지션이 발생하지 않으면 최초 한번만 UI를 구성하고 변하지 않는다. 상태 'State<T>'가 변경될 경우 내부적으로 변경된 부분을 파악하고 해당 컴포저블 함수만 리컴포지션하게 된고 컴포지션 트리 구조 업데이트가 발생한다. 상태가 변경될 필요가 없는 컴포저블 함수까지 재구성하게 된다면 성능적으로 이슈가 발생하기 때문에 사용할때 주의가 필요하다.

Leave the Composition (컴포지션 종료)

특정 컴포저블이 컴포지션 트리에서 제거될때 발생한다. DisposableEffect를 사용해 명시적으로 리소스 정리 작업이 가능하다.

2. 생명주기 비교

Activity / Fragment Compose
onCreate() Enter the Composition
onResume() / onPause() Recompse 0 or more times
onDestory() Leave the Composition

 

3. 컴포지션(Composition)의 컴포저블(Composable) 분석

Compose 컴파일러는 각 호출을 고유한것으로 간주하기 때문에 하나의 컴포저블을 여러 곳에서 호출하게 되면 각각의 컴포저블별로 인스턴스를 생성한다. 만일 리컴포지션 경우 이전에 호출된 컴포저블과 호출 되지 않은 새로운 컴포저블을 식별해 새로운 컴포저블만 재구성한다.

컴포지션 LoginScreen이 Recomposition이 일어난 경우 LoginScreen과 LoginInput은 이전에 호출되었던 컴포저블이기 때문에 상태를 유지하지고 새로운 LoginError 컴포저블만 호출된다.

리컴포지션과 재사용

  • Recomposition
    • 상태 변화나 데이터의 변경이 발생해 UI 트리의 일부 또는 전체를 다시 그리는 과정이다.
    • 컴포저블 함수가 재호출 될 경우 변경된 상태로 새로운 UI를 그린다.
    • 변경된 상태를 스스로 추적해 필요한 부분만 다시 그린다.
  • 재사용(Reuse)
    • 이미 렌더링 된 UI 노드를 다시 사용한다.
    • 이전 컴포지션 트리에서 사용된 뷰와 컴포저블의 상태와 위치를 최대한 유지하면서 변경이 필요한 최소화의 UI만 업데이트한다.
    • 이전에 생성된 인스턴스 객체를 재사용하기 때문에 성능 최적화다.

컴포저블 호출을 식별할 수 있는 고유의 정보가 없기 때문에 인스턴스를 구분하기 위해 실행 순서를 사용한다.

 

MovieOverview 컴포저블을 하나 더 추가했을 경우 첫번째와 두번째의 순서는 유지되기 때문에 동일한 인스턴스 객체를 재사용하는 것을 확인 할수있다. 하지만 상단 또는 가운데에 항목이 추가되거나 삭제하게되어 재정렬 후 Column의 MovieOverview 목록이 변경되게 되면 위치가 변경된 모든 MovieOverview에 리컴포지션이 발생된다.

 

key 컴포저블을 사용하면 Compose가 컴포지션에서 컴포저블 인스턴스 식별이 가능하다. MoviesScreenWithKey() 처럼 동일한 위치에서 반복적으로 컴포저블을 호출하는 경우 key가 없다면 각각의 인스턴스 식별이 어려울 수 있다. key를 사용하게 된다면 각각의 컴포저블 인스턴스를 고유하게 식별 가능하다.

data class Movie(
    val id: String,
    val name: String
)

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) {
                MoviesOverView(movie)
            }
        }
    }
}

@Composable
fun MoviesOverView(movie: Movie) {
    // ...
}

 

Column은 VerticalLinearLayout과 동일하게 기능한다. movies의 요소들을 세로형 UI로 만들어주기 위해 for문을 사용했다.
MoviesOverView()를 여러번 호출해서 그릴 수도 있지만 @Composable은 코틀린이기 때문에 for문 사용이 가능하다. 

 

 

 

Effect API

 

 

1. SideEffect란 ?

@Composable(컴포저블 함수) 밖에서 발생하는 앱 상태의 변경을 의미한다.

  • 컴포저블에서 컴포저블이 아닌 앱 상태에 대해 변화를 주는 경우
  • 중첩된 컴포저블은 기본적으로 바깥쪽에서 안쪽으로 State를 내려주지만 반대인 경우

컴포저블은 단방향으로만 State 전달하고 각각의 Lifecylce을 가지고 있다. 컴포저블에는 부수 효과는 없는게 좋지만, 앱 상태 변경이 필요한 경우 부수 효과가 예측 가능한 방식으로 실행 될 수 있도록 Effect API를 사용해야한다.

2. Effect API란 ?

SideEffect를 처리하기 위해 제공되는 3개의 API와 여러개의 State 함수이다.

 

Effect API

  • LaunchedEffect : suspend 함수에서 실행하기 위해 사용된다.
  • DisposableEffect : Dipose되는 경우 정리될 Side Effect를 정의한다.
  • SideEffect : Compose State를 Compose가 관리하지 않는 객체와 공유하기 위해 사용한다.

State 함수

  • rememberCoroutineScope : Composable의 Scope를 참조해 외부에서 실행할 수 있다.
  • rememberUpdatedState : Launched Effect의 State가 변경되어도 재실행 하지 않아도 되는 경우에 사용한다.
  • produceState : Compose State가 아닌것을 State로 변경하기 위해 사용한다.
  • derivedStateOf : 다른 State로 변경하기 위해 사용한다.
  • snapshotFlow : State를 Flow로 변환한다.

 

 

Compose Rendering

 

 

1. 렌더링

렌더링이란 ?

렌더링은 사용자에게 보여지는 UI를 그리는 과정을 이야기한다. 

 

2. Compose 렌더링 단계

 

  • Composition
    • 화면에 무엇을 보여줄지
    • 구성 가능한 함수를 실행하고 UI 설명을 생성한다.
  • Layout
    • 각 요소를 어디에 배치할지
    • 레이아웃 요소는 레이아웃 트리에 있는 노드 레이아웃 요소 및 하위 요소를 2D 좌표에 측정하고 배치한다.
  • Drawing
    • 각 요소를 어떻게 나타낼지
    • UI 요소는 캔버스에 그려진다.

 

1 단계 : 구성 (Composition)

Composable 함수의 설명을 생성하고 여러 메모리 슬록을 할당하는 과정이 시작된다. 생성된 슬롯들은 각 Composable 함수를 메모이즈하여 런타임 동안 효율적인 호출, 실행을 가능하게 만든다.

2 단계 : 레이아웃 (Layout)

Composable 트리 내에서 각 Composable 노드의 위치가 설정된다. 주로 노드의 측정 및 적절한 배치를 포함해 UI의 전체 구조 내에서 모든 요소를 정확하게 배치되도록 보장한다.

3 단계 : 그리기 (Drawing)

마지막 단계로 Composable 노드가 캔버스에 렌더링 된다. 사용자 상호작용이 가능하도록 시각적 표현을 생성한다.

렌더링에서 Recompostiton이란?

이미 렌더링이 끝난 Composable 함수의 크기, 색상과 같은 UI 요소가 변경되었을때 Drawing 단계가 끝났기 때문에 Compose는 업데이트 사항을 적용하기 위해선 Composition 부터 다시 실행하게 된다.

 

 

 

Compose Layout

 
 

1. 표준 레이아웃 구성요소

Column

LinearLayout에 Vertical과 동일한 기능을 한다.

@Composable
fun ArtistCardColumn() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

// 디버깅을 하지 않아도 확인이 가능하다.
@Preview(showBackground = true)
@Composable
fun PreviewArtistCardColumn() {
    Compose_studyTheme {
        ArtistCardColumn()
    }
}

 

출력화면

Row

LinearLayout에 horizontal과 동일한 기능을 한다.

@Composable
fun ArtistCardColumn() {
    Row {
        Text("Alfred Sisley")
        Text("3 minutes ago", color = Color.Blue)
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewArtistCardColumn() {
    Compose_studyTheme {
        ArtistCardColumn()
    }
}

 

출력화면

 

Box

Box는 FrameLayout과 흡사하며 요소 위에 요소를 중첩시켜 구성이 가능하다.

@Composable
fun ArtistCardColumn() {
    Box{
        Image(painter = painterResource(id = R.drawable.ic_svg), contentDescription = "image")
        Icon(Icons.Filled.Check, contentDescription = "Check mark")
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewArtistCardColumn() {
    Compose_studyTheme {
        ArtistCardColumn()
    }
}

 

출력화면

 

기존 안드로이드 View는 레이아웃을 중첩 시키면 성능 저하의 문제가 있었다. 하지만 Compose는 중첩된 레이아웃을 효율적으로 처리할 수 있기 때문에 성능에 큰 문제가 없어 복잡한 UI를 구성하는데 큰 장점을 가지고 있다.

 

2. UI 트리

UI 트리는 단일 패스로 배치된다.

  • 노드는 자체 측정을 요청 받는다.
  • 하위 요소를 측정 후 크기 제약 조건을 트리 아래 하위 요소로 전달한다.
  • 반복
  • 마지막 하위 요소의 측정이 끝나면 하위 요소부터 순차적으로 크기를 결정한다.

 

 

 

 

 

 

 

 

 

 

 

'Android > Jetpack' 카테고리의 다른 글

Room + ALL WAL(Write-Ahead Logging)  (0) 2024.11.27