[Android] Compose Fundamentals

easyhooon·2025년 6월 4일
3
post-thumbnail

Manifest Android Interview 책을 읽고 Practical Questions 에 대한 답변을 작성해보고, 카테고리 내에 특정 개념에 대한 딥다이브 및 스터디 시간에 얘기 나누면 좋을 내용들을 적어보는 글입니다.

답변 정리는 LLM의 도움을 받았습니다.

Practical Questions

Q) 0. Jetpack Compose의 구조는 무엇인가?

Jetpack Compose는 선언형 UI 프레임워크로, 주요 구조는 다음과 같음

  • Compose Compiler: @Composable 함수의 코드를 변환해, UI 트리와 상태를 효율적으로 관리할 수 있도록 지원
  • Compose Runtime: 상태 변화 감지, 재구성(Recomposition), 스냅샷 관리 등 UI의 동적 업데이트를 담당
  • UI Toolkit: 레이아웃, 위젯, Material Design 컴포넌트 등 실제 UI 요소를 제공

Q1) Compose Compiler의 역할은 무엇이며, 기존의 KAPT나 KSP와는 어떻게 다른가?

  • Compose Compiler는 Jetpack Compose에서 @Composable 함수에 특수 처리를 적용해, UI의 상태 추적 및 재구성(recomposition) 로직을 컴파일 타임에 자동으로 생성하는 컴파일러 플러그인
    선언형 UI의 핵심인 상태 관리와 효율적인 UI 업데이트를 위해, 함수 내부를 변환하고 필요한 코드를 삽입하는 역할을 함

  • 기존 KAPT(Kotlin Annotation Processing Tool)나 KSP(Kotlin Symbol Processing)는 주로 코드 생성 및 어노테이션 기반 프로세싱에 사용되지만, Compose Compiler는 컴파일 타임에 함수 변환 및 상태 추적 코드를 삽입해 UI 재구성의 핵심 역할을 수행

  • 즉, Compose Compiler는 UI 선언과 상태 관리에 특화된 컴파일러 플러그인이고, KAPT/KSP는 일반적인 코드 생성 도구

특수 처리?

Compose Compiler의 “특수 처리”란 Jetpack Compose에서 @Composable 함수에 대해 일반적인 Kotlin 컴파일러가 하지 않는 여러 가지 자동 코드 변환과 최적화 작업을 의미

  1. Composer 매개변수 자동 주입
    이 매개변수는 Compose 런타임이 UI 상태와 변경 사항을 추적하는 데 핵심적인 역할을 수행. 개발자가 직접 작성하지 않아도 컴파일 타임에 삽입됨

  2. 상태 추적 및 재구성 로직 삽입
    Compose Compiler는 함수 내부에 UI 상태 추적과 효율적인 재구성을 위한 코드를 삽입. 예를 들어, 어떤 값이 바뀌었을 때 해당 부분만 다시 그리도록(스마트 리컴포지션) 최적화 코드를 추가
    -> 이 과정에서 함수의 인자나 내부에서 사용하는 객체의 “안정성(stability)“을 분석해, 값이 변하지 않았다면 불필요한 UI 업데이트를 건너뜀

  3. 람다와 remember 최적화
    외부 변수를 캡처하지 않는 람다는 싱글톤으로 만들어져 불필요한 객체 생성을 막고, 캡처하는 람다는 상황에 맞게 개별 인스턴스로 처리
    remember 등 상태 저장 함수의 동작을 최적화하여, 불필요한 recomposition(재구성)을 방지

  4. Live Literals 등 개발 편의 기능 지원
    코드 내 상수 값을 실시간으로 변경해 UI에 바로 반영할 수 있도록, 각 상수에 대한 별도의 상태 관리 코드를 생성

  5. 정적 분석 및 타입/선언 검사
    컴파일 시점에 Composable 함수의 타입, 선언, 어노테이션 적용 여부 등을 검사해 잘못된 사용을 미리 차단. 예를 들어, @Composable이 누락된 람다나 잘못된 타입 사용을 컴파일 오류로 알려줌

예시 코드:

@Composable
fun Greeting(name: String) {
    Text("Hello, $name!")
}

-> Compose Compiler가 변환 후(개념적 예시)
fun Greeting(name: String, $composer: Composer, $changed: Int) {
    // 상태 추적 및 재구성 로직 자동 삽입
    // ...
    Text("Hello, $name!", $composer, $changed)
}

@Stable vs @Immutable

Q2) Compose Runtime은 재구성과 상태를 어떻게 관리하며, 내부적으로 어떤 자료 구조를 사용하는가?

  • Compose Runtime은 상태가 변경될 때 어떤 Composable이 영향을 받는지 추적하고, 필요한 부분만 재구성(Recomposition)함

  • 내부적으로는 Slot Table이라는 자료구조를 사용해 UI 트리와 상태를 효율적으로 저장·관리

  • Slot Table은 각 Composable의 위치, 상태, 값 등을 기록하며, 변경된 부분만 빠르게 찾아내어 최소한의 연산으로 UI를 갱신

Q) 1. Compose의 단계(Phase)들은 무엇인가

Compose는 다음 세 가지 주요 단계를 거침:

  • Composition: Composable 함수 실행, UI 트리 생성
  • Layout: 각 UI 요소의 크기와 위치 결정
  • Drawing: 실제 UI를 화면에 렌더링

Q1) Composition 단계에서 어떤 일이 발생하며, 이것이 Recomposition(재구성)과 어떤 관련이 있는가?

  • Composition 단계에서는 Composable 함수가 실행되어 UI 트리가 처음 만들어짐
  • Recomposition은 상태 변화 등으로 인해 일부 또는 전체 UI 트리를 다시 생성하는 과정으로, Composition 단계가 다시 실행되는 것을 의미
  • 즉, Recomposition은 Composition의 반복 실행이며, 상태 변화에 따른 UI 업데이트의 핵심

Q2) Layout 단계는 어떻게 동작하는가?

  • Layout 단계에서는 각 Composable의 크기와 위치를 계산
  • 부모 Composable이 자식에게 측정 요구사항을 전달하고, 자식은 자신의 크기를 부모에게 반환
  • 이 과정을 통해 UI 요소들이 화면에 어떻게 배치될지 결정

Q) 2. 왜 Jetpack Compose는 선언형 UI 프레임워크인가?

Compose는 UI의 “상태”를 기반으로 UI를 선언적으로 기술. 즉, “이 상태일 때 UI가 이렇게 보여야 한다”라는 방식으로 작성하며, 상태 변화가 자동으로 UI에 반영됨.

Q1) Jetpack Compose의 선언형 특성은 기존 명령형 XML UI 개발 방식과 어떻게 다르며, 어떤 장점을 제공하는가?

선언형 Compose와 명령형 XML UI의 차이 및 장점:

  • 기존 명령형(XML + View): UI 요소를 명시적으로 생성·변경해야 하며, 상태 변화에 따라 직접 뷰를 갱신해야 함
  • 선언형(Compose): 상태에 따라 UI를 선언하면, Compose가 내부적으로 필요한 UI 변경을 자동으로 처리
  • 장점: 코드가 간결해지고, 상태 관리가 명확해지며, 버그가 줄고 유지보수가 쉬워짐

Q2) Jetpack Compose는 컴포저블에서 어떤 방식으로 멱등성을 달성하며, 선언형 UI 시스템에서 이것이 왜 중요한가?

Compose의 멱등성과 중요성:

  • Compose는 Composable 함수가 동일 입력(상태)에서 항상 동일 UI를 반환하도록 설계되어 있음(멱등성)
  • 멱등성은 재구성 시 예측 가능성과 안정성을 보장하며, 선언형 UI 시스템에서 불필요한 부작용을 방지하는 데 필수적

어떤 방식으로?

  • 모든 입력을 명시적 매개변수로 받음
    컴포저블 함수는 UI를 그리는 데 필요한 모든 상태(데이터)를 매개변수로 받아, 동일한 입력이면 항상 동일한 UI 트리를 생성하도록 설계됨

  • 함수 실행 결과가 입력값에만 의존
    함수 내부에서 외부 변수나 전역 상태에 의존하지 않고, 입력값만을 사용해 결과를 만듦
    내부 상태가 필요할 땐 Compose의 상태 관리 API(예: remember)로 Compose 런타임이 추적/관리 함

  • 사이드 이펙트는 Compose의 이펙트 핸들러로만 실행
    네트워크 요청, 데이터 저장 등 부수효과는 반드시 SideEffect, LaunchedEffect 등 Compose 전용 API로 통제된 환경에서 실행하여, 예측 가능성과 멱등성을 유지함

  • @Stable, @Immutable 등 안정성 어노테이션 활용
    데이터의 변경 여부를 컴파일러와 런타임이 추적할 수 있게 하여, 값이 바뀌지 않으면 불필요한 재구성을 방지함

Q) 3. Recomposition이란 무엇이며, 언제 발생하는가? 또한 앱 성능과는 어떤 관련이 있는가?

  • Recomposition은 상태 변화 등으로 인해 Composable 함수가 다시 실행되어 UI가 갱신되는 과정
  • 상태가 변경될 때, 해당 상태를 참조하는 Composable만 재구성됨
  • 불필요한 Recomposition이 많아지면 CPU 사용량이 늘고, UI가 느려질 수 있으므로 최적화가 중요

Q) 불필요한 Recomposition을 줄이고 앱 성능을 최적화한 경험이 있는가? 그렇다면 어떤 전략을 사용했는지 설명하라. 또는 앞으로 이를 완화하기 위해 어떤 전략을 사용할 수 있는가?

불필요한 Recomposition 최적화 경험 및 전략:

  • remember, derivedStateOf, key 등 Compose API를 활용해 상태 범위를 최소화하고, 필요한 부분만 재구성하도록 설계
  • Stateless composable 설계, Stable/Immutable 데이터 구조 사용, LazyColumn 등 효율적 리스트 컴포넌트 활용
  • DiffUtil과 유사하게, 리스트 아이템에 고유 key를 부여해 최소한의 아이템만 재구성

Q) 4. Composable 함수는 내부적으로 어떻게 작동하는가?

  • @Composable 어노테이션이 붙은 함수는 Compose Compiler에 의해 특수 처리되어, UI 트리의 노드로 등록되고 상태 추적 및 재구성 대상이 됨
  • Compose Runtime이 이 함수들을 실행하고, 상태 변화에 따라 필요한 부분만 다시 호출

특수한에 대한 구체적인 서술 필요!

Q) 함수에 @Composable 어노테이션을 붙이면 무슨 일이 일어나는가?

@Composable 어노테이션의 효과:

  • 함수가 Compose의 Composition 트리에 포함되고, 상태 변화에 따라 자동으로 재구성됨
  • 컴파일 타임에 추가 코드가 삽입되어, Compose Runtime이 상태 및 재구성 타이밍을 관리할 수 있게 됨

Q) 5. Jetpack Compose에서 안정성이란 무엇이며, 성능과는 어떤 관련이 있는가?

Compose의 안정성(Stability)과 성능의 관계:

  • 안정성(Stability)은 데이터가 변경되지 않았을 때 Compose가 해당 Composable의 재구성을 건너뛸 수 있음을 의미
  • 안정성이 높을수록 불필요한 재구성이 줄어들고, 앱 성능이 향상됨

Q1) Compose 컴파일러는 파라미터가 안정한지 불안정한지를 어떻게 판단하며, 이 판단은 왜 재구성에 있어서 중요한가?

  • 파라미터 타입이 Stable(불변, 값이 바뀌지 않음)하거나 Immutable(내부 값도 불변)으로 판단되면, Compose는 해당 파라미터가 변경되지 않는 한 재구성을 건너뜀
  • 불안정한 타입(예: 일반 List, Mutable 객체 등)은 매번 재구성될 수 있음

Q2) @Stable 및 @Immutable 어노테이션은 Jetpack Compose에서 어떤 역할을 하며, 언제 사용하는 것이 적절한가?

  • @Stable: 객체의 프로퍼티가 변경될 수 있지만, 변경 시 Compose가 이를 감지할 수 있도록 명시
  • @Immutable: 객체와 내부 프로퍼티 모두 불변임을 보장
  • 데이터 클래스를 Compose에서 효율적으로 사용하고 싶을 때, 또는 커스텀 타입의 안정성을 명확히 하고 싶을 때 사용

Q) 6. Compose의 성능을 안정성 향상을 통해 최적화한 경험이 있는가?

Q) List를 파라미터로 받는 Composable 함수가 불필요한 재구성을 일으킬 때, 어떻게 최적화하겠는가?

  • List를 파라미터로 받는 Composable이 불필요하게 재구성된다면, immutable list를 사용하거나, @Stable/@Immutable 어노테이션을 활용해 Compose가 리스트의 안정성을 인식하도록 함
  • remember, derivedStateOf, key 등으로 상태 범위와 재구성 범위를 최소화

Q) 재구성 효율을 높이기 위해 어떤 API나 Compose 컴파일러 기능을 사용해 보았는가?

재구성 효율을 높이는 API/컴파일러 기능:

  • remember, rememberUpdatedState, derivedStateOf, key
  • Stable/Immutable 어노테이션
  • LazyColumn/LazyRow 등 Lazy 컴포넌트

Q) 7. Composition이란 무엇이며, 어떻게 생성하는가?

  • Composition은 Composable 함수 실행을 통해 UI 트리를 만드는 과정
  • 일반적으로 setContent, ComposeView.setContent 등에서 Composition이 시작됨

Q) ComposeView는 전통적인 View 시스템과 Compose UI 시스템 사이를 어떻게 연결하며, 이를 언제 사용하는가?

ComposeView와 View 시스템 연결:

  • ComposeView는 기존 View 시스템 안에 Compose UI를 포함할 수 있도록 연결해주는 브리지 역할을 수행
  • 점진적 마이그레이션, 기존 View와의 혼합 사용이 필요한 상황에서 유용

Q) 8. XML 기반 프로젝트를 Jetpack Compose로 마이그레이션하기 위한 전략은 무엇인가?

Q1) XML에서 Compose로 마이그레이션할 때, 기존 View 기반 레이아웃 안에 컴포저블을 어떻게 통합할 것이며, 이 방식은 어떤 상황에서 가장 유용한가?

  • ComposeView를 활용해 기존 View 레이아웃에 Compose UI를 삽입, 점진적으로 화면 또는 컴포넌트 단위로 전환
  • 복잡한 화면 전체를 한 번에 옮기기보다, 중요도가 낮거나 단순한 컴포넌트, 신규 기능, 또는 작은 UI 요소부터 점진적으로 Compose로 전환하는 것이 더 안전하고 실용적

어떤 상황?
ComposeView를 통한 통합은 대규모 레거시 프로젝트, 신규 기능 추가, 기존 View와의 혼합 운영, 점진적 마이그레이션 등에서 리스크를 줄이고, Compose 도입 효과를 빠르게 확인할 수 있어 실용적

Q2) Android 앱을 Jetpack Compose로 화면 단위(screen-by-screen)로 마이그레이션하는 것과 컴포넌트 단위(component-by-component)로 마이그레이션하는 것의 장단점은 무엇인가?

화면 단위 vs. 컴포넌트 단위 마이그레이션 장단점:

화면 단위(screen-by-screen):
장점: 구조가 명확, 테스트 용이, 일관성 유지
단점: 대규모 리팩토링 부담, 위험도 증가

컴포넌트 단위(component-by-component):
점진적 적용, 리스크 분산, 기존 코드와 혼합 가능
일관성 저하, 코드 복잡도 증가

Q) 9. 왜 항상 Jetpack Compose 성능 테스트는 릴리스 모드에서 해야 하는가?

  • 디버그 모드에서는 Compose의 최적화가 비활성화되어, 실제 성능을 제대로 측정할 수 없음
  • 릴리스 모드 + R8 최적화 활성화 시, 코드가 축소·최적화되어 실제 사용자 환경과 동일한 성능을 확인할 수 있음

Q) R8은 Jetpack Compose 성능 최적화에서 어떤 역할을 하며, 릴리스 빌드에서 어떤 구체적인 개선을 제공하는가?

  • R8은 불필요한 코드 제거, 인라이닝, 클래스 축소 등 다양한 최적화 기법을 적용
  • Compose에서는 R8 최적화로 앱 크기 감소, 불필요한 재구성 로직 제거, 실행 속도 개선 등 이점을 제공

Q) 10. Jetpack Compose에서 자주 사용되는 Kotlin 관용구(idiom)는 무엇인가?

  • 트레일링 람다(trailing lambda): Composable 함수에 UI 구조를 람다로 전달해, 가독성과 선언적 스타일을 극대화
  • 고차 함수(higher-order function): 이벤트 처리, 상태 전달, UI 구조화 등에 활용되어, 재사용성과 유연성을 높임

Q) 트레일링 람다(trailing lambdas)와 고차 함수(higher-order functions)는 컴포저블 함수 구조화에서 어떤 역할을 하는가?

트레일링 람다 & 고차 함수의 역할:

  • Composable의 UI 계층 구조를 명확히 표현하고, 콜백/이벤트 전달을 간결하게 처리할 수 있음

예시 코드:

Button(onClick = { /* ... */ }) {
    Text("확인")
}

스터디 언급

1. Strong Skipping Mode가 등장한 시점에서, immutable collections을 굳이 사용해야 하는가?

결론부터 요약:

  • Strong Skipping Mode가 활성화되면, Compose는 불안정(unstable)한 파라미터(예: 일반 List, Map, Set)를 가진 컴포저블도 Skip 최적화대 대상에 포함시킴

  • 그러나, immutable collections을 완전히 대체하지는 못함. Strong Skipping Mode가 모든 문제를 해결하지는 않으며, 여전히 주의해야 할 점이 존재

상세 설명:

1. Strong Skipping Mode의 동작 방식

  • Strong Skipping Mode가 활성화되면,
    Compose는 unstable 파라미터의 변경 여부를 “참조 동등성(===)“으로 판단, stable 파라미터의 변경 여부는 equals()로 판단(기존과 같음)
  • 즉, List의 내용이 바뀌어도, 같은 객체(참조)라면 변경되지 않은 것으로 간주하여 재구성을 건너뜀

예시 코드:

var list by remember { mutableStateOf(mutableListOf("A")) }

Button(onClick = {
    list.add("B") // list 내용은 바뀌지만, list 객체(참조)는 그대로
}) {
    Text("Add B")
}

MyList(list)
  • 반대로, 내용이 같아도 새로운 객체(참조)라면 변경된 것으로 간주하여 재구성이 발생

예시 코드:

var list by remember { mutableStateOf(listOf("A", "B")) }

Button(onClick = {
    list = listOf("A", "B") // 내용은 동일하지만, 새로운 객체 할당
}) {
    Text("Replace List")
}

MyList(list)

2. 일반 Kotlin 컬렉션(List, Map 등) 사용 시 주의점

  • Compose 컴파일러는 List, Map, Set 등 Kotlin 표준 컬렉션 타입을 항상 unstable로 간주
  • Strong Skipping Mode가 활성화되어도, 컬렉션의 내용이 바뀌었는데 참조가 유지되면 UI가 갱신되지 않는 문제가 있음
    (예: list.add()로 내부 값을 바꿨지만, 참조는 동일한 경우 → Compose는 변경을 감지하지 못함)
  • 즉, 불변 컬렉션을 쓰지 않으면, 데이터 동기화와 UI 일관성에 문제가 생길 수 있음

3. immutable collections의 장점

  • Compose 컴파일러는 kotlinx-collections-immutable 등 진짜 불변 컬렉션(ImmutableList, ImmutableSet 등)을 사용할 때만 해당 파라미터를 안정적(stable)으로 간주

  • 불변 컬렉션은 값이 변경되면 항상 새로운 객체가 생성되므로, 참조가 달라지고 Compose가 안전하게 변경을 감지할 수 있음

  • 따라서 불필요한 재구성은 줄이고, 필요한 경우에만 UI가 갱신되어 성능과 안정성 모두 확보할 수 있음

결론 및 권장 사항

  • Strong Skipping Mode가 있다고 해서, 일반 List/Map/Set을 무작정 써도 되는 것은 아님!

  • 컬렉션의 변경을 안전하게 Compose에 알리고 싶다면, 여전히 immutable collections을 사용하는 것이 가장 확실

  • 특히 데이터가 자주 변경되거나, UI 일관성이 중요한 경우에는 immutable collections 사용을 권장

  • 단, 아주 단순한 화면이거나, 컬렉션 참조가 항상 새로 할당되는 구조라면 일반 컬렉션도 실용적으로 쓸 수 있음. 하지만, 이 경우에도 실수로 참조만 유지되고 내용이 바뀌는 버그에 주의해야 함

컬렉션 타입Compose 인식Strong Skipping Mode 적용시안전한 변경 감지
List/Map/Setunstable참조(===) 비교X
ImmutableList 등stableequals() 비교O

참조 동등성(Reference Equality) 설명

참조 동등성은 두 객체가 메모리에서 “정확히 같은 인스턴스”를 가리키고 있는지를 비교하는 개념

즉, 두 변수의 값이 객체의 “내용”이 아니라, 메모리 주소(참조값)가 같은지를 확인

Java/C#/Kotlin 등에서 == 연산자로 비교할 때 참조 타입(객체)의 경우, 두 변수가 같은 객체를 참조하면 true, 아니면 false

예시 코드:

Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
Person p3 = p1;

System.out.println(p1 == p2); // false (내용은 같아도 서로 다른 객체)
System.out.println(p1 == p3); // true (동일한 객체를 참조)

이처럼 참조 동등성은 객체의 “동일성(Identity)”을 비교하는 것이며, 동등성(Equality)(내용이 같은지, equals() 오버라이드로 비교)과는 다름

리컴포지션 발생조건!

  • State 객체(예: mutableStateOf, remember, mutableStateListOf 등)의 값이 변경될 때

    Compose는 해당 State를 읽는 모든 컴포저블을 추적하고 있으며,
    이 값이 바뀌면 관련된 컴포저블 함수가 자동으로 다시 실행되어 UI가 갱신됨

  • 컴포저블의 입력 파라미터가 변경될 때
    상위 컴포저블이 하위 컴포저블에 전달하는 인자(파라미터)가 바뀌면
    해당 하위 컴포저블이 리컴포지션됨

  • 컴포저블의 호출 구조(호출 사이트)가 변경될 때
    조건문, 리스트 등에서 컴포저블의 호출 위치나 호출 여부가 바뀌면
    해당 컴포저블이 새로 추가되거나 제거되면서 리컴포지션이 발생

요약

  • 리컴포지션은 “관찰 가능한 상태(State)의 값이 변경될 때” 자동으로 발생

  • 이 상태를 읽는 컴포저블, 혹은 해당 상태를 파라미터로 받는 하위 컴포저블이 다시 실행되어 UI가 최신 상태로 갱신

  • 단, 일반 변수나 컬렉션의 내부 값 변경 등 Compose가 추적하지 않는 변경에는 리컴포지션이 발생하지 않음

즉, Compose가 추적하는 상태(State 객체)의 값이 바뀔 때마다, 그 상태를 읽는 컴포저블에서 리컴포지션이 일어남

reference)
http://stackoverflow.com/questions/69718059/android-jetpack-compose-mutablestatelistof-not-doing-recomposition

https://developer.android.com/develop/ui/compose/performance/stability/strongskipping?hl=ko

https://github.com/JetBrains/kotlin/blob/master/plugins/compose/design/strong-skipping.md

2. stability.config.conf

https://developer.android.com/develop/ui/compose/performance/stability/fix?hl=ko

stability.config.conf 를 극한으로 사용한 예시)
https://github.com/chrisbanes/tivi/blob/main/compose-stability.conf

reference)
https://haeti.palms.blog/compose-stability
https://thinking-face.tistory.com/369

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글