-
iOS에서 외부 지도 앱 스킴 기반 라우팅 구현하기/iOS 📱 2026. 4. 7. 21:41반응형
들어가며
iOS에서 외부 지도 앱으로 길찾기를 넘길 때 가장 먼저 떠오르는 방식은 URL Scheme입니다. 이번 실습에서는 네이버지도, TMAP, 카카오맵 세 가지 provider를 하나의 라우터로 묶고, 앱이 설치되어 있지 않은 경우 fallback까지 처리하는 흐름을 정리해봤습니다.
핵심은 단순히 UIApplication.shared.open 을 호출하는 것이 아니라, provider별 URL 생성 책임을 분리하고, canOpenURL 검사와 fallback 정책을 라우터에서 일관되게 다루는 구조를 만드는 것입니다.
이번 실습에서 잡은 구조
프로젝트는 크게 네 가지 레이어로 나눴습니다.
- RouteDestination: 출발지와 도착지 좌표를 표현하는 모델
- MapRouteProvider: 지도 앱별 스킴 URL 생성 책임
- ExternalMapRouter: 실행 가능 여부 판단, fallback 처리, 오류 반환
- MapRoutingViewModel: UI에서 provider를 선택했을 때 라우터 결과를 alert로 보여주는 계층
이렇게 분리해두면 새로운 지도 앱이 추가되어도 ViewModel을 거의 건드리지 않고 provider만 하나 더 붙이면 됩니다.
1. 먼저 스킴 조회 허용부터 설정하기
canOpenURL 를 사용하려면 Info.plist의 LSApplicationQueriesSchemes 에 조회할 스킴을 등록해야 합니다. 이 설정이 없으면 설치된 앱이어도 검사 단계에서 막힐 수 있습니다.
<key>LSApplicationQueriesSchemes</key> <array> <string>nmap</string> <string>tmap</string> <string>kakaomap</string> </array>실습 코드에서는 네이버지도, TMAP, 카카오맵 세 가지 스킴을 등록했습니다.
2. provider별 URL 생성 책임 분리
지도 앱마다 요구하는 파라미터 이름이 모두 다릅니다. 그래서 한 군데에서 if문으로 처리하기보다 provider별 URL builder로 책임을 쪼개는 편이 훨씬 관리하기 쉽습니다.
struct NaverMapRouteProvider: MapRouteProvider { let provider: MapProvider = .naver func makeRouteURL(for destination: RouteDestination) -> URL? { guard destination.isValid else { return nil } var components = URLComponents() components.scheme = "nmap" components.host = "route" components.queryItems = [ .init(name: "dlat", value: String(destination.destination.coordinate.latitude)), .init(name: "dlng", value: String(destination.destination.coordinate.longitude)), .init(name: "dname", value: destination.destination.name), .init(name: "appname", value: "com.example.routing3rdMapsTests") ] return components.url } }여기서 중요한 포인트는 URLComponents 를 사용했다는 점입니다. 문자열 더하기로 스킴 URL을 만들면 공백이나 한글이 섞일 때 인코딩 이슈가 생기기 쉽습니다. 반면 URLComponents 는 query item 단위로 안전하게 조합할 수 있습니다.
3. 라우터에서 실행 정책을 일관되게 관리하기
실제 분기 처리는 ExternalMapRouter 에서 담당합니다. destination 유효성 검사, provider 존재 여부, 앱 설치 여부, fallback 실행 여부를 한 군데에서 정리하니 흐름이 눈에 잘 들어옵니다.
@MainActor func route(to destination: RouteDestination, via provider: MapProvider) async -> Result<ExternalMapRouteResult, ExternalMapRoutingError> { guard destination.isValid else { return .failure(.invalidDestination) } guard let routeProvider = providers[provider] else { return .failure(.schemeCreationFailed(provider)) } guard let appURL = routeProvider.makeRouteURL(for: destination) else { return .failure(.schemeCreationFailed(provider)) } if opener.canOpenURL(appURL) { let opened = await opener.open(appURL) return opened ? .success(.openedApp(provider)) : .failure(.openFailed(provider)) } guard let fallbackURL = routeProvider.fallbackURL(for: destination) else { return .failure(.fallbackUnavailable(provider)) } guard opener.canOpenURL(fallbackURL) else { return .failure(.appNotInstalled(provider)) } let opened = await opener.open(fallbackURL) return opened ? .success(.openedFallback(provider)) : .failure(.openFailed(provider)) }이 구조의 장점은 명확합니다.
- 앱이 설치되어 있으면 스킴으로 바로 진입
- 앱이 없으면 App Store 또는 모바일 웹으로 fallback
- 실패 원인을 enum으로 구분해서 UI에 그대로 전달 가능
4. UIApplication 의존성도 추상화하기
보통 이런 코드는 UIApplication.shared 에 바로 붙여 쓰기 쉽습니다. 하지만 테스트까지 생각하면 추상화가 훨씬 낫습니다.
protocol ApplicationOpening: Sendable { func canOpenURL(_ url: URL) -> Bool func open(_ url: URL) async -> Bool }이 인터페이스 덕분에 테스트에서는 mock opener를 넣어서, 실제 외부 앱을 실행하지 않고도 fallback 동작을 검증할 수 있습니다. 외부 앱 연동 로직은 사용자 기기 상태에 따라 달라지기 때문에 테스트 더블을 두는 편이 훨씬 안정적입니다.
5. 테스트로 최소 동작 보장하기
이번 실습에서는 URL builder와 fallback 동작을 XCTest로 확인했습니다.
@MainActor func testRouterFallsBackWhenAppIsMissing() async { let provider = KakaoMapRouteProvider() let fallback = provider.fallbackURL(for: destination)! let opener = MockApplicationOpener(allowedURLs: [fallback]) let router = ExternalMapRouter(opener: opener, providers: [.kakao: provider]) let result = await router.route(to: destination, via: .kakao) XCTAssertEqual(result, .success(.openedFallback(.kakao))) }외부 앱 연동은 UI 테스트로만 보장하려고 하면 비용이 커집니다. 반면 URL 생성 규칙과 fallback 정책을 단위 테스트로 분리해두면, 적어도 우리가 의도한 라우팅 정책이 깨지지는 않는다는 확신을 가질 수 있습니다.
실습하면서 느낀 포인트
처음에는 단순히 지도 앱별 스킴 문자열만 알면 끝날 줄 알았습니다. 그런데 실제로 코드를 구성해보니 더 중요한 건 스킴 자체보다도 실패했을 때의 흐름을 어떻게 설계하느냐 였습니다.
특히 아래 세 가지가 중요했습니다.
- 좌표값이 잘못된 경우를 미리 막기
- provider별 URL 생성 책임을 분리하기
- 앱 미설치 상황에서 사용자 경험이 끊기지 않도록 fallback 제공하기
여기에 provider가 더 늘어나더라도 구조를 유지할 수 있도록 enum, protocol, router로 역할을 분리한 점이 이번 실습에서 가장 만족스러웠습니다.
마무리
외부 앱 스킴 연동은 기능 자체는 작아 보여도, 실제 서비스 코드에서는 자주 재사용되는 포인트입니다. 그래서 한 번 구현할 때 단순 호출 수준에서 끝내기보다, provider 추상화와 fallback 정책까지 함께 묶어두면 이후 확장이 훨씬 편해집니다.
다음에는 여기서 한 단계 더 나아가, provider별 최신 공식 문서를 기준으로 파라미터를 재검증하고, 현재 위치 기반 길찾기나 즐겨찾기 목적지 연동까지 붙여보면 더 실전적인 예제가 될 것 같습니다.
반응형' > iOS 📱' 카테고리의 다른 글
애플 로그인 구현 (0) 2026.05.10 [iOS] SPM Package 등에서 커스텀 폰트 (Custom Font) 추가하기 (0) 2026.01.28 [iOS] 커스텀 폰트 (Custom Font) 추가하기 (0) 2026.01.28 [iOS] MultipeerConnectivity(P2P 프로토콜) 소개 및 예제 프로젝트 (1) 2025.05.12 [iOS] Pinch Gesture와 Double Tap으로 구현하는 이미지 확대/축소 기능 (0) 2025.04.24