개발을 준비하고 있는 여행 관련 프로젝트에서 다음과 같은 두 가지 사전 과제를 요구받았다.
- 백그라운드에서 실시간으로 사용자의 위치를 받아올 것
- 지도에 사용자의 위치를 선으로 연결해서 보여줄 것
아직 개발을 본격적으로 시작하지는 않았지만 서비스에 대해 간단한 소개를 하자면,
사용자의 이동 경로를 지도에 표시하여 커뮤니티에 공유할 수 있는 느낌이다.
따라서 위에서 작성한 두 가지가 우리 프로젝트의 핵심 기능이었다.
지도에는 구글, 네이버, 카카오 등의 다양한 SDK를 사용할 수 있지만, 그 중에서 내가 선택한 건 ⭐️네이버 지도⭐️였다.
선택한 이유는 크게 아래의 3가지였다.
1. UI가 직관적이고,
2. 국내에서 많이 사용되며,
3. 네이버 측에서 개발 가이드가 친절하게 작성되어 있었기 때문이다.
실제로 평소에 길을 찾을 때도 네이버 지도를 항상 사용하고 있고, 지도 디자인도 개인적으로 가장 예쁘다고 생각하고 있었던 참에,
실시간으로 위치를 받아와 지도에 표시해주는 건 처음 도전해보는 기능이라 가이드가 잘 나와있는 플랫폼으로 선택하게 되었다. (카카오맵은 코틀린 가이드를 못 찾았다!!)
이번 편에서는 어떻게 네이버 지도를 불러오고, 현재 위치를 표시했는지만 다루도록 하겠다.
우선, 네이버 클라우드(ncloud)에 접속한 뒤 콘솔에 들어간다.
그리고 콘솔 좌측 메뉴에서 'AI·NAVER API > Application' 메뉴를 선택한 다음, 약관 동의 후 Application 등록
을 진행한다. ncloud 서비스를 이용하려면 결제 수단 등록도 필요하다.
Application의 이름을 선택하고 사용할 서비스 유형(여기서는 Maps)를 택한 뒤,
모바일로 개발할 것이므로 Moblie Dynamic Map 선택 후 Android 앱 패키지 이름까지 등록해 준다.
Android에서의 가이드는 네이버 가이드에서 확인할 수 있다.
공식 문서에서는
루트 프로젝트의 build.gradle
에 allproject 안에 코드를 추가하라고 하던데, 나는 어떤 방법을 써도 아래처럼 allprojects에서
only buildscript {}, pluginManagement {} and other plugins {} script blocks are allowed before plugins {} blocks, no other statements are allowed
라는 오류가 떴다.
allprojects를 아예 buildscript로 바꿔보기도 했지만, 그렇게 하니 네이버맵을 사용할 때 implement 표시가 뜨지 않았다. (이걸로 꽤나 삽질을 했다)
📌 allprojects에서 오류가 날 때의 해결 방법
build.gradle 말고setting.gradle
의 dependencyResolutionManagement 안에 아래와 같이 코드를 추가해 준다.dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url 'https://repository.map.naver.com/archive/maven' } } }
모듈 단위의 build.gradle
에서도 네이버 지도 sdk에 대한 의존성을 추가해 준다.
dependencies {
// 네이버 지도 SDK
implementation 'com.naver.maps:map-sdk:3.18.0'
}
클라이언트 ID를 등록하는 방법에는
AndroidManifest.xml에서 설정하는 방법과 API를 호출하는 방법으로 2가지가 있는데,
나는 그 중에서 매니페스트에서 추가하는 쪽으로 택했다.
공식 문서에는 이렇게 과정이 나타나 있지만, 외부에 노출될 것에 대비해 클라이언트 id를 숨기는 과정을 한 번 더 거쳐보자.
우선, Application 탭의 인증 정보에서 Cliend ID를 복사해 준다.
⭐️중요⭐️ 시크릿 키는 노츨되면 안 되니까 아래처럼 local.properties
에 숨겨주자!
naver_client_id=abcdefg
처럼 키를 바로 입력해주면 된다.
다음으로는, 매니페스트 파일에서 local.properties에 저장된 클라이언트 아이디를 가져와 쓸 수 있도록 모듈 단위의 build.gradle
에서 아래 코드를 추가해 준다.
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def naverClientId = properties.getProperty('naver_client_id')
android {
defaultConfig {
// ...
manifestPlaceholders = [NAVER_CLIENT_ID : naverClientId]
}
}
마지막으로, AndroidMenifest.xml
에서의 코드이다.
<meta-data
android:name="com.naver.maps.map.CLIENT_ID"
android:value="${NAVER_CLIENT_ID}" />
build.gradle에서 manifestPlaceholders로 지정해준 그대로 사용해주면 된다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/map_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.naver.maps.map.MapFragment"/>
</androidx.constraintlayout.widget.ConstraintLayout>
나는 별도의 프래그먼트 없이 MainActivity에서 바로 지도를 불러오도록 했다.
class MainActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var binding: ActivityMainBinding
private lateinit var naverMap: NaverMap
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationSource: FusedLocationSource
private var locationService: LocationService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
if (!hasPermission()) {
requestLocationPermission() // 권한 요청
} else {
initMapView() // 맵뷰 초기화
}
}
// 위치 권한이 있을 경우 true, 없을 경우 false 반환
private fun hasPermission(): Boolean {
for (permission in PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
// 위치 권한 요청
private fun requestLocationPermission() {
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_CODE_LOCATION_PERMISSION
)
}
private fun initMapView() {
val fm = supportFragmentManager
val mapFragment = fm.findFragmentById(R.id.map_fragment) as MapFragment?
?: MapFragment.newInstance().also {
fm.beginTransaction().add(R.id.map_fragment, it).commit()
}
// fragment의 getMapAsync() 메서드로 OnMapReadyCallback 콜백을 등록하면 비동기로 NaverMap 객체를 얻을 수 있다.
mapFragment.getMapAsync(this)
locationSource = FusedLocationSource(this, REQUEST_CODE_LOCATION_PERMISSION)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_LOCATION_PERMISSION && grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLocationService()
} else {
Toast.makeText(this, "Permission denied!", Toast.LENGTH_SHORT).show()
}
}
}
override fun onMapReady(naverMap: NaverMap) {
this.naverMap = naverMap
// 내장 위치 추적 기능
naverMap.locationSource = locationSource
// 현재 위치 버튼 기능
naverMap.uiSettings.isLocationButtonEnabled = true
// 위치를 추적하면서 카메라도 따라 움직인다.
naverMap.locationTrackingMode = LocationTrackingMode.Follow
// 사용자 현재 위치 받아오기
var currentLocation: Location?
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
fusedLocationClient.lastLocation
.addOnSuccessListener { location: Location? ->
currentLocation = location
// 위치 오버레이의 가시성은 기본적으로 false로 지정되어 있습니다. 가시성을 true로 변경하면 지도에 위치 오버레이가 나타납니다.
// 파랑색 점으로 현재 위치 표시
naverMap.locationOverlay.run {
isVisible = true
position = LatLng(currentLocation!!.latitude, currentLocation!!.longitude)
}
// 카메라 현재위치로 이동
val cameraUpdate = CameraUpdate.scrollTo(
LatLng(
currentLocation!!.latitude,
currentLocation!!.longitude
)
)
naverMap.moveCamera(cameraUpdate)
}
}
companion object {
private const val REQUEST_CODE_LOCATION_PERMISSION = 1
private val PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
}
}
현재 위치를 나타내기 위해서는 권한을 요청해야 하기 때문에 ACCESS_FINE_LOCATION
와 ACCESS_COARSE_LOCATION
권한을 companion object안에 PERMISSONS
로 묶어 준다.
hasPermission을 통해 위치 권한이 허용됐는지 여부를 체크하고, 허용되었다면 맵뷰를 초기화 해준다.
// 내장 위치 추적 기능
naverMap.locationSource = locationSource
// 현재 위치 버튼 기능
naverMap.uiSettings.isLocationButtonEnabled = true
// 위치를 추적하면서 카메라도 따라 움직인다.
naverMap.locationTrackingMode = LocationTrackingMode.Follow
// 사용자 현재 위치 받아오기
var currentLocation: Location?
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
fusedLocationClient.lastLocation
.addOnSuccessListener { location: Location? ->
currentLocation = location
// 위치 오버레이의 가시성은 기본적으로 false로 지정되어 있습니다. 가시성을 true로 변경하면 지도에 위치 오버레이가 나타납니다.
// 파랑색 점으로 현재 위치 표시
naverMap.locationOverlay.run {
isVisible = true
position = LatLng(currentLocation!!.latitude, currentLocation!!.longitude)
}
// 카메라 현재위치로 이동
val cameraUpdate = CameraUpdate.scrollTo(
LatLng(
currentLocation!!.latitude,
currentLocation!!.longitude
)
)
naverMap.moveCamera(cameraUpdate)
}
해당 코드를 통해 현재 위치를 표시해 준다.
위처럼 파란색 점으로 현재 위치가 표시되고, 지도도 내 위치로 포커스되는 모습을 확인할 수 있다!
다음 편에서는 사용자의 이동 경로를 선으로 연결해주고, 핀으로 표시해 주는 콘텐츠를 들고 오도록 하겠다.