카메라의 위치를 읽기
MapConductor의 컨트롤러에 getCameraPosition()은 없습니다. 3개 플랫폼 모두 같습니다. 지도를 다루면서 처음 찾는 메서드이므로, 대신 무엇을 쓰는지, 그리고 왜 마련하지 않았는지를 여기에 써 둡니다.
한마디로 말하면, 카메라는 가지러 가는 것이 아니라 도착하는 것이기 때문입니다. 지도 SDK 쪽이 카메라의 변화를 알고 있고, 이쪽은 받기만 하면 됩니다.
01 · 3가지 경로
용도에 따라 나눠 씁니다. 어느 것이나 같은 MapCameraPosition이 도착하며, 표시 범위는 visibleRegion에 실려 있습니다.
mapViewState.cameraPosition — 언제든 읽을 수 있습니다. 표시 범위는 cameraPosition.visibleRegion.bounds.onCameraMove / onCameraMoveEnd — 이동 중에도 발화합니다. UI를 따라가게 하려면 이것.onCameraChanged를 받습니다. 지도 컨트롤러의 단일 슬롯 리스너를 서로 빼앗지 않아도 됩니다.val camera = mapViewState.cameraPosition
val bounds = camera.visibleRegion?.bounds
// 변화를 따라간다
MapView(
state = mapViewState,
onCameraMoveEnd = { camera -> reload(camera.visibleRegion?.bounds) },
)let camera = mapViewState.cameraPosition
let bounds = camera.visibleRegion?.bounds
// 변화를 따라간다
MapLibreMapView(state: mapViewState, onCameraMoveEnd: { camera in
reload(camera.visibleRegion?.bounds)
})const camera = mapViewState.cameraPosition;
const bounds = camera.visibleRegion?.bounds;
// 변화를 따라간다
<MapLibreMapView
state={mapViewState}
onCameraMoveEnd={(camera) => reload(camera.visibleRegion?.bounds)}
/>02 · 왜 취득 메서드가 없는가
MapConductor는 3개 플랫폼 모두 선언적 UI(Compose / SwiftUI / React) 위에 얹혀 있습니다. 선언적 UI에서는, 상태는 「지금 어떻게 되어 있는가」를 보관하는 장소가 하나 있고, UI는 그것을 비추기만 하는 형태로 합니다. 카메라도 상태이므로, 놓을 자리는 state입니다.
컨트롤러에 취득 메서드를 더하면, 같은 값의 출처가 2개가 됩니다. 하나는 state, 다른 하나는 SDK 직접 읽기. 양쪽은 같아야 하는데, 어긋날 여지만 늘어납니다. 실제로 그렇게 되어 있었고, 삭제 전에는 같은 프레임에서 2번 카메라를 조립하고 있었습니다 ― 1번은 SDK의 이벤트용, 또 1번은 다시 그릴 때마다 만들어 내던 분입니다.
Camera event from the map SDK
↓ the provider reads the SDK's raw values, converts to unified zoom, attaches visibleRegion
state.updateCameraPosition(camera)
↓
├─ mapViewState.cameraPosition (readable at any time)
├─ onCameraMove / onCameraMoveEnd (the app's callbacks)
└─ onCameraChanged on registered overlays (extension modules)03 · pull의 비용
설계의 이야기만으로는 「1개쯤 더해도」가 되기 쉬우므로, 실측을 두어 둡니다. 카메라를 1번 조립하는 것은 싸지 않습니다.
1번의 호출로 하고 있는 것
SDK에서 center / zoom / bearing / tilt를 읽는다 → 프로바이더마다의 ZoomAltitudeConverter로 통일 줌으로 변환한다 → 화면 네 모서리를 역투영해 visibleRegion을 조립한다. 마지막 것이 가장 무겁고, 단순한 게터가 아닙니다.
실측값입니다. MapLibre의 마커 페이지를 Chromium에서 열고, 1번의 드래그(약 20프레임 분의 팬)로 getCameraPosition()이 몇 번 호출되는지를 셌습니다.
약 65% 줄었습니다. 줄어든 만큼은 전부 다시 만들기입니다. SDK의 이벤트로 1번 조립해 state로 흘려보냈는데, 다시 그릴 때마다 뷰가 한 번 더 같은 값을 조립하고 있었습니다. 호출한 곳은 13개의 뷰 전부에 들어 있었고, 어느 것이나 직전에 push한 값을 버리고 있었던 셈입니다.
덧붙여 정지 중에는 0번입니다. 폭주하고 있었던 것이 아니라, 조작하고 있는 동안에만 확실히 효과가 나타나는 종류의 비용이었습니다. 지도가 가장 무거운 순간에 얹히기 때문에, 체감에 나오기 쉬운 부분입니다.
var camera by remember { mutableStateOf<MapCameraPosition?>(null) }
MapLibreMapView(
state = mapViewState,
modifier = Modifier.weight(1f),
// Fires throughout the gesture — keep this handler cheap
onCameraMove = { position -> camera = position },
// Once, when the map settles. Fetching and redrawing belong here
onCameraMoveEnd = { position -> viewModel.onCameraSettled(position) },
)
camera?.let { position ->
Text("lat %.5f".format(position.position.latitude))
Text("lng %.5f".format(position.position.longitude))
Text("zoom %.2f".format(position.zoom))
// Read the visible region off the value you were handed; do not rebuild it
position.visibleRegion?.bounds?.let { bounds ->
Text("SW ${bounds.southWest} NE ${bounds.northEast}")
}
}04 · 1프레임의 지연
mapViewState.cameraPosition은 「마지막에 push된 값」이므로, 이치상으로는 SDK의 현재 값보다 1프레임 오래된 경우가 있습니다. 여기가 취득 메서드를 더하고 싶어지는 유일한 실질적 이유이므로, 솔직하게 써 둡니다.
onCameraMove는 이동 중에도 매 프레임 발화하므로, state는 항상 직전 프레임까지 따라잡고 있습니다. 표시・데이터 취득・범위 판정 어느 것에서도 체감 차이는 없습니다. 실제로, 취득 메서드를 뺀 뒤에도 전 41건의 브라우저 테스트가 그대로 통과하고 있습니다.
드래그 중인 프레임에 완전히 동기시키고 싶은 렌더링. 다만 그것은 지도 SDK의 렌더링 루프에 얹히는 이야기이므로, 통일 API가 아니라 후술하는 통로를 써야 할 영역입니다.
05 · 그래도 원래 값이 필요할 때
state 경유로는 부족하다 ― 예를 들어 지도 SDK의 렌더링 루프에 동기시키고 싶다 ― 는 경우는, 통일 API를 우회하지 말고 MapViewHolder로 네이티브의 지도 인스턴스를 꺼내세요. 프로바이더 고유의 코드가 되지만, 그만큼 가장 빠르고 가장 정확합니다.
const holder = mapViewState.getMapViewHolder(); const map = holder?.map as maplibregl.Map | undefined; const liveZoom = map?.getZoom();