[Android] 외부 앱 실행(Intent) 안전하게 구현하기 - 스킴 검증, resolveActivity, Fallback 처리
앱을 개발하고 운영하다 보면 외부 앱을 열어줘야 하는 경우가 정말 많습니다. 길찾기 버튼을 누르면 카카오맵이나 네이버지도를 열고, 고객센터 번호를 누르면 전화 앱을 띄우고, 외부 링크는 브라우저로 연결하는 식이죠.
개발 기기에서는 필요한 앱들이 다 깔려있으니 대개 잘 돌아갑니다. 하지만 실제 사용자 기기 환경은 전혀 다릅니다. 특정 지도 앱이 없을 수도 있고, 브라우저가 비활성화되어 있거나, 플레이스토어가 없는 기기(원스토어 전용 단말 등)도 있습니다.
예외 처리를 안 해두면 버튼을 눌렀을 때 앱이 그냥 죽어버리거나(Crash), 아무 반응이 없어서 사용자가 고장 난 줄 알게 됩니다. 저도 운영하면서 매번 놓치기 쉽고 헷갈리는 부분이라, 안전하게 외부 Intent를 실행하기 위한 기준과 예외 처리(Fallback) 방법을 기록해 둡니다.
왜 Fallback 설계가 필요한가?
외부 앱 실행은 앱 내부 화면 이동보다 변수가 훨씬 많습니다. 설치 여부, OS 버전, 기본 앱 설정, URI 형식에 따라 결과가 계속 달라지기 때문입니다. 예외 처리가 없으면 사용자 경험이 크게 망가집니다.
| 상황 | 문제 발생 | Fallback 대응 방향 |
| 지도 앱 미설치 | 길찾기 실행 실패 | 플레이스토어 이동 또는 웹 지도(브라우저) 안내 |
| 스킴 오류 | 잘못되거나 변조된 앱 실행 시도 | 허용된 스킴(Allowlist)만 필터링하여 실행 |
| 브라우저 없음 | 외부 링크 열기 실패 | 사용자 안내 메시지 노출 또는 인앱 웹뷰 검토 |
| 마켓 앱 없음 | 플레이스토어 실행 실패 | 웹 버전 플레이스토어 URL로 브라우저 오픈 |
막상 운영을 해보면 외부 앱 연동은 성공 케이스보다 실패했을 때 어떻게 유저를 잡아둘 것인가(Fallback) 가 훨씬 중요해집니다.
Intent 실행 전 필수 체크 사항 (코드 예시)
핵심은 외부나 서버, 웹뷰에서 넘어오는 임의의 스킴을 그대로 startActivity()에 던지지 않는 것입니다. 반드시 검증 단계를 거쳐야 합니다.
아래는 외부 앱을 실행하기 전에 허용된 스킴인지 확인하고, 처리 가능한 앱이 없을 때 마켓으로 우회시키는 기본적인 구현 예시입니다.
fun openExternalApp(context: Context, uriText: String, marketUrl: String) {
val uri = Uri.parse(uriText)
val allowedSchemes = setOf("https", "kakaomap", "nmap", "market")
// 1. 허용된 스킴인지 1차 검증
if (uri.scheme !in allowedSchemes) {
Toast.makeText(context, "지원하지 않는 링크입니다.", Toast.LENGTH_SHORT).show()
return
}
val intent = Intent(Intent.ACTION_VIEW, uri)
// 2. 이 Intent를 처리할 수 있는 앱이 기기에 있는지 확인
val canOpen = intent.resolveActivity(context.packageManager) != null
if (canOpen) {
context.startActivity(intent)
return
}
// 3. 앱이 없다면 마켓(또는 웹)으로 fallback
val fallbackIntent = Intent(Intent.ACTION_VIEW, Uri.parse(marketUrl))
if (fallbackIntent.resolveActivity(context.packageManager) != null) {
context.startActivity(fallbackIntent)
} else {
Toast.makeText(context, "실행할 수 있는 앱이나 마켓이 없습니다.", Toast.LENGTH_SHORT).show()
}
}
⚠️ 중요 (Target SDK 관련 주의사항):
Android 11(API 30)부터는 패키지 가시성(Package Visibility) 정책이 강화되었습니다. AndroidManifest.xml에 <queries> 설정을 넣어두지 않으면, 기기에 앱이 설치되어 있어도 resolveActivity()가 null을 반환합니다. 호출할 앱의 스킴이나 패키지명을 매니페스트에 반드시 등록해 줘야 합니다.
허용할 스킴(Scheme) 제한하기
서버 응답이나 웹뷰 브릿지를 통해 들어오는 URI는 더 꼼꼼하게 도메인과 스킴을 묶어둬야 안전합니다. 보안 취약점이 될 수 있거든요.
- https: 브라우저나 웹 지도 연결 시 사용. 허용할 특정 도메인이 있다면 도메인까지 체크하는 것이 안전합니다.
- tel: 전화 앱 실행. 넘겨받은 텍스트가 올바른 전화번호 형식인지 검증 후 실행합니다.
- market: 플레이스토어 이동. 마켓 앱이 없는 환경을 고려해 웹 주소(https://play.google.com/...) 형태의 Fallback을 준비해야 합니다.
- kakaomap, nmap: 특정 지도 네이티브 앱 실행. 미설치 시 마켓이나 웹 링크 전환이 필수입니다.
- javascript, file: 일반적인 외부 앱 실행에서는 보안상 차단하는 편이 안전합니다.
사용자 안내 문구 및 실무 기획 기준
외부 앱 실행에 실패했을 때 단순히 "오류가 발생했습니다"라고만 띄우면 사용자는 이게 앱 버그인지 본인 폰 문제인지 알 수 없습니다. 다음 행동을 유도하는 문구가 좋습니다.
- 친절한 상황 설명: "지도 앱이 설치되어 있지 않아 스토어로 이동합니다. 이동하시겠어요?"처럼 사용자가 선택할 수 있는 팝업(다이얼로그)을 보여주는 것이 흐름상 자연스럽습니다.
- 대체 수단 제공: 만약 고객센터 전화 앱을 열지 못하는 상황(태블릿 기기 등)이라면, 전화번호를 클립보드에 자동으로 복사해 주고 "전화번호가 복사되었습니다"라는 안내를 띄워주는 식의 디테일이 필요합니다.
- 남 탓 금지: "사용자 기기에 앱이 없습니다" 같이 책임을 전가하는 톤보다는, 해결 방법을 직관적으로 제시하는 것이 좋습니다.
배포 전 체크리스트 (테스트 기준)
개발 단계에서 아래 케이스들을 한 번씩 누락 없이 확인해 봐야 프로덕션에서 크래시 리포트를 안 보게 됩니다.
- [ ] target SDK 기준에 맞게 AndroidManifest.xml에 <queries> 설정을 완료했는가?
- [ ] 연동할 외부 앱이 기기에 올바르게 깔려있을 때 정상 작동하는가?
- [ ] 외부 앱을 삭제한 상태에서 의도한 Fallback(마켓 이동 혹은 웹뷰 전환)이 동작하는가?
- [ ] 플레이스토어 앱 자체가 없는 환경에서 웹 플레이스토어 주소로 정상 전환되는가?
- [ ] 허용하지 않은 이상한 커스텀 스킴이 들어왔을 때 크래시 없이 차단 메시지가 뜨는가?
- [ ] 서버나 웹에서 넘겨받은 링크에 관리자 페이지 주소나 내부 토큰 등 민감한 파라미터가 유출되지 않는가?
요약
외부 앱 연동은 단순히 startActivity() 한 줄로 끝나는 기능이 아닙니다. [스킴 검증 ➡️ resolveActivity 검사 ➡️ 패키지 가시성 체크 ➡️ Fallback 대응 및 유저 안내] 가 한 세트로 묶여야 안전한 기능이 됩니다.
처음 구조를 잡을 때 유틸리티 클래스로 잘 분리해 두면 다음 프로젝트에서도 요긴하게 복사해서 쓸 수 있으니, 예외 처리를 꼼꼼하게 설계해 보시는 것을 추천합니다.