아키텍처 개요
MapConductor는 앱과 각종 지도 SDK의 다리를 놓습니다. 앱은 통일된 지도 API만을 호출하고, MapConductor가 선택된 프로바이더로 처리를 전송합니다. Android(Kotlin + Compose)・iOS(Swift + SwiftUI)・React(Web)에서, 패키지의 나누는 방식도 내부의 역할 분담도 같은 설계로 되어 있습니다.
01 · 전체상
어느 플랫폼에서도 처리의 흐름은 같습니다. 앱 → 통일된 지도 API → Core → 각 지도 SDK 드라이버 → 각 지도 SDK. 위의 2층만이 앱이 쓰는 코드이고, 아래 2층은 교체 가능합니다.
UI와 비즈니스 로직
통합 지도 API
핵심 기능(지도・마커・도형・이벤트)
프로바이더 드라이버
지도 SDK(네이티브 / JavaScript)
프로바이더를 바꿀 때 교체되는 것은 최하층의 2개뿐입니다. 화면의 코드는 통일 API에 대해 쓰여 있기 때문에, 그대로 동작합니다.
02 · 플랫폼별 구성
Core와 드라이버는 패키지로 나뉘어 있습니다. 앱은 Core와, 쓰는 프로바이더의 드라이버만을 의존에 더합니다.
android-sdk-core가 공통 모델과 차분 적용을 가지고, android-sdk-compose가 Compose용 상태 홀더를 제공합니다. GeoJSON・클러스터링・히트맵은 추가 모듈(android-geojson-layer / android-marker-clustering / android-heatmap)로 독립되어 있습니다.
ios-sdk-core가 공통 모델과 SwiftUI 뷰의 토대를 가지고, 각 SwiftUI 뷰는 프로바이더 패키지(ios-for-*)가 제공합니다. MapKit 드라이버는 Apple 순정 SDK용으로, 추가 과금이나 API 키 없이 쓸 수 있습니다.
js-sdk-core는 React에 의존하지 않는 TypeScript의 코어로, 공통 모델과 차분 적용을 가집니다(Kotlin / Swift의 core와 거의 같은 구성입니다). js-sdk-react가 그것을 React에 얹고, Marker / Polygon / Polyline / Circle / GroundImage / RasterLayer / InfoBubble 같은 컴포넌트를 제공합니다. 지도 뷰 자체는 각 드라이버(react-for-*)가 MapLibreMapView / MapLibreMapView2D와 useMapLibreViewState처럼 「뷰 + 훅」의 짝으로 공개합니다. GeoJSON・클러스터링・히트맵・아이콘은 추가 패키지(react-geojson-layer / react-marker-clustering / react-heatmap / react-icons)로 독립되어 있습니다.
03 · 레이어 구성
플랫폼을 넘어 재사용할 수 있도록, 책무를 6개의 레이어로 나누고 있습니다. 위의 레이어는 아래 레이어의 실체를 모릅니다.
이 분리에 의해, 「어느 플랫폼인가」가 아니라 「지도로 무엇을 하고 싶은가」에 집중한 코드를 그대로 여러 환경으로 전개할 수 있습니다. 플랫폼 고유의 차분은, 원칙적으로 크로스 플랫폼 층의 설치/선언 방법만으로 가둬집니다.
React Native의 경우
Web과 React Native는, 앱이 쓰는 코드를 공유합니다. 상태 객체도 컴포넌트도 같고, 고르는 MapView를 교체하기만 하면 됩니다. 다른 것은 그 아래입니다. Web에서는 드라이버가 같은 실행 환경의 JavaScript 지도 라이브러리를 호출하지만, React Native에서는 브리지를 거쳐 MapConductor의 네이티브 SDK로 넘어가고, 최종적으로 네이티브의 지도 SDK가 렌더링합니다.
React
React Native
위 표에서 말하는 레이어 3이 브리지입니다. Web에서는 UI와 렌더링이 같은 실행 환경에서 완결되므로 개재하지 않습니다. React Native에서는 양쪽이 다른 실행 환경으로 나뉘기 때문에, 여기가 필요합니다. 플랫폼마다 다른 것은 이 층뿐이고, 그 위는 달라지지 않습니다.
React Native에서 쓸 수 있는 프로바이더는 Google Maps・MapLibre・ArcGIS・HERE의 4개입니다. 네이티브 쪽에 대응하는 모듈이 있는 것에 한정되기 때문에, Web의 13개 프로바이더보다 적어집니다.
04 · Core의 내부 구조
Core는 마커・폴리라인・폴리곤・원・지상 이미지・래스터 레이어 같은 요소마다, 같은 7개의 역할을 반복하는 형태로 구성되어 있습니다. 하나를 외우면, 다른 요소도 같은 읽는 법으로 따라갈 수 있습니다.
중요한 것은 경계의 위치입니다. Core 쪽(State / Entity / Manager / Controller / Overlay)은 3개 언어에서 거의 같은 코드로 쓰여 있고, 프로바이더마다 쓰는 것은 Renderer와, 그 요소를 다룰 수 있다는 선언뿐입니다.
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonCapable.ts
AbstractPolygonOverlayRenderer.ts
05 · 차분 적용의 흐름
앱은 「지금 있어야 할 상태의 배열」을 넘기기만 하면 됩니다. Core가 지난번과의 차분을 구해, 늘어난 것・바뀐 것・사라진 것으로 나누어 드라이버에 넘깁니다. 지도 오브젝트를 다시 만들지 않으므로, 대량의 요소에서도 렌더링이 안정됩니다.
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의 모델이나 차분 로직에는 손을 대지 않습니다.
// 드라이버는 추상 렌더러를 상속해 세 가지 조작만 채운다
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가 끝낸 상태로 호출됩니다.
// Kotlin과 같은 형태. 추상 렌더러를 상속해 세 가지 조작만 채운다
final class MapKitPolygonOverlayRenderer: AbstractPolygonOverlayRenderer<MKPolygon> {
override func createPolygon(state: PolygonState) async -> MKPolygon? { /* MKPolygon(...) */ }
override func updatePolygonProperties(polygon, current, prev) async -> MKPolygon? { /* 차분만 반영 */ }
override func removePolygon(entity: PolygonEntity<MKPolygon>) async { /* mapView.removeOverlay */ }
}
// 컨트롤러는 Core의 제네릭 구현을 그대로 쓴다
final class MapKitPolygonController: PolygonController<MKPolygon, MapKitPolygonOverlayRenderer> { }Swift에서는 「그 프로바이더가 가지는 컨트롤러」가 기능 일람이 됩니다. 폴리곤의 구멍이나 마커 애니메이션처럼 SDK 쪽이 갖지 않은 기능은, 대응하는 렌더러를 구현하지 않거나, 코어 쪽의 대체 표현(구멍을 갖지 않는 단순 링으로 나누는 PolygonHoleSplit 등)으로 전환함으로써 표현합니다.
// Kotlin / Swift와 같은 형태. 추상 렌더러를 상속해 세 가지 조작만 채운다
export class MapLibrePolygonOverlayRenderer extends AbstractPolygonOverlayRenderer<
MapLibreMapViewHolder,
MapLibreActualPolygon
> {
async createPolygon(state: PolygonState) { /* GeoJSON 피처를 만든다 */ }
async updatePolygonProperties({ current, prev }) { /* 차분만 반영 */ }
async removePolygon(entity: PolygonEntity<MapLibreActualPolygon>) { /* 제거한다 */ }
// 차분 적용 후, 남은 Entity로 소스를 다시 쓴다
override async onPostProcess() { this.layer.draw(this.polygonManager.allEntities()); }
}
// 다룰 수 있는 요소는 Capable 인터페이스를 implements 해서 타입으로 선언한다
export class MapLibreViewController extends BaseMapViewController
implements MapViewControllerInterface, MarkerCapable, PolygonCapable, /* … */ { }TypeScript에서도 생성・갱신・삭제의 3가지만이 프로바이더 고유입니다. MapLibre처럼 레이어에 한꺼번에 그리는 SDK에서는, 1건씩의 삭제가 아니라 onPostProcess로 소스 전체를 다시 쓰는 식의 그리는 방식의 차이도 렌더러 안에 닫힙니다. 드라이버가 다룰 수 있는 요소는 MapLibreViewController가 구현하는 Capable 인터페이스(PolygonCapable 등)로 나타나며, 쓸 수 있는지 여부는 타입으로 알 수 있습니다.
07 · 추상화의 범위와 이스케이프 해치
MapConductor는 각 지도 SDK의 모든 기능을 감싸지 않습니다. 공통적으로 자주 쓰이는 조작에 초점을 맞추고, 그것을 넘는 요구에는 2가지 통로를 마련하고 있습니다.
// 공통 인터페이스의 map은 unknown. 프로바이더의 타입으로 좁힌 뒤에 쓴다
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;
map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });공통 API는 단순한 채로, 각 프로바이더 고유의 강점도 해치지 않는 설계입니다. 자세히는 「네이티브 확장」을 참조하세요.