지도 디자인(타일)을 전환하기
지도의 겉모습은 mapDesignType 하나로 정해집니다. 프로바이더마다 마련된 프리셋을 고르거나, 스타일 JSON・타일 URL을 지정해 독자적인 디자인을 정의합니다. 전환은 프로퍼티에 대입하기만 하면 되고, 지도를 다시 만들 필요는 없습니다.
01 · 디자인의 타입
어느 프로바이더의 디자인이든 MapDesignTypeInterface(iOS에서는 MapDesignTypeProtocol)를 구현합니다. 공통의 3요소만 외우면, 프로바이더가 바뀌어도 다루는 방식은 같습니다.
디자인을 식별하는 값. 프로바이더의 네이티브 값(Google의 mapType 상수 등)이나 문자열.
프로바이더의 지도 엔진에 넘기는 실제 값. 스타일 JSON의 URL이나 타일 정의를 포함합니다.
줌이나 표시 범위에 따라 출처 표기를 전환하는 규칙. 독자 타일을 쓸 때 필수입니다.
02 · 프리셋
각 프로바이더의 패키지에, 그 프로바이더에서 쓸 수 있는 표준 디자인이 상수로 들어 있습니다. 샘플 앱은 이 일람을 그대로 선택지로 쓰고 있습니다.
iOS · React
React: MapboxDesign
React는 Streets · Outdoors · Light · Dark · SatelliteStreets + OsmBright / MapTiler 계열의 스타일 JSON
iOS / React: ArcGISMapDesign
React: TomTomDesign
React는 추가로 StandardLight / Dark · DrivingLight / Dark · MonoLight / Dark
React
React
React
React
플랫폼에서 차이가 있는 것만 주기하고 있습니다. 주기가 없는 행은, 타입 이름도 프리셋의 상수 이름도 3개 플랫폼에서 같습니다.
03 · 전환하기
지도 뷰의 상태 객체의 mapDesignType에 대입합니다. 상태는 감시되고 있으므로, 대입한 시점에 지도의 겉모습이 바뀝니다.
// state는 MapViewStateInterface<*>. 대입하기 전에 프로바이더의 타입으로 좁힌다
when (state) {
is GoogleMapViewStateInterface ->
state.mapDesignType = GoogleMapDesign.Satellite
is MapLibreViewStateInterface ->
state.mapDesignType = MapLibreDesign.OsmBrightJa
is MapTilerViewStateInterface ->
state.mapDesignType = MapTilerDesign.Toner
}// 초기 디자인은 state를 만들 때 넘긴다
@StateObject private var mapLibreState = MapLibreViewState(
mapDesignType: MapLibreDesign.DemoTiles,
cameraPosition: viewModel.initCameraPosition
)
// 전환은 @Published 프로퍼티에 대입하기만 하면 된다
mapLibreState.mapDesignType = MapLibreDesign.OsmBrightJaconst [mapViewState, setMapViewState] =
useState<MapViewStateInterface<MapDesignTypeInterface<unknown>> | null>(null);
const handleDesignChange = (designId: string) => {
const option = mapDesignOptions.find(item => item.design.id === designId);
if (!mapViewState || !option) return;
mapViewState.mapDesignType = option.design;
};04 · 커스텀 디자인을 정의하기
프리셋의 타입은 그대로 생성자로 공개되어 있습니다. id와, 스타일 JSON의 URL(벡터 계열) 또는 타일 URL 템플릿(래스터 계열)을 넘기면, 자사 스타일이나 지리원 타일을 프리셋과 같은 취급으로 쓸 수 있습니다.
// MapLibreDesign은 data class. 스타일 JSON의 URL을 넘기면 나만의 디자인이 된다
val GsiPale =
MapLibreDesign(
id = "gsi-pale",
styleJsonURL = "https://example.com/styles/gsi-pale/style.json",
attributionRules =
listOf(
AttributionRule(attribution = "GSI 타일"),
AttributionRule(attribution = "GEBCO", minZoom = 5, maxZoom = 8),
),
)private val mapLibreDesigns =
listOf(
MapDesignOption(label = "OsmBright", design = MapLibreDesign.OsmBright),
MapDesignOption(label = "GSI Pale", design = GsiPale),
)// 네이티브 mapType 상수를 그대로 감싸고 출처 표기만 덧붙인다
val NormalWithNotice =
GoogleMapDesign.Custom(
id = MAP_TYPE_NORMAL,
attributionRules = listOf(AttributionRule(attribution = "자체 데이터 2026")),
)Google Maps처럼 지도의 종류가 네이티브 상수로 정해지는 프로바이더에서는 GoogleMapDesign.Custom을 쓰고, id에 상수를 넘겨 출처 표기만을 교체합니다. MapTiler・Mapbox・HERE 등도 같은 형태의 생성자를 가집니다.
// 직접 만든 디자인의 입구는 public init(id:styleJsonURL:attributionRules:)
let gsiPale = MapLibreDesign(
id: "gsi-pale",
styleJsonURL: "https://example.com/styles/gsi-pale/style.json",
attributionRules: [
AttributionRule(attribution: "GSI 타일"),
AttributionRule(attribution: "GEBCO", minZoom: 5, maxZoom: 8),
]
)private let mapLibreDesigns = [
MapDesignOption(label: "DemoTiles", design: MapLibreDesign.DemoTiles),
MapDesignOption(label: "GSI Pale", design: gsiPale),
]
// 적용할 때 프로바이더의 타입으로 캐스트한다
if let design = option.design as? MapLibreDesign {
mapLibreState.mapDesignType = design
}구조체이므로 let으로 정의해 어디서든 공유할 수 있습니다. ViewModel의 MapDesignOption은 design: Any를 가지므로, 적용 시에 as?로 프로바이더의 타입으로 좁힙니다.
const GSI_STANDARD_DESIGN = new LeafletDesign({
id: 'gsi-standard',
tileUrl: 'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
tileOptions: { minZoom: 5, maxZoom: 18, tileSize: 256 },
attributionRules: GSI_STANDARD_ATTRIBUTION_RULES,
});
export const LEAFLET_DESIGNS: MapDesignOption[] = [
{ label: 'OpenStreetMap', design: LeafletDesign.OpenStreetMap },
{ label: 'GSI Standard', design: GSI_STANDARD_DESIGN },
{ label: 'None', design: LeafletDesign.None },
];// new MapLibreDesign(id, styleJsonURL, attributionRules?)
const GSI_PALE = new MapLibreDesign(
'gsi-pale',
'https://example.com/styles/gsi-pale/style.json',
[{ attribution: GSI_ATTRIBUTION }],
);
export const MAPLIBRE_DESIGNS: MapDesignOption[] = [
{ label: 'OsmBrightJa', design: MapLibreDesign.OsmBrightJa },
{ label: 'GSI Pale', design: GSI_PALE },
];Leaflet은 래스터 타일, MapLibre는 스타일 JSON으로 정의합니다. 디자인의 메타데이터는 각 프로바이더의 무거운 런타임 SDK와 같은 패키지에 들어 있기 때문에, 샘플에서는 프로바이더마다 동적 import 해서 청크를 분리하고 있습니다.
05 · 출처 표기(attributionRules)
독자 타일을 쓸 때는, 타일 제공처의 표기 조건을 규칙으로 가지게 합니다. 카메라의 줌과 표시 범위에서 필요한 표기만이 자동으로 골라집니다.
06 · 샘플 앱
3개 플랫폼 모두 디자인 전환 UI가 들어 있습니다. UI의 만듦새는 플랫폼마다 솔직한 구현이고, 대입하는 처리만이 공통입니다.
val mapViewState = rememberMapLibreMapViewState(
mapDesign = MapLibreDesign.DemoTiles,
cameraPosition = MapCameraPosition(
position = GeoPoint.fromLatLong(21.382314, -157.933097),
zoom = 12.0,
),
)
val designs = listOf(
"Demo" to MapLibreDesign.DemoTiles,
"OSM Bright" to MapLibreDesign.OsmBrightJa,
"Toner" to MapLibreDesign.MapTilerTonerJa,
)
MapLibreMapView(state = mapViewState, modifier = Modifier.weight(1f))
Row {
designs.forEach { (label, design) ->
// Just an assignment. The map is not rebuilt, so the camera stays where it is
Button(onClick = { mapViewState.mapDesignType = design }) { Text(label) }
}
}