MapView의 라이프사이클과 이벤트
지도는 뷰와 상태 객체의 2가지로 다룹니다. 상태(mapViewState)가 카메라・디자인・조작의 창구이고, 뷰는 각 플랫폼의 UI 트리에 놓기만 하면 됩니다. 초기화는 공통의 단계를 밟고, 읽기 완료와 사용자 조작은 이벤트로 받습니다.
01 · mapViewState
mapViewState는 지도 한 면에 대응하는 상태 객체입니다. 프로바이더마다 구현은 있지만, 공개되어 있는 멤버는 공통이며, 앱의 코드는 공통 인터페이스만 보고 있으면 충분합니다.
상태는 뷰보다 오래 삽니다. 뷰가 다시 만들어져도 카메라 위치나 디자인은 상태 쪽에 남기 때문에, 지도는 그대로 복원됩니다.
// state는 remember*로 만들면 화면 회전에도 같은 지도가 유지된다
val mapViewState =
rememberGoogleMapViewState(
mapDesign = GoogleMapDesign.Normal,
cameraPosition = initCameraPosition,
)
GoogleMapView(state = mapViewState, modifier = Modifier.fillMaxSize())// ObservableObject이므로 뷰에서 @StateObject로 보관한다
@StateObject private var mapLibreState = MapLibreViewState(
mapDesignType: MapLibreDesign.OsmBright,
cameraPosition: viewModel.initCameraPosition
)
MapLibreMapView(state: mapLibreState) { MapViewContent() }const [mapViewState, setMapViewState] =
useState<MapViewStateInterface<MapDesignTypeInterface<unknown>> | null>(null);
// MapViewContainer는 샘플 앱의 프로바이더 전환용 래퍼로, SDK의 일부가 아니다
<MapViewContainer initialCamera={INIT_CAMERA} onStateReady={setMapViewState}>
<Markers states={markerStates} />
</MapViewContainer>02 · 초기화의 라이프사이클
지도의 초기화는 어느 프로바이더에서도 같은 단계를 밟습니다. SDK의 읽기, 뷰의 생성, 지도 인스턴스의 생성, 그리고 타일의 렌더링 완료. 단계가 InitState로 공통화되어 있으므로, 「아직 조작해서는 안 되는 시기」를 프로바이더마다 외울 필요가 없습니다.
카메라 조작이나 오버레이의 추가는 MapCreated 이후에 받아들여지며, 내부에서 큐에 들어갑니다. 읽기 완료를 기다린 뒤에 무언가 하고 싶은 경우는 onMapLoaded를 씁니다.
지도 SDK를 읽어 들입니다. Web에서는 스크립트의 주입, 모바일에서는 초기화 처리. 실패하면 Failed로 진행합니다.
네이티브 지도 인스턴스를 홀더로 감쌉니다. 이후 SDK 고유의 타입은 여기에 숨습니다.
홀더를 컨트롤러로 감싸, 공통 API와 이벤트 배선을 준비합니다. 상태에 컨트롤러가 접속됩니다.
화면 회전・재생성을 넘어서
Android에서는 상태가 rememberSaveable로 저장되어, 화면 회전이나 설정 변경 뒤에도 카메라 위치와 디자인이 복원됩니다. 구성 변경 시에는 지도 뷰 자체를 파기하지 않고 재사용합니다.
파기될 때
진짜 파기(화면 이탈)에서는 컨트롤러가 가진 오버레이 관리나 타일 서버의 라우트, 코루틴 스코프를 한꺼번에 해제합니다. 프로바이더를 전환했을 때도 같은 경로로 이전 지도가 정리됩니다.
var ready by remember { mutableStateOf(false) }
MapLibreMapView(
state = mapViewState,
// Called once, when the tiles have finished drawing
onMapLoaded = { state ->
ready = true
state.fitBounds(routeBounds, padding = 48)
},
) {
// Declarations here are not lost before MapCreated — they are queued internally
Marker(markerState)
}
if (!ready) {
// Your own loading overlay, if you want one. The map does not need blocking
Box(Modifier.fillMaxSize()) { CircularProgressIndicator() }
}03 · 이벤트
뷰에 넘기는 핸들러는 3개 플랫폼에서 같은 구성입니다. 타입도 공통이며, 읽기 완료는 상태 객체, 탭은 좌표, 카메라 변화는 카메라 위치를 받습니다.
GoogleMapView(
state = mapViewState,
onMapLoaded = { state -> viewModel.onMapLoaded(state) },
onMapClick = { point -> viewModel.onMapClick(point) },
onCameraMove = { camera -> viewModel.onCameraChanged(camera) },
onCameraMoveEnd = { camera -> viewModel.onCameraSettled(camera) },
) { /* markers, overlays */ }MapLibreMapView(
state: mapLibreState,
onMapLoaded: { state in viewModel.onMapLoaded(state) },
onCameraMoveStart: viewModel.onMapCameraMoveStart,
onCameraMove: viewModel.onCameraChanged,
onCameraMoveEnd: viewModel.onMapCameraMoveEnd
) {
MapViewContent()
}<MapViewContainer
initialCamera={INIT_CAMERA}
onMapClick={() => setSelected(null)}
onCameraMove={setCameraPosition}
>
<Markers states={markerStates} />
</MapViewContainer>카메라 계열의 이벤트는 상태 객체의 cameraPosition도 동시에 갱신하기 때문에, 핸들러를 붙이지 않아도 최신의 카메라는 항상 읽어 낼 수 있습니다. 이동 중의 재취득을 피하고 싶은 경우는 onCameraMoveEnd만 쓰세요.
04 · 네이티브로 가는 통로
공통 API로 부족할 때를 위해, 네이티브 지도 인스턴스로 내려가는 창구를 마련해 두었습니다. 평소에는 쓰지 않지만, 프로바이더 고유의 기능을 한 군데만 쓰고 싶은 경우의 통로입니다.
네이티브 지도를 감싸는 홀더
홀더는 플랫폼의 뷰와 지도 인스턴스 2가지를 가집니다. SDK 고유의 코드는 여기서부터 앞에 가둬 두고, 공통 코드에는 새지 않게 하는 구성입니다.
좌표와 화면 위치의 변환
지리 좌표와 화면상의 픽셀 위치를 서로 변환할 수 있습니다. 지도 위에 독자적인 UI를 겹칠 때 씁니다.
// 네이티브 API가 정말 필요할 때만 꺼낸다 val holder = mapViewState.getMapViewHolder() val nativeMap = holder?.map // GoogleMap / MapLibreMap / ... val offset = holder?.toScreenOffset(point)
if let holder = mapViewState.getMapViewHolder() {
let nativeMap = holder.map
let offset = holder.toScreenOffset(position: point)
}const holder = mapViewState.getMapViewHolder(); const nativeMap = holder?.map; // google.maps.Map / maplibregl.Map / ...
홀더를 쓴 코드는 프로바이더에 의존합니다. 공통인 채로 두고 싶은 부분과, 일부러 고유 API를 쓰는 부분을 의식해서 나누세요.