[Android] Compose NaverMap 적용 방법 (MapType, Layer 변경)
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.kts에 naver-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 파일에 저장하여 빌드 시 참조하도록 설정합니다.
- NCP 콘솔에서 Mobile Dynamic Map 서비스를 추가하고, 클라이언트 ID를 발급받습니다.
- 발급받은 클라이언트 ID를 local.properties에 추가합니다.
- NCP 콘솔에서 Application 추가 시 프로젝트의 정확한 packageName을 입력해야 인증 오류가 발생하지 않습니다.
local.properties에 naver.client.id 값은 본인이 실제로 발급받은 클라이언트 ID를 입력합니다.
이후 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가 변경될 수 있도록 합니다.
- 맵 타입 변경
- 라디오 버튼을 사용하여 일반 지도, 위성 지도, 지형도를 선택할 수 있습니다.
- 레이어 선택
- 체크박스를 통해 건물, 대중교통, 자전거, 교통정보, 지적편집도, 등산로를 활성화하거나 비활성화할 수 있습니다.
@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를 참고하여 개발을 진행해 보시기 바랍니다.
긴 글 읽어주셔서 감사합니다!