웹페이지는 어떻게 설치된 앱을 실행할 수 있을까?

easyhooon·2026년 7월 15일
post-thumbnail

서두

광고 배너나 카카오톡 같은 SNS로 공유된 링크를 누르면, 해당 디바이스에 설치된 앱이 열리는 경우가 있다.

링크를 한 번 눌렀을 뿐인데 앱의 홈 화면도 아닌, 광고에서 보던 상품이나 공유받은 게시글 화면이 바로 나타난다.

웹 링크를 눌렀는데 어떻게 설치된 앱의 상품 상세 화면까지 이동한 걸까?

얼핏 보면 별로 특별하지 않은 기능처럼 보인다.

그런데 생각해보면 조금 이상하다.

링크는 카카오톡에서 곧바로 앱으로 연결되기도 하고, 브라우저에 열린 웹페이지에서 앱으로 전환되기도 한다. 출발점은 달라도 서로 다른 애플리케이션 사이의 이동이라는 점은 같다.

그럼 링크를 누르는 것만으로 어떻게 디바이스에 설치된 앱을 찾아 실행할 수 있는 걸까?

myapp://products/42

이런 문자열 하나만으로 브라우저가 설치된 앱을 알고 있는 걸까?

브라우저의 마법인가..?

그렇다면 그 뒤에서는 어떤 일들이 벌어지고 있는 걸까?

Custom Scheme, Android App Links, iOS Universal Links가 실제로 어떤 과정을 거쳐 앱을 실행하는지 알아보도록 하자.

본론

우선, 결론부터 말하면 웹페이지가 앱을 직접 실행하는 것은 아니다.

웹페이지는 URL로 이동해달라고 요청할 뿐이다. 브라우저가 이 요청을 검사하고 시스템에 넘기면, 운영체제가 해당 URL을 처리할 수 있는 앱을 찾아 실행한다.

웹페이지는 URL을 만든다.
브라우저는 외부 이동을 허용할지 판단한다.
OS는 URL을 처리할 앱을 찾는다.
앱은 전달받은 URL을 내부 화면으로 변환한다.

전체 흐름을 먼저 정리하면 대략 이렇다.

사용자가 링크 클릭
    ↓
웹페이지가 URL 탐색 요청
    ↓
브라우저가 외부 앱 전환 정책 검사
    ↓
OS의 URL / Intent Resolver에 전달
    ↓
설치된 앱의 등록 정보와 URL 비교
    ↓
앱 실행 또는 포그라운드 전환
    ↓
URL을 앱에 전달
    ↓
앱 내부 Router가 특정 화면으로 이동

Deep Link는 하나의 기술 이름이 아니다

먼저 용어부터 정리해보자.

Deep Link는 특정 구현 방식의 이름이라기보다, 사용자를 앱의 특정 콘텐츠나 화면으로 바로 이동시키는 링크를 뜻하는 상위 개념에 가깝다.

앱 홈 화면으로 이동

이 아니라,

상품 ID가 42인 상세 화면으로 이동

하게 만드는 것이 핵심이다.

이를 구현하는 대표적인 방식이 다음 세 가지다.

Custom Scheme
Android App Links
iOS Universal Links

세 방식 모두 최종적으로 앱의 특정 화면을 연다는 목적은 같지만, 앱을 찾는 방법과 링크의 신뢰성을 보장하는 방식이 다르다.

URL의 Scheme은 누가 처리할지를 결정한다

URL의 가장 앞부분에는 scheme이 있다.

https://example.com/products/42
└───┘
 scheme

https는 브라우저가 기본적으로 처리할 수 있는 scheme이다. 브라우저는 서버에 HTTP 요청을 보내고 응답받은 HTML을 렌더링한다.

다만 App Links나 Universal Links처럼 검증된 연결 관계가 있으면, OS가 host와 path까지 확인해 앱으로 라우팅할 수 있다.

반면 아래와 같은 URL은 어떨까?

myapp://products/42

브라우저는 myapp이라는 프로토콜로 네트워크 요청을 보내는 방법을 모른다. 대신 이 scheme을 처리할 외부 애플리케이션이 있는지 시스템에 요청한다.

WHATWG HTML 표준에서는 이를 브라우저가 URL을 external software로 hand-off 한다고 표현한다.

즉 scheme은 단순한 문자열 prefix가 아니라, 처리 주체를 고르는 첫 번째 dispatch key로 볼 수 있다.

HTTPS 링크에서는 여기에 도메인 연결 정보와 사용자의 선택이 더해져 최종 목적지가 결정된다.

Custom Scheme은 어떻게 앱을 찾을까?

예를 들어 쇼핑 앱에서 다음 Custom Scheme을 사용한다고 해보자.

myshop://products/42

앱은 설치될 때 자신이 myshop scheme을 처리할 수 있다는 정보를 OS에 등록한다.

Android에서는 AndroidManifest.xmlintent-filter를 사용한다.

<intent-filter>
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:scheme="myshop"
        android:host="products" />
</intent-filter>

BROWSABLE은 브라우저 같은 외부 애플리케이션에서 이 Activity를 실행할 수 있다는 의미다.

iOS에서는 앱의 URL Types에 처리할 scheme을 등록한다.

URL Schemes: myshop

이 정보들은 앱이 설치될 때 OS가 관리하는 애플리케이션 등록 정보에 포함된다.

이후 웹페이지에서 사용자가 아래 링크를 누른다.

<a href="myshop://products/42">앱에서 보기</a>

브라우저는 myshop을 직접 처리할 수 없으므로 시스템에 URL을 넘긴다. OS는 설치된 앱의 등록 정보를 조회하고, myshop을 처리할 수 있는 앱을 찾는다.

처리할 앱이 발견되면 OS가 앱을 실행하거나 이미 실행 중인 앱을 포그라운드로 전환한다. 그리고 원래 URL을 앱의 lifecycle callback으로 전달한다.

Android에서는 보통 Intent의 data로 URL이 전달된다.

val deepLinkUri = intent.data

iOS에서는 URL open callback을 통해 전달받는다.

앱은 전달받은 URL을 파싱해 내부 화면으로 이동한다.

myshop://products/42
        └──────┬──────┘
          상품 ID 42
               ↓
       ProductDetailScreen(42)

결국 브라우저는 어떤 Activity나 ViewController를 열어야 하는지 모른다.

브라우저는 URL까지만 전달한다. 어떤 앱을 실행할지는 OS가 결정하고, 앱 안에서 어느 화면을 열지는 앱의 Router가 결정한다.

Custom Scheme의 문제

Custom Scheme은 간단하지만 몇 가지 문제가 있다.

가장 큰 문제는 scheme 이름에 소유권이 없다는 점이다.

myshop://products/42

다른 앱도 myshop을 처리할 수 있다고 등록할 수 있다.

Android에서는 여러 앱이 같은 URL과 일치하면 사용자의 기본 앱, 유일한 처리 후보, 앱 선택 창 등의 정책에 따라 대상이 정해진다.

iOS에서는 여러 앱이 같은 Custom Scheme을 등록했을 때 어떤 앱이 대상이 될지 정의되어 있지 않다.

myshop이라는 이름을 사용한다고 해서, 그 앱이 진짜 myshop의 소유자라는 보장은 없다.

또한 앱이 설치되지 않았다면 해당 URL을 처리할 주체가 없다.

myshop://products/42
        ↓
설치된 Handler 없음
        ↓
실행 실패 또는 브라우저 안내 UI

같은 URL로 자연스럽게 웹페이지를 여는 fallback도 기본적으로 존재하지 않는다.

웹페이지는 앱이 열렸는지 알 수 있을까?

여기서 이런 생각이 들 수 있다.

앱이 열렸는지 확인하고, 열리지 않았다면 앱스토어로 보내면 되지 않을까?

문제는 일반 웹페이지에 앱 실행 성공 여부를 알려주는 표준 callback이 없다는 점이다.

웹페이지는 Custom Scheme으로 이동을 요청할 수 있지만, OS가 실제로 앱을 찾았는지, 사용자가 실행을 취소했는지, 앱이 정상적으로 화면을 열었는지 확실하게 전달받지 못한다.

그래서 과거에는 blur, visibilitychange, timer 등을 이용한 우회 방식이 많이 사용됐다.

Custom Scheme 실행 시도
    ↓
일정 시간 대기
    ↓
페이지가 background로 갔다면 앱이 열린 것으로 추측
    ↓
그렇지 않다면 앱스토어로 이동

하지만 이는 어디까지나 추측이다.

앱 실행이 느릴 수도 있고, 시스템 확인창이 표시됐을 수도 있으며, 사용자가 다른 화면으로 이동했을 수도 있다.

Android Chrome의 intent:// 문법은 browser_fallback_url을 지정할 수 있지만, 이는 Chrome과 Android Intent를 위한 별도의 기능이다.

<a href="intent://products/42#Intent;scheme=myshop;package=com.example.myshop;S.browser_fallback_url=https%3A%2F%2Fexample.com%2Fproducts%2F42;end">
    앱에서 보기
</a>

처리 가능한 앱이 없거나 Chrome이 외부 앱 실행을 차단하면 fallback URL로 이동할 수 있다.

Custom Scheme의 충돌과 fallback 문제를 해결하기 위해 Android와 iOS는 검증된 HTTPS 링크 방식을 제공한다.

Android에서는 App Links, iOS에서는 Universal Links라고 부른다.

https://shop.example.com/products/42

이번에는 별도의 Custom Scheme이 아니라 일반적인 HTTPS URL이다.

앱이 설치되지 않았다면 브라우저가 웹페이지를 열면 된다. 앱이 설치되어 있고 해당 도메인과 앱의 관계가 검증되어 있다면 OS가 앱으로 라우팅한다.

App Links와 Universal Links는 별도의 앱 실행 마법이 아니다.
HTTPS URL을 특정 앱에 안전하게 위임할 수 있도록 소유권 검증을 추가한 방식이다.

Android 앱은 Manifest에 처리할 HTTPS 주소와 android:autoVerify="true"를 선언한다.

<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:scheme="https"
        android:host="shop.example.com" />
</intent-filter>

웹사이트는 아래 경로에 Digital Asset Links 파일을 제공한다.

https://shop.example.com/.well-known/assetlinks.json

파일에는 이 도메인의 링크를 처리하도록 허용한 앱의 package name과 서명 인증서 fingerprint가 들어간다.

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myshop",
      "sha256_cert_fingerprints": ["... SHA-256 ..."]
    }
  }
]

Android는 앱이 설치된 뒤 이 파일을 가져와 다음 내용을 확인한다.

Manifest에 선언된 domain
        ↕
assetlinks.json의 domain 위임 정보
        ↕
앱 package name + signing certificate

검증이 성공하면 해당 앱을 이 도메인의 승인된 Handler로 저장한다.

이후 사용자가 일치하는 링크를 누르면 Android는 저장된 검증 상태를 확인하고 앱으로 라우팅한다.

Android 12 이상에서는 검증되지 않은 일반 HTTP/HTTPS 링크가 앱의 intent-filter와 일치하더라도 기본 브라우저에서 열린다.

다만 사용자가 시스템 설정에서 해당 앱을 도메인의 링크 처리 앱으로 직접 허용한 경우는 예외다.

자동으로 앱에 연결하려면 Manifest에 도메인을 적는 것만으로는 부족하고, 웹사이트가 assetlinks.json을 통해 앱을 승인해야 한다.

iOS도 비슷한 방식으로 앱과 웹사이트의 관계를 양쪽에서 선언한다.

앱은 Associated Domains entitlement에 도메인을 등록한다.

applinks:shop.example.com

웹사이트는 apple-app-site-association, 줄여서 AASA 파일을 제공한다.

https://shop.example.com/.well-known/apple-app-site-association

AASA에는 어떤 앱이 어떤 URL path를 처리할 수 있는지에 대한 정보가 포함된다.

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAM_ID.com.example.myshop"],
        "components": [
          { "/": "/products/*" }
        ]
      }
    ]
  }
}

iOS는 앱 설치 시 Associated Domains entitlement와 웹사이트의 AASA 파일을 확인한다.

관계가 검증된 상태에서 사용자가 일치하는 HTTPS 링크를 누르면 시스템이 앱을 실행하거나 활성화한다. iOS는 NSUserActivity 객체의 webpageURL에 원래 URL을 담아 앱에 전달한다.

여기서 NSUserActivity는 Android의 화면 컴포넌트인 Activity와는 다른 개념이다. 사용자가 수행하던 작업의 상태를 표현하는 Foundation 객체이며, Universal Link에서는 activityTypeNSUserActivityTypeBrowsingWeb으로 설정된다.

앱이 설치되지 않았다면 같은 URL이 브라우저에서 열린다.

다만 Universal Link라고 해서 항상 앱이 열리는 것은 아니다.

Safari에서 현재 보고 있는 페이지와 같은 도메인의 Universal Link를 누르면, 사용자가 브라우저 안에서 계속 이동하려는 것으로 판단해 Safari에 남을 수 있다.

또한 주소창에 URL을 직접 입력한 경우에도 앱을 열지 않는다. 사용자가 이전에 앱 대신 웹사이트에서 열기를 선택한 기록 역시 이후 동작에 영향을 줄 수 있다.

즉 Universal Link의 최종 결과에는 도메인 검증뿐만 아니라 사용자의 의도와 선택도 포함된다.

세 방식 비교

구분Custom SchemeAndroid App LinksiOS Universal Links
URL 형태myapp://...https://...https://...
앱 등록Intent Filter / URL TypesVerified Intent FilterAssociated Domains
웹사이트 측 파일없음assetlinks.jsonAASA
소유권 검증없음도메인 + package + 앱 서명도메인 + 앱 식별자
앱 미설치자연스러운 웹 fallback 없음웹페이지웹페이지
충돌 가능성다른 앱이 같은 scheme 등록 가능검증된 관계로 제한검증된 관계로 제한

App Links와 Universal Links가 더 안전한 이유는 앱의 선언만 믿지 않기 때문이다.

앱은 "내가 이 도메인을 처리한다"고 선언하고, 웹사이트도 "이 앱이 내 링크를 처리해도 된다"고 선언해야 한다.

양쪽의 정보가 일치해야 OS가 해당 앱을 도메인의 공식 Handler로 인정한다.

브라우저는 실행 주체가 아니라 "문지기"이다

브라우저가 URL을 시스템에 넘긴다고 해서 모든 요청을 그대로 허용하는 것은 아니다.

만약 웹페이지가 로드되자마자 마음대로 외부 앱을 실행할 수 있다면 어떻게 될까?

광고 페이지가 수십 개의 앱을 연속으로 실행하거나, 숨겨진 iframe과 JavaScript timer를 이용해 사용자를 계속 다른 앱으로 보낼 수도 있다.

그래서 브라우저는 외부 앱 전환 전에 다양한 신호를 검사한다.

실제 사용자 클릭에서 시작됐는가?
JavaScript가 자동으로 실행했는가?
top-level navigation인가, iframe인가?
redirect를 거쳤는가?
짧은 시간에 반복 호출하고 있는가?
사용자가 이전에 허용하거나 차단했는가?

예를 들어 Chrome은 처리 가능한 앱이 없거나 사용자 제스처 없는 JavaScript timer가 intent: URL을 실행한 경우 외부 앱을 열지 않는다.

다만 정확한 차단 조건은 모든 브라우저에서 동일하지 않다.

외부 프로토콜 실행의 확인 UI, 사용자 활성화가 유지되는 시간, redirect와 iframe 허용 정책 등은 브라우저와 OS 버전에 따라 달라질 수 있다.

따라서 아래처럼 일반화하는 것은 위험하다.

딥링크는 클릭에서만 동작한다.

조금 더 정확하게 표현하면 이렇다.

현대 브라우저는 사용자가 원하지 않는 외부 앱 실행을 막기 위해, 사용자 의도를 확인할 수 있는 신호를 요구하는 경우가 많다.

앱이 실행된 다음에는?

OS가 앱을 실행했다고 해서 딥링크 처리가 끝난 것은 아니다.

OS가 담당하는 범위는 적절한 앱을 찾고 URL을 전달하는 것까지다.

OS
  └─ 이 URL을 처리할 앱은 무엇인가?

App
  └─ 이 URL을 어떤 화면으로 연결할 것인가?

앱이 종료된 상태라면 cold start 과정에서 URL을 받는다. 이미 실행 중이라면 기존 Activity나 Scene이 새로운 URL을 받을 수 있다.

앱 내부 Router는 scheme, host, path, query parameter를 검증한 뒤 화면으로 변환한다.

https://shop.example.com/products/42?ref=web
                         └──┬──┘
                         productId
                              ↓
                    ProductDetailScreen(42)

여기서 URL은 외부 입력값이다.

앱이 열렸다는 이유만으로 모든 path와 query parameter를 신뢰하면 안 된다. 존재하지 않는 ID, 잘못된 문자열, 허용되지 않은 동작을 검증하고 안전하게 실패해야 한다.

결국 딥링크는 브라우저와 OS의 라우팅 문제이면서, 동시에 앱 내부 navigation과 입력값 검증 문제이기도 하다.

왜 마법처럼 보였을까?

사용자에게 보이는 과정은 단순하다.

링크 클릭
    ↓
앱의 특정 화면

하지만 두 화면 사이에는 여러 단계가 숨어 있다.

[앱 설치 또는 업데이트 시]
앱의 선언 + 웹사이트의 association 파일 검증
                     ↓
              검증 상태 저장

[링크가 활성화될 때]
브라우저 또는 호출 앱이 시스템에 URL 처리 요청
                     ↓
OS가 앱 등록 정보와 도메인 검증 상태 조회
                     ↓
앱 실행 또는 포그라운드 전환
                     ↓
URL / Intent / NSUserActivity 전달
                     ↓
앱 내부 Router가 특정 화면으로 이동

이 과정은 대부분 시스템 내부에서 이어지므로 사용자 화면에는 드러나지 않는다. 사용자가 보는 것은 링크를 누르기 전 화면과 전환된 앱 화면뿐이다.

마법처럼 보였던 건 과정이 없어서가 아니라, 그 과정이 잘 숨겨져 있었기 때문이다.

결론

처음 질문으로 돌아가보자.

웹페이지는 어떻게 해당 디바이스에 설치된 앱을 직접 실행할 수 있을까?

정확히는 웹페이지가 앱을 직접 실행하는 것이 아니다.

웹페이지는 URL 탐색을 요청한다. 브라우저는 이 요청이 외부 앱으로 이동해도 되는 요청인지 판단한다.

요청이 시스템에 전달되면 OS가 설치된 앱의 URL 등록 정보와 검증 상태를 조회한다. 처리할 앱이 정해지면 OS가 앱을 실행하거나 포그라운드로 전환하고 원래 URL을 전달한다.

마지막으로 앱 내부 Router가 URL을 특정 화면으로 변환한다.

Web Page  -> URL 탐색 요청
Browser   -> 외부 이동 정책 검사
OS        -> 설치 앱 탐색 및 실행
App       -> URL을 내부 화면으로 라우팅

Custom Scheme은 OS에 등록된 scheme 이름을 기준으로 앱을 찾는다. 간단하지만 소유권 검증과 자연스러운 웹 fallback이 없다.

App Links와 Universal Links는 HTTPS URL에 웹사이트와 앱의 양방향 신뢰 관계를 추가한다. 앱이 없으면 웹으로 가고, 검증된 앱이 있으면 OS가 앱으로 라우팅한다.

결국 URL은 단순한 웹 주소가 아니다.

어떤 주체가 이 요청을 처리할지 결정하기 위한 라우팅 메시지에 가깝다.

링크 한 번 뒤에서는 앱과 도메인의 연결 정보가 미리 등록되고 검증된다. 실제 링크가 눌리면 브라우저가 요청을 통제하고, OS가 목적지를 찾고, 앱이 전달받은 URL을 화면으로 라우팅한다.

너무 짧은 순간에 이어서 실행되기 때문에 그 과정이 보이지 않았을 뿐이다. 내부를 끝까지 따라가 보니 생략된 단계는 없었다.

마법은 없었다.

Reference

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글