관광 앱 지도 마커 성능 최적화 및 데이터 설계 방법 (클러스터링, 위치 권한 Fallback)

관광 관련 앱이나 공공데이터를 활용한 서비스를 개발하다 보면 필수적으로 들어가는 기능이 바로 지도 화면입니다. 처음에는 장소 데이터가 몇 개 없어서 그냥 좌표 받아오는 대로 지도에 마커를 다 찍어도 잘 돌아가는 것처럼 보입니다.

하지만 관광지, 음식점, 숙소, 축제 정보까지 데이터가 수백, 수천 개로 늘어나면 이야기가 달라집니다. 마커들이 서로 겹쳐서 뭐가 뭔지 구분도 안 되고, 지도를 조금만 움직여도 화면이 뚝뚝 끊기는 현상이 발생하죠. 게다가 위치 권한을 꺼둔 사용자 대응이나 좌표 데이터 보안 문제까지 얽히면 머리가 아파집니다.

저의 경우도 예전에 공공데이터 기반으로 지도 화면을 대충 구현했다가, 데이터가 쌓이면서 성능이 감당 안 돼서 구조를 완전히 갈아엎은 적이 있습니다. 나중에 또 삽질하지 않으려고 실무에서 바로 쓸 수 있는 지도 마커 설계와 최적화 기준을 메모해 둡니다.

1. 지도 데이터는 화면 범위(Viewport)와 확대 수준(Zoom)에 맞추기

지도를 구현할 때 가장 많이 하는 실수가 "모든 데이터를 일단 다 가져와서 그리려고 하는 것"입니다. 핵심은 현재 사용자가 보고 있는 영역과 확대 수준에 맞는 데이터만 처리하는 것입니다. 화면에 보이지도 않는 전국 관광지 좌표를 앱 메모리에 올려둘 이유가 전혀 없습니다.

운영하면서 정립한 확대 수준별 표시 기준은 아래와 같습니다.

상황표시 기준추천 처리 방식
전국/도 단위 축소장소 개별 표시보다 개수 중심클러스터(묶음) 표시
시/군/구 단위주요 카테고리 중심클러스터 + 대표 마커
동/읍/면 단위 확대개별 장소 상세 표시개별 마커 표시
검색 결과 지도검색 결과 내 데이터만 제한필터 적용 후 마커 생성
상세 화면 지도단일 장소 표시단일 마커 + 길찾기 버튼

실무 한 줄 팁: 지도 화면은 데이터를 '얼마나 많이 가져오느냐'보다, 카메라가 움직였을 때 '언제, 어떤 타이밍에 데이터를 갱신할 것인가'를 제어하는 게 훨씬 중요합니다.

2. 마커 데이터 모델은 무조건 가볍게 (메모리 관리)

처음 개발할 때 귀찮다고 장소의 상세 설명, 이미지 리스트, HTML 태그까지 통째로 마커 객체에 넣어버리는 경우가 있습니다. 이렇게 하면 마커가 수백 개만 넘어가도 앱이 메모리 부족으로 뻗어버립니다.

마커 모델에는 딱 지도에 아이콘을 그리고, 선택했을 때 구별할 수 있는 최소한의 정보만 담아야 합니다.

Kotlin
data class TourMapMarker(
    val placeId: String,       // 장소 식별자
    val name: String,          // 마커 선택 시 보여줄 이름
    val categoryCode: String,  // 카테고리 (관광, 음식, 숙소 등)
    val latitude: Double,      // 위도
    val longitude: Double,     // 경도
    val markerType: MarkerType,
)

enum class MarkerType {
    TOUR, FOOD, STAY, FESTIVAL
}

상세한 소개 글이나 이미지 정보는 사용자가 마커를 '클릭'했을 때, 그 placeId를 가지고 별도의 로컬 DB나 서버 API에서 필요한 것만 그때그때 땡겨오는 구조가 가장 깔끔합니다.

3. 지도 상태(State)와 Repository 구조 잡기

지도 화면은 사용자의 현재 위치, 카메라 좌표, 확대 레벨, 필터 조건 등이 복잡하게 얽혀서 돌아가는 이벤트 덩어리입니다. 변수들을 여기저기 흩어놓으면 디버깅할 때 지옥을 맛보게 되니, 단일 상태 모델로 묶어 관리하는 게 편합니다.

Kotlin
data class TourMapState(
    val cameraLatitude: Double,
    val cameraLongitude: Double,
    val zoomLevel: Float,
    val selectedCategoryCode: String? = null,
    val selectedPlaceId: String? = null,
    val userLocationAvailable: Boolean = false,
)

아래는 지도 범위(MapBounds)와 확대 수준에 따라 서버 데이터(클러스터링용)를 가져올지, 로컬 DB(개별 마커용)에서 빠르게 긁어올지 분기하는 Repository 예시 코드입니다.

Kotlin
data class MapBounds(
    val south: Double, val west: Double, val north: Double, val east: Double
)

class TourMapRepository(
    private val localDataSource: TourMapLocalDataSource,
    private val remoteDataSource: TourMapRemoteDataSource,
) {
    suspend fun getMarkers(
        bounds: MapBounds,
        zoomLevel: Float,
        categoryCode: String?,
    ): List<TourMapMarker> {
        // 지도가 너무 축소되어 있으면 서버에서 묶음(클러스터) 처리된 데이터를 가져옴
        val markers = if (requiresRemoteMarkers(zoomLevel)) {
            remoteDataSource.fetchMarkers(bounds, categoryCode)
        } else {
            // 확대되어 있으면 로컬에서 빠르게 개별 좌표 조회
            localDataSource.findMarkers(bounds, categoryCode)
        }

        return markers
            .filter { it.hasValidCoordinate() } // 비정상 좌표 필터링
            .distinctBy { it.placeId }          // 중복 마커 제거
    }

    private fun requiresRemoteMarkers(zoomLevel: Float): Boolean {
        return zoomLevel < 10f // 줌 레벨이 10 미만으로 낮을 때
    }

    private fun TourMapMarker.hasValidCoordinate(): Boolean {
        return latitude in -90.0..90.0 && longitude in -180.0..180.0
    }
}

초기 단계에서는 클라이언트 단에서 라이브러리로 클러스터링(Client-side clustering)을 처리해도 되지만, 데이터가 수천 개를 넘어가면 서버에서 아예 구역별로 개수를 계산해서 내려주는 Server-side clustering을 고려하셔야 합니다.

4. 위치 권한 거부(Fallback) 처리와 마커 UX

은근히 많은 앱들이 사용자가 위치 권한을 거부했을 때 처리 로직을 빠뜨려서 화면이 허옇게 나오거나 에러가 터집니다. 위치 권한은 '필수'가 아니라 기능을 편리하게 돕는 '옵션'으로 접근해야 합니다.

  • 위치 권한 허용 시: 사용자 현재 위치를 중심으로 지도를 보여주고, 주변 장소 거리순 정렬을 활성화합니다.

  • 위치 권한 거부 시: 앱의 메인 타깃 지역(예: 제주 중심가, 서울시청 등) 또는 사용자가 마지막으로 조회했던 지역을 기본 시작점으로 세팅합니다. "내 위치 기준" 버튼은 비활성화하거나 권한 유도 팝업을 띄워줍니다.

마커 아이콘 디자인도 카테고리가 많다고 수십 가지 색상을 다 쓰면 가독성이 완전히 무너집니다. 관광/음식/숙소/축제 같은 큰 틀의 대분류 위주로 3~4개 색상만 지정하고, 선택된 마커만 크기를 키우거나 테두리를 굵게 강조하는 방식이 사용자 입장에서 훨씬 보기 편합니다.

5. 실무 운영 시 필수 체크리스트 (성능 및 보안)

앱을 배포하기 전에 아래 항목들은 무조건 코드를 다시 확인해 보는 것이 좋습니다.

  • Debounce 적용 여부: 지도를 드래그할 때 매 프레임마다 API 요청을 때리면 서버도 죽고 지도 SDK 비용도 폭탄 맞습니다. 카메라 이동이 완전히 멈춘 시점(idle)에만 요청을 보내도록 제한해야 합니다.

  • 과도한 Bitmap 생성 방지: 마커 아이콘을 그릴 때 루프 안에서 매번 이미지 비트맵을 새로 생성하고 있지 않은지 확인하세요. 리소스 캐싱은 필수입니다.

  • 좌표 데이터 보안: 디버깅 편하자고 로그(Logcat 등)에 사용자 현재 위치 좌표를 그대로 찍어 누르는 짓은 절대 금물입니다. 로그에는 시/도 단위의 대략적인 코드만 남기거나 아예 마스킹 처리를 해야 개인정보 보호 정책에 걸리지 않습니다.

  • API Key 관리: 블로그에 삽질 기록 올리거나 GitHub에 푸시할 때 AndroidManifest.xml이나 소스코드에 지도 API Key가 생으로 노출되어 있는지 눈 불을 켜고 찾아보세요. 잘못하면 남이 내 키로 지도 API를 긁어 써서 요금 폭탄을 맞을 수 있습니다.

요약

관광 앱에서 지도는 화면에 좌표를 '그리는' 것보다, 필요 없는 데이터를 어떻게 '덜어낼 것인가'의 싸움입니다.

처음부터 완벽하게 만들기는 어렵겠지만, 최소한 1) 마커 데이터 모델 가볍게 유지하기, 2) 지도 줌 레벨/범위 기반으로 데이터 필터링하기, 3) 위치 권한 거부 대응(Fallback) 마련하기 이 세 가지만 제대로 뼈대를 잡아두면, 나중에 장소 데이터가 아무리 늘어나도 큰 공사 없이 안정적으로 서비스를 운영할 수 있습니다.

댓글

이 블로그의 인기 게시물

안드로이드 화면 꺼짐 방지 FLAG_KEEP_SCREEN_ON 및 WakeLock 적용 방법

안드로이드 백그라운드 타이머 구현 및 Foreground Service 알림 설계 정리

관광 앱 다국어 데이터 모델 및 검색 색인 설계 (자꾸 까먹어서 정리)