문서 / 확장 레이어 / 확장 모듈 만들기

확장 모듈 만들기

히트맵・GeoJSON 레이어・마커 클러스터링은 모두 Core를 바꿔 쓰지 않고 밖에서 더한 별도 패키지입니다. 같은 창구가 공개되어 있으므로, 제3자도 같은 방식으로 자신의 확장을 배포할 수 있습니다. 이 페이지는 그 절차입니다.

ANDROID
com.mapconductor:core
iOS
MapConductorCore
REACT
@mapconductor/js-sdk-core

00 · 확장 모듈의 형태

확장 모듈이란 코어 모듈에만 의존하는 독립된 패키지를 말합니다. 프로바이더의 패키지(react-for-*, android-for-*, ios-for-*)에는 의존하지 않습니다. 이것이 지켜져 있으면, 이용자가 어느 지도 프로바이더를 골라도 당신의 확장은 동작합니다.

OK

코어의 공개 타입(상태・컬렉터・컨트롤러 계약・타일 서버・서비스 레지스트리)만 씁니다.

NG

프로바이더의 패키지를 import 한다. 컨트롤러를 다른 타입으로 캐스트해 내부 필드를 들여다본다.

출하된 3가지가 그대로 참고 구현입니다. 만들고 싶은 것에 가까운 것을 읽는 것이 지름길입니다.

참고할 구현
heatmap            draws raster tiles itself      · tile server + RasterLayer
geojson-layer      turns bulk vectors into tiles  · tile server + RasterLayer
marker-clustering  reworks markers and hands back · MarkerState + service registry
플랫폼

01 · 패키지를 만든다

의존은 코어만으로 합니다. 프로바이더는 이용자의 앱이 고르는 것이므로, 당신의 패키지에서는 보이지 않습니다.

build.gradle.kts
dependencies {
    implementation("com.mapconductor:core")
    implementation("com.mapconductor:compose") // Composable을 공개한다면
    // android-for-* 에는 의존하지 않는다
}

02 · 상태를 정의한다

이용자가 만지는 것은 상태 객체입니다. 코어의 상태 타입(MarkerState 등)과 같은 작법으로 해 두면, 이용자가 다른 것을 외우지 않아도 됩니다. 값이 바뀌었음을 알리는 구조(핑거프린트)를 갖게 해 두면, 코어의 컬렉터에 실어 차분 갱신을 받을 수 있습니다.

코어가 마련해 둔 것
OverlayCollector       an id → state map; gathers add/remove/diff and change notifications
ComponentState         the contract for a state that carries an id (Android)
fingerPrint()          a comparison value holding only what affects drawing

03 · 렌더링의 출구를 고른다

여기가 가장 중요한 설계 판단입니다. 프로바이더마다 렌더링 코드를 쓰는 것이 아니라, 코어가 이미 가지고 있는 출구로 흘려 넣습니다. 출구는 2가지 있습니다.

A · RasterLayer

직접 타일을 그린다

로컬 타일 서버에 렌더링 함수를 등록하고, 그 URL 템플릿을 가리키는 RasterLayer를 한 장 놓습니다. 히트맵과 GeoJSON 레이어가 이것입니다. 건수가 많을수록 유리하고, 어느 프로바이더에서도 겉모습이 완전히 일치합니다.

B · 기존의 오버레이

마커나 도형으로 반환한다

입력을 가공해 MarkerState / PolygonState 등으로 변환하고, 컬렉터에 씁니다. 그 다음은 프로바이더의 통상 렌더링 경로가 처리합니다. 클러스터링이 이것입니다. 클릭이나 드래그가 그대로 듣습니다.

A · 타일 서버 + RasterLayer(android-heatmap과 같음)
val tileServer = TileServerRegistry.get()
val renderer = MyTileRenderer(tileSize = 256)   // 타일 1장을 그리는 함수를 가진다

DisposableEffect(groupId) {
    tileServer.register(groupId, renderer)
    onDispose { tileServer.unregister(groupId) }
}

val layer = remember {
    RasterLayerState(
        id = "my-ext-$groupId",
        source = RasterLayerSource.UrlTemplate(
            template = tileServer.urlTemplate(groupId, renderer.tileSize),
            tileSize = renderer.tileSize,
            scheme = TileScheme.XYZ,
        ),
    )
}
RasterLayer(layer)
B를 고르는 경우, React에서는 컬렉터에 직접 쓰지 말고, 프로바이더가 등록한 MarkerRenderingSupport를 서비스 레지스트리에서 받아 렌더러를 만듭니다(05 참조). 프로바이더 쪽이 렌더링 경로를 교체할 수 있도록 하기 위해서입니다.

04 · 카메라와 뒷정리

줌이나 팬에 따라가고 싶은 경우, 지도 컨트롤러의 이벤트를 직접 건드려서는 안 됩니다. 단일 슬롯의 리스너를 서로 빼앗게 되어, 확장을 2개 얹은 이용자의 환경에서 망가집니다. 오버레이 컨트롤러로 등록하면, 다른 오버레이와 같은 경로로 카메라 변경이 도착합니다.

HeatmapCameraController.kt와 같은 형태
class MyCameraController(
    private val renderer: MyTileRenderer,
) : OverlayControllerInterface<Unit, Unit>, OnCameraChangeReceiverInterface {
    override val zIndex: Int = 0
    override suspend fun add(data: List<Unit>) {}
    override suspend fun update(state: Unit) {}
    override suspend fun clear() {}
    override fun find(position: GeoPointInterface): Unit? = null

    override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) {
        renderer.updateCameraZoom(mapCameraPosition.zoom)
    }

    override fun destroy() {}
}

// 등록
val mapController = LocalMapViewController.current
DisposableEffect(mapController, cameraController) {
    mapController.registerOverlayController(cameraController)
    onDispose { cameraController.destroy() }
}
등록한 것은 반드시 해제하세요. 지도를 파기하지 않고 프로바이더만 교체하는 이용자가 있기 때문에, 해제 누락은 이전 프로바이더의 렌더러를 붙잡은 채로 남습니다.

05 · 기능을 주고받는다

여기까지의 4가지로 부족한 경우에만 씁니다. 프로바이더만이 만들 수 있는 것이 필요할 때, MapServiceRegistry를 통해 그것을 받습니다. 타입이 붙은 키로 등록・취득하므로 캐스트는 필요 없습니다.

실례는 마커 클러스터링입니다. 네이티브의 마커 타입은 프로바이더마다 다르기 때문에, 클러스터링은 스스로 렌더러를 만들 수 없습니다. 그래서 프로바이더가 등록한 MarkerRenderingSupport를 찾아, 거기서 렌더러를 만들게 합니다.

해결하는 쪽(확장 모듈)
// 키는 singleton object로 정의한다
object MyCapabilityKey : MapServiceKey<MyCapability>

// 꺼낸다. 미등록이면 null이므로, 기능을 빼고 계속할지 포기할지 정한다
val services = LocalMapServiceRegistry.current
val capability = services.get(MyCapabilityKey) ?: return
등록하는 쪽(프로바이더)
// 맵 하나당 레지스트리 하나. state가 가지고 있다(react / ios와 같음)
state.serviceRegistry.put(MyCapabilityKey, myCapability)

// 사라질 때는 clear()가 아니라 remove(). 다른 capability를 말려들게 하지 않는다
DisposableEffect(state) {
    onDispose { state.serviceRegistry.remove(MyCapabilityKey) }
}
키는 반드시 해결하는 쪽(확장 모듈)이 공개하세요. 프로바이더는 그 키를 import 해서 등록합니다. 반대로 하면 프로바이더가 확장에 의존하게 되어, 의존의 방향이 뒤집힙니다.

지킬 것

이 4가지를 지키고 있으면, 이용자가 프로바이더를 교체해도, 다른 확장과 동시에 써도 망가지지 않습니다.

1 · 프로바이더의 패키지를 import 하지 않는다

필요해졌다면, 그것은 서비스 레지스트리로 받아야 할 기능입니다.

2 · 컨트롤러를 캐스트해 내부를 들여다보지 않는다

react-heatmap은 이전에 이것을 하고 있어서, 단일 슬롯의 카메라 리스너를 대피・복원하고 있었습니다. 확장이 2개 얹히면 서로 덮어씁니다. 공개된 registerOverlayController로 교체했습니다.

3 · 등록한 것은 반드시 해제한다

오버레이 컨트롤러, 타일 서버의 그룹, 서비스 레지스트리의 키. remove()는 1건만 취소합니다.

4 · 미등록이면 조용히 비활성화한다

필요한 capability가 없는 프로바이더에서는, 예외를 던지지 말고 아무것도 그리지 마세요. 이용자는 다른 부분을 계속 동작시킬 수 있습니다.

관련 페이지