Safari에서 URL 스킴 폴백 시 발생하는 '주소가 유효하지 않음' 오류 해결 방법

opoinstall
2026-10-08
5 min read

Safari에서 URL 스킴 사용 시 왜 '주소가 유효하지 않음' 오류가 발생할까요? Safari는 시스템이 처리할 수 없는 사용자 지정 URL 스킴으로 웹페이지가 이동을 시도할 때 '주소가 유효하지 않음' 또는 '페이지를 열 수 없음' 오류를 표시할 수 있습니다. 이 문제를 해결하려면 검증된 유니버설 링크(Universal Links)로 전환하거나, 앱이 설치되지 않은 사용자에게 앱 스토어로 안내하는 사용자 동작 기반 폴백(user-gesture-driven fallbacks)을 구현해야 합니다.

“Safari에서 주소가 유효하지 않아 페이지를 열 수 없습니다”라는 알림은 모바일 Safari가 대상 네이티브 앱이나 처리 가능한 핸들러가 없는 기기에서 사용자 지정 URL 스킴으로 이동하려고 할 때 발생할 수 있습니다. 이 문제를 해결하려면 기존 URI 스킴에서 검증된 유니버설 링크로 전환하거나, 프로토콜 오류를 발생시키지 않고 앱이 설치되지 않은 사용자를 앱 스토어로 라우팅하는 사용자 동작 준수형 폴백 아키텍처를 배포해야 합니다.

용어 정의 관련 항목 검색 의도
사용자 지정 URL 스킴 외부 웹 링크가 네이티브 앱을 실행할 수 있도록 앱에서 정의한 URI 프로토콜입니다. 딥링크 라우팅 정보 제공 / 상업적
유니버설 링크 검증된 웹 도메인을 네이티브 iOS 앱 뷰와 직접 연결하는 표준 HTTPS 메커니즘입니다. 모바일 딥링크 기술 / 정보 제공
웹-투-앱 (Web to App) 웹 브라우저 방문자를 네이티브 모바일 앱으로 라우팅하는 아키텍처 프로세스입니다. 전환 퍼널 정보 제공

Safari에서 사용자 지정 스킴 사용 시 '주소가 유효하지 않음' 오류가 발생하는 이유

요청된 프로토콜에 대한 네이티브 핸들러가 없을 때 Safari 사용자 지정 스킴이 실패함.

근본 원인: WebKit이 미등록 URI 프로토콜에 대응하는 방식

사용자가 모바일 웹페이지의 링크와 상호작용하면 브라우저의 렌더링 엔진은 URI 스킴을 평가하여 적절한 전송 프로토콜이나 앱 핸들러를 결정합니다. Apple Safari(WebKit 엔진 기반)에서는 http:// 및 https://와 같은 표준 웹 프로토콜을 네트워크 리소스 로더가 내부적으로 처리합니다.

웹페이지가 Safari에 사용자 지정 URI 스킴(예: myapp://product/detail/1024)으로 이동하도록 지시하면, 운영 체제는 CFBundleURLTypes 번들 구성 내에 해당 스킴을 등록한 앱을 찾습니다. 대상 앱이 설치되어 있으면 iOS가 네이티브 앱을 실행합니다. 그러나 앱이 설치되지 않은 경우 표준 DNS나 웹 전송 계층에서 해당 스킴을 확인할 수 없습니다. Safari에는 사용자 지정 스킴을 위한 내부 웹 핸들러가 없기 때문에, 처리되지 않은 사용자 지정 프로토콜로 이동하려 하면 '주소가 유효하지 않아 페이지를 열 수 없다'는 알림 대화 상자가 나타날 수 있습니다.

샌드박스 장벽: JavaScript가 네이티브 앱 설치 상태를 쿼리할 수 없는 이유

프론트엔드 개발자는 스킴을 트리거하기 전에 앱 설치 여부를 확인하는 클라이언트 측 JavaScript를 작성하여 이 알림을 우회하려고 시도하곤 합니다. 하지만 Apple의 OS 보안 및 개인정보 보호 아키텍처상, 이러한 확인은 웹 콘텐츠에서 구조적으로 불가능합니다.

모바일 Safari는 웹 콘텐츠와 호스트 운영 체제 간에 엄격한 샌드박스 격리를 시행합니다. 웹페이지 JavaScript는 로컬 파일 시스템 레지스트리를 쿼리하거나, 설치된 앱 패키지를 검사하거나, 외부 URI 스킴의 활성 핸들러 여부를 확인하는 것이 금지됩니다. 브라우저가 사전에 설치 상태를 파악할 수 없기 때문에 앱이 없는 기기에서 처리되지 않은 사용자 지정 스킴을 실행하면 WebKit 오류 알림이 트리거될 위험이 있습니다.

사용자 경험 저하: 네이티브 시스템 알림이 웹 랜딩 페이지의 이탈률을 높이는 방식

“주소가 유효하지 않음”이라는 시스템 알림은 사용자 신뢰를 떨어뜨리고 전환 퍼널을 방해합니다:

  • 보안 불안: 사용자는 '주소가 유효하지 않음' 알림을 웹사이트 손상, 신뢰할 수 없는 소프트웨어 또는 보안 경고의 신호로 해석할 수 있습니다.
  • 퍼널 중단: 사용자가 페이지를 다시 사용하기 전에 차단 대화 상자를 확인하고 닫아야 하므로 이탈률이 증가합니다.
  • 단절된 스토어 이동: 처리되지 않은 알림이 보조 스토어 리다이렉션 스크립트와 동시에 나타나면 앱 스토어로의 전환 과정이 단절된 것처럼 느껴집니다.

최신 WebKit 버전에서 과거의 우회 기법이 실패하는 이유

최신 Safari에서 숨겨진 아이프레임(Iframe) 탐지의 한계

초기 iOS 버전에서는 개발자들이 종종 숨겨진 아이프레임 탐지를 사용했습니다. 스크립트가 DOM에 보이지 않는 <iframe> 요소를 삽입하고 그 소스를 사용자 지정 스킴(myapp://)으로 설정하는 동시에 자바스크립트 타이머를 실행하는 방식입니다. 의도는 설치된 앱이 실행되는 동안 최상위 창 이동은 일어나지 않게 하고, 앱이 없으면 아이프레임 내에서 조용히 실패하도록 하는 것이었습니다.

최신 모바일 브라우저에서 이 방식은 신뢰할 수 없습니다:

  • 최신 WebKit은 아이프레임(특히 샌드박스 처리된 프레임)에서의 외부 프로토콜 핸드오프를 제한하는 탐색 및 샌드박스 제한을 적용합니다.
  • 아이프레임 내에서 등록되지 않은 스킴을 로드하려고 하면 여전히 브라우저 수준의 오류 대화 상자가 트리거되거나, 깔끔한 폴백 없이 조용히 실패할 수 있습니다.
  • 아이프레임 탐지는 iOS 릴리스와 샌드박스 상황에 따라 일관성이 없으므로 앱 존재 여부를 평가하는 신뢰할 수 있는 메커니즘으로 간주해서는 안 됩니다.

기존 사용자 지정 스킴 타이머는 앱 실행 시도와 스토어 폴백 간의 경쟁 상태를 생성함.

타이머 기반 window.location 캐스케이드: 최신 브라우저가 자동 리다이렉트를 제한하는 이유

또 다른 전통적인 기술은 window.location.href를 사용한 타이머 기반 캐스케이드를 실행하는 것이었습니다:

// 기존 안티 패턴: 최신 브라우저에서 제한되거나 취약함
window.location.href = "myapp://product/detail";
setTimeout(function() {
    window.location.href = "https://apps.apple.com/app/id123456789";
}, 2000);

이 방식은 다음과 같은 사용자 경험 및 기술적 실패 모드를 생성합니다:

  1. 중복 알림: 앱이 설치되어 있지 않으면 Safari는 사용자 지정 스킴을 평가할 때 '주소가 유효하지 않음' 팝업을 표시할 수 있으며, 사용자는 타이머가 보조 이동을 시작하는 동안 이 알림을 닫아야 합니다.
  2. 의도치 않은 리다이렉션: 앱이 설치되어 성공적으로 열리더라도, 사용자가 Safari로 돌아왔을 때 브라우저가 백그라운드 재개 시 보류 중인 타이머를 실행하여 사용자를 불필요하게 앱 스토어로 리다이렉션할 수 있습니다.

사용자 활성화 및 브라우저 탐색 정책

최신 모바일 브라우저는 사용자 상호작용 없이 이루어지는 탐색을 제한하는 사용자 활성화 정책을 시행합니다. WebKit은 백그라운드 타이머, 비동기 콜백 또는 최근 사용자 상호작용 없이 로드되는 스크립트에서 발생하는 자동 창 리다이렉션 및 프로토콜 핸드오프를 제한합니다.

직접적인 사용자 상호작용 없이 실행되는 프로그램적 핸드오프는 예측 가능성이 낮으며 브라우저 환경에 따라 억제될 수 있습니다. 안정적인 라우팅을 위해서는 대화형 요소에 대한 물리적 탭과 같은 명시적 사용자 동작에서 직접 네이티브 앱 핸드오프가 시작되어야 합니다.

Apple이 유니버설 링크를 권장 솔루션으로 개발한 이유

독점적인 URL 스킴의 실패 모드를 없애기 위해 Apple은 iOS 9에서 유니버설 링크를 도입했습니다. 유니버설 링크는 사용자 지정 스킴(myapp://)을 표준화된 검증된 HTTPS 웹 URL(https://app.example.com/product/1024)로 대체합니다.

딥링크를 표준 HTTPS 인프라에 고정함으로써 Apple은 미등록 프로토콜 실패 모드를 제거했습니다. 앱이 설치되어 있고 현재 탐색 상황에서 연결 가능한 경우 iOS는 링크를 네이티브 핸들러로 직접 라우팅합니다. 앱이 설치되어 있지 않으면 Safari는 HTTPS URL을 일반 웹 리소스로 계속 탐색하여 프로토콜 경고 없이 웹페이지나 스토어 폴백을 로드합니다.

유니버설 링크가 '주소가 유효하지 않음' 알림을 제거하는 방법

유니버설 링크는 검증된 HTTPS를 사용하므로 네이티브 핸드오프 실패 시 유효한 웹 목적지로 전환됨.

HTTPS 기반: 미등록 프로토콜 실패 모드 제거

사용자 지정 URL 스킴과 유니버설 링크의 주요 차이점은 브라우저 네트워크 스택이 요청된 URL을 평가하는 방식에 있습니다:

  • 사용자 지정 스킴 (myapp://): 비표준 프로토콜입니다. WebKit은 DNS나 표준 웹 전송을 통해 이를 확인할 수 없습니다. 스킴을 처리하는 등록된 앱이 없으면 요청이 '주소가 유효하지 않음' 오류를 발생시킬 수 있습니다.
  • 유니버설 링크 (https://app.example.com): 완전한 표준 HTTPS URL입니다. WebKit은 HTTPS 주소를 네이티브로 확인하고 로드합니다.

유니버설 링크는 근본적으로 유효한 웹 URL이므로 Safari는 미등록 프로토콜을 마주치지 않습니다. 네이티브 앱 핸드오프가 발생하지 않으면 Safari는 해당 주소에 호스팅된 웹 콘텐츠를 로드하기만 합니다.

양방향 연결: 호스팅된 AASA 파일과 네이티브 권한 조정

유니버설 링크는 모바일 앱 바이너리와 웹사이트 도메인 간의 연결을 통해 검증된 라우팅을 설정합니다:

  1. 앱 권한: iOS 앱은 대상 도메인 문자열인 applinks:app.example.com을 포함하는 Associated Domains 권한을 선언합니다.
  2. 서버 선언: 웹사이트 도메인은 https://app.example.com/.well-known/apple-app-site-association (AASA)에 JSON 파일을 호스팅합니다. 이 파일은 권한이 부여된 앱 식별자와 경로 일치 구성 요소를 지정합니다.
  3. OS 수준 확인: 사용자가 앱을 설치하면 iOS는 도메인 연결을 확인합니다. 연결된 링크를 탭하면 운영 체제는 대상 목적지를 처리할 수 있는 앱이 있는지 평가합니다.

우아한 웹 성능 저하: 앱이 설치되지 않았을 때의 동작

앱이 설치되지 않은 사용자가 유니버설 링크를 탭하면:

  1. iOS 운영 체제는 검증된 연결 레지스트리와 URL을 대조합니다.
  2. 도메인과 일치하는 설치된 앱을 찾지 못하면 iOS는 링크를 표준 웹 탐색으로 Safari에 위임합니다.
  3. Safari는 시스템 오류 경고 없이 해당 URL에 호스팅된 웹페이지를 로드합니다.
  4. 호스팅된 웹페이지는 관련 제품 콘텐츠를 표시하거나 앱 스토어 CTA를 보여주거나 지연 파라미터 복구 기능을 조정할 수 있습니다.

전용 하위 도메인을 사용하여 Safari 도메인 내 탐색 주의 사항 관리

유니버설 링크를 웹 페이지에 배포할 때는 Apple 개발자 문서에 문서화된 Safari의 동일 도메인 탐색 동작을 고려해야 합니다.

사용자가 https://example.com/promo에 호스팅된 웹 페이지를 탐색하다가 동일한 도메인을 가리키는 유니버설 링크(https://example.com/product/1024)를 탭하면, Safari는 사용자가 계속 웹사이트를 탐색하려는 것으로 간주하여 네이티브 앱을 여는 대신 웹 페이지를 로드합니다.

별도로 연결된 라우팅 호스트를 사용하면 문서화된 동일 도메인 지속 사례를 방지하고, 도메인 연결이 유효할 때 유니버설 링크가 네이티브 라우팅을 위해 평가되도록 허용할 수 있습니다:

  • 기본 웹사이트를 루트 도메인이나 웹 하위 도메인(https://www.example.com)에 호스팅합니다.
  • 유니버설 링크 라우팅은 별도로 연결된 전용 하위 도메인(https://app.example.com)을 통해 구성합니다.

별도의 하위 도메인 경계를 넘어서 탭하면 Safari의 탐색 휴리스틱을 충족하여 직접 네이티브 앱 실행을 지원합니다.

JavaScript SDK를 사용한 안정적인 웹-투-앱 핸드오프 구현

다단계 폴백 아키텍처: 유니버설 링크 우선, 명시적 폴백 차선

성공적인 웹-투-앱 아키텍처는 다단계 리다이렉션 캐케이드를 배포합니다:

  • 1단계 (유니버설 링크): 기본 CTA 버튼은 연결된 하위 도메인을 가리키는 검증된 유니버설 링크를 호출합니다. 앱이 설치된 기기에서는 미등록 사용자 지정 스킴 알림 없이 네이티브 라우팅을 활성화합니다.
  • 2단계 (컨텍스트 웹 폴백): 앱이 설치되지 않은 경우 유니버설 링크는 호스팅된 웹 랜딩 페이지로 원활하게 이동하여 앱 스토어 다운로드 버튼을 보여줍니다.
  • 3단계 (사용자 지정 스킴 폴백): 기존 사용자 지정 스킴(myapp://)이 구버전 OS나 특정 내장 컨테이너를 위해 유지되는 경우, 자동 스크립트보다는 명시적인 사용자 상호작용에서 비롯되는 폴백으로 호출합니다.

기존 스킴 폴백은 사용자 트리거를 유지하고 가시성을 억제 휴리스틱으로만 사용해야 함.

페이지 가시성 API를 휴리스틱 억제 신호로 사용

사용자 지정 스킴과 함께 폴백 타이머를 구현할 때 클라이언트 스크립트는 문서가 포그라운드 가시성을 잃었는지 평가하여 보류 중인 스토어 리다이렉트를 취소합니다. JavaScript는 네이티브 프로세스 실행을 직접 검사할 수 없으므로, 프론트엔드 아키텍처는 WHATWG HTML 표준의 페이지 가시성을 활용합니다.

외부 핸드오프 후 브라우저 탭이 백그라운드로 전환되면 스크립트가 가시성 변경을 감지합니다:

// 예시 폴백 지연; 앱 UX 요구 사항에 따라 조정
var fallbackTimer = setTimeout(function() {
    if (!document.hidden) {
        // 문서가 포그라운드에 유지됨; 폴백 CTA 진행
        window.location.href = "https://apps.apple.com/app/id123456789";
    }
}, 2000);

document.addEventListener("visibilitychange", function() {
    if (document.hidden) {
        // 문서가 숨겨짐; 보류 중인 폴백 타이머 삭제
        clearTimeout(fallbackTimer);
    }
});

가시성 변경은 문서가 숨겨졌음을 나타내며, 이는 잘못된 스토어 리다이렉트를 방지하는 유용한 억제 신호 역할을 합니다. 하지만 가시성 변경만으로는 특정 대상 앱이 성공적으로 열렸다는 증거가 되지 않습니다(탭 전환, 브라우저 최소화, 기기 잠금 등도 백그라운드 전환을 트리거하기 때문). 2000ms 지연은 예시 휴리스틱일 뿐이며 표준 프로토콜 임계값으로 취급해서는 안 됩니다.

사용자 동작과 점진적 유니버설 링크 앵커 연결

직접 링크 라우팅을 위해 프론트엔드 개발자는 점진적 앵커 요소를 검증된 유니버설 링크 끝점에 직접 연결합니다. 사용자가 클릭하면 브라우저가 HTTPS 링크로 이동하여 iOS가 경로를 가로챌 수 있게 합니다.

고급 획득 퍼널에서 OpoInstall 같은 플랫폼은 별도의 보조 수집 채널로 지연 파라미터 복원을 지원합니다. 클릭 당시의 웹 컨텍스트를 기록하고 네이티브 SDK 훅을 통해 설치 후 실행 신호와 연결함으로써, 네이티브 앱은 표준 유니버설 링크 URL 검증을 변경하지 않고도 초기 실행 시 맞춤형 캠페인 파라미터를 검색할 수 있습니다. 네이티브 유니버설 링크 핸들러와 함께 지연된 기여 리스너를 통합하는 자세한 내용은 SDK 통합 문서를 검토하세요.

[사용자 웹 CTA 버튼 탭]
             │
             ▼
[라우팅 기본 평가]
   ┌─────────┴─────────┐
   ▼                   ▼
[사용자 지정 스킴: myapp://] [유니버설 링크: https://]
   │                           │
   ▼                           ▼
[Safari 확인 시도]             [OS 연결 평가]
├─ 앱 확인 -> 앱 열림       ├─ 앱 설치 + 연결 가능 -> 네이티브 앱
└─ 핸들러 없음 / 차단 ->      └─ 미설치 ->
   "주소가 유효하지 않음"         웹 랜딩 페이지 우아하게 로드
   시스템 알림 발생 가능           │
                                  ▼
                                  [앱 스토어 또는 웹 폴백 표시]

클라이언트 측 구현: 유니버설 링크 라우팅 및 폴백 처리

프론트엔드 HTML/JavaScript에서 최신 유니버설 링크 리다이렉션 스크립트 구성

프론트엔드 구현은 검증된 유니버설 링크 URL에 직접 연결되는 대화형 앵커 요소를 구성하여, 스크립트 실행이 차단될 경우에 대비한 점진적 향상 폴백을 제공합니다.

장면 기반 수명 주기 아키텍처에서의 네이티브 iOS 수신

장면 기반 iOS 앱의 경우 Safari에서 전달된 유니버설 링크는 UIWindowSceneDelegate 수명 주기를 통해 처리됩니다: 콜드 런치 시 scene(_:willConnectTo:options:), 앱이 실행 중이거나 메모리에 일시 중단된 경우 scene(_:continue:)를 사용합니다. 네이티브 구현은 수신된 NSUserActivity의 활동 유형이 NSUserActivityTypeBrowsingWeb인지 확인하고, webpageURL을 추출하여 경로를 검증합니다.

아래 기술 구현은 프론트엔드 점진적 앵커를 구성하고 네이티브 Swift에서 들어오는 유니버설 링크 URL을 안전하게 처리하는 방법을 보여줍니다.

// 웹: 점진적 앵커 폴백을 사용한 프론트엔드 유니버설 링크 핸드오프
// 클라이언트 측 쿼리 삭제가 포함된 깔끔한 HTTPS 유니버설 링크 목적지를 구성합니다.
(function() {
    var ctaButton = document.getElementById("openAppBtn");
    if (!ctaButton) return;

    // 1. 초기 상태: 전용 하위 도메인의 검증된 유니버설 링크는 동일 도메인 Safari 지속을 방지함
    var targetBaseUrl = "https://app.example.com/detail/1024";

    // 2. 현재 페이지 URL에서 동적 쿼리 파라미터 추출 및 삭제
    var urlParams = new URLSearchParams(window.location.search);
    var rawId = urlParams.get("id") || "";
    var rawPromo = urlParams.get("promo_code") || "";
    var rawSource = urlParams.get("utm_source") || "web_landing";

    var idRegex = /^[A-Za-z0-9_-]{1,64}$/;
    var targetId = idRegex.test(rawId) ? rawId : "";
    var promoCode = idRegex.test(rawPromo) ? rawPromo : "";
    var utmSource = idRegex.test(rawSource) ? rawSource : "web_landing";

    var finalUrl = targetBaseUrl + "?utm_source=" + encodeURIComponent(utmSource);
    if (targetId.length > 0) {
        finalUrl += "&id=" + encodeURIComponent(targetId);
    }
    if (promoCode.length > 0) {
        finalUrl += "&promo_code=" + encodeURIComponent(promoCode);
    }

    // 점진적 향상: 앵커 href는 직접적이고 알림 없는 유니버설 링크 탐색을 제공함
    if (ctaButton.tagName.toLowerCase() === "a") {
        ctaButton.setAttribute("href", finalUrl);
    } else {
        ctaButton.addEventListener("click", function(e) {
            e.preventDefault();
            window.location.assign(finalUrl);
        });
    }
})();
// iOS: SceneDelegate.swift - 유니버설 링크 처리 및 경로 삭제
// 참조 통합 예시. 배포된 아키텍처에 따라 메서드 서명 및 라우팅을 검증하세요.
import UIKit

struct ValidatedAppRoute {
    let path: String
    let queryParams: [String: String]
}

class AppRouteValidator {
    private static let allowedHosts = Set(["app.example.com"])
    private static let allowedPathPrefixes = ["/detail/", "/promo/"]
    private static let allowedKeys = Set(["id", "promo_code", "utm_source"])

    static func validate(url: URL) -> ValidatedAppRoute? {
        guard let scheme = url.scheme?.lowercased(), scheme == "https" else {
            return nil
        }
        guard let host = url.host?.lowercased(), allowedHosts.contains(host) else {
            return nil
        }

        let path = url.path
        guard allowedPathPrefixes.contains(where: { path.hasPrefix($0) }) else {
            return nil
        }

        var sanitizedParams: [String: String] = [:]
        var seenKeys = Set<String>()

        if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
           let queryItems = components.queryItems {
            let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
            for item in queryItems {
                // Fail-closed 유효성 검사: 알 수 없는 쿼리 키가 있으면 URL 거부
                guard allowedKeys.contains(item.name), !seenKeys.contains(item.name) else {
                    return nil
                }
                seenKeys.insert(item.name)

                let value = item.value ?? ""
                if value.count <= 64 && value.rangeOfCharacter(from: validChars.inverted) == nil {
                    sanitizedParams[item.name] = value
                } else {
                    return nil
                }
            }
        }

        return ValidatedAppRoute(path: path, queryParams: sanitizedParams)
    }
}

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let _ = (scene as? UIWindowScene) else { return }

        // 유니버설 링크를 통한 콜드 런치 처리
        if let userActivity = connectionOptions.userActivities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb }),
           let webpageURL = userActivity.webpageURL {
            processIncomingUniversalLink(url: webpageURL)
        }
    }

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        // 유니버설 링크를 통한 워밍 재개 처리
        if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
           let webpageURL = userActivity.webpageURL {
            processIncomingUniversalLink(url: webpageURL)
        }
    }

    private func processIncomingUniversalLink(url: URL) {
        // 유니버설 링크 수신 시 엄격한 허용 목록 및 삭제 적용
        if let route = AppRouteValidator.validate(url: url) {
            DispatchQueue.main.async {
                AppNavigator.shared.navigateTo(path: route.path, params: route.queryParams)
            }
        } else {
            DispatchQueue.main.async {
                AppNavigator.shared.navigateToDefaultHome()
            }
        }
    }
}

// 앱별 내비게이션 조정자 (SDK API 아님)
class AppNavigator {
    static let shared = AppNavigator()

    func navigateTo(path: String, params: [String: String]) {
        // 경로와 쿼리 파라미터에 따라 내부 UI 뷰 컨트롤러 전환 실행
    }

    func navigateToDefaultHome() {
        // 잘못되거나 인식할 수 없는 딥링크 시 안전하게 홈 화면으로 폴백
    }
}

인바운드 파라미터 삭제: 네이티브 경로에 엄격한 허용 목록 필터링 적용

OWASP 모바일 앱 보안 테스트 가이드의 불안전한 딥링크 지침에 따라, 유니버설 링크를 통해 전달되는 모든 파라미터는 신뢰할 수 없는 입력으로 취급해야 합니다:

  • 경로 유효성 검사: URL 경로가 권한이 부여된 뷰 컨트롤러 허용 목록(/detail/, /promo/)과 일치하는지 확인합니다.
  • 쿼리 필터링: 허용된 쿼리 키(id, promo_code, utm_source)를 시행하고 예상치 못한 키는 삭제합니다.
  • 길이 및 문자 제한: 파라미터 값을 영숫자 문자 집합으로 제한합니다(≤ 64자).

Safari 딥링크 프로토콜 및 오류 완화 매트릭스

종합적인 프로토콜 비교 및 오류 예방 체크리스트

적절한 딥링크 프로토콜을 선택하는 것은 WebKit 탐색 오류를 방지하는 데 중요합니다. 아래 매트릭스는 오류 동작 및 플랫폼 요구 사항 전반에 걸쳐 주요 딥링크 메커니즘을 비교합니다:

오류 동작 전반에서 URL 스킴, 유니버설 링크 및 스마트 앱 배너 비교

라우팅 프로토콜 기본 프로토콜 앱이 설치되었을 때의 동작 앱이 설치되지 않았을 때의 동작 '주소가 유효하지 않음' 알림 위험
사용자 지정 URL 스킴 myapp:// 등록된 경우 네이티브 앱 실행 Safari에서 '주소가 유효하지 않음' 알림 발생 가능 가능 (스킴을 처리하는 앱이 없을 때 발생)
유니버설 링크 https:// 현재 상황에서 적합하면 연결된 앱 열기 호스팅된 랜딩 페이지로 웹 탐색 계속 낮음 (미등록 스킴 오류 모드 제거)
Apple 스마트 앱 배너 네이티브 WebKit <meta> 앱 열기 네이티브 제안 표시 앱 스토어 보기 네이티브 제안 표시 미등록 사용자 지정 스킴 실패에는 적용 안 됨
사용자 지정 웹 배너 JavaScript + 유니버설 링크 SDK를 통한 직접 앱 웨이크업 실행 스토어 리다이렉션 또는 웹 CTA 트리거 낮음 (검증된 HTTPS 라우팅 사용)

자주 묻는 질문(FAQ)

URL 스킴을 트리거하기 전에 JavaScript를 사용하여 iOS 앱 설치 여부를 감지할 수 있나요?
아니요. Apple의 OS 보안 및 개인정보 보호 아키텍처상, Safari에서 실행되는 웹페이지 JavaScript는 설치된 앱을 검사하거나 로컬 프로토콜 레지스트리를 쿼리할 수 없습니다. 등록된 앱이 응답하지 않을 때 처리되지 않은 사용자 지정 스킴으로 직접 이동을 시도하면 WebKit이 주소가 유효하지 않다는 오류를 표시할 수 있습니다.
유니버설 링크는 Safari에서 어떻게 주소가 유효하지 않다는 오류를 방지하나요?
유니버설 링크는 AASA(Apple App Site Association) 파일을 통해 검증된 표준 HTTPS URL(`https://app.example.com/...`)을 사용합니다. URL이 표준 웹 주소이므로 앱이 설치되어 있지 않아도 Safari는 인식되지 않는 프로토콜을 마주치지 않고 웹 목적지나 스토어 리다이렉트로 탐색을 계속합니다.
유니버설 링크가 때때로 Safari에서 앱 대신 웹사이트를 여는 이유는 무엇인가요?
사용자가 현재 보고 있는 웹페이지와 동일한 도메인에 있는 유니버설 링크를 탭하면, Safari는 사용자가 사이트를 계속 탐색하려는 것으로 간주하여 웹페이지를 로드합니다. Safari의 이러한 동일 도메인 지속 동작을 방지하려면 기본 웹사이트와 구분되는 전용 하위 도메인(예: `app.example.com`)에서 유니버설 링크를 구성하세요.

요약 및 의사 결정 프레임워크

“Safari에서 주소가 유효하지 않아 페이지를 열 수 없습니다”라는 알림은 해당 앱 핸들러가 없는 기기에서 사용자 지정 URI 스킴을 사용할 때 발생하는 운영 결과입니다. 기존의 숨겨진 아이프레임 탐지나 자동 타이머 캐스케이드에 의존하는 것은 탐색 취약성을 초래하고 웹-투-앱 전환 퍼널을 손상시킵니다.

검증된 유니버설 링크로 마이그레이션하면 미등록 사용자 지정 프로토콜 실패 모드가 제거되고 안정적인 HTTPS 폴백 경로가 제공됩니다. 검증된 HTTPS 연결과 사용자 동작을 준수하는 웹 통합 패턴을 결합함으로써 엔지니어링 팀은 파괴적인 브라우저 알림을 줄이고, 스토어 다운로드 전반에 걸쳐 마케팅 파라미터를 보존하며, 모바일 웹 퍼널 전반에서 안정적인 온보딩 경험을 지원할 수 있습니다.

유니버설 링크를 구현하고 파라미터를 자동으로 전달하는 방법을 알아보려면 SDK 통합 문서를 검토하세요.

관련 자료

Share this article