Compose 심화 — CompositionLocal·제스처·View interop·접근성
Compose의 마지막 조각들을 모은다 — 트리 전체에 값을 암묵 전달하는 CompositionLocal, 터치·드래그를 다루는 제스처, 기존 View와 섞어 쓰는 interop, 모두가 쓸 수 있게 하는 접근성. 이 넷이 갖춰지면 기존 앱에 Compose를 섞고, 복잡한 입력을 다루고, 장애가 있는 사용자까지 쓸 수 있는 실무 UI가 완성된다. (출처: Android — CompositionLocal / Pointer input / Interoperability / Accessibility.)
CompositionLocal — 트리에 값을 암묵 전달
화면을 그리다 보면 테마·Context·화면 밀도 같은 값을 깊은 곳의 컴포저블까지 전달해야 할 때가 많다. 이걸 파라미터로 줄줄이 넘기면(prop drilling) — 중간의 컴포저블들이 자기는 안 쓰는 값을 그저 아래로 전달하기 위해 받아야 해 시그니처가 지저분해진다. CompositionLocal은 이를 푼다 — 트리 위에서 값을 한 번 제공하면 하위 어디서든 파라미터 없이 읽는다. MaterialTheme([14])이 색·타이포를 트리 전체에 뿌리는 것도 바로 이 위에 서 있다.
val context = LocalContext.current // 내장: Context (어디서든)
val density = LocalDensity.current // 내장: 밀도(dp↔px 변환)
// 커스텀 — 현재 로그인 사용자를 트리 전체에 제공
val LocalUser = compositionLocalOf<User?> { null }
CompositionLocalProvider(LocalUser provides currentUser) {
ProfileScreen() // 내부 어디서든 LocalUser.current 로 읽음
}
남용 주의 — 암묵 전달은 양날의 검이다. 데이터 흐름이 시그니처에 안 보여 코드를 추적하기 어려워진다. 그래서 테마·Context·밀도처럼 광범위하고 안정적인 것에만 쓰고, 화면이 다루는 일반 데이터(목록·상태)는 파라미터로 명시적으로 넘긴다 — 명시적 데이터 흐름이 기본, CompositionLocal은 횡단 관심사용이다.
제스처 — 탭·드래그·스와이프
사용자 입력은 복잡도에 따라 계층화된 API로 다룬다. 간단한 건 Modifier 하나로 — clickable·combinedClickable(롱클릭·더블탭)·draggable·swipeable이면 대부분 끝난다. 더 정교한 제어(멀티터치·커스텀 드래그)는 pointerInput + 제스처 감지기로 저수준 포인터 이벤트를 직접 다룬다.
Box(Modifier.pointerInput(Unit) {
detectTapGestures(onLongPress = { showMenu() }, onDoubleTap = { zoom() })
})
Box(Modifier.pointerInput(Unit) {
detectDragGestures { change, drag -> offset += drag } // 손가락을 따라 이동
})
nestedScroll로 바깥 스크롤과 안쪽 스크롤이 협조하게 만들고(접히는 상단바), Animatable([16])과 엮으면 드래그를 놓았을 때 관성으로 튕기는(fling) 제스처 기반 애니메이션을 만든다. 핵심은 — *간단하면 Modifier, 정교하면 pointerInput*으로 필요한 만큼만 내려가는 것이다.
View interop — 기존 View와 섞기
현실의 앱은 하루아침에 Compose로 다 못 바꾼다 — 거대한 기존 화면을 점진적으로 전환한다. 또 Compose에 아직 없는 View(MapView·WebView·차트 라이브러리)도 써야 한다. 그래서 두 방향의 다리가 있다.
AndroidView— Compose 안에 기존 View를 호스팅한다. Compose 버전이 없는MapView·WebView나 레거시 커스텀 View를 Compose 화면 안에서 쓴다.ComposeView— 기존 View 계층(XML)/Activity/Fragment 안에 Compose를 끼운다(setContent { }). 점진 전환의 핵심 — 화면 하나, 컴포넌트 하나씩 Compose로 바꿔 나간다.
접근성 — 모두가 쓸 수 있게
화면을 눈으로 못 보는 사용자는 화면 낭독기(TalkBack)로, 작은 글씨를 못 읽는 사용자는 큰 글씨로 앱을 쓴다. 이들을 위해 시맨틱(semantics) — *UI의 "의미"*를 챙겨야 한다. 아이콘 버튼처럼 텍스트가 없는 요소는 낭독기가 읽을 게 없으니 contentDescription을 줘야 하고, *논리적 그룹·역할(제목·버튼)·상태(선택됨)*를 Modifier.semantics로 보강한다. 터치 타깃은 최소 48dp(손가락으로 누를 크기), 명암비는 충분히.
Icon(Icons.Default.Favorite, contentDescription = "좋아요") // 낭독기가 "좋아요"로 읽음
Modifier.semantics { heading() } // "이건 제목이다"
Compose 기본 컴포넌트(Button·Text)는 기본 시맨틱을 갖지만, 커스텀 UI·아이콘만 있는 버튼은 직접 설명을 줘야 낭독기 사용자가 쓸 수 있다. 접근성은 덤이 아니라 "모두가 쓰는 앱"의 기본 요건이다.
정리 — 점진 도입까지 갖춘다
Compose 심화의 핵심 — CompositionLocal로 횡단 관심사(테마·Context)를 암묵 전달(일반 데이터는 파라미터로 명시), 제스처는 Modifier(간단)→pointerInput(정교)로 필요한 만큼만, interop은 AndroidView(Compose 안 View)·ComposeView(View 안 Compose)로 점진 전환, 접근성은 contentDescription·시맨틱·48dp로 모두가 쓰게. 이로써 실무 Compose가 완성된다.
한 줄 요약 — CompositionLocal=트리에 암묵 전달(prop drilling 회피·테마/LocalContext/LocalDensity/커스텀 provides, 단 일반 데이터는 파라미터로 명시). 제스처: Modifier
clickable/draggable(간단)→pointerInput+detectTapGestures/detectDragGestures(정교)·nestedScroll·fling. interop:AndroidView(View in Compose)·ComposeView(Compose in View)로 점진 도입. 접근성: contentDescription·semantics(heading/role/state)·48dp.
(출처: Android — CompositionLocal / Gestures / Interoperability APIs / Accessibility in Compose.)