우아한테크코스 론칭 페스티벌에서 발표 직전, 서비스를 시연하고자 했던 기기에서 앱을 실행하자마자 종료되는 일이 생겼다...
로그를 확인해보니 위치 정보가 null이었다.. 위치 권한을 허용한 사용자에 한해서 현 위치를 추적하도록 구현했는 데 대체 왜 Location이 null이죠..?!?!
그 이후로 위치 설정 및 위치 권한 설정 늪에 빠지게 되었는 데... 🫠
그래서 같은 실수를 반복하지 않고자 스타카토 프로젝트에서 진행했던 위치 설정 및 위치 권한 요청과 관련해 정리해보려고 한다!
왜 Location이 null이었을까?
Location이 null이었던 이유는 위치 설정(GPS)이 off 상태였기 때문이었다.
안드로이드에는 아래와 같이 상태바를 내려보면 위치를 on/off
할 수 있는 버튼이 있다.
위치 설정(GPS)
이 꺼져있다면 앱에서 사용자의 위치를 얻을 수 없다.
즉, 위치 권한을 요청하기 전 위치 설정(GPS) 여부를 확인하는 작업이 선행되어야 한다.
필자는 위치 설정(GPS)
여부를 먼저 확인하지 않고 위치 권한
만 확인했기에, 사용자가 위치 권한을 허용했음에도 Location 값이 null로 반환되었던 것이다.
위치 설정(GPS) 여부는 어떻게 확인할 수 있을까?
1. LocationRequest 객체 정의
우선 LocationRequest
를 만들어야 한다. LocationRequest
란 위치 데이터를 수집할 때 사용하는 클래스이다.
이 클래스는 Google의 위치 서비스 API인 FusedLocationProviderClient
와 함께 사용되며, 사용자가 위치를 업데이트 받는 빈도, 정확성, 우선순위 등을 설정할 수 있다.
가능한 가장 정확한 위치를 요청하기 위해 우선순위는 Priority.PRIORITY_HIGH_ACCURACY로 설정했고, 위치 업데이트 요청 간격은 10초(10000L)
로 설정했다.
fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
// 1. LocationRequest 객체 정의
val locationRequest: LocationRequest = buildLocationRequest()
...
}
private fun buildLocationRequest(): LocationRequest =
// 우선 순위 : Priority.PRIORITY_HIGH_ACCURACY
// 위치 업데이트 요청 간격 : 10000L
LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, INTERVAL_MILLIS)
// 권한에 따른 정밀도 설정
// GRANULARITY_PERMISSION_LEVEL : 앱의 위치 권한 수준에 따라 COARSE 또는 FINE 정밀도를 자동으로 선택
.setGranularity(Granularity.GRANULARITY_PERMISSION_LEVEL)
// 정확한 위치를 기다리도록 설정
.setWaitForAccurateLocation(true)
.build()
companion object {
private const val INTERVAL_MILLIS = 10000L
}
2. 현재 위치 설정 가져오기
사용자 디바이스의 현재 위치 설정 상태를 가져오려면 LocationSettingsRequest.Builder을 생성하고, LocationRequest
를 추가해 줘야 한다.
fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
// 1. LocationRequest 객체 정의
...
// 2. 현재 위치 설정 가져오기
val builder =
LocationSettingsRequest
.Builder()
.addLocationRequest(locationRequest)
}
3. SettingsClient를 사용하여 어떤 설정이 사용되는지 가져오기
SettingsClinet
를 사용하면 어떤 설정이 사용되는 지 확인할 수 있다. settingsClient
의 checkLocationSettings
메서드를 호출하면 위치 설정이 필요한 조건에 맞는지 확인하는 작업(Task<LocationSettingsResponse>
)을 요청한다.
Task<LocationSettingsResponse>
가 완료되면 LocationSettingsResponse
객체의 상태 코드를 확인하여 위치 설정을 확인 할 수 있다.
fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
// 1. LocationRequest 객체 정의
...
// 2. 현재 위치 설정 가져오기
...
// 3. SettingsClient를 사용하여 어떤 설정이 사용되는지 확인
val settingsClient: SettingsClient = LocationServices.getSettingsClient(activity)
// task가 완료되면 LocationSettingsResponse 객체의 상태 코드로 위치 설정을 확인할 수 있음
val locationSettingsResponse: Task<LocationSettingsResponse> =
settingsClient.checkLocationSettings(builder.build())
}
4. 위치 설정 여부에 따라 동작 처리하기
위치 설정 여부를 확인했다 설정 여부에 따라 동작을 처리해야 한다.
위치 설정이 off
상태라면 사용자에게 위치 설정을 변경하라는 메시지를 표시
해야 한다.
startResolutionForResult()
를 호출하여 사용자에게 위치 설정을 수정할 수 있는 권한을 묻는 다이얼로그를 표시할 수 있다.
위치 설정이 on
상태일 때는 위치 권한 설정 여부를 확인하도록 구현했다.
fun checkLocationSetting(actionWhenHavePermission: () -> Unit) {
// 1. LocationRequest 객체 정의
...
// 2. 현재 위치 설정 가져오기
...
// 3. SettingsClient를 사용하여 어떤 설정이 사용되는지 확인
...
// task가 완료되면 LocationSettingsResponse 객체의 상태 코드로 위치 설정을 확인할 수 있음
...
// 4. 위치 설정에 따른 동작 처리
locationSettingsResponse.handleLocationSettings(actionWhenHavePermission, activity)
}
private fun Task<LocationSettingsResponse>.handleLocationSettings(
actionWhenHavePermission: () -> Unit,
activity: Activity,
) {
// 위치 설정 on 상태일 때 처리할 동작
addOnSuccessListener { actionWhenHavePermission() }
// 위치 설정 off 상태일 때 처리할 동작
addOnFailureListener { exception ->
exception.actionWhenHaveNoPermission(activity)
}
}
private fun Exception.actionWhenHaveNoPermission(activity: Activity) {
if (this is ResolvableApiException) {
// 위치 설정 dialog를 띄움
startResolutionForResult(
activity,
REQUEST_CODE_LOCATION,
)
}
}
앞서 위치 설정에 대해 알아보았으니 이제 위치 권한 요청에 대해 알아보자!
🤔 권한 요청은 자주 하는 것이 좋을까?
아니다. 앱 품질을 개선하고 사용자 개인 정보를 보호하기 위해서는 앱의 권한 요청 및 사용을 최소화
하는 것이 좋다.
특히 민감한 권한과 API를 사용하려면 데이터 액세스, 수집, 사용 및 공유에 관한 인앱 정보를 공개해야 한다.
🙅♂️ 사용자가 권한 요청을 거부했다면?
사용자가 권한 요청을 거부했다면 앱은 권한이 없어서 작동하지 않는 기능을 사용자가 이해하도록 지원해야 한다.
어떻게 지원할 수 있을까?
앱에 대략적인 위치만 필요한 경우 정확한 위치 정보(ACCESS_FINE_LOCATION)
를 제외하고 대략적인 위치 정보(ACCESS_COARSE_LOCATION)
만을 요청하는 것이 좋다.
대략적인 위치(ACCESS_COARSE_LOCATION)란?
정확한 위치(ACCESS_FINE_LOCATION)란?
LocationManagerService
또는 FusedLocationProvider
에서 가져온 경우 이 위치 추정치의 오차 범위는 일반적으로 약 50미터 이내이며 몇 미터 이내 또는 그 이상으로 정확할 때도 있다.ACCESS_FINE_LOCATION
권한을 선언하면 앱이 이 수준의 정확도로 위치를 수신할 수 있다.ACCESS_FINE_LOCATION
권한을 선언해도 되는 유일한 경우다.아래 1️⃣~4️⃣까지의 시나리오를 모두 만족할 수 있도록 처리했다!
1️⃣ 앱 다운로드 후
첫 실행
시
시스템 권한 창
을 이용해 위치 권한을 요청한다.위치 권한 설정 다이얼로그(교육용 UI)
를 사용자에게 표시해야 한다.2️⃣ 앱
재 접속
시
위치 권한 설정 다이얼로그(교육용 UI)
를 사용자에게 표시해야 한다.3️⃣
위치 권한 설정 다이얼로그(교육용 UI)
에서취소
버튼을 눌렀을 때
설정
으로 이동 했을 때위치 권한 설정 다이얼로그(교육용 UI)
를 사용자에게 표시해야 한다.위치 권한 설정 다이얼로그(교육용 UI)
를 사용자에게 표시하지 않는다.위치 권한 설정 다이얼로그(교육용 UI)
는 사용자에게 표시되지 않는다.4️⃣
위치 권한 설정 다이얼로그(교육용 UI)
에서설정
버튼을 눌렀을 때
위치 권한 설정 다이얼로그(교육용 UI)
를 사용자에게 표시해야 한다.코드 설명
private fun enableMyLocation() {
when {
// 앱에 이미 권한이 부여 되었는 지 확인
checkSelfLocationPermission() -> {
// 권한이 부여 되었을 때 수행되어야하는 동작
}
// ContextCompat.checkSelfPermission() 메서드가 PERMISSION_DENIED 을 반환하면
// shouldShowRequestPermissionRationale()를 호출
// true를 반환하면 교육용 UI를 사용자에게 표시
// 1. UI에서 사용자가 사용 설정하려는 기능에 특정 권한이 필요한 경우를 설명
// 2. 앱이 위치, 마이크, 카메라와 관련된 권한을 요청하는 경우 이 정보에 앱이 액세스 해야 하는 이유를 설명
shouldShowRequestLocationPermissionsRationale() -> {
// 권한을 명시적으로 거부한 경우 true
// 처음보거나, 다시묻지 않음을 선택한 경우 false
observeIsLocationDenial { showLocationRequestRationaleDialog() }
}
// 사용자에게 교육용 UI가 표시되거나 shouldShowRequestPermissionRationale()의 반환 값에서
// 교육용 UI를 표시하지 않아도 된다고 나타내면 권한을 요청
else -> {
observeIsLocationDenial { requestPermissionLauncher.launch(locationPermissions) }
}
}
}
// RequestPermission 계약 -> 시스템이 권한 요청 코드를 관리하도록 허용
// RequestPermission() -> 단일 권한 요청
// RequestMultiplePermissions() -> 여러 권한을 동시에 요청
private fun initRequestPermissionsLauncher() =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
...
}
적용된 교육용 UI
Android Docs: 위치 정보 액세스 권한 요청
Android Docs: 런타임 권한 요청
안드로이드 권한 관련 이야기
위치 서비스(GPS) 기능 설정 팝업
Android Request To Turn On GPS Pragmatically