우아한테크코스 론칭 페스티벌에서 발표 직전, 서비스를 시연하고자 했던 기기에서 앱을 실행하자마자 종료되는 일이 생겼다...
로그를 확인해보니 위치 정보가 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