개발/Android

[Android] Compose NaverMap 적용 방법 (MapType, Layer 변경)

wom-bat 2024. 12. 2. 20:43

Compose 기반 UI 개발이 주류로 자리 잡으면서, 기존의 View 클래스 기반으로 설계된 API는 AndroidView를 사용해 Compose 환경에서 활용할 수 있었습니다. 하지만 이 접근법은 제약이 많아 완전한 Compose 개발의 장점을 누리기 어렵다는 한계가 있었습니다.

 

Compose에 대한 지원이 확대되면서 네이버 지도 SDK에서도 naver-map-compose를 제공하여 Compose 환경에 적합한 지도 구현이 가능해졌습니다. 이번 포스팅에서는 naver-map-compose를 활용하여 Android Compose 프로젝트에서 지도를 표시하고, 맵 타입과 레이어를 동적으로 변경하는 방법을 다뤄보겠습니다.

 

 

환경 설정

안드로이드 스튜디오에서 프로젝트를 생성할 때, Compose 기반의 Empty Activity 템플릿을 선택합니다.

 

naver-map-compose는 내부적으로 네이버 지도 SDK를 사용하므로, 프로젝트의 settings.gradle.kts에 Maven 저장소 설정을 추가해야 합니다.

pluginManagement {
    repositories {
        google {
            content {
                includeGroupByRegex("com\\.android.*")
                includeGroupByRegex("com\\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
        maven("https://repository.map.naver.com/archive/maven") //추가
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven("https://repository.map.naver.com/archive/maven") //추가
    }
}

rootProject.name = "ComposeMapExample"
include(":app")

 

또한, app 모듈의 build.gradle.ktsnaver-map-compose와 위치 추적 기능을 위한 naver-map-location 의존성을 추가해 줍니다. 

 

libs.versions.toml

[version]
...
# naver Map
naver-map = "3.18.0"
naver-map-compose = "1.7.2"
naver-map-location = "21.0.1"
play-services-location = "21.0.1"
...
[libraries]
...
# naver map
naver-map = { group = "com.naver.maps", name = "map-sdk", version.ref = "naver-map" }
naver-map-compose = { group = "io.github.fornewid", name = "naver-map-compose", version.ref = "naver-map-compose" }
naver-map-location = { group = "io.github.fornewid", name = "naver-map-location", version.ref = "naver-map-location" }

# play services location
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "play-services-location" }
...

 

build.gradle.ktx (:app)

dependencies {
	...
    // naver map
    implementation(libs.naver.map)
    implementation(libs.naver.map.compose)
    implementation(libs.naver.map.location)
    implementation(libs.play.services.location)
  	...
}

 

 

네이버 맵 클라이언트 ID 설정

naver map client id를 소스에 하드코딩하면 보안상 위험하므로, local.properties 파일에 저장하여 빌드 시 참조하도록 설정합니다.

 

  1. NCP 콘솔에서 Mobile Dynamic Map 서비스를 추가하고, 클라이언트 ID를 발급받습니다.
  2. 발급받은 클라이언트 ID를 local.properties에 추가합니다.
  3. NCP 콘솔에서 Application 추가 시 프로젝트의 정확한 packageName을 입력해야 인증 오류가 발생하지 않습니다.

Naver Cloud Platform Console

 

local.properties에 naver.client.id 값은 본인이 실제로 발급받은 클라이언트 ID를 입력합니다.

local.properties

 

이후 build.gradle.kts에 프로퍼티를 읽는 함수를 정의하고 defaultConfig에서 이를 사용해 빌드 시 클라이언트 ID를 설정합니다.

android {
    namespace = "com.wombat.composemapexample"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.wombat.composemapexample"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

        // 빌드 시 client id 설정
        buildConfigField("String", "NAVER_MAP_CLIENT_ID", getClientId("naver.client.id"))

    }

    ...
    buildFeatures {
        compose = true
        buildConfig = true // buildConfig true로 설정
    }
}

// property를 읽는 함수 정의
fun getClientId(propertyKey: String): String {
    return gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
}

 

Application 클래스 초기화

Application 클래스를 정의하고, onCreate 시점에 NaverMapSdk의 Client 객체를 초기화합니다. 이를 통해 네이버 지도를 사용할 준비를 완료합니다.

import android.app.Application
import com.naver.maps.map.NaverMapSdk
import com.wombat.composemapexample.BuildConfig

class MapExampleApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        NaverMapSdk.getInstance(this).client = NaverMapSdk.NaverCloudPlatformClient(BuildConfig.NAVER_MAP_CLIENT_ID)
    }
}

 

지도표시

MainActivity에서 NaverMap 컴포즈 함수를 선언하여 지도를 화면에 표시합니다.
이 단계까지 진행하면 기본적인 네이버 지도를 확인할 수 있습니다.

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeMapExampleTheme {
                MapScreen()
            }
        }
    }
}

@OptIn(ExperimentalNaverMapApi::class)
@Composable
fun MapScreen() {
    NaverMap(
        modifier = Modifier.fillMaxSize(),
    )
}

 

이제 앱을 빌드하고 실행하면 Compose로 구성된 네이버 지도를 확인할 수 있습니다.

 

네이버 지도

 

 

맵 타입 및 레이어 변경 기능

MapScreen 구성

맵의 타입과 레이어를 변경할 수 있도록, MapScreen에서 mapProperties를 관리합니다.
mapProperties는 변경될 때마다 NaverMap이 recomposition 될 수 있도록 remember를 사용해 정의합니다.

@OptIn(ExperimentalNaverMapApi::class)
@Composable
fun MapScreen() {
    var mapProperties by remember {
        mutableStateOf(
            MapProperties(
                mapType = MapType.Basic,
                isBuildingLayerGroupEnabled = false,
                isTransitLayerGroupEnabled = false,
                isBicycleLayerGroupEnabled = false,
                isTrafficLayerGroupEnabled = false,
                isCadastralLayerGroupEnabled = false,
                isMountainLayerGroupEnabled = false
            )
        )
    }
    Box(modifier = Modifier.fillMaxSize()) {
        NaverMap(
            modifier = Modifier.fillMaxSize(),
            properties = mapProperties
        )
        MapTypeButton(
            modifier = Modifier
                .align(Alignment.TopEnd)
                .padding(vertical = 32.dp, horizontal = 16.dp),
            onMapPropertiesChange = { newProperties ->
                mapProperties = newProperties
            }
        )
    }
}

 

MapType과 Layer들을 변경할 수 있는 바텀 시트를 열고 닫을 수 있도록 활용하여 MapTypeButton을 정의해 줍니다.

@Composable
fun MapTypeButton(
    modifier: Modifier,
    onMapPropertiesChange: (MapProperties) -> Unit
) {
    var showBottomSheet by remember { mutableStateOf(false) }
    IconButton(
        modifier = modifier,
        onClick = {
            showBottomSheet = true
        }
    ) {
        Icon(
            imageVector = Icons.Filled.Menu,
            contentDescription = "Layer"
        )
    }
    if (showBottomSheet) {
        MapTypeBottomSheet(
            onDismiss = {
                showBottomSheet = false
            },
            onPropertiesChange = onMapPropertiesChange
        )
    }
}

 

 

바텀 시트에서 라디오 버튼으로 MapType을 변경할 수 있도록 하고, 체크박스로 Layer를 선택할 수 있도록 Compose 함수를 작성하고 변경되면 람다 콜백으로 NaverMap의 properties가 변경될 수 있도록 합니다.

  1. 맵 타입 변경
    • 라디오 버튼을 사용하여 일반 지도, 위성 지도, 지형도를 선택할 수 있습니다.
  2. 레이어 선택
    • 체크박스를 통해 건물, 대중교통, 자전거, 교통정보, 지적편집도, 등산로를 활성화하거나 비활성화할 수 있습니다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapTypeBottomSheet(
    onDismiss: () -> Unit,
    onPropertiesChange: (MapProperties) -> Unit
) {
    val sheetState = rememberModalBottomSheetState()

    var selectMapType by remember { mutableIntStateOf(0) }
    val mapTypeName = mapOf(0 to "일반 지도", 1 to "위성 지도", 2 to "지형도")
    val checkLayers = remember { mutableStateListOf(false, false, false, false, false, false) }
    var changeCounter by remember { mutableIntStateOf(0) }
    val layersName =
        mapOf(0 to "건물", 1 to "대중교통", 2 to "자전거", 3 to "교통정보", 4 to "지적편집도", 5 to "등산로")

    ModalBottomSheet(
        modifier = Modifier.fillMaxHeight(0.6f),
        onDismissRequest = {
            onDismiss()
        },
        containerColor = Color.White,
        sheetState = sheetState
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                Row {
                    for (i in 0..2) {
                        Row(verticalAlignment = Alignment.CenterVertically) {
                            RadioButton(
                                selected = selectMapType == i,
                                onClick = { selectMapType = i }
                            )
                            Text("${mapTypeName[i]}")
                        }
                    }
                }
                for (i in 0..5) {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        Checkbox(
                            checked = checkLayers[i],
                            onCheckedChange = { isChecked ->
                                checkLayers[i] = isChecked
                                changeCounter++
                            }
                        )
                        Text("${layersName[i]}")
                    }
                }
            }
        }
        LaunchedEffect(selectMapType, changeCounter) {
            onPropertiesChange(
                MapProperties(
                    mapType = when (selectMapType) {
                        0 -> MapType.Basic
                        1 -> MapType.Satellite
                        2 -> MapType.Terrain
                        else -> MapType.Basic
                    },
                    isBuildingLayerGroupEnabled = checkLayers[0],
                    isTransitLayerGroupEnabled = checkLayers[1],
                    isBicycleLayerGroupEnabled = checkLayers[2],
                    isTrafficLayerGroupEnabled = checkLayers[3],
                    isCadastralLayerGroupEnabled = checkLayers[4],
                    isMountainLayerGroupEnabled = checkLayers[5]
                )
            )
        }
    }
}

 

 

이제 바텀 시트를 통해 MapType과 Layer들을 선택하여 지도에 반영할 수 있습니다.

 

 

 

 

이번 포스팅에서는 naver-map-compose를 활용하여 네이버 지도를 Compose 기반으로 표시하고, 지도 타입과 레이어를 동적으로 변경하는 기본적인 기능을 구현해 보았습니다.

naver-map-compose는 지도 타입 및 레이어 설정뿐만 아니라 카메라 제어, 오버레이, 마커 표시 등 다양한 기능을 Compose 환경에서 간편하게 사용할 수 있도록 지원하니 자세한 내용과 추가적인 기능 구현 방법은 공식 GitHub 페이지 naver-map-compose를 참고하여 개발을 진행해 보시기 바랍니다.


긴 글 읽어주셔서 감사합니다!