Android 지도 앱 개발 완벽 가이드 — Google Maps SDK, 마커, 클러스터링, GeoJSON과 오프라인 지도 설계
Android 앱에서 지도 기능은 다양한 분야에 활용됩니다.
대표적으로 주변 시설 검색, 여행 안내도, GPS 이동 경로, 관광지 위치 표시, 동식물 관찰 기록 등이 있습니다.
예를 들어 조류 도감 앱에 사용자가 관찰한 새의 위치를 지도에 표시하거나, 정원 안내도 앱에 산책로와 주요 시설의 위치를 표시하는 기능을 구현할 수 있습니다.
이러한 기능을 구현할 때 가장 먼저 검토할 수 있는 도구가 Google Maps SDK for Android입니다.
하지만 지도에 마커 하나를 표시하는 기능과 수천 개의 장소를 효율적으로 관리하는 기능은 구현 방법이 다릅니다.
또한 인터넷이 없는 환경에서 지도를 사용해야 한다면 지도 데이터의 제공 방식과 라이선스까지 고려해야 합니다.
이번 글에서는 다음 내용을 살펴보겠습니다.
Google Maps SDK 프로젝트 설정
XML Fragment에서 지도 표시
지도 마커와 정보창 구현
SQLite에 저장된 장소 표시
대량 마커 클러스터링
GPS 이동 경로 Polyline 표시
Polygon으로 구역 표시
GeoJSON 데이터 가져오기와 내보내기
MVVM 구조에서 지도 상태 관리
오프라인 지도 구현 시 고려사항
API 키 보호와 Google Maps 비용 관리
개발 환경: Kotlin, XML ViewBinding, MVVM, Coroutine, SQLiteOpenHelper, Google Maps SDK for Android, Maps SDK Utility Library. minSdk 24 이상. 라이브러리 버전과 Google Maps Platform 과금 정책은 실제 적용 시 공식 문서를 확인해야 합니다.
1. Google Maps SDK란?
Google Maps SDK for Android는 Android 앱 안에 Google 지도를 표시하고 지도 위에 다양한 정보를 추가할 수 있도록 제공하는 SDK입니다.
대표적으로 다음 기능을 사용할 수 있습니다.
| 기능 | 설명 |
|---|---|
| 지도 표시 | 일반 지도, 위성 지도 등 |
| Marker | 특정 위치 표시 |
| InfoWindow | 마커 정보 표시 |
| Polyline | 이동 경로 및 선 표시 |
| Polygon | 영역 표시 |
| Circle | 원형 범위 표시 |
| Camera | 지도 중심 및 확대 수준 변경 |
| Cluster | 여러 마커를 그룹으로 표시 |
| GeoJSON | 지리 공간 데이터 표시 |
클러스터링과 GeoJSON 처리는 Google Maps SDK 자체의 핵심 기능 외에 Maps SDK Utility Library를 활용할 수 있습니다.
Google Maps 프로젝트 설정
2. Google Cloud 프로젝트 준비
Google Maps SDK를 사용하려면 Google Cloud 프로젝트를 준비해야 합니다.
일반적인 설정 순서는 다음과 같습니다.
Google Cloud 프로젝트 생성 또는 선택
결제 계정 설정
Maps SDK for Android 활성화
API 키 생성
Android 앱 제한 설정
프로젝트에 API 키 연결
Google Maps Platform은 사용량에 따라 비용이 발생할 수 있습니다.
무료 사용 범위와 요금 정책은 변경될 수 있으므로 출시 전에 공식 가격 페이지를 확인해야 합니다.
3. Gradle 의존성 추가
앱 모듈의 build.gradle.kts에 Google Maps SDK를 추가합니다.
dependencies {
implementation(
"com.google.android.gms:play-services-maps:<version>"
)
implementation(
"com.google.maps.android:android-maps-utils:<version>"
)
}<version>은 공식 릴리스에서 확인한 버전으로 교체합니다.
첫 번째 라이브러리는 지도 표시를 담당합니다.
두 번째 라이브러리는 클러스터링, GeoJSON 등의 부가 기능을 구현할 때 사용합니다.
Google Maps API 키 설정
4. API 키를 코드에 직접 작성하지 않기
다음과 같이 API 키를 Kotlin 코드에 직접 작성하는 방식은 피하는 것이 좋습니다.
val mapsApiKey = "AIza..."대신 Gradle 설정과 Manifest Placeholder 등을 활용해 키를 주입할 수 있습니다.
다만 APK에 포함된 Google Maps API 키는 완전한 비밀로 보호할 수 없습니다.
중요한 것은 API 키를 숨기는 것보다 Google Cloud에서 해당 키의 사용 범위를 제한하는 것입니다.
5. Android 앱 제한 설정
Google Cloud Console에서 API 키를 설정할 때 다음 항목을 확인합니다.
애플리케이션 제한: Android 앱
패키지 이름 등록
SHA-1 인증서 지문 등록
API 제한: Maps SDK for Android 등 필요한 API만 허용
디버그 서명 인증서와 Google Play App Signing의 앱 서명 인증서는 다를 수 있습니다.
따라서 개발 환경에서는 지도가 정상적으로 표시되는데 Google Play에 배포한 앱에서는 지도가 표시되지 않는 문제가 발생할 수 있습니다.
이 경우 패키지 이름과 서명 인증서 지문을 확인해야 합니다.
6. AndroidManifest.xml 설정
Manifest에 API 키를 등록합니다.
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
</application>MAPS_API_KEY는 Gradle의 Manifest Placeholder를 통해 전달한다고 가정합니다.
인터넷 사용 권한도 확인합니다.
<uses-permission
android:name="android.permission.INTERNET" />지도 표시 자체에는 위치 권한이 필요하지 않습니다.
사용자의 현재 위치를 표시하거나 위치를 수집할 때 별도의 위치 권한을 고려하면 됩니다.
XML에서 지도 표시
7. SupportMapFragment 사용
XML 기반 프로젝트에서는 SupportMapFragment를 사용할 수 있습니다.
fragment_map.xml 예제입니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mapContainer"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />Fragment에서는 getMapAsync()를 이용해 지도 객체를 전달받습니다.
8. GoogleMap 초기화
class MapFragment : Fragment(R.layout.fragment_map),
OnMapReadyCallback {
private var googleMap: GoogleMap? = null
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
val mapFragment =
childFragmentManager.findFragmentById(
R.id.mapContainer
) as? SupportMapFragment
mapFragment?.getMapAsync(this)
}
override fun onMapReady(map: GoogleMap) {
googleMap = map
val initialLocation = LatLng(
37.5665,
126.9780
)
map.moveCamera(
CameraUpdateFactory.newLatLngZoom(
initialLocation,
13f
)
)
}
override fun onDestroyView() {
googleMap = null
super.onDestroyView()
}
}예제의 좌표는 서울 중심부를 나타내는 초기 지도 위치입니다.
지도 객체는 Fragment View 생명주기와 함께 관리하는 것이 좋습니다.
지도 마커 표시
9. Marker 추가
지도 위에 특정 장소를 표시하려면 MarkerOptions를 사용할 수 있습니다.
val location = LatLng(
37.5796,
126.9770
)
googleMap.addMarker(
MarkerOptions()
.position(location)
.title("경복궁")
.snippet("서울의 대표적인 궁궐")
)title과 snippet은 마커 정보창에 표시할 수 있는 내용입니다.
10. 마커 클릭 이벤트
googleMap.setOnMarkerClickListener { marker ->
val title = marker.title.orEmpty()
Log.d(
"Map",
"Marker clicked: $title"
)
false
}여기서 false를 반환하면 기본 마커 클릭 동작을 계속 수행할 수 있습니다.
마커를 클릭했을 때 상세 화면을 열고 싶다면 별도의 클릭 처리 로직을 구현할 수 있습니다.
장소 데이터 모델
11. Place 모델 정의
SQLite에 저장된 장소를 지도에 표시한다고 가정하겠습니다.
data class MapPlace(
val id: Long,
val name: String,
val description: String?,
val latitude: Double,
val longitude: Double
)지도에서는 위도와 경도를 이용해 마커를 생성합니다.
장소의 고유 ID는 상세 화면 이동이나 데이터 조회에 활용할 수 있습니다.
SQLite 장소 관리
12. 장소 테이블 생성
CREATE TABLE map_places (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
latitude REAL NOT NULL,
longitude REAL NOT NULL
);장소가 특정 공원이나 관광지에 속한다면 site_id 같은 컬럼을 추가할 수도 있습니다.
예를 들어 하나의 앱에서 여러 관광지 안내도를 관리하는 경우 다음 구조를 고려할 수 있습니다.
tourist_sites
└── map_places이렇게 하면 장소별 데이터를 분리해서 관리할 수 있습니다.
13. SQLite 장소 조회
fun getAllPlaces(
db: SQLiteDatabase
): List<MapPlace> {
val places = mutableListOf<MapPlace>()
db.rawQuery(
"""
SELECT
id,
name,
description,
latitude,
longitude
FROM map_places
ORDER BY name
""".trimIndent(),
null
).use { cursor ->
while (cursor.moveToNext()) {
places.add(
MapPlace(
id = cursor.getLong(0),
name = cursor.getString(1),
description = cursor.getString(2),
latitude = cursor.getDouble(3),
longitude = cursor.getDouble(4)
)
)
}
}
return places
}데이터베이스 조회는 메인 스레드가 아닌 Dispatchers.IO에서 실행하는 것이 좋습니다.
SQLite 장소를 지도에 표시
14. 여러 마커 추가
fun showPlaces(
map: GoogleMap,
places: List<MapPlace>
) {
places.forEach { place ->
val position = LatLng(
place.latitude,
place.longitude
)
map.addMarker(
MarkerOptions()
.position(position)
.title(place.name)
.snippet(place.description)
)?.tag = place.id
}
}여기서는 Marker의 tag에 장소 ID를 저장했습니다.
이 값을 이용하면 마커 클릭 시 해당 장소의 상세 데이터를 조회할 수 있습니다.
15. 마커 클릭 후 상세 화면 이동
googleMap.setOnInfoWindowClickListener { marker ->
val placeId = marker.tag as? Long
?: return@setOnInfoWindowClickListener
viewModel.openPlaceDetail(placeId)
}이 예제는 ViewModel에 openPlaceDetail()이라는 이벤트 처리 함수가 있다고 가정합니다.
실제 프로젝트에서는 Navigation Component를 이용해 상세 화면으로 이동하도록 구성할 수 있습니다.
지도 마커가 많아지는 문제
16. 수천 개의 마커를 표시하면?
장소 데이터가 많아지면 모든 마커를 개별적으로 표시하는 방식은 비효율적일 수 있습니다.
예를 들어 지도에 3,000개의 장소를 동시에 표시한다고 가정하겠습니다.
다음과 같은 문제가 발생할 수 있습니다.
마커가 서로 겹침
지도 화면이 복잡해짐
렌더링 부담 증가
사용자가 장소를 선택하기 어려움
지도 이동과 확대·축소 시 처리량 증가
이러한 문제를 줄이기 위해 사용할 수 있는 기능이 마커 클러스터링입니다.
Marker Clustering
17. 클러스터링이란?
클러스터링은 가까운 위치의 마커들을 하나의 그룹으로 묶어 표시하는 방식입니다.
예를 들어 다음과 같습니다.
지도 축소 상태
[12] [35] [8]
지도 확대
[장소 A] [장소 B] [장소 C]지도를 확대하면 묶여 있던 장소가 개별 마커로 표시될 수 있습니다.
18. ClusterItem 구현
Maps SDK Utility Library의 ClusterItem 인터페이스를 구현합니다.
class PlaceClusterItem(
val place: MapPlace
) : ClusterItem {
override fun getPosition(): LatLng {
return LatLng(
place.latitude,
place.longitude
)
}
override fun getTitle(): String {
return place.name
}
override fun getSnippet(): String? {
return place.description
}
override fun getZIndex(): Float? {
return null
}
}19. ClusterManager 초기화
private var clusterManager:
ClusterManager<PlaceClusterItem>? = null지도 준비 후 다음과 같이 생성합니다.
val manager =
ClusterManager<PlaceClusterItem>(
requireContext(),
googleMap
)
clusterManager = manager
googleMap.setOnCameraIdleListener(manager)
googleMap.setOnMarkerClickListener(manager)
googleMap.setOnInfoWindowClickListener(manager)클러스터 매니저는 지도 이동과 마커 클릭 이벤트를 처리합니다.
실제 프로젝트에서 다른 지도 리스너도 사용한다면 이벤트가 충돌하지 않도록 연결 방식을 관리해야 합니다.
20. 클러스터 데이터 추가
fun showClusteredPlaces(
places: List<MapPlace>
) {
val manager = clusterManager ?: return
manager.clearItems()
val items = places.map { place ->
PlaceClusterItem(place)
}
manager.addItems(items)
manager.cluster()
}이렇게 하면 장소가 많을 때 개별 마커를 모두 표시하는 것보다 화면을 정리하기 쉽습니다.
단, 클러스터링 자체가 데이터베이스 조회량을 줄여주는 것은 아닙니다.
대규모 장소 데이터라면 지도 화면 범위에 따라 데이터를 조회하는 구조도 고려해야 합니다.
지도 화면 범위 기반 조회
21. 현재 지도 영역 확인
GoogleMap에서는 현재 화면에 표시되는 지도 범위를 확인할 수 있습니다.
val bounds =
googleMap.projection.visibleRegion.latLngBounds이 값에는 남서쪽과 북동쪽 좌표가 포함됩니다.
이를 이용해 현재 화면에 필요한 장소만 조회할 수 있습니다.
22. SQLite 범위 검색
일반적인 지역 범위에서는 다음과 같은 쿼리를 사용할 수 있습니다.
SELECT *
FROM map_places
WHERE latitude BETWEEN ? AND ?
AND longitude BETWEEN ? AND ?;단, 경도 범위가 ±180도 날짜변경선을 가로지르는 경우에는 단순한 BETWEEN 조건으로 처리할 수 없습니다.
전 세계 지도를 다루는 앱이라면 이러한 지리적 경계 조건을 별도로 처리해야 합니다.
23. 지도 이동 시 데이터 조회
지도 이동이 끝났을 때 현재 범위를 기준으로 데이터를 조회할 수 있습니다.
googleMap.setOnCameraIdleListener {
val bounds =
googleMap.projection.visibleRegion.latLngBounds
viewModel.loadPlacesInBounds(bounds)
}하지만 지도를 조금 움직일 때마다 SQLite를 조회하면 불필요한 작업이 발생할 수 있습니다.
따라서 다음과 같은 방법을 고려할 수 있습니다.
짧은 Debounce 적용
이전 조회 영역과 비교
확대 수준에 따라 데이터 조회
이미 로드한 영역 캐시
조회 결과 변경 시에만 마커 갱신
클러스터링을 사용하는 경우 CameraIdleListener 연결을 ClusterManager와 함께 처리해야 합니다.
GPS 경로 표시
24. Polyline이란?
Polyline은 지도 위에 여러 좌표를 연결한 선을 표시하는 기능입니다.
GPS 트래킹 앱에서는 이동 경로를 표시할 때 사용할 수 있습니다.
GPS 좌표 목록
↓
LatLng 변환
↓
PolylineOptions
↓
GoogleMap.addPolyline()25. GPS 경로 표시 코드
fun drawTrack(
map: GoogleMap,
points: List<TrackPoint>
): Polyline {
val latLngPoints = points.map { point ->
LatLng(
point.latitude,
point.longitude
)
}
return map.addPolyline(
PolylineOptions()
.addAll(latLngPoints)
.width(8f)
.color(Color.BLUE)
.geodesic(false)
)
}여기서 TrackPoint는 이전 GPS 트래킹 글에서 정의한 데이터 모델입니다.
geodesic(false)는 지도 투영에서 선분을 연결하는 일반적인 표시 방식입니다.
짧은 산책로 기록에서는 이 설정으로 시작할 수 있습니다.
실시간 경로 업데이트
26. 새로운 GPS 좌표 추가
사용자가 이동할 때마다 Polyline을 새로 생성할 필요는 없습니다.
기존 Polyline의 좌표 목록을 갱신할 수 있습니다.
private var trackPolyline: Polyline? = null
private val trackLatLngs =
mutableListOf<LatLng>()새로운 위치가 전달되면 다음과 같이 처리합니다.
fun addTrackPoint(
location: Location
) {
trackLatLngs.add(
LatLng(
location.latitude,
location.longitude
)
)
trackPolyline?.points =
trackLatLngs.toList()
}좌표가 매우 많아지면 전체 목록을 계속 복사하는 비용도 고려해야 합니다.
장시간 기록 앱에서는 화면에 표시할 좌표 수를 줄이거나, 표시용 경로와 원본 경로를 분리하는 전략이 유용할 수 있습니다.
지도 구역 표시
27. Polygon 사용
Polygon은 여러 좌표로 이루어진 영역을 지도에 표시할 때 사용합니다.
예를 들어 정원의 특정 구역이나 공원 경계를 표시할 수 있습니다.
val polygonOptions =
PolygonOptions()
.add(
LatLng(37.1000, 127.1000),
LatLng(37.1000, 127.1010),
LatLng(37.1010, 127.1010),
LatLng(37.1010, 127.1000)
)
.strokeColor(Color.GREEN)
.strokeWidth(4f)
.fillColor(
Color.argb(
60,
0,
180,
0
)
)
googleMap.addPolygon(polygonOptions)위 좌표는 설명을 위한 가상 데이터입니다.
Polygon을 활용하면 특정 시설 구역이나 탐방 제한 구역을 표시할 수 있습니다.
GeoJSON이란?
28. GeoJSON 기본 개념
GeoJSON은 지리 공간 데이터를 JSON 형식으로 표현하는 표준입니다.
대표적으로 다음과 같은 지오메트리를 표현할 수 있습니다.
Point
LineString
Polygon
MultiPoint
MultiLineString
MultiPolygon
지도 데이터 교환에 널리 활용됩니다.
29. GeoJSON Point 예제
{
"type": "Feature",
"properties": {
"name": "관찰 지점 A",
"description": "조류 관찰 위치"
},
"geometry": {
"type": "Point",
"coordinates": [
127.123456,
37.123456
]
}
}여기서 중요한 점은 GeoJSON 좌표 순서가 경도, 위도라는 것입니다.
Android의 LatLng 생성자는 위도, 경도 순서입니다.
LatLng(latitude, longitude)반면 GeoJSON에서는 다음 순서를 사용합니다.
[longitude, latitude]이 순서를 혼동하면 지도에 전혀 다른 위치가 표시될 수 있습니다.
GeoJSON 파일 표시
30. GeoJsonLayer 사용
Maps SDK Utility Library에서는 GeoJsonLayer를 사용할 수 있습니다.
예를 들어 assets에 places.geojson 파일이 있다고 가정하겠습니다.
val layer = GeoJsonLayer(
googleMap,
R.raw.places,
requireContext()
)
layer.addLayerToMap()이 예제에서는 res/raw/places.geojson 파일을 사용합니다.
GeoJSON 데이터가 크다면 파일 파싱과 지도 렌더링이 메인 스레드에 부담을 주지 않도록 설계해야 합니다.
GPS 경로를 GeoJSON으로 내보내기
31. LineString 구조
GPS 이동 경로는 GeoJSON의 LineString으로 표현할 수 있습니다.
{
"type": "Feature",
"properties": {
"name": "정원 산책로"
},
"geometry": {
"type": "LineString",
"coordinates": [
[127.1000, 37.1000],
[127.1002, 37.1003],
[127.1005, 37.1007]
]
}
}좌표 배열에는 최소 두 개의 위치가 필요합니다.
32. Kotlin GeoJSON 생성
다음은 GPS 좌표 목록을 GeoJSON LineString으로 변환하는 예제입니다.
fun createGeoJson(
points: List<TrackPoint>
): String {
require(points.size >= 2) {
"LineString에는 좌표가 2개 이상 필요합니다."
}
val coordinates = JSONArray()
points.forEach { point ->
val coordinate = JSONArray().apply {
put(point.longitude)
put(point.latitude)
}
coordinates.put(coordinate)
}
val geometry = JSONObject().apply {
put("type", "LineString")
put("coordinates", coordinates)
}
val properties = JSONObject().apply {
put("name", "GPS Track")
}
val feature = JSONObject().apply {
put("type", "Feature")
put("properties", properties)
put("geometry", geometry)
}
return JSONObject().apply {
put("type", "FeatureCollection")
put(
"features",
JSONArray().put(feature)
)
}.toString(2)
}이 함수는 경로 데이터를 GeoJSON 문자열로 변환합니다.
파일로 저장하려면 Storage Access Framework의 CreateDocument를 사용할 수 있습니다.
GPX와 GeoJSON 비교
33. 어떤 형식을 사용할까?
| 구분 | GPX | GeoJSON |
|---|---|---|
| 기본 형식 | XML | JSON |
| 주요 용도 | GPS 경로·트랙 | 다양한 지리 공간 데이터 |
| 경로 기록 | 적합 | 적합 |
| 장소 정보 | Waypoint | Point Feature |
| 영역 표현 | 제한적 | Polygon 지원 |
| 웹 지도 활용 | 변환이 필요할 수 있음 | 비교적 편리 |
| Android 처리 | XML 파싱 | JSON 파싱 |
GPS 트래킹 기록을 교환하는 목적이라면 GPX가 유용합니다.
반면 장소, 경로, 구역을 하나의 데이터 형식으로 관리하려면 GeoJSON이 편리할 수 있습니다.
MVVM 지도 아키텍처
34. Repository와 ViewModel 분리
지도 앱에서도 MVVM 구조를 적용할 수 있습니다.
MapFragment
│
▼
MapViewModel
│
▼
MapRepository
│
├── SQLiteOpenHelper
├── GPS Track Repository
└── GeoJSON RepositoryMapFragment는 지도 객체와 마커 렌더링을 담당합니다.
MapViewModel은 지도에 표시할 데이터와 선택 상태를 관리합니다.
MapRepository는 SQLite와 파일 데이터에 접근합니다.
35. 지도 UI 상태 모델
data class MapUiState(
val places: List<MapPlace> = emptyList(),
val selectedPlaceId: Long? = null,
val isLoading: Boolean = false,
val errorMessage: String? = null
)ViewModel에서는 StateFlow를 사용할 수 있습니다.
class MapViewModel(
private val repository: MapRepository
) : ViewModel() {
private val _uiState =
MutableStateFlow(MapUiState())
val uiState =
_uiState.asStateFlow()
fun loadPlaces() {
viewModelScope.launch {
_uiState.update {
it.copy(isLoading = true)
}
try {
val places =
repository.getPlaces()
_uiState.update {
it.copy(
places = places,
isLoading = false,
errorMessage = null
)
}
} catch (e: Exception) {
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "장소 조회 실패"
)
}
}
}
}
}실제 프로젝트에서는 Repository의 SQLite 조회가 IO 스레드에서 수행되도록 구성해야 합니다.
오프라인 지도 구현
36. Google Maps SDK로 완전한 오프라인 지도를 만들 수 있을까?
이 부분은 주의해야 합니다.
Google Maps SDK는 지도 표시 과정에서 내부적으로 일부 데이터를 캐시할 수 있지만, 개발자가 원하는 지역의 Google 지도 타일을 자유롭게 다운로드하고 영구 저장해 사용하는 범용 오프라인 지도 API를 제공하는 것은 아닙니다.
따라서 Google Maps SDK의 캐시에 의존해 완전한 오프라인 지도를 구현하는 것은 적절하지 않습니다.
오프라인 지도가 필수인 앱이라면 처음부터 별도의 지도 엔진과 오프라인 데이터 공급 방식을 검토해야 합니다.
또한 Google 지도 타일을 임의로 다운로드하거나 별도 캐시에 저장하는 방식은 서비스 약관에 위배될 수 있습니다.
37. 오프라인 지도 대안
오프라인 지도 구현에 활용할 수 있는 대표적인 선택지는 다음과 같습니다.
| 방식 | 특징 | 고려사항 |
|---|---|---|
| MapLibre Native | 오픈소스 지도 렌더링 | 타일 데이터 및 스타일 준비 |
| Mapsforge | 오프라인 벡터 지도 활용 | 지도 데이터 형식과 렌더링 |
| 자체 지도 이미지 | 특정 시설 안내도에 적합 | 좌표 변환 직접 구현 |
| 상용 오프라인 지도 SDK | 공급업체 기능 활용 | 라이선스 및 비용 |
각 방식은 제공 기능과 Android 지원 상태가 다르므로 프로젝트 요구사항에 맞춰 확인해야 합니다.
MapLibre 활용
38. MapLibre란?
MapLibre는 오픈소스 지도 렌더링 기술입니다.
지도 스타일과 타일 데이터를 적절하게 구성하면 자체 지도 서비스나 오프라인 지도 기능을 구현하는 데 활용할 수 있습니다.
다만 MapLibre를 설치한다고 오프라인 지도 데이터가 자동으로 제공되는 것은 아닙니다.
별도의 지도 타일 데이터와 사용 권한이 필요합니다.
39. 오프라인 지도 데이터
오프라인 지도에서는 일반적으로 다음과 같은 데이터를 고려합니다.
지도 타일
벡터 타일
지도 스타일
글꼴 및 아이콘
장소 데이터
경로 데이터
지도 데이터는 OpenStreetMap 기반 자료 등을 활용할 수 있지만, 데이터 및 타일 제공자의 라이선스와 이용 조건을 확인해야 합니다.
특히 공개 타일 서버에서 대량 데이터를 무단 다운로드해 오프라인 패키지로 만드는 방식은 피해야 합니다.
정원 안내도 앱 설계
40. 특정 정원만 표시한다면?
전국 지도나 세계 지도가 아니라 특정 정원 한 곳의 안내도를 제공하는 앱이라면 접근 방식이 달라질 수 있습니다.
예를 들어 다음과 같은 데이터가 있다고 가정하겠습니다.
정원 안내도 이미지
│
├── 입구
├── 주차장
├── 화장실
├── 산책로
├── 전망대
└── 휴게 공간이 경우에는 지도 이미지 위에 좌표 기반 마커를 표시하는 방식도 고려할 수 있습니다.
하지만 이미지 픽셀 좌표와 실제 위도·경도를 연결하려면 지리 참조 작업이 필요합니다.
단순히 이미지의 가로·세로 비율만으로 실제 GPS 위치를 정확하게 계산할 수는 없습니다.
이미지 지도와 GPS 좌표 연결
41. 지리 참조란?
지리 참조(Georeferencing)는 지도 이미지의 특정 위치를 실제 지리 좌표와 연결하는 작업입니다.
예를 들어 지도 이미지에서 다음 지점을 확인했다고 가정하겠습니다.
| 이미지 위치 | 실제 지리 좌표 |
|---|---|
| 왼쪽 위 기준점 | 위도·경도 A |
| 오른쪽 위 기준점 | 위도·경도 B |
| 왼쪽 아래 기준점 | 위도·경도 C |
이러한 기준점을 이용해 이미지 좌표를 실제 지도 좌표로 변환할 수 있습니다.
다만 실제 변환에는 지도 투영법, 이미지 회전, 왜곡 등을 고려해야 합니다.
정밀한 지도 제작에서는 QGIS 같은 GIS 도구를 활용하는 것이 좋습니다.
지도 API 비용 관리
42. Google Maps Platform 비용
Google Maps Platform은 사용량에 따라 요금이 발생할 수 있습니다.
따라서 개인 개발자라면 다음 항목을 확인하는 것이 좋습니다.
Maps SDK 사용량
활성화한 API 종류
결제 계정 연결 상태
사용량 알림
예산 알림
API 키 제한
비정상 요청 여부
Google Cloud 예산 알림은 비용 발생을 알려주는 기능이며, 일반적으로 설정한 금액에서 자동으로 모든 요청을 차단하는 하드 한도는 아닙니다.
따라서 예산 알림만으로 비용이 완전히 제한된다고 생각하면 안 됩니다.
지도 성능 최적화
43. 대량 데이터 처리
지도에 많은 데이터를 표시하는 경우 다음 방법을 검토할 수 있습니다.
마커 클러스터링
화면 범위 기반 데이터 조회
SQLite 인덱스 활용
불필요한 마커 재생성 방지
지도 이동 이벤트 Debounce
큰 GeoJSON 파일 분할
경로 표시용 좌표 단순화
화면에 필요한 데이터만 유지
특히 수천 개의 장소를 한 번에 로드하는 것보다 현재 지도 영역에 필요한 데이터를 조회하는 방식이 효과적일 수 있습니다.
실제 프로젝트 적용 사례
44. 조류 관찰 지도
조류 관찰 앱에서는 다음 기능을 구현할 수 있습니다.
관찰 위치 마커
관찰 날짜별 필터
조류 종류별 마커 구분
관찰 위치 클러스터링
탐조 경로 Polyline
GPX 경로 불러오기
GeoJSON 데이터 내보내기
다만 희귀 조류나 보호종의 민감한 서식지 위치가 공개되지 않도록 주의해야 합니다.
45. 관광지·정원 안내도
관광지 안내도 앱에서는 다음 기능을 구현할 수 있습니다.
시설별 마커
장소 상세 설명
현재 위치 표시
산책로 표시
구역별 Polygon
장소 검색
GeoJSON 장소 데이터 관리
오프라인 안내도
정해진 관광지 내부의 시설 안내가 주목적이라면 일반 지도 SDK와 별도로 이미지 기반 안내도를 제공하는 방식도 검토할 수 있습니다.
Android 지도 앱 체크리스트
46. 출시 전 점검
Google Cloud 프로젝트 준비
Maps SDK 활성화
API 키 생성
Android 앱 제한 설정
패키지 이름 확인
SHA-1 인증서 확인
API 사용 범위 제한
지도 표시 확인
SupportMapFragment 생명주기 처리
마커 표시
마커 클릭 처리
SQLite 장소 데이터 연동
클러스터링 적용
지도 범위 기반 조회 검토
GPS Polyline 표시
Polygon 구역 표시
GeoJSON 좌표 순서 확인
GeoJSON 가져오기
GeoJSON 내보내기
대량 데이터 성능 테스트
오프라인 지도 요구사항 검토
지도 데이터 라이선스 확인
API 비용 모니터링
개인정보 및 위치 정보 보호
실제 Android 기기 테스트
47. 마무리
Android 지도 앱은 단순히 Google 지도를 화면에 표시하는 것에서 시작할 수 있습니다.
하지만 실제 서비스에서는 장소 데이터 관리, 마커 성능, GPS 경로, 지도 데이터 형식, 오프라인 지원, API 비용까지 함께 고려해야 합니다.
이번 글의 핵심을 정리하면 다음과 같습니다.
Google Maps SDK를 이용해 Android 앱에 지도를 표시할 수 있습니다.
Marker로 장소를 표시하고 상세 화면과 연결할 수 있습니다.
SQLite에 저장된 장소 데이터를 지도에 표시할 수 있습니다.
대량 마커에는 클러스터링을 활용할 수 있습니다.
Polyline으로 GPS 이동 경로를 표시할 수 있습니다.
Polygon으로 정원이나 시설 구역을 표현할 수 있습니다.
GeoJSON은 장소·경로·영역 데이터를 교환하는 데 유용합니다.
GeoJSON 좌표 순서는 경도, 위도입니다.
Google Maps SDK만으로 완전한 오프라인 지도를 구현한다고 가정해서는 안 됩니다.
오프라인 지도가 필요하다면 별도의 지도 엔진과 데이터 공급 방식을 검토해야 합니다.
특히 조류 관찰 앱이나 관광지 안내도 앱에서는 SQLite 장소 데이터와 지도 마커를 연결하는 구조부터 구현하는 것이 좋습니다.
이후 GPS 경로 표시, 클러스터링, GeoJSON, 오프라인 지도 기능으로 확장할 수 있습니다.
Android 지도 앱 개발의 핵심은 지도를 표시하는 것뿐 아니라, 실제 장소와 위치 데이터를 효율적으로 관리하는 구조를 만드는 것입니다.
참고 자료
Google Maps SDK for Android
https://developers.google.com/maps/documentation/android-sdk/overviewGoogle Maps SDK 시작하기
https://developers.google.com/maps/documentation/android-sdk/startGoogle Maps Marker
https://developers.google.com/maps/documentation/android-sdk/markerGoogle Maps Shapes
https://developers.google.com/maps/documentation/android-sdk/shapesMaps SDK Utility Library
https://github.com/googlemaps/android-maps-utilsGoogle Maps Marker Clustering
https://developers.google.com/maps/documentation/android-sdk/utility/marker-clusteringGoogle Maps GeoJSON
https://developers.google.com/maps/documentation/android-sdk/utility/geojsonGoogle Maps Platform 가격
https://mapsplatform.google.com/pricing/MapLibre
https://maplibre.org/GeoJSON RFC 7946
https://www.rfc-editor.org/rfc/rfc7946
태그: Android, Kotlin, Google Maps SDK, Android 지도 앱, Marker, Marker Clustering, Polyline, Polygon, GeoJSON, SQLite, MVVM, GPS, MapLibre, 오프라인 지도, 관광지 안내도, 조류 관찰 앱, Android 16