문서 / 개념 / 아키텍처

아키텍처 개요

MapConductor는 앱과 각종 지도 SDK의 다리를 놓습니다. 앱은 통일된 지도 API만을 호출하고, MapConductor가 선택된 프로바이더로 처리를 전송합니다. Android(Kotlin + Compose)・iOS(Swift + SwiftUI)・React(Web)에서, 패키지의 나누는 방식도 내부의 역할 분담도 같은 설계로 되어 있습니다.

ANDROID
android-sdk-core
iOS
ios-sdk-core
REACT
js-sdk-core / js-sdk-react

01 · 전체상

어느 플랫폼에서도 처리의 흐름은 같습니다. 앱 → 통일된 지도 API → Core → 각 지도 SDK 드라이버 → 각 지도 SDK. 위의 2층만이 앱이 쓰는 코드이고, 아래 2층은 교체 가능합니다.

APP

UI와 비즈니스 로직

React · Compose · SwiftUI
UNIFIED API

통합 지도 API

MapViewState · Marker · Camera
CORE

핵심 기능(지도・마커・도형・이벤트)

Manager · Controller · Overlay
DRIVER

프로바이더 드라이버

*-for-googlemaps / -maplibre / …
MAP SDK

지도 SDK(네이티브 / JavaScript)

Google Maps · MapLibre · Mapbox · MapKit · ArcGIS · HERE …

프로바이더를 바꿀 때 교체되는 것은 최하층의 2개뿐입니다. 화면의 코드는 통일 API에 대해 쓰여 있기 때문에, 그대로 동작합니다.

플랫폼

02 · 플랫폼별 구성

Core와 드라이버는 패키지로 나뉘어 있습니다. 앱은 Core와, 쓰는 프로바이더의 드라이버만을 의존에 더합니다.

android-sdk-core가 공통 모델과 차분 적용을 가지고, android-sdk-compose가 Compose용 상태 홀더를 제공합니다. GeoJSON・클러스터링・히트맵은 추가 모듈(android-geojson-layer / android-marker-clustering / android-heatmap)로 독립되어 있습니다.

03 · 레이어 구성

플랫폼을 넘어 재사용할 수 있도록, 책무를 6개의 레이어로 나누고 있습니다. 위의 레이어는 아래 레이어의 실체를 모릅니다.

#
레이어
역할
1
UI 프레임워크
React / Vue / Jetpack Compose / SwiftUI 등, 앱이 쓰는 선언적 UI.
2
통일된 지도 API
마커・도형・카메라・이벤트를 공통의 개념으로 다루는 MapConductor의 API. 앱이 실제로 쓰는 코드는 여기에 대해 기술합니다.
3
브리지
통일 API의 조작을 실행 환경에 따라 하위로 다리 놓습니다. UI와 렌더링이 같은 실행 환경에서 완결되는 경우는 불필요하고, 양쪽이 다른 실행 환경으로 나뉠 때만 개재합니다.
4
크로스 플랫폼 층
네이티브 지도 SDK의 넣는 방법과 선언 방법은, 이 층만이 플랫폼마다 다릅니다.
5
네이티브 SDK / 드라이버
카메라・스타일・오버레이의 렌더링을 실제로 맡는 층(Android / iOS의 네이티브, 또는 Web의 JS 드라이버).
6
지도 SDK
Google Maps / MapKit / MapLibre / Mapbox / ArcGIS / HERE 등의 실체.

이 분리에 의해, 「어느 플랫폼인가」가 아니라 「지도로 무엇을 하고 싶은가」에 집중한 코드를 그대로 여러 환경으로 전개할 수 있습니다. 플랫폼 고유의 차분은, 원칙적으로 크로스 플랫폼 층의 설치/선언 방법만으로 가둬집니다.

React Native의 경우

Web과 React Native는, 앱이 쓰는 코드를 공유합니다. 상태 객체도 컴포넌트도 같고, 고르는 MapView를 교체하기만 하면 됩니다. 다른 것은 그 아래입니다. Web에서는 드라이버가 같은 실행 환경의 JavaScript 지도 라이브러리를 호출하지만, React Native에서는 브리지를 거쳐 MapConductor의 네이티브 SDK로 넘어가고, 최종적으로 네이티브의 지도 SDK가 렌더링합니다.

같은 코드가, 어디에서 갈라지는가
SHARED

앱이 쓰는 코드

js-sdk-core · js-sdk-react
WEB

React

DRIVER
JavaScript 드라이버
react-for-*
브리지가 필요 없습니다(같은 실행 환경)
MAP SDK
JavaScript 지도 SDK
Google Maps · MapLibre · Leaflet · Cesium …
REACT NATIVE

React Native

DRIVER
네이티브로 넘기는 드라이버
reactnative-for-*
BRIDGE
RN 네이티브 모듈
레이어 3
NATIVE SDK
MapConductor 네이티브 SDK → 지도 SDK
Google Maps · MapLibre · ArcGIS · HERE

위 표에서 말하는 레이어 3이 브리지입니다. Web에서는 UI와 렌더링이 같은 실행 환경에서 완결되므로 개재하지 않습니다. React Native에서는 양쪽이 다른 실행 환경으로 나뉘기 때문에, 여기가 필요합니다. 플랫폼마다 다른 것은 이 층뿐이고, 그 위는 달라지지 않습니다.

React Native에서 쓸 수 있는 프로바이더는 Google Maps・MapLibre・ArcGIS・HERE의 4개입니다. 네이티브 쪽에 대응하는 모듈이 있는 것에 한정되기 때문에, Web의 13개 프로바이더보다 적어집니다.

04 · Core의 내부 구조

Core는 마커・폴리라인・폴리곤・원・지상 이미지・래스터 레이어 같은 요소마다, 같은 7개의 역할을 반복하는 형태로 구성되어 있습니다. 하나를 외우면, 다른 요소도 같은 읽는 법으로 따라갈 수 있습니다.

역할
책무
구현 장소
State
앱이 넘기는 불변의 값. 좌표・색・zIndex 등. fingerPrint()로 내용의 해시를 반환합니다.
core
Entity
State와, 프로바이더가 실제로 만든 객체, 그때의 fingerPrint를 묶은 1건 분의 기록.
core
Manager
Entity를 id로 보관하는 대장. 좌표로부터의 히트 테스트(find)도 여기가 담당합니다.
core
Controller
add / update / clear / find / onCameraChanged / destroy를 가지는 조작의 창구. 차분의 판정을 합니다.
core
Overlay
같은 종류의 요소를 모은 렌더링 단위. zIndex에 의한 겹침 순서를 정합니다.
core
OverlayRenderer
onAdd / onChange / onRemove / onPostProcess를 각 지도 SDK의 호출로 변환합니다. 요소마다 마련된 추상 클래스(AbstractPolygonOverlayRenderer 등)를 상속해 3개의 메서드를 채우기만 하면 됩니다.
driver
Capable
그 프로바이더가 어느 요소를 다룰 수 있는지를 타입으로 선언합니다(compositionPolygons / updatePolygon / hasPolygon). 요소마다 하나씩 마련되며, Kotlin에서는 PolygonCapableInterface, TypeScript에서는 PolygonCapable처럼 명명됩니다. Swift는 프로토콜이 아니라, 그 프로바이더가 PolygonController를 가지는지 여부로 나타냅니다.
driver

중요한 것은 경계의 위치입니다. Core 쪽(State / Entity / Manager / Controller / Overlay)은 3개 언어에서 거의 같은 코드로 쓰여 있고, 프로바이더마다 쓰는 것은 Renderer와, 그 요소를 다룰 수 있다는 선언뿐입니다.

ANDROID · Kotlin
iOS · Swift
REACT · TypeScript
core/polygon/PolygonManager.kt
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonManager.swift
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonManager.ts
polygon/PolygonCapable.ts
AbstractPolygonOverlayRenderer.ts

05 · 차분 적용의 흐름

앱은 「지금 있어야 할 상태의 배열」을 넘기기만 하면 됩니다. Core가 지난번과의 차분을 구해, 늘어난 것・바뀐 것・사라진 것으로 나누어 드라이버에 넘깁니다. 지도 오브젝트를 다시 만들지 않으므로, 대량의 요소에서도 렌더링이 안정됩니다.

controller/OverlayRendererInterface.ts · 3개 플랫폼 공통의 계약
export interface OverlayRendererInterface<ActualType, StateType, EntityType> {
    onAdd(data: StateType[]): Promise<Array<ActualType | null>> | Array<ActualType | null>;
    onChange(data: Array<ChangeParamsInterface<EntityType>>): Promise<Array<ActualType | null>> | Array<ActualType | null>;
    onRemove(data: EntityType[]): Promise<void> | void;
    onPostProcess(): Promise<void> | void;
}

onChange는 지난번의 Entity(prev)와 이번의 State(current) 양쪽을 받습니다. 드라이버는 바뀐 프로퍼티만을 갱신할 수 있고, Kotlin / Swift에서도 시그니처는 같습니다.

06 · 드라이버가 구현하는 것

새로운 지도 SDK에 대응하는 작업은, 요소마다의 추상 렌더러를 상속해 3가지 조작을 채우는 것으로 닫힙니다. Core의 모델이나 차분 로직에는 손을 대지 않습니다.

android-for-googlemaps · 렌더러의 구현
// 드라이버는 추상 렌더러를 상속해 세 가지 조작만 채운다
internal class GoogleMapPolygonOverlayRenderer(
    override val holder: GoogleMapViewHolder,
    override val coroutine: CoroutineScope,
) : AbstractPolygonOverlayRenderer<GoogleMapActualPolygon>() {

    override suspend fun createPolygon(state: PolygonState) = /* map.addPolygon(...) */
    override suspend fun updatePolygonProperties(polygon, current, prev) = /* 차분만 반영 */
    override suspend fun removePolygon(entity: PolygonEntityInterface<GoogleMapActualPolygon>) =
        entity.polygon.remove()
}

생성・갱신・삭제의 3가지만이 프로바이더 고유입니다. 차분의 판정・대장의 관리・히트 테스트는 Core가 끝낸 상태로 호출됩니다.

07 · 추상화의 범위와 이스케이프 해치

MapConductor는 각 지도 SDK의 모든 기능을 감싸지 않습니다. 공통적으로 자주 쓰이는 조작에 초점을 맞추고, 그것을 넘는 요구에는 2가지 통로를 마련하고 있습니다.

getMapViewHolder() · 네이티브 기능을 직접 호출하기
// 공통 인터페이스의 map은 unknown. 프로바이더의 타입으로 좁힌 뒤에 쓴다
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;

map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });

공통 API는 단순한 채로, 각 프로바이더 고유의 강점도 해치지 않는 설계입니다. 자세히는 「네이티브 확장」을 참조하세요.

관련 페이지