Manifest Android Interview 책을 읽고 Practical Questions 에 대한 답변을 작성해보고, 카테고리 내에 특정 개념에 대한 딥다이브 및 스터디 시간에 얘기 나누면 좋을 내용들을 적어보는 글입니다.
답변 정리는 LLM의 도움을 받았습니다.
Q) 0. Jetpack Compose의 구조는 무엇인가?
Jetpack Compose는 선언형 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 컴파일러가 하지 않는 여러 가지 자동 코드 변환과 최적화 작업을 의미
Composer 매개변수 자동 주입
이 매개변수는 Compose 런타임이 UI 상태와 변경 사항을 추적하는 데 핵심적인 역할을 수행. 개발자가 직접 작성하지 않아도 컴파일 타임에 삽입됨
상태 추적 및 재구성 로직 삽입
Compose Compiler는 함수 내부에 UI 상태 추적과 효율적인 재구성을 위한 코드를 삽입. 예를 들어, 어떤 값이 바뀌었을 때 해당 부분만 다시 그리도록(스마트 리컴포지션) 최적화 코드를 추가
-> 이 과정에서 함수의 인자나 내부에서 사용하는 객체의 “안정성(stability)“을 분석해, 값이 변하지 않았다면 불필요한 UI 업데이트를 건너뜀
람다와 remember 최적화
외부 변수를 캡처하지 않는 람다는 싱글톤으로 만들어져 불필요한 객체 생성을 막고, 캡처하는 람다는 상황에 맞게 개별 인스턴스로 처리
remember
등 상태 저장 함수의 동작을 최적화하여, 불필요한 recomposition(재구성)을 방지
Live Literals 등 개발 편의 기능 지원
코드 내 상수 값을 실시간으로 변경해 UI에 바로 반영할 수 있도록, 각 상수에 대한 별도의 상태 관리 코드를 생성
정적 분석 및 타입/선언 검사
컴파일 시점에 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)
}
Q2) Compose Runtime은 재구성과 상태를 어떻게 관리하며, 내부적으로 어떤 자료 구조를 사용하는가?
Compose Runtime은 상태가 변경될 때 어떤 Composable이 영향을 받는지 추적하고, 필요한 부분만 재구성(Recomposition)함
내부적으로는 Slot Table이라는 자료구조를 사용해 UI 트리와 상태를 효율적으로 저장·관리
Slot Table은 각 Composable의 위치, 상태, 값 등을 기록하며, 변경된 부분만 빠르게 찾아내어 최소한의 연산으로 UI를 갱신
Q) 1. Compose의 단계(Phase)들은 무엇인가
Compose는 다음 세 가지 주요 단계를 거침:
Q1) Composition 단계에서 어떤 일이 발생하며, 이것이 Recomposition(재구성)과 어떤 관련이 있는가?
Q2) Layout 단계는 어떻게 동작하는가?
Q) 2. 왜 Jetpack Compose는 선언형 UI 프레임워크인가?
Compose는 UI의 “상태”를 기반으로 UI를 선언적으로 기술. 즉, “이 상태일 때 UI가 이렇게 보여야 한다”라는 방식으로 작성하며, 상태 변화가 자동으로 UI에 반영됨.
Q1) Jetpack Compose의 선언형 특성은 기존 명령형 XML UI 개발 방식과 어떻게 다르며, 어떤 장점을 제공하는가?
선언형 Compose와 명령형 XML UI의 차이 및 장점:
Q2) Jetpack Compose는 컴포저블에서 어떤 방식으로 멱등성을 달성하며, 선언형 UI 시스템에서 이것이 왜 중요한가?
Compose의 멱등성과 중요성:
어떤 방식으로?
모든 입력을 명시적 매개변수로 받음
컴포저블 함수는 UI를 그리는 데 필요한 모든 상태(데이터)를 매개변수로 받아, 동일한 입력이면 항상 동일한 UI 트리를 생성하도록 설계됨
함수 실행 결과가 입력값에만 의존
함수 내부에서 외부 변수나 전역 상태에 의존하지 않고, 입력값만을 사용해 결과를 만듦
내부 상태가 필요할 땐 Compose의 상태 관리 API(예: remember)로 Compose 런타임이 추적/관리 함
사이드 이펙트는 Compose의 이펙트 핸들러로만 실행
네트워크 요청, 데이터 저장 등 부수효과는 반드시 SideEffect, LaunchedEffect 등 Compose 전용 API로 통제된 환경에서 실행하여, 예측 가능성과 멱등성을 유지함
@Stable, @Immutable 등 안정성 어노테이션 활용
데이터의 변경 여부를 컴파일러와 런타임이 추적할 수 있게 하여, 값이 바뀌지 않으면 불필요한 재구성을 방지함
Q) 3. Recomposition이란 무엇이며, 언제 발생하는가? 또한 앱 성능과는 어떤 관련이 있는가?
Q) 불필요한 Recomposition을 줄이고 앱 성능을 최적화한 경험이 있는가? 그렇다면 어떤 전략을 사용했는지 설명하라. 또는 앞으로 이를 완화하기 위해 어떤 전략을 사용할 수 있는가?
불필요한 Recomposition 최적화 경험 및 전략:
Q) 4. Composable 함수는 내부적으로 어떻게 작동하는가?
특수한에 대한 구체적인 서술 필요!
Q) 함수에 @Composable 어노테이션을 붙이면 무슨 일이 일어나는가?
@Composable 어노테이션의 효과:
Q) 5. Jetpack Compose에서 안정성이란 무엇이며, 성능과는 어떤 관련이 있는가?
Compose의 안정성(Stability)과 성능의 관계:
Q1) Compose 컴파일러는 파라미터가 안정한지 불안정한지를 어떻게 판단하며, 이 판단은 왜 재구성에 있어서 중요한가?
Q2) @Stable 및 @Immutable 어노테이션은 Jetpack Compose에서 어떤 역할을 하며, 언제 사용하는 것이 적절한가?
Q) 6. Compose의 성능을 안정성 향상을 통해 최적화한 경험이 있는가?
Q) List를 파라미터로 받는 Composable 함수가 불필요한 재구성을 일으킬 때, 어떻게 최적화하겠는가?
Q) 재구성 효율을 높이기 위해 어떤 API나 Compose 컴파일러 기능을 사용해 보았는가?
재구성 효율을 높이는 API/컴파일러 기능:
Q) 7. Composition이란 무엇이며, 어떻게 생성하는가?
Q) ComposeView는 전통적인 View 시스템과 Compose UI 시스템 사이를 어떻게 연결하며, 이를 언제 사용하는가?
ComposeView와 View 시스템 연결:
Q) 8. XML 기반 프로젝트를 Jetpack Compose로 마이그레이션하기 위한 전략은 무엇인가?
Q1) XML에서 Compose로 마이그레이션할 때, 기존 View 기반 레이아웃 안에 컴포저블을 어떻게 통합할 것이며, 이 방식은 어떤 상황에서 가장 유용한가?
어떤 상황?
ComposeView를 통한 통합은 대규모 레거시 프로젝트, 신규 기능 추가, 기존 View와의 혼합 운영, 점진적 마이그레이션 등에서 리스크를 줄이고, Compose 도입 효과를 빠르게 확인할 수 있어 실용적
Q2) Android 앱을 Jetpack Compose로 화면 단위(screen-by-screen)로 마이그레이션하는 것과 컴포넌트 단위(component-by-component)로 마이그레이션하는 것의 장단점은 무엇인가?
화면 단위 vs. 컴포넌트 단위 마이그레이션 장단점:
화면 단위(screen-by-screen):
장점: 구조가 명확, 테스트 용이, 일관성 유지
단점: 대규모 리팩토링 부담, 위험도 증가
컴포넌트 단위(component-by-component):
점진적 적용, 리스크 분산, 기존 코드와 혼합 가능
일관성 저하, 코드 복잡도 증가
Q) 9. 왜 항상 Jetpack Compose 성능 테스트는 릴리스 모드에서 해야 하는가?
Q) R8은 Jetpack Compose 성능 최적화에서 어떤 역할을 하며, 릴리스 빌드에서 어떤 구체적인 개선을 제공하는가?
Q) 10. Jetpack Compose에서 자주 사용되는 Kotlin 관용구(idiom)는 무엇인가?
Q) 트레일링 람다(trailing lambdas)와 고차 함수(higher-order functions)는 컴포저블 함수 구조화에서 어떤 역할을 하는가?
트레일링 람다 & 고차 함수의 역할:
예시 코드:
Button(onClick = { /* ... */ }) {
Text("확인")
}
결론부터 요약:
Strong Skipping Mode가 활성화되면, Compose는 불안정(unstable)한 파라미터(예: 일반 List, Map, Set)를 가진 컴포저블도 Skip 최적화대 대상에 포함시킴
그러나, immutable collections을 완전히 대체하지는 못함. Strong Skipping Mode가 모든 문제를 해결하지는 않으며, 여전히 주의해야 할 점이 존재
상세 설명:
1. Strong Skipping Mode의 동작 방식
예시 코드:
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 등) 사용 시 주의점
list.add()
로 내부 값을 바꿨지만, 참조는 동일한 경우 → Compose는 변경을 감지하지 못함)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/Set | unstable | 참조(===) 비교 | X |
ImmutableList 등 | stable | equals() 비교 | O |
참조 동등성은 두 객체가 메모리에서 “정확히 같은 인스턴스”를 가리키고 있는지를 비교하는 개념
즉, 두 변수의 값이 객체의 “내용”이 아니라, 메모리 주소(참조값)가 같은지를 확인
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 객체)의 값이 바뀔 때마다, 그 상태를 읽는 컴포저블에서 리컴포지션이 일어남
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
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