[Android] 구글·네이버·카카오 지도 길찾기 연동 및 Android 11 패키지 가시성(Queries) 대응
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
관광 앱이나 위치 기반 서비스를 만지다 보면 장소 상세 화면에서 외부 지도 앱(구글지도, 네이버지도, 카카오맵)으로 길찾기를 연결해야 하는 경우가 필수적으로 생깁니다.
처음에는 단순하게 URI 스킴 하나 호출하면 끝날 줄 알았는데, 막상 여러 기기에서 테스트하고 운영하다 보면 예외 상황이 계속 터지더군요. 특정 지도 앱이 안 깔려 있어서 앱이 뻗거나, 서버에서 넘어온 좌표 데이터가 일시적으로 누락되어 엉뚱한 곳을 가리키는 등의 문제가 발생합니다.
매번 화면마다 코드를 복사해서 붙여넣기에는 관리도 안 되고 조만간 지도 앱 정책이 바뀌면 골치가 아파집니다. 그래서 아예 외부 앱 실행 기능을 공통 모듈로 분리해서 안전하게 처리하는 방식으로 구현했습니다. 나중에도 꺼내 쓰려고 블로그에 기록해 둡니다.
1. 내가 겪은 문제와 설계 기준
이래저래 앱을 배포하고 운영하면서 자주 놓치기 쉬운 예외 상황들은 대략 이렇습니다.
사용자 기기에 특정 지도 앱이 무조건 설치되어 있다고 가정하고 호출함 (미설치 시 크래시 위험)
좌표 정보(위경도)가 비어 있거나 잘못되었는데도 그대로 Intent를 던짐
앱이 없을 때 마켓(Play 스토어)으로 보내거나 웹 지도(브라우저)로 띄워주는 안전장치(Fallback)가 없음
Android 11(API 30) 이상에서 도입된 패키지 가시성(Package Visibility) 정책을 고려하지 않아 앱 감지가 안 됨
테스트 코드나 공개 저장소에 실제 운영 서버 URL이나 특정 관광지 좌표를 그대로 하드코딩함
결국 핵심은 단순히 "지도 앱을 여는 것"이 아니라, [좌표 검증 → 앱 설치 여부 확인 → 안전한 Intent 생성 → 실패 시 웹/마켓 Fallback 처리 → 사용자 안내]까지의 흐름을 하나의 파이프라인으로 묶어주는 것입니다.
지도 앱 연동 흐름도
| 단계 | 설명 | 실패 시 처리 |
| 좌표 검증 | 위도와 경도 값이 정상 범위에 있는지 확인 | 사용자에게 "위치 정보가 없다"고 안내 후 중단 |
| 지도 앱 선택 | 사용자가 원하는 지도 브라우저/앱 선택 | 다이얼로그 또는 기본 설정값 활용 |
| Intent 생성 | 앱별 고유 URI 스킴으로 Intent 빌드 | 잘못된 포맷이면 실행 취소 |
| 설치 여부 확인 | 해당 패키지를 처리할 수 있는 앱이 기기에 있는지 조회 | 앱이 없으면 웹 지도 URL이나 마켓으로 전환 |
| 최종 실행 | 외부 지도 앱 구동 | 예외 발생 시 안전하게 Toast 안내 |
2. 해결 코드: MapRouteLauncher 공통 모듈
화면단(Activity나 Fragment)에 스킴 코드를 파편화하지 않고, 하나의 헬퍼 클래스로 묶어서 처리한 구조입니다. 아래 예제 코드는 실제 프로젝트에 적용했던 구조에서 중요 정보와 좌표를 더미 값으로 걷어낸 형태입니다. 바로 복사해서 프로젝트 환경에 맞게 변형해 쓰시면 됩니다.
data class PlaceLocation(
val name: String,
val latitude: Double,
val longitude: Double,
)
enum class MapApp {
GOOGLE,
NAVER,
KAKAO,
WEB,
}
class MapRouteLauncher(
private val context: Context,
) {
fun openRoute(mapApp: MapApp, location: PlaceLocation) {
// 1. 좌표 및 데이터 검증
if (!isValidLocation(location)) {
showMessage("위치 정보가 올바르지 않습니다.")
return
}
// 2. 선택한 앱에 따른 Intent 생성
val intent = when (mapApp) {
MapApp.GOOGLE -> googleMapIntent(location)
MapApp.NAVER -> naverMapIntent(location)
MapApp.KAKAO -> kakaoMapIntent(location)
MapApp.WEB -> webMapIntent(location)
}
// 3. 앱 실행 가능 여부 확인 후 분기
if (canOpen(intent)) {
context.startActivity(intent)
return
}
// 4. 네이티브 앱 실행 실패 시 브라우저(Web)를 최종 안전장치로 활용
val fallback = webMapIntent(location)
if (canOpen(fallback)) {
context.startActivity(fallback)
} else {
showMessage("길찾기를 실행할 수 있는 앱이나 브라우저가 없습니다.")
}
}
private fun googleMapIntent(location: PlaceLocation): Intent {
val uri = Uri.parse("google.navigation:q=${location.latitude},${location.longitude}")
return Intent(Intent.ACTION_VIEW, uri).apply {
setPackage("com.google.android.apps.maps")
}
}
private fun naverMapIntent(location: PlaceLocation): Intent {
// 장소명은 반드시 URI 인코딩 처리를 해줘야 깨지지 않습니다.
val encodedName = Uri.encode(location.name)
val uri = Uri.parse("nmap://route/public?dlat=${location.latitude}&dlng=${location.longitude}&dname=$encodedName")
return Intent(Intent.ACTION_VIEW, uri).apply {
setPackage("com.nhn.android.nmap")
}
}
private fun kakaoMapIntent(location: PlaceLocation): Intent {
val uri = Uri.parse("kakaomap://look?p=${location.latitude},${location.longitude}")
return Intent(Intent.ACTION_VIEW, uri).apply {
setPackage("net.daum.android.map")
}
}
private fun webMapIntent(location: PlaceLocation): Intent {
// 실제 운영 시에는 자체 안내 페이지나 각 지도사 fallback 웹 링크를 사용합니다.
val uri = Uri.parse("https://www.example.com/maps?lat=${location.latitude}&lng=${location.longitude}")
return Intent(Intent.ACTION_VIEW, uri)
}
private fun canOpen(intent: Intent): Boolean {
return intent.resolveActivity(context.packageManager) != null
}
private fun isValidLocation(location: PlaceLocation): Boolean {
return location.latitude in -90.0..90.0 &&
location.longitude in -180.0..180.0 &&
location.name.isNotBlank()
}
private fun showMessage(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
3. 제일 중요: Android 11 이상 AndroidManifest.xml 설정
코드를 아무리 잘 짜도 이 설정을 빼먹으면 Android 11(Target SDK 30) 이상 기기에서는 무조건 웹 지도로만 튕겨 나갑니다. OS 단에서 다른 앱의 패키지 정보를 함부로 조회하지 못하게 막아두었기 때문입니다.
AndroidManifest.xml 파일의 <manifest> 태그 바로 아래에 우리가 연동할 지도 앱들의 패키지명을 명시해 주어야 resolveActivity가 정상적으로 인텐트를 찾아냅니다.
<queries>
<package android:name="com.google.android.apps.maps" />
<package android:name="com.nhn.android.nmap" />
<package android:name="net.daum.android.map" />
</queries>
주의하세요
간혹 귀찮다고
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />권한을 주는 경우가 있는데, 이렇게 하면 구글 플레이 스토어 심사 거절 사유가 됩니다. 딱 필요한 지도 앱 패키지만<queries>에 선언하는 것이 가장 깔끔하고 안전합니다.
4. 실무 운영 시 참고할 UX 가이드
각 지도 앱별로 장단점이 뚜렷하기 때문에, 유저 인터페이스를 설계할 때 아래 기준을 참고하시면 좋습니다.
지도 앱 선택 다이얼로그 방식 (추천): 길찾기 버튼을 눌렀을 때 유저가 평소에 자주 쓰는 앱을 고를 수 있게 바텀 시트나 다이얼로그를 띄워주는 게 관광 앱 특성상 반응이 제일 좋습니다. (이후 '기본값으로 기억하기' 옵션을 주면 클릭 동선을 줄일 수 있습니다.)
설치된 앱만 노출하기: 기기를 조회해서 인텐트 실행이 가능한 앱만 다이얼로그 선택지에 노출하면 유저가 미설치 앱을 눌러서 발생하는 피로감을 줄일 수 있습니다.
장소명 인코딩: 카카오나 네이버의 경우 앱 호출 시 뒤에 목적지 텍스트(
dname)를 붙일 수 있는데, 띄어쓰기나 특수문자가 포함되면 인텐트가 깨질 수 있으니 꼭Uri.encode()처리를 거치셔야 합니다.
5. 최종 배포 전 체크리스트
[ ] 지도사별 최신 개발자 문서에서 URI 스킴 형식이 바뀌지 않았는지 확인했는가?
[ ] 공통 런처 모듈을 구현하여 장소/축제/숙소 상세 화면에서 동일하게 호출하고 있는가?
[ ] 위도·경도 데이터가 null이거나 0.0으로 들어올 때의 예외 처리가 작동하는가?
[ ] 외부 앱 호출 실패 시 크래시가 나지 않고 웹 브라우저나 마켓으로 정상 유도되는가?
[ ] 코드 내부에 불필요한 운영 서버 하드코딩 주소나 API 키가 남아있지 않은가?
막상 붙여보면 별거 아닌 기능 같아도, 디테일한 예외 처리를 안 해두면 CS가 종종 들어오는 영역입니다. 관광 앱처럼 이동이 잦은 서비스를 빌드할 때는 이런 외부 앱 연동 인터페이스를 초기에 탄탄하게 잡아두는 편이 정신 건강에 이롭습니다. 자꾸 잊어버리시는 분들은 위 코드를 템플릿 삼아 공통 헬퍼로 묶어서 활용해 보시기 바랍니다.
댓글
댓글 쓰기