NAVER-Map-Compose 분석

Jetpack Compose 런타임 흐름 실전으로 이해하기

Ji Sungbin
성빈랜드
8 min readJun 27, 2022

--

Photo by delfi de la Rua on Unsplash
https://github.com/fornewid/naver-map-compose

저번달에 성용님이 NAVER-Map-Compose 를 공개하셨습니다. 공개되지 마자 코드를 빠르게 훑어보고 PR 을 넣었는데…. 리젝당하고 배울 수 있었습니다.

어제 코드를 제대로 다시 살펴보았고, 생각보다 쉬워서 공유해보려고 합니다.

우선 NaverMap 컴포저블을 사용하여 가장 기초적인 지도를 추가할 수 있습니다. NaverMap 의 내부를 살펴보면 AndroidView 로 MapView 안드로이드 뷰를 추가하고 있습니다. 결국 핵심은 AndroidView 입니다.

MapLifecycle 을 이용하여 MapView 에 생명주기를 입혀주고 있습니다.

컴포즈도 Owner 를 통해 비슷하게 생명추기 처리를 해주고 있습니다.

컴포즈에서는 ComposeView 를 Owner 로 받아서 생명주기에 따라 PausableMonotonicFrameClock 를 제어하고 있지만, NAVER-Map-Compose 에서는 MapLifecycle 의 mapView 인자가 Owner 가 되고 이를 이용하여 바로 MapView 를 제어하고 있습니다.

다시 아까 NaverMap 코드로 돌아와서 나머지 부분을 이어서 보면 LaunchedEffect 에서 disposingComposition { mapView.newComposition() } 으로 이어가고 있습니다.

disposingComposition 으로 LaunchedEffect scope 가 cancel 되면 Composition 을 dispose 해주고 있고, MapView.newComposition 으로 MapApplier 를 사용해 Composition 을 생성하고 있습니다. 그리고 이 Composition 에 NaverMap 안에 들어갈 content 를 표시하고 있습니다.

newComposition 에서 MapView receiver 를 이용하는 곳이 awaitMap() 밖에 없어서 그냥 인자로 받았어도 될 거 같은데, receiver 로 받으신 이유가 궁금해지는 곳입니다.

Composition 생성시에 사용하고 있는 MapApplier 를 보겠습니다.

UiApplier 과 동일하게 중복 알림을 피하기 위해 상향식 삽입을 사용하고 있으며, MapNode 는 UI 노드가 아니기 때문에 object MapNodeRoot : MapNode 로 간단하게 만들어서 root 로 주고 있습니다. MapNode 는 NaverMap 에서 사용할 노드를 나타내는 인터페이스이며, MapNode 생명주기를 관리할 onAttached(), onRemoved(), onCleared() 함수로 구성돼 있습니다. 이 함수들을 이용하여 AbstractApplier 의 onClear, insertBottomUp, remove 에서 각각 생명주기에 맞는 MapNode 의 함수들을 호출하고 있습니다.

MapApplier 의 인자로 val map: NaverMap 을 받고 있다는 사실을 기억하고, 다시 NaverMap 컴포저블로 돌아가 보겠습니다.

지금까지 disposingCompositionmapView.newComposition 에서 무슨 일이 일어나는지 보았고, 이제 MapUpdater 만 남았습니다. 이것만 보면 NAVER-Map-Compose 의 분석이 끝납니다!

표시할 코드들이 너무 길어서 사진으로 준비하였고 중요한 부분만 강조 처리 해보았습니다. 인자로는 NaverMap 의 옵션들을 받고 있고, 이 인자들을 그대로 MapPropertiesNode 로 래핑하여 방출하고 있습니다. 이 코드에서 131번째 줄에 있는 map 을 가져오는 코드와, 144~244번째 줄로 구성된 update 부분을 유심히 봐야 합니다.

아까 MapApplier 에서 map 을 인자로 받고 있었습니다. 하지만 정작 MapApplier 에선 map 을 쓰는 곳이 없었습니다. 이 map 이 바로 여기서 쓰입니다.

아까 mapView.newComposition 에서 MapApplier 로 Composition 을 만들어 주었고, 이곳에 setContent 로 MapUpdater 를 사용하고 있습니다. 따라서 MapUpdater 의 currentComposer 는 Compose-UI 에서 사용하는(UiApplier 가 배정된) Composer 가 아닌, NaverMap 에서 사용하는(MapApplier 가 배정된) Composer 를 가져오게 됩니다. 이를 이용하여 currentComposer 에서 applier 를 가져와 MapApplier 로 캐스팅해도 오류가 발생하지 않고, 여기에서 바로 map 을 가져올 수 있습니다.

이는 아주 중요한 역할을 합니다. 네이버 지도 커스텀마이징은 MapView 안드로이드 뷰의 함수들을 통해 할 수 있습니다. NAVER-Map-Compose 에서는 AndroidView 컴포저블을 통해 MapView 안드로이드 뷰를 호스팅하고 있고, 이를 다른 컴포저블을 통해 커스텀마이징 하기 위해선 AndroidView 에서 MapView 안드로이드 뷰를 가져와야 합니다. 하지만 이는 불가능한 방법이고, 따라서 이렇게 Composition 을 직접 만드는 방법을 채택하신거 같습니다.

이를 이용해 위처럼 MapView 안드로이드 뷰를 컴포저블끼리 가져올 수 있고, update 에서는 다 MapView 안드로이드 뷰에 값 업데이트 작업만 해주고 있습니다. 무려 100줄이 다 set 과 update 만 해주고 있습니다.

MapUpdater 가 방출하는 MapPropertiesNode 에서는 아까 MapNode 인터페이스에서 보았던 생명주기 함수들을 구현하고 있습니다.

이렇게 단 3가지의 파일로 NAVER-Map-Compose 의 핵심 분석이 끝납니다.

나머지 MapView 옵션 컴포저블들도 다 동일하게 Update 작업만 해주고 있습니다. 올리자니 다 코드가 너무 길어서 올리진 못하겠고, 궁금하신 분들은 코드를 바로 확인하실 수 있습니다.

https://github.com/fornewid/naver-map-compose/blob/main/naver-map-compose/src/main/java/com/naver/maps/map/compose/CircleOverlay.kt

https://github.com/fornewid/naver-map-compose/blob/main/naver-map-compose/src/main/java/com/naver/maps/map/compose/Marker.kt

https://github.com/fornewid/naver-map-compose/blob/main/naver-map-compose/src/main/java/com/naver/maps/map/compose/PathOverlay.kt

끝!

이번엔 컴포즈 런타임을 커스텀해서 구현된 NAVER-Map-Compose 를 분석해 보았습니다. 이 분석이 여러분들의 컴포즈 런타임 이해에 도움이 되셨길 바랍니다.

안드로이드 개발자 분들을 위한 카카오톡 오픈 채팅방을 운영하고 있습니다.

--

--

Ji Sungbin
성빈랜드

Experience Engineers for us. I love development that creates references.