네이버 지도 연동, 다음 지도 연동 등 많이 해봤지만 다 xml 위에 해봤었지
이번엔 Jetpack Compose 로 개발하는 앱에 네이버 지도 연동해보려 한다.
1. 네이버 클라우드 플랫폼의 지도에 어플리케이션 등록한다
(1) Menu - Service - Maps 들어간다(2) Application 등록한다 나는 Dynamic Map, GeoCoding, Reverse Geocoding 선택함.
Android 앱 패키지 이름도 등록한 후에 확인 누르면 끝
▼ 공식 문서 참고하였음
시작하기 · 네이버 지도 안드로이드 SDK
2. Gradle 의존성 추가
아래 코드를 libs.versions.toml 에 추가해준다
[versions] naverMap = "3.19.1" [libraries] naver-map-sdk = { group = "com.naver.maps", name = "map-sdk", version.ref = "naverMap" }
app수준의 build.gradle.kts 에 아래 코드 추가
dependencies { // ... 기존 것들 ... // Naver Map implementation("com.naver.maps:map-sdk:3.23.3") }
저장소 추가 필요 — 루트 settings.gradle.kts에 네이버 maven 저장소 등록:
dependencyResolutionManagement { repositories { google() mavenCentral() maven("https://repository.map.naver.com/archive/maven") } }
참고로 공식문서에 따르면 클라이언트 ID 지정할 때 2가지 방식이 있다.
1. AndroidManifest.xml에 지정
2. API를 호출해 지정
나는 API를 호출해 지정하는 방식을 사용해보겠다. ( AndroidManifest를 수정하지 않다고 된다고함)
3. API 키를 안전하게 보관 (하드코딩 금지)
(1) local.properties에 추가 (이 파일은 이미 gitignore 되어있어서 안전)
NCP_KEY_ID=발급받은_클라이언트_ID
(2) app수준의 build.gradle.kts 에서 local.properties의 NCP_KEY_ID값을 읽어서 AndroidManifest.xml 에 주입
defaultConfig { val localProperties = Properties().apply { load(rootProject.file("local.properties").inputStream()) } anifestPlaceholders["NCP_KEY_ID"] = localProperties.getProperty("NCP_KEY_ID") ?: "" buildConfigField("String", "NCP_KEY_ID", "\"${localProperties.getProperty("NCP_KEY_ID")}\"")import도 해줘야함.
4. HereLogApp.kt — 코드로 직접 클라이언트 지정 (핵심)
@HiltAndroidApp class HereLogApp : Application() { override fun onCreate() { super.onCreate() NaverMapSdk.getInstance(this).client = NaverMapSdk.NcpKeyClient(BuildConfig.NCP_KEY_ID) } }
5. 지도 컴포넌트 만들기
NaverMapView.kt 파일 생성하여 아래와 같이 코드작성
package com.psg.herelog.ui.main import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner import com.naver.maps.map.MapView import com.naver.maps.map.NaverMap @Composable fun NaverMapView( modifier: Modifier = Modifier, onMapReady: (NaverMap) -> Unit = {} ){ val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val mapView = remember { MapView(context) } // 아래에서 context 수정 예쩡 // MapView는 Activity/Fragment 생명주기에 맞춰 onCreate/onResume/onPause/onDestroy를 직접 호출해줘야 함 DisposableEffect(lifecycleOwner) { val lifecycleObserver = LifecycleEventObserver{ _, event -> when (event){ Lifecycle.Event.ON_CREATE -> mapView.onCreate(null) Lifecycle.Event.ON_START -> mapView.onStart() Lifecycle.Event.ON_RESUME -> mapView.onResume() Lifecycle.Event.ON_PAUSE -> mapView.onPause() Lifecycle.Event.ON_STOP -> mapView.onStop() Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() else -> {} } } lifecycleOwner.lifecycle.addObserver(lifecycleObserver) onDispose { lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) } } AndroidView( modifier = modifier, factory = { mapView.apply { onCreate(null) getMapAsync { naverMap -> onMapReady(naverMap) } } } ) }
MainScreen.kt 파일에서 naver지도 넣기
// 2. 순수 UI 담당 (Preview 기능) @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainScreenContent( uiState: MainUiState, onSearchQueryChange: (String) -> Unit, onFilterClick: () -> Unit, onFilterDismiss: () -> Unit ){ Box(modifier = Modifier.fillMaxSize()){ NaverMapView( modifier = Modifier.fillMaxSize(), onMapReady = {naverMap -> } ) Column( modifier = Modifier .padding(16.dp) ) { } // 상단 검색창 + 필터 버튼 Row( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { OutlinedTextField( value = uiState.searchQuery, onValueChange = onSearchQueryChange, placeholder = {Text("상호명, 지번으로 검색")}, leadingIcon = {Icon(Icons.Default.Search, contentDescription = "검색")}, singleLine = true, modifier = Modifier .weight(1f) .background(MaterialTheme.colorScheme.surface, RoundedCornerShape(12.dp)) ) Spacer(modifier = Modifier.height(8.dp)) IconButton( onClick = onFilterClick, modifier = Modifier.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(12.dp)) ) { Icon(Icons.Default.Build, contentDescription = "필터") } } } // 필터 바텀시트 if(uiState.showFilterSheet){ ModalBottomSheet(onDismissRequest = onFilterDismiss) { Column(modifier = Modifier.padding(24.dp)) { Text("필터", style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.height(16.dp)) Text("카테고리, 평점 등 필터 옵션 (추후 구현)") Spacer(modifier = Modifier.height(24.dp)) } } } }
- 인증을 Manifest meta-data가 아니라 HereLogApp.onCreate()에서 코드로 직접 설정하는 방식 사용
- BuildConfig.NCP_KEY_ID로 안전하게 값 전달 (하드코딩 없음) - 보안 굿
- 변수명을 NCP_KEY_ID 하나로 통일해서 local.properties ↔ build.gradle.kts ↔ 코드 전부 일치시킴
▼ 최종적으로 잘 뜸을 볼 수 있다

사실 처음엔 익숙한 AndroidManifest.xml 에 meta써서 작성하는 방식을 썼다가 401에러가 자꾸 떠서.
직접 api호출하느나 식으로 변경해보았다. (처음 써봄)
이게 더 나은 것 같기도 하다. 잘 떠서 다행이오
다들 좋은 수요일되십쇼




