[Android] Compose Runtime

easyhooon·2025년 6월 11일
0
post-thumbnail

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

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

Practical Questions

Q) 11. State란 무엇이며, 어떤 API를 통해 상태를 관리하는가?

State는 앱 내에서 시간에 따라 변할 수 있는 모든 값을 의미하며, 이는 UI의 데이터, 애니메이션 상태, 사용자 입력 등 매우 광범위하게 적용됨

Jetpack Compose에서는 상태를 명시적으로 관리하며, 대표적으로 mutableStateOf, remember, rememberSaveable 등의 API를 통해 상태를 선언하고 관리함

이러한 상태 객체가 변경되면 해당 상태를 읽는 컴포저블이 자동으로 리컴포지션 됨

Q) 상태가 Recomposition과 어떻게 관련되며, Recomposition 중에 어떤 일이 발생하는가?

Compose는 선언형 UI 프레임워크이기 때문에, 상태가 변경되면 해당 상태를 읽는 컴포저블이 다시 실행(recomposition)되어 UI가 새로운 상태를 반영하도록 갱신됨

Recomposition 시, Compose는 변경된 상태를 추적하고 필요한 부분만 효율적으로 다시 그림

Q) 12. State Hoisting 의 장점은 무엇인가?

State Hoisting은 컴포저블 내부의 상태를 외부(호출자)로 끌어올려 컴포저블을 Stateless하게 만드는 패턴

주요 장점:

  • 단일 소스의 진실(Single source of truth): 상태가 한 곳에만 존재해 버그 발생 가능성이 줄어듬
  • 재사용성: Stateless 컴포저블은 다양한 상황에서 재사용이 용이
  • 테스트 용이성: 외부에서 상태를 주입받으므로, 테스트 시 다양한 상태를 쉽게 주입할 수 있음
  • 상태 공유: 여러 컴포저블이 동일한 상태를 공유할 수 있음
  • 캡슐화 및 디커플링: 상태 변경 이벤트를 외부에서 제어할 수 있어 컴포저블 간 결합도가 낮아짐

Q1) 상태 호이스팅(state hoisting)이 컴포저블 함수의 재사용성과 테스트 용이성을 어떻게 향상시키는가?

Stateless 컴포저블은 내부에 상태가 없으므로, 다양한 상태 값을 외부에서 주입받아 여러 상황에 쉽게 재사용할 수 있고, 테스트 시에도 원하는 상태를 주입해 동작을 검증하기 쉬움

Q2) 어떤 상황에서 상태 호이스팅을 피하고 상태를 컴포저블 내부에 유지하는 것이 더 적합한가?

  • 상태가 해당 컴포저블 내부에서만 의미가 있고, 외부에서 제어할 필요가 없는 경우

  • 외부에 상태를 노출하면 오히려 API가 복잡해지고, 불필요한 상태 공유로 인해 버그가 발생할 수 있는 경우

  • 예를 들어, 임시적으로만 사용되는 UI 상태(애니메이션 상태 등)는 내부에 유지하는 것이 적합

Q) 13. remember와 rememberSaveable의 차이점은 무엇인가?

  • remember: 컴포저블이 Composition에 남아 있는 동안(즉, recomposition이 반복되어도) 상태를 유지
    즉, recomposition이 일어나더라도 remember로 저장된 값은 그대로 유지됨
    하지만 컴포저블이 Composition에서 완전히 제거되면(예: 화면에서 사라지거나 네비게이션으로 이동 등), remember로 저장된 상태는 사라집니다.

  • rememberSaveable: remember와 달리, 화면 회전과 같은 구성 변경(configuration change)이나 프로세스가 일시적으로 종료됐다가 복원되는 상황에서도 상태가 유지됨
    내부적으로 Bundle 또는 SavedStateHandle 등에 값을 저장하여, 앱이 강제 종료되지 않는 한 상태를 복원할 수 있음

Q1) 어떤 상황에서 rememberSaveableremember 대신 사용하는 것이 더 적절한가? 그리고 어떤 트레이드오프가 있을까?

  • rememberSaveable은 화면 회전, 프로세스 종료 등 구성 변경에도 반드시 상태를 보존해야 할 때 적합
  • 단점은 저장 가능한 타입(Serializable, Parcelable 등)으로 제한되고, Bundle에 복사되므로 메모리 사용이 증가할 수 있음
    단순하고 일시적인 상태에는 remember만 사용하는 것이 효율적

Q2) Bundle에 바로 저장할 수 없는 사용자 정의 상태 객체(non-primitive)를 rememberSaveable로 안전하게 저장하고 복원하려면 어떤 방법을 사용해야 하는가?

Bundle에서 지원하지 않는 타입은 Saver를 직접 구현해 저장 및 복원 로직을 지정해야 함

Saver를 직접 구현하는 방법:
rememberSaveable은 기본적으로 primitive 타입(Int, String 등)이나 Bundle에 저장 가능한 타입만 자동으로 저장함

사용자 정의 타입(예: data class 등)은 직접 저장/복원 규칙을 지정해야 하며, 이를 위해 Saver를 구현해야 함

1. Parcelable을 사용하는 방법
가장 간단한 방법은 data class에 @Parcelize 어노테이션을 붙이고 Parcelable을 구현하는 것

@Parcelize
data class Person(val name: String, val age: Int, val isMan: Boolean) : Parcelable

@Composable
fun PersonScreen() {
    var person by rememberSaveable { mutableStateOf(Person("Alice", 30, false)) }
    // ...
}

이렇게 하면 Compose가 자동으로 Bundle에 저장/복원을 지원

2.Custom Saver 구현 예시
만약 Parcelable을 사용할 수 없는 상황이라면, Saver를 직접 구현해야 함
Saver는 객체를 저장 가능한 형태(예: Map, List 등)로 변환하고, 복원할 때 다시 원래 객체로 변환하는 역할을 함

data class User(val name: String, val age: Int)

val UserSaver = Saver<User, Map<String, Any>>(
    save = { mapOf("name" to it.name, "age" to it.age) },
    restore = { User(it["name"] as String, it["age"] as Int) }
)

@Composable
fun CustomSaverExample() {
    var user by rememberSaveable(stateSaver = UserSaver) {
        mutableStateOf(User(name = "Akshay", age = 28))
    }
    // ...
}

이렇게 하면 User 객체도 안전하게 저장/복원할 수 있음

3. mapSaver, listSaver 활용
Compose에서는 mapSaver 또는 listSaver 유틸리티 함수를 제공하므로, 간단히 사용할 수 있음

data class City(val name: String, val country: String)

val CitySaver = mapSaver(
    save = { mapOf("name" to it.name, "country" to it.country) },
    restore = { City(it["name"] as String, it["country"] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity by rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
    // ...
}

또는 listSaver를 사용할 수도 있음

data class Point(val x: Int, val y: Int)

val PointSaver = listSaver<Point, Int>(
    // Point 객체를 List로 변환
    save = { listOf(it.x, it.y) },
    // List에서 다시 Point 객체로 복원 
    restore = { Point(it[0], it[1]) }
)

@Composable
fun PointScreen() {
    var point by rememberSaveable(stateSaver = PointSaver) {
        mutableStateOf(Point(10, 20))
    }
    // point 값을 안전하게 저장/복원할 수 있음
}

정리:

  • Parcelable: @Parcelize를 붙이면 가장 간단하게 저장/복원 가능
  • Custom Saver: Saver, mapSaver, listSaver를 이용해 저장/복원 규칙을 직접 지정
  • rememberSaveable(stateSaver = …)로 커스텀 Saver를 전달해 사용.

이렇게 하면 기본적으로 저장되지 않는 사용자 정의 상태도 안전하게 rememberSaveable로 관리할 수 있음

Q) 14. 컴포저블 함수 내에서 코루틴 스코프를 안전하게 생성하려면 어떻게 해야 하는가?

  • Effect API(예: LaunchedEffect, rememberCoroutineScope)를 사용해야 함
    이 API들은 컴포저블의 생명주기를 자동으로 추적해, 컴포저블이 사라질 때 코루틴도 함께 취소함

  • 위험 요소: 컴포저블 내부에서 직접 CoroutineScope를 생성하면, 리컴포지션마다 새로운 코루틴이 쌓여 메모리 누수 및 예기치 않은 동작이 발생할 수 있음

Q) 컴포저블 내부에서 직접 코루틴을 실행할 때의 위험 요소는 무엇이며, 이를 어떻게 방지할 수 있을까?

  • 반드시 rememberCoroutineScope 또는 Effect API를 사용해 컴포저블의 생명주기를 따르도록 함

Q) 15. 컴포저블 함수 내에서 side effect는 어떻게 처리하는가?

Compose는 부수 효과(Side Effect)를 명시적으로 관리하기 위해 Effect API(LaunchedEffect, SideEffect, DisposableEffect 등)를 제공

Q1) LaunchedEffect는 suspend 함수를 어떻게 관리하며, 키 값이 변경되면 어떤 일이 발생하는가?

LaunchedEffect는 suspend 함수를 실행하며, 지정한 key가 변경되거나 최초 composition 시 실행됨

key가 변경되면 기존 코루틴은 취소되고 새로운 코루틴이 실행됨

Q2) 어떤 상황에서 LaunchedEffect 대신 DisposableEffect를 사용해야 하는가?

DisposableEffect는 리스너 등록/해제, 리소스 할당/해제 등 컴포저블이 Composition에서 제거될 때 반드시 정리해야 하는 작업에 적합
-> onDispose 블록에서 정리 작업을 수행

Q3) SideEffect의 사용 사례를 설명하고 LaunchedEffect와 어떤 차이가 있는지 말하라

SideEffect는 suspend가 아닌 코드로, 매번 성공적으로 recomposition이 끝난 후 실행됨
예를 들어, 외부 시스템(Analytics 등)과의 동기화에 적합

반면, LaunchedEffect는 suspend 함수를 실행하며, 키 값이 변경될 때마다 실행됨

Q) 16. rememberUpdatedState는 언제 사용하며, 어떤 역할을 하는가?

rememberUpdatedState는 최신 값을 항상 유지하는 State를 반환
오래 실행되는 코루틴이나 람다가 최신 파라미터 값을 참조해야 할 때 사용

Q1) rememberUpdatedStateremember는 어떤 점에서 다르며, 각각 언제 사용하는 것이 좋을까?

remember는 최초 값만 기억하고, 파라미터가 변경되어도 값을 갱신하지 않음
반면 rememberUpdatedState는 파라미터가 변경될 때마다 value 값을 갱신
최신 값을 참조해야 하는 콜백, 코루틴 등에서 rememberUpdatedState를 사용

Q2) LaunchedEffect 내부에서 오래 실행되는 코루틴이 최신 상태 값을 참조하려면 어떻게 해야 하는가?

LaunchedEffect 내부에서 rememberUpdatedState로 래핑한 값을 사용하면, 코루틴이 오래 실행되더라도 항상 최신 값을 참조할 수 있음

Q) 17. produceState의 목적은 무엇이며, 어떻게 동작하는가?

produceState는 Compose 외부의 비 Compose 상태(예: 네트워크, 데이터베이스 등)를 Compose의 State로 변환하는 API

내부적으로 코루틴을 실행해 값을 갱신하고, State로 노출recomposition이나 구성 변경에도 안전하게 동작

Q) 컴포저블 함수에서 코루틴 작업을 실행하고 그 결과를 상태로 관찰해야 할 때, LaunchedEffectrememberCoroutineScope를 사용하지 않고 어떻게 구현할 수 있는가?

produceState를 사용하면 코루틴에서 값을 갱신하고, 그 결과를 State로 노출할 수 있음
이 방식은 외부 데이터 소스를 Compose에 자연스럽게 연결할 때 유용

Q) 18. snapshotFlow는 무엇이며, 어떻게 동작하는가?

snapshotFlow는 Compose의 상태를 Flow로 변환하는 API

Compose의 상태가 변경될 때마다 Flow에서 값을 방출(emit)
Compose의 상태를 관찰해 비 Compose 코드(예: ViewModel)와 연동할 때 유용

Q) 어떤 상황에서 ViewModel에서 직접 Flow를 관찰하는 것보다 snapshotFlow를 사용하는 것이 더 적합하며, snapshotFlow의 방출(emit) 동작을 어떻게 최적화할 수 있는가?

snapshotFlow는 Compose 상태에 직접 반응하므로, UI 상태와 비즈니스 로직의 연결이 자연스러움

emit 최적화를 위해서는 snapshotFlow 내에서 불필요한 상태 참조를 줄이고, distinctUntilChanged 등 연산자를 활용해 중복 방출을 방지할 수 있음

Q) 19. derivedStateOf의 목적은 무엇이며 recomposition 최적화에 어떤 도움을 주는가?

Q) 어떤 상황에서 derivedStateOf를 사용하는가? 또한, 다른 상태 변수로부터 값을 계산하더라도 derivedStateOf 사용을 피해야 하는 경우는 언제인가?

derivedStateOf는 여러 상태 변수로부터 파생된 값을 계산할 때 사용
내부적으로 캐싱되어, 참조한 상태가 변경될 때만 recomposition이 발생
이를 통해 불필요한 recomposition을 줄여 성능을 최적화할 수 있다

사용 시점 및 피해야 할 경우:

  • 파생 값이 여러 상태에 의존하고, 계산 비용이 크거나 recomposition을 최소화하고 싶을 때 사용
  • 단, 파생 값이 자주 변경되거나, 단순한 값이라 recomposition 최적화가 의미 없을 때는 사용을 피하는 것이 좋음

Q) 20. 컴포저블 함수 또는 Composition의 생명주기는 어떻게 되는가?

  • 컴포저블은 Composition에 진입(enter), 0회 이상 recomposition, Composition에서 퇴장(leave)의 생명주기를 갖음

Q) 컴포저블 함수의 생명주기 단계를 설명하고, 상태가 변경되었을 때 Compose가 리컴포지션을 어떻게 처리하는지 설명하라

  • 상태가 변경되면 Compose는 해당 상태를 읽는 컴포저블만 다시 실행(recomposition)해 변경 사항을 반영
    이때, 불필요한 recomposition을 최소화하기 위해 Compose가 내부적으로 최적화(Smart Recomposition)

어떻게가 빠짐

Q) 21. SaveableStateHolder란 무엇인가?

SaveableStateHolder는 rememberSaveable로 저장된 상태를 컴포저블이 Composition에서 사라지기 전에 저장하고, 동일한 key로 다시 Composition에 진입하면 상태를 복원해주는 API

여러 화면(탭 등)의 상태(스크롤 위치, 입력 값 등)를 독립적으로 저장/복원할 수 있음

Q) Jetpack Navigation 라이브러리를 사용하지 않고도, 탭 기반 UI에서 각 탭의 스크롤 위치나 입력 상태를 화면 전환 후에도 유지하려면 어떻게 구현하겠는가?

각 탭을 SaveableStateProvider로 감싸고, 탭 전환 시 각 탭의 key를 유지하면, 각 탭의 상태가 독립적으로 저장/복원됨

Q) 22. snapshot 시스템의 목적은 무엇인가?

Compose의 Snapshot 시스템은 현재 상태를 특정 시점에 캡처하여, 멀티스레드 환경에서도 일관되고 안전하게 상태를 관리하고, UI 업데이트를 트랜잭션처럼 처리할 수 있게 함

Q) Compose에서 상태를 직접 관찰하는 대신, Snapshot.takeSnapshot()을 사용하는 것이 더 적절한 시나리오는 어떤 경우인가?

멀티스레드 환경에서 상태의 일관성을 보장하거나, 특정 시점의 상태를 캡처해 분석/디버깅해야 할 때 적합

그렇다고 함... 직접 사용해본 적은 없음

Q) 23. mutableStateListOf와 mutableStateMapOf는 무엇인가?

mutableStateListOf, mutableStateMapOf는 리스트/맵의 변경 사항을 Compose가 추적할 수 있도록 래핑한 상태 객체
이 객체를 사용하면 리스트/맵의 변경이 UI에 자동 반영

Q1) mutableStateOf로 감싼 일반 mutableListOf를 수정해도 왜 Compose에서는 리컴포지션이 발생하지 않는가? 이 문제를 어떻게 해결할 수 있을까?

mutableStateOf로 감싸도 내부 리스트의 변경은 Compose가 추적하지 못함

mutableStateListOf를 사용해야 리스트의 변경이 Compose에 감지되어 리컴포지션이 발생

예시 필요

Q2) LazyColumn에서 리스트 항목을 동적으로 추가하거나 제거할 때, UI 업데이트를 효율적으로 추적하려면 어떻게 해야 하는가?

mutableStateListOf를 사용하면 리스트 변경 시 LazyColumn이 자동으로 업데이트됨
각 항목에 고유 key를 부여하면 더 효율적으로 변경 사항을 추적할 수 있음

Q) 24. Kotlin의 Flow를 메모리 누수 없이 안전하게 Composable 함수에서 수집하려면 어떻게 해야 하는가?

Q) collectAsState는 생명주기를 인식하지 못하기 때문에 UI가 보이지 않는 동안에도 수집을 계속할 수 있으며, 이로 인해 메모리 누수가 발생할 수 있다. 이 문제를 어떻게 해결할 수 있을까?

collectAsState 등 Compose-aware API를 사용해 Flow를 수집해야 함
단, collectAsState는 컴포저블의 생명주기를 완전히 인식하지 못해, UI가 보이지 않아도 Flow를 계속 수집할 수 있음

메모리 누수 방지 방법:

Flow 수집을 composable의 생명주기(Composition)에 맞춰 중단하려면, DisposableEffect 등으로 수집을 관리하거나, collectAsStateWithLifecycle 등 생명주기 인식이 강화된 API를 활용해야 함

Q) 25. CompositionLocal의 역할은 무엇인가?

Q) CompositionLocal이란 무엇이며, 어떤 상황에서 Composable 함수에 매개변수를 전달하는 대신 사용할 수 있는가?

CompositionLocal은 데이터(테마, 로케일, 환경 등)를 컴포저블 트리 전체에 암시적으로 전달하는 메커니즘
매개변수로 전달하기 번거로운 cross-cutting concern(테마, 권한 등)에 적합하다

cross-cutting concern?

여러 모듈이나 계층에 걸쳐 공통적으로 필요한 기능이나 관심사

Q) CompositionLocalProvider는 어떻게 작동하며, CompositionLocal에 값을 제공하지 않고 접근하려고 하면 어떤 일이 발생하는가?

CompositionLocalProvider는 특정 CompositionLocal에 값을 제공하고, 해당 트리 하위의 컴포저블에서 current로 값을 읽을 수 있게 함

값을 제공하지 않으면 기본값(default)이 사용된다. 만약 기본값이 없으면 예외가 발생할 수 있음

스터디 언급

1. CompositionLocal을 이용한 전역 이벤트 관리(럭키 이벤트 버스?)

Android에서 Props Drilling을 해결해보자 with SnackBar

props drilling

JimSproch X

2. Recomposition 최적화 필드 -> 람다

Composable 함수 상태값 전달에서, Field vs Lambda 주입의 차이

reference)
https://velog.io/@squart300kg/Composable%ED%95%A8%EC%88%98-%EC%83%81%ED%83%9C%EA%B0%92-%EC%A0%84%EB%8B%AC%EC%97%90%EC%84%9C-Field-vs-Lambda-%EC%A3%BC%EC%9E%85%EC%9D%98-%EC%B0%A8%EC%9D%B4

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

0개의 댓글