Android 오프라인 지도 구현 완벽 가이드 — MapLibre, MBTiles, 벡터 타일과 지도 데이터 캐싱

Android 지도 앱을 개발하다 보면 인터넷이 연결되지 않은 환경에서도 지도를 표시해야 하는 상황이 발생합니다.

예를 들어 산속의 탐방로, 정원 내부 안내도, 관광지 산책로, 조류 관찰 장소처럼 통신 상태가 불안정한 지역에서는 온라인 지도에만 의존하기 어렵습니다.

일반적인 지도 앱에서는 Google Maps SDK를 사용하는 경우가 많습니다.

하지만 Google Maps SDK는 개발자가 원하는 지역의 지도 타일을 자유롭게 다운로드해 영구적으로 보관하는 범용 오프라인 지도 기능을 제공하지 않습니다.

따라서 완전한 오프라인 지도가 필요하다면 처음부터 별도의 지도 엔진과 지도 데이터 관리 방식을 고려해야 합니다.

이번 글에서는 오픈소스 지도 렌더링 엔진인 MapLibre, 지도 타일 저장 형식인 MBTiles, 그리고 GeoJSON과 SQLite를 활용한 오프라인 지도 아키텍처를 살펴보겠습니다.

특히 단순히 지도를 표시하는 것에서 끝나지 않고, 실제 Android 앱에서 지도 파일을 배포하고 장소와 경로를 표시하는 방법까지 정리하겠습니다.

개발 환경: Kotlin, XML, MVVM, SQLiteOpenHelper, Coroutine, MapLibre Native Android, GeoJSON, MBTiles, QGIS. minSdk 24 이상을 고려합니다. MapLibre의 API 및 패키징 방식은 SDK 버전에 따라 달라질 수 있으므로 실제 구현 시 사용 중인 버전의 공식 문서를 확인해야 합니다.


1. 오프라인 지도가 필요한 이유

지도 앱에서 인터넷 연결이 끊기면 다음과 같은 문제가 발생할 수 있습니다.

  • 지도 타일이 표시되지 않음

  • 새로운 지역으로 이동할 때 지도가 비어 있음

  • 장소 검색이 실패함

  • 경로 안내 기능이 중단됨

  • 지도 이미지 로딩이 지연됨

특히 정원이나 산책로 안내도처럼 특정 지역을 대상으로 하는 앱에서는 지도 데이터를 미리 준비해 제공하는 방식이 유용합니다.

예를 들어 앱에 특정 정원의 지도 데이터를 포함하면 인터넷 연결 없이도 다음 기능을 제공할 수 있습니다.

  1. 정원 지도 표시

  2. 시설 위치 마커 표시

  3. 산책로 표시

  4. 현재 GPS 위치 표시

  5. 장소 이름 검색

  6. 장소 상세 정보 조회

단, GPS 위치 측정과 지도 표시 기능은 서로 구분해야 합니다.

스마트폰은 인터넷이 없어도 위성 신호를 이용해 위치를 측정할 수 있지만, 최초 위치 획득 시간과 정확도는 환경에 따라 달라질 수 있습니다.


Google Maps SDK와 MapLibre 비교

2. 두 지도 엔진의 차이

구분Google Maps SDKMapLibre
지도 렌더링Google 지도 기반개발자가 지정한 지도 데이터 기반
기본 지도 데이터Google 제공별도 준비 필요
지도 스타일제공 범위 내 설정스타일과 데이터 구성 가능
오프라인 지도범용 다운로드 기능 미제공오프라인 데이터 구성 가능
지도 데이터 관리Google 플랫폼 정책 적용데이터 제공자 라이선스 적용
자체 타일 서버일반적인 사용 방식 아님구성 가능
비용Google Maps Platform 과금 체계엔진 자체와 데이터·호스팅 비용 구분

MapLibre는 오픈소스 지도 렌더링 엔진입니다.

하지만 MapLibre를 설치했다고 지도 데이터까지 자동으로 제공되는 것은 아닙니다.

실제 지도 화면을 표시하려면 지도 스타일과 타일 데이터를 준비해야 합니다.


지도 타일 이해하기

3. 지도 타일이란?

지도는 하나의 거대한 이미지로만 구성되는 것이 아닙니다.

일반적인 온라인 지도에서는 지도 영역을 작은 타일로 나누어 전달합니다.

예를 들어 다음과 같은 구조입니다.

지도 화면
   │
   ├── Tile A
   ├── Tile B
   ├── Tile C
   └── Tile D

사용자가 지도를 이동하거나 확대하면 필요한 타일을 추가로 불러옵니다.

이러한 구조를 이용하면 전체 지도 데이터를 한 번에 전송하지 않아도 됩니다.


4. XYZ 타일 좌표

일반적인 웹 지도 타일은 다음과 같은 좌표 구조를 사용합니다.

/{z}/{x}/{y}

여기서 각 값의 의미는 다음과 같습니다.

값의미
z확대 수준
x가로 타일 인덱스
y세로 타일 인덱스

예를 들어 다음과 같은 주소 형태를 볼 수 있습니다.

https://example.com/tiles/12/3456/1567.png

이 주소는 설명을 위한 예시입니다.

실제 타일 URL은 데이터 제공자의 규격과 이용 조건을 따라야 합니다.


래스터 타일과 벡터 타일

5. 래스터 타일

래스터 타일은 이미지로 구성된 지도 타일입니다.

대표적인 형식은 PNG, JPEG, WebP 등입니다.

장점은 구조가 단순하다는 것입니다.

이미 만들어진 지도 이미지를 표시하므로 렌더링 방식이 비교적 간단합니다.

반면 확대 수준마다 이미지 타일을 준비해야 하며, 지도 스타일을 동적으로 변경하기 어렵습니다.


6. 벡터 타일

벡터 타일은 도로, 건물, 하천 등의 지리 공간 객체를 벡터 데이터로 전달하는 방식입니다.

일반적으로 Mapbox Vector Tile(MVT) 형식을 사용하는 경우가 많습니다.

벡터 타일은 렌더링 시 스타일을 적용할 수 있습니다.

따라서 같은 지도 데이터라도 스타일에 따라 다른 형태로 표시할 수 있습니다.

다만 지도 스타일, 글꼴, 아이콘 등 추가 리소스를 준비해야 할 수 있습니다.


7. 어떤 타일을 선택해야 할까?

사용 목적검토할 방식
특정 정원의 고정 안내도래스터 이미지
관광지 전체 지도래스터 또는 벡터 타일
다양한 확대 수준 지원벡터 타일
지도 스타일 변경벡터 타일
단순한 오프라인 시설 안내이미지 오버레이
대규모 지역 지도벡터 타일 기반 설계

정원 안내도처럼 제한된 지역만 표시하는 앱에서는 반드시 벡터 타일을 사용할 필요는 없습니다.

오히려 고해상도 안내도 이미지를 지도 좌표에 맞춰 표시하는 방식이 더 단순할 수 있습니다.


MBTiles란?

8. MBTiles 기본 개념

MBTiles는 지도 타일을 하나의 SQLite 데이터베이스 파일에 저장하는 형식입니다.

일반적인 MBTiles 파일에는 다음과 같은 정보가 포함됩니다.

  • 지도 메타데이터

  • 확대 수준

  • 타일 좌표

  • 타일 데이터

파일 확장자는 일반적으로 .mbtiles입니다.

하나의 파일로 지도 타일을 배포할 수 있다는 장점이 있습니다.


9. MBTiles 내부 구조

전통적인 MBTiles 구조에는 다음과 같은 테이블이 사용됩니다.

CREATE TABLE metadata (
    name TEXT,
    value TEXT
);

타일 테이블은 다음과 같은 구조를 가질 수 있습니다.

CREATE TABLE tiles (
    zoom_level INTEGER,
    tile_column INTEGER,
    tile_row INTEGER,
    tile_data BLOB
);

tile_data에는 이미지 타일이나 벡터 타일 데이터가 저장됩니다.

다만 모든 MBTiles 파일이 내부적으로 완전히 동일한 물리 테이블 구조를 사용하는 것은 아닙니다.

일부 파일은 View나 별도의 정규화된 테이블 구조를 사용할 수 있습니다.


MBTiles 좌표 체계

10. XYZ와 TMS 차이

MBTiles를 다룰 때 주의해야 할 부분이 타일 Y 좌표입니다.

일반적인 XYZ 타일과 MBTiles의 전통적인 TMS 방식은 Y 좌표 방향이 다릅니다.

변환 공식은 다음과 같습니다.

[
y_{\mathrm{TMS}} = 2^z - 1 - y_{\mathrm{XYZ}}
]

예를 들어 확대 수준이 3이고 XYZ의 Y가 2라면 다음과 같습니다.

z = 3
XYZ y = 2

TMS y = 2^3 - 1 - 2
      = 5

이 차이를 처리하지 않으면 지도 타일이 잘못된 위치에 표시될 수 있습니다.


MapLibre Android 설정

11. Gradle 의존성

MapLibre Native Android를 사용하려면 라이브러리를 추가합니다.

dependencies {

    implementation(
        "org.maplibre.gl:android-sdk:<version>"
    )
}

<version>은 공식 릴리스에서 확인한 버전으로 변경합니다.

MapLibre Native Android의 API와 배포 방식은 버전에 따라 달라질 수 있습니다.


12. AndroidManifest.xml

온라인 타일이나 외부 스타일을 사용할 가능성이 있다면 인터넷 권한을 선언합니다.

<uses-permission
    android:name="android.permission.INTERNET" />

완전히 로컬 리소스만 사용하는 앱이라면 네트워크가 필수인 것은 아닙니다.

그러나 SDK 초기화, 스타일 리소스, 타일 제공 방식에 따라 필요한 구성이 달라질 수 있습니다.


MapView 구성

13. XML 레이아웃

다음은 MapLibre MapView를 배치하는 예제입니다.

<?xml version="1.0" encoding="utf-8"?>
<org.maplibre.android.maps.MapView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

이 예제는 org.maplibre.gl:android-sdk 계열의 패키지 구조를 기준으로 합니다.

사용 중인 SDK 버전에 따라 실제 클래스 이름을 확인해야 합니다.


14. MapView 초기화

Activity에서 MapView를 초기화하는 기본 구조는 다음과 같습니다.

class OfflineMapActivity : AppCompatActivity() {

    private lateinit var binding:
        ActivityOfflineMapBinding

    override fun onCreate(
        savedInstanceState: Bundle?
    ) {
        super.onCreate(savedInstanceState)

        binding = ActivityOfflineMapBinding.inflate(
            layoutInflater
        )

        setContentView(binding.root)

        binding.mapView.onCreate(savedInstanceState)

        binding.mapView.getMapAsync { map ->

            map.setStyle(
                Style.Builder()
                    .fromUri("asset://offline_style.json")
            )
        }
    }

    override fun onStart() {
        super.onStart()
        binding.mapView.onStart()
    }

    override fun onResume() {
        super.onResume()
        binding.mapView.onResume()
    }

    override fun onPause() {
        binding.mapView.onPause()
        super.onPause()
    }

    override fun onStop() {
        binding.mapView.onStop()
        super.onStop()
    }

    override fun onDestroy() {
        binding.mapView.onDestroy()
        super.onDestroy()
    }

    override fun onLowMemory() {
        super.onLowMemory()
        binding.mapView.onLowMemory()
    }
}

이 코드는 MapView 생명주기 연결을 보여주는 예제입니다.

실제 프로젝트에서는 MapLibre SDK 초기화 방식과 스타일 URI 지원 여부를 해당 버전 문서에서 확인해야 합니다.

또한 화면 회전 시 상태 복원이 필요하다면 onSaveInstanceState() 처리도 추가해야 합니다.


오프라인 스타일 파일

15. 스타일 JSON의 역할

MapLibre에서는 스타일 JSON을 이용해 지도에 표시할 데이터와 렌더링 방식을 정의할 수 있습니다.

스타일에는 다음과 같은 정보가 포함될 수 있습니다.

  • 지도 소스

  • 배경색

  • 레이어

  • 글꼴

  • 아이콘

  • 선과 면의 스타일

  • 타일 URL

예를 들어 다음과 같은 구조입니다.

{
  "version": 8,
  "name": "Offline Map",
  "sources": {},
  "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": {
        "background-color": "#f2f2f2"
      }
    }
  ]
}

이 스타일은 배경만 표시하는 최소 예제입니다.

실제 지도를 표시하려면 지도 소스와 레이어를 추가해야 합니다.


MBTiles와 MapLibre 연결

16. MBTiles 파일을 직접 열 수 있을까?

이 부분은 오프라인 지도 구현에서 특히 중요합니다.

MapLibre Native Android의 일반적인 스타일 소스 설정에 .mbtiles 파일 경로만 지정한다고 자동으로 MBTiles 지도가 표시되는 것은 아닙니다.

MBTiles는 SQLite 기반 타일 저장 형식이고, MapLibre의 지도 스타일은 타일 소스 규격에 따라 데이터를 요청합니다.

따라서 MBTiles를 사용하려면 타일을 MapLibre가 이해할 수 있는 방식으로 제공하는 계층이 필요합니다.

대표적인 방법은 다음과 같습니다.

  1. MBTiles를 읽어 타일 요청에 응답하는 로컬 타일 제공 계층 구현

  2. MBTiles 데이터를 지원하는 별도의 타일 서버 사용

  3. SDK가 지원하는 오프라인 데이터 패키징 방식 활용

  4. MBTiles를 다른 지원 형식으로 변환

실제 구현 방식은 MapLibre SDK 버전과 사용하는 타일 형식에 따라 달라집니다.


로컬 타일 서버 방식

17. Android 내부 타일 서버

MBTiles를 사용하는 한 가지 방법은 앱 내부에서 로컬 HTTP 서버를 실행하는 것입니다.

구조는 다음과 같습니다.

MapLibre MapView
       │
       ▼
http://127.0.0.1:포트/tiles/{z}/{x}/{y}
       │
       ▼
Local Tile Server
       │
       ▼
MBTiles SQLite
       │
       ▼
Tile Data

이 방식에서는 MapLibre가 일반적인 타일 URL로 데이터를 요청하고, 로컬 서버가 MBTiles에서 해당 타일을 조회해 응답합니다.

다만 Android 내부 HTTP 서버 운영에는 생명주기, 포트 관리, 보안, 성능, 백그라운드 실행 제한 등을 고려해야 합니다.


18. MBTiles에서 타일 조회

다음은 MBTiles SQLite 파일에서 타일을 조회하는 기본 함수입니다.

fun readMbTile(
    db: SQLiteDatabase,
    zoom: Int,
    x: Int,
    xyzY: Int
): ByteArray? {

    require(zoom in 0..30)

    val maxIndex = (1L shl zoom) - 1L

    if (
        x.toLong() !in 0L..maxIndex ||
        xyzY.toLong() !in 0L..maxIndex
    ) {
        return null
    }

    val tmsY = maxIndex - xyzY

    db.rawQuery(
        """
        SELECT tile_data
        FROM tiles
        WHERE zoom_level = ?
          AND tile_column = ?
          AND tile_row = ?
        LIMIT 1
        """.trimIndent(),
        arrayOf(
            zoom.toString(),
            x.toString(),
            tmsY.toString()
        )
    ).use { cursor ->

        return if (cursor.moveToFirst()) {
            cursor.getBlob(0)
        } else {
            null
        }
    }
}

이 예제는 전통적인 MBTiles tiles 테이블 구조를 가정합니다.

실제 파일을 사용할 때는 메타데이터와 스키마를 먼저 확인해야 합니다.


로컬 서버 구현 시 주의사항

19. HTTP 응답과 MIME 타입

로컬 타일 서버는 요청한 타일 형식에 맞는 응답을 반환해야 합니다.

예를 들어 다음과 같습니다.

타일 형식일반적인 MIME 타입
PNGimage/png
JPEGimage/jpeg
WebPimage/webp
MVTapplication/vnd.mapbox-vector-tile

벡터 타일은 압축 방식에 따라 추가 HTTP 헤더가 필요할 수 있습니다.

예를 들어 gzip으로 압축된 타일을 그대로 전달한다면 적절한 Content-Encoding 처리가 필요할 수 있습니다.

또한 Android의 Cleartext Traffic 정책 때문에 로컬 HTTP 접근이 제한될 수 있습니다.

가능하면 필요한 로컬 호스트 통신만 허용하도록 구성해야 합니다.


MBTiles 배포 방식

20. assets에 포함하기

작은 지역의 지도라면 MBTiles 파일을 앱 assets에 포함할 수 있습니다.

app/
 └── src/
     └── main/
         └── assets/
             └── maps/
                 └── garden.mbtiles

다만 Android assets에 포함된 파일은 일반적인 SQLite 파일 경로처럼 직접 열 수 없는 경우가 있습니다.

따라서 앱 내부 저장소로 복사한 뒤 읽기 전용으로 열 수 있습니다.


21. assets 파일 복사

fun copyAssetToFiles(
    context: Context,
    assetName: String,
    targetFile: File
) {

    targetFile.parentFile?.mkdirs()

    context.assets.open(assetName).use { input ->

        FileOutputStream(targetFile).use { output ->
            input.copyTo(output)
        }
    }
}

예를 들어 다음과 같이 사용할 수 있습니다.

val targetFile = File(
    filesDir,
    "maps/garden.mbtiles"
)

if (!targetFile.exists()) {

    copyAssetToFiles(
        this,
        "maps/garden.mbtiles",
        targetFile
    )
}

대용량 파일은 메인 스레드에서 복사하지 않는 것이 좋습니다.

실제 앱에서는 복사 중단, 저장 공간 부족, 파일 손상 등을 고려해 임시 파일에 복사한 뒤 검증 후 교체하는 방식을 권장합니다.


SQLite로 MBTiles 열기

22. 읽기 전용 데이터베이스

MBTiles는 SQLite 기반이므로 Android의 SQLiteDatabase로 열 수 있습니다.

val db = SQLiteDatabase.openDatabase(
    targetFile.absolutePath,
    null,
    SQLiteDatabase.OPEN_READONLY
)

다만 MBTiles는 앱의 일반적인 데이터베이스와 역할이 다릅니다.

따라서 사용자 메모나 설정을 저장하는 SQLite와 지도 타일을 저장하는 MBTiles를 분리하는 것이 좋습니다.


오프라인 장소 데이터

23. 지도 타일과 장소 데이터는 분리하기

오프라인 지도에서는 지도 타일과 장소 정보를 별도로 관리하는 것이 좋습니다.

예를 들어 다음과 같은 구조입니다.

Offline Map
    │
    ├── garden.mbtiles
    │      └── 지도 타일
    │
    └── places.sqlite
           ├── 장소 이름
           ├── 위도·경도
           ├── 카테고리
           └── 상세 설명

이렇게 구성하면 지도 타일을 변경하지 않고도 장소 정보를 갱신할 수 있습니다.


24. SQLite 장소 테이블

CREATE TABLE map_places (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    category TEXT,
    latitude REAL NOT NULL,
    longitude REAL NOT NULL,
    description TEXT
);

장소 검색에는 SQLite의 LIKE나 FTS를 사용할 수 있습니다.

따라서 인터넷이 없는 상태에서도 앱 내부 장소를 검색할 수 있습니다.


GeoJSON과 오프라인 지도

25. GeoJSON으로 장소 표시

MapLibre에서는 GeoJSON을 지도 데이터 소스로 활용할 수 있습니다.

예를 들어 다음과 같은 GeoJSON 파일을 준비할 수 있습니다.

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "정원 입구"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          127.1000,
          37.1000
        ]
      }
    }
  ]
}

GeoJSON의 좌표 순서는 경도, 위도입니다.

MapLibre에서는 GeoJSON 소스와 SymbolLayer 등을 이용해 장소를 표시할 수 있습니다.


오프라인 GPS 경로 표시

26. GPS는 인터넷 없이 사용할 수 있을까?

스마트폰의 GNSS 수신기는 인터넷 연결이 없어도 위성 신호를 이용해 위치를 측정할 수 있습니다.

따라서 오프라인 지도 앱에서도 현재 위치를 표시하거나 GPS 이동 경로를 기록할 수 있습니다.

다만 다음과 같은 차이가 발생할 수 있습니다.

  • 최초 위치 획득 시간이 길어질 수 있음

  • 실내나 숲에서는 정확도가 낮아질 수 있음

  • 보조 위치 정보 이용이 제한될 수 있음

  • 지도 타일이 없으면 위치만 표시되고 배경 지도가 보이지 않을 수 있음

따라서 오프라인 GPS 앱에서는 위치 측정과 지도 데이터 준비를 별도로 검증해야 합니다.


지도 데이터 업데이트

27. 오프라인 지도 업데이트 전략

앱에 지도 데이터를 포함하면 업데이트가 필요할 수 있습니다.

예를 들어 정원에 새로운 시설이 추가되거나 산책로가 변경될 수 있습니다.

이 경우 다음과 같은 업데이트 전략을 고려할 수 있습니다.

방식장점단점
앱 업데이트구현이 단순지도 수정마다 앱 배포
지도 파일 다운로드지도만 교체 가능다운로드 기능 필요
변경 데이터 동기화전송량 감소 가능구현 복잡
GeoJSON만 업데이트장소·경로 수정에 적합기본 지도 변경에는 한계

작은 정원 안내도 앱이라면 지도 타일은 앱에 포함하고 장소 데이터만 별도로 갱신하는 방식도 유용합니다.


28. 지도 파일 버전 관리

지도 파일에는 버전 정보를 관리하는 것이 좋습니다.

예를 들어 다음과 같은 구조입니다.

{
  "mapId": "garden_001",
  "version": 3,
  "fileName": "garden_v3.mbtiles",
  "checksum": "SHA256_VALUE"
}

새로운 지도 파일을 다운로드한 뒤 체크섬을 검증하면 파일 손상 여부를 확인할 수 있습니다.

다운로드가 완료되지 않은 파일로 기존 지도를 덮어쓰지 않도록 해야 합니다.


오프라인 지도 캐싱

29. 캐시와 오프라인 패키지는 다르다

지도 캐시는 한 번 사용한 데이터를 다시 사용할 수 있도록 임시 보관하는 방식입니다.

반면 오프라인 지도 패키지는 특정 지역의 지도 데이터를 사전에 준비해 인터넷 없이 사용할 수 있도록 하는 방식입니다.

두 방식은 목적이 다릅니다.

캐시
 └── 사용 중 가져온 데이터 재사용

오프라인 패키지
 └── 필요한 지역 데이터를 미리 준비

완전한 오프라인 기능이 필요하다면 캐시만으로 해결하려고 하지 않는 것이 좋습니다.


오프라인 지도 크기 최적화

30. 확대 수준과 파일 크기

지도 파일의 크기는 지역 범위와 확대 수준에 크게 영향을 받습니다.

일반적인 타일 지도에서는 확대 수준이 한 단계 증가할 때 같은 지역을 덮는 데 필요한 타일 수가 대략 네 배로 늘어날 수 있습니다.

따라서 모든 확대 수준을 포함하면 파일 크기가 빠르게 증가할 수 있습니다.

작은 정원 안내도라면 실제로 필요한 확대 수준만 포함하는 것이 좋습니다.


QGIS와 오프라인 지도 제작

31. QGIS에서 지도 데이터 준비

QGIS에서는 다음과 같은 작업을 수행할 수 있습니다.

  • 장소 좌표 편집

  • 산책로 LineString 작성

  • 구역 Polygon 작성

  • 좌표계 변환

  • GeoJSON 내보내기

  • 정사영상 확인

  • 지도 스타일 구성

다만 QGIS에서 GeoJSON을 내보내는 것과 MapLibre용 MBTiles를 만드는 것은 별개의 작업입니다.

MBTiles 제작에는 타일 생성 도구나 별도의 변환 과정이 필요할 수 있습니다.


WebODM 정사영상 활용

32. 드론 정사영상을 오프라인 지도로 사용하기

드론 사진으로 만든 정사영상을 활용하면 특정 지역의 고해상도 오프라인 안내도를 제작할 수 있습니다.

예를 들어 다음과 같은 흐름입니다.

드론 촬영
    │
    ▼
WebODM 정사영상 생성
    │
    ▼
QGIS에서 확인
    │
    ▼
좌표계 및 범위 확인
    │
    ▼
래스터 타일 생성
    │
    ▼
MBTiles 또는 타일 패키지
    │
    ▼
Android MapLibre 표시

이 방식에서는 정사영상의 지리 참조 정보가 중요합니다.

드론 사진의 GPS 오차가 크거나 정사영상의 위치가 부정확하면 Android 지도에서 시설 위치가 어긋날 수 있습니다.


정원 안내도 앱 설계

33. 권장 구조

정원 안내도 앱을 만든다고 가정하면 다음과 같은 구조를 고려할 수 있습니다.

Android App
    │
    ├── MapLibre MapView
    │
    ├── Offline Map Package
    │      └── MBTiles
    │
    ├── Place Repository
    │      └── SQLite
    │
    ├── Route Repository
    │      └── GeoJSON / SQLite
    │
    └── Location Repository
           └── FusedLocationProviderClient

MapLibre는 지도 표시를 담당합니다.

SQLite는 장소 정보를 저장합니다.

GPS는 현재 위치와 이동 경로를 수집합니다.

이렇게 역할을 분리하면 유지보수가 쉬워집니다.


오프라인 지도 성능

34. 성능 최적화 방법

오프라인 지도에서는 다음 사항을 고려하는 것이 좋습니다.

  1. 필요한 지역만 포함

  2. 확대 수준 제한

  3. 타일 형식 선택

  4. 지도 파일 압축 및 크기 관리

  5. SQLite 읽기 전용 접근

  6. 대량 장소 데이터 분할

  7. 지도 화면 범위 기반 데이터 조회

  8. GeoJSON 데이터 크기 제한

  9. 메인 스레드 파일 I/O 방지

  10. 지도 리소스 생명주기 관리

특히 대용량 MBTiles 파일을 앱 실행 시마다 복사하거나 검증하면 시작 속도가 느려질 수 있습니다.

따라서 최초 설치와 업데이트 시점에만 필요한 작업을 수행하도록 설계하는 것이 좋습니다.


오프라인 지도 보안과 라이선스

35. 지도 데이터 사용 권한

오프라인 지도 앱에서는 지도 엔진의 라이선스뿐 아니라 지도 데이터의 라이선스도 확인해야 합니다.

예를 들어 OpenStreetMap 데이터를 사용하는 경우 ODbL 조건을 검토해야 합니다.

또한 타일 서버 운영자가 제공하는 타일 이미지를 임의로 대량 다운로드하거나 재배포하는 행위는 별도 이용 약관에 의해 제한될 수 있습니다.

따라서 다음 항목을 구분해서 확인해야 합니다.

  • 지도 렌더링 엔진 라이선스

  • 원본 지도 데이터 라이선스

  • 타일 제공 서비스 약관

  • 지도 데이터 재배포 가능 여부

  • 출처 표시 의무


오프라인 지도 테스트

36. 테스트 시나리오

오프라인 지도 기능은 실제로 인터넷 연결을 차단한 상태에서 테스트해야 합니다.

대표적인 테스트 항목은 다음과 같습니다.

  • 최초 지도 파일 준비

  • 지도 파일 복사

  • MBTiles 파일 열기

  • 타일 조회

  • XYZ/TMS 좌표 변환

  • 지도 스타일 로딩

  • 비행기 모드에서 지도 표시

  • GPS 현재 위치 표시

  • 오프라인 장소 검색

  • GeoJSON 경로 표시

  • 지도 확대·축소

  • 지도 이동

  • 파일 손상 처리

  • 저장 공간 부족 처리

  • 지도 업데이트

  • 구버전 지도 교체

  • 대용량 지도 성능

  • Android 13~16 기기 테스트

  • 지도 데이터 라이선스 확인


37. 마무리

Android 오프라인 지도는 단순히 온라인 지도를 캐시에 저장하는 방식으로 해결하기 어렵습니다.

완전한 오프라인 기능이 필요하다면 지도 엔진, 지도 데이터 형식, 저장 방식, 업데이트 구조를 함께 설계해야 합니다.

이번 글의 핵심을 정리하면 다음과 같습니다.

  1. Google Maps SDK는 범용 오프라인 지도 패키징 기능을 제공하지 않습니다.

  2. MapLibre는 자체 지도 데이터와 스타일을 활용할 수 있는 오픈소스 지도 엔진입니다.

  3. MBTiles는 지도 타일을 SQLite 파일에 저장하는 형식입니다.

  4. XYZ와 TMS의 Y 좌표 차이를 고려해야 합니다.

  5. MapLibre에서 MBTiles를 사용하려면 적절한 타일 제공 계층이 필요합니다.

  6. 지도 타일과 장소 데이터는 분리해서 관리하는 것이 좋습니다.

  7. GeoJSON으로 장소와 경로를 관리할 수 있습니다.

  8. 인터넷 없이도 GPS 위치 측정이 가능할 수 있습니다.

  9. QGIS와 WebODM을 이용해 특정 지역의 오프라인 지도 데이터를 준비할 수 있습니다.

  10. 오프라인 지도에서는 파일 크기, 업데이트, 라이선스를 함께 고려해야 합니다.

특히 정원이나 관광지 안내도처럼 제한된 지역을 대상으로 한다면 처음부터 전국 지도를 제공할 필요는 없습니다.

필요한 지역의 지도 데이터를 준비하고, 장소와 경로를 SQLite와 GeoJSON으로 관리하는 방식부터 시작하는 것이 효율적입니다.

오프라인 지도 개발의 핵심은 인터넷 없이 지도를 표시하는 것뿐 아니라, 필요한 지도 데이터를 안정적으로 배포하고 갱신할 수 있는 구조를 만드는 것입니다.


참고 자료

태그: Android, Kotlin, 오프라인 지도, MapLibre, MBTiles, 벡터 타일, 래스터 타일, SQLite, GeoJSON, QGIS, WebODM, GPS, 지도 캐싱, 관광지 안내도, 정원 안내도, Android 앱 개발