Android Deep Link와 App Links 완벽 가이드 — Intent Filter, assetlinks.json, Navigation Component와 보안
Android 앱을 개발하다 보면 웹사이트와 앱을 연결해야 하는 상황이 생깁니다.
예를 들어 조류 도감 앱에서 참새 상세 정보를 다른 사람에게 공유한다고 가정하겠습니다.
일반적으로 다음과 같은 URL을 전달할 수 있습니다.
https://jkpark.kr/birds/123이 주소를 받은 사람이 Android 스마트폰에서 링크를 누르면 어떻게 될까요?
앱이 설치되어 있다면 조류 도감 앱의 참새 상세 화면을 바로 열고, 앱이 설치되어 있지 않다면 웹사이트에서 정보를 보여줄 수 있다면 편리할 것입니다.
이러한 기능을 구현할 때 사용하는 기술이 Deep Link와 Android App Links입니다.
하지만 단순히 AndroidManifest.xml에 Intent Filter를 추가한다고 해서 모든 링크가 앱으로 연결되는 것은 아닙니다.
특히 Android 12 이후에는 웹 도메인 검증이 중요해졌습니다.
이번 글에서는 다음 내용을 실전 코드와 함께 살펴보겠습니다.
Deep Link와 Android App Links의 차이
AndroidManifest.xml의 Intent Filter 설정
웹사이트 도메인 소유권 검증
assetlinks.json생성 및 Nginx 배포Google Play 앱 서명 인증서 확인
Navigation Component를 이용한 상세 화면 이동
Activity에서 직접 Deep Link 처리
앱이 설치되지 않은 경우 웹사이트 연결
ADB를 이용한 검증과 오류 해결
보안 문제와 Android 15 이후 동적 App Links
작성 기준: 2026년 10월. Kotlin, XML, AndroidX Navigation, minSdk 24 이상을 가정합니다. 예제의 패키지명과 URL은 설명용이며 실제 프로젝트 환경에 맞게 변경해야 합니다.
1. Deep Link란?
Deep Link는 앱의 첫 화면이 아니라 특정 기능이나 콘텐츠 화면으로 직접 이동하도록 만드는 링크입니다.
예를 들어 일반적인 앱 실행 흐름은 다음과 같습니다.
앱 아이콘 선택
↓
메인 화면
↓
검색 화면
↓
참새 검색
↓
참새 상세 화면Deep Link를 사용하면 다음과 같이 이동할 수 있습니다.
https://jkpark.kr/birds/123
↓
조류 도감 앱 실행
↓
참새 상세 화면사용자는 앱 안에서 여러 화면을 거칠 필요가 없습니다.
이 기능은 콘텐츠 공유, 알림, 이메일, QR 코드, 웹사이트 연결 등에 활용할 수 있습니다.
2. Deep Link와 Android App Links의 차이
Android에서는 비슷한 목적의 링크를 여러 방식으로 구현할 수 있습니다.
| 구분 | 일반 Deep Link | Android App Links |
|---|---|---|
| URL 형식 | 커스텀 스킴 또는 HTTP(S) | HTTP(S) |
| 예시 | birdsapp://birds/123 | https://jkpark.kr/birds/123 |
| 도메인 검증 | 일반적으로 없음 | Digital Asset Links 검증 |
| 다른 앱과 충돌 | 가능 | 검증된 도메인에 대해 신뢰성 향상 |
| 앱 미설치 시 | 별도 대응 필요 | 일반 웹 URL 사용 가능 |
| 추천 용도 | 내부 연동·특수한 앱 간 호출 | 웹사이트와 앱의 콘텐츠 연결 |
핵심은 App Links가 도메인 소유권 검증을 사용하는 HTTPS 기반 Deep Link라는 점입니다.
웹사이트와 앱을 함께 운영한다면 일반적으로 App Links를 우선 고려하는 것이 좋습니다.
3. 커스텀 URL 스킴 방식
커스텀 URL 스킴은 다음과 같은 주소를 사용합니다.
birdsapp://birds/123AndroidManifest.xml에는 다음과 같이 등록할 수 있습니다.
<activity
android:name=".MainActivity"
android:exported="true">
<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="birdsapp"
android:host="birds" />
</intent-filter>
</activity>이렇게 하면 birdsapp://birds/123 형식의 URI를 처리할 수 있습니다.
하지만 커스텀 스킴에는 한계가 있습니다.
다른 앱에서도 동일한 스킴을 등록할 수 있기 때문입니다.
따라서 커스텀 스킴만으로 앱의 신원을 검증하거나 민감한 인증 정보를 전달해서는 안 됩니다.
4. Android App Links의 동작 구조
App Links는 HTTPS 주소와 Android 앱을 연결합니다.
사용자
│
▼
https://jkpark.kr/birds/123
│
▼
Android 링크 처리
│
├── 도메인 검증 및 사용자 설정 확인
│ │
│ └── 앱으로 열기
│ │
│ ▼
│ 조류 상세 화면
│
└── 웹에서 열기
│
▼
웹사이트App Links를 구성하려면 크게 세 가지 요소가 필요합니다.
Android 앱의 Intent Filter
웹사이트의
assetlinks.json앱 서명 인증서의 SHA-256 지문
이 세 요소가 일치해야 도메인 검증이 성공할 수 있습니다.
5. AndroidManifest.xml에 App Links 등록
먼저 앱이 처리할 웹 주소를 선언합니다.
<activity
android:name=".MainActivity"
android:exported="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="jkpark.kr"
android:pathPrefix="/birds/" />
</intent-filter>
</activity>여기서 중요한 속성을 살펴보겠습니다.
android
Android에 도메인 소유권 검증을 요청합니다.
android:autoVerify="true"단, 이 속성만 추가한다고 검증이 완료되는 것은 아닙니다.
웹사이트에 올바른 Digital Asset Links 파일이 있어야 합니다.
android
URL의 프로토콜입니다.
android:scheme="https"App Links에는 HTTPS를 사용합니다.
android
연결할 도메인입니다.
android:host="jkpark.kr"android
앱에서 처리할 URL 경로를 제한합니다.
android:pathPrefix="/birds/"예를 들어 다음 URL은 매칭됩니다.
https://jkpark.kr/birds/123반면 다음 URL은 해당 필터에 매칭되지 않습니다.
https://jkpark.kr/privacy이렇게 경로를 제한하면 앱과 관련 없는 웹페이지까지 가로채는 문제를 줄일 수 있습니다.
6. Intent Filter의 세 가지 필수 구성 요소
App Links를 설정할 때 다음 세 가지를 자주 사용합니다.
<action
android:name="android.intent.action.VIEW" />
<category
android:name="android.intent.category.DEFAULT" />
<category
android:name="android.intent.category.BROWSABLE" />각 역할은 다음과 같습니다.
| 구성 요소 | 역할 |
|---|---|
| ACTION_VIEW | URI 콘텐츠 열기 |
| DEFAULT | 일반적인 암시적 Intent 처리 |
| BROWSABLE | 브라우저 등 외부에서 링크 실행 허용 |
외부 앱에서 링크를 통해 Activity를 실행하려면 적절한 Intent Filter가 필요합니다.
또한 Android 12(API 31) 이상을 대상으로 하고 Intent Filter가 있는 Activity는 android:exported 값을 명시해야 합니다.
외부 링크를 받는 Activity에는 일반적으로 다음과 같이 설정합니다.
android:exported="true"다만 외부에 노출된 Activity는 신뢰할 수 없는 입력을 받을 수 있다는 점도 함께 고려해야 합니다.
7. www 도메인도 자동으로 연결될까?
다음 두 주소는 서로 다른 호스트입니다.
https://jkpark.kr/birds/123
https://www.jkpark.kr/birds/123Manifest에서 jkpark.kr만 선언했다면 www.jkpark.kr이 자동으로 동일하게 처리된다고 가정해서는 안 됩니다.
두 호스트를 모두 지원하려면 각 호스트에 대한 Intent Filter와 도메인 검증을 구성해야 합니다.
예를 들어 다음과 같이 별도 필터를 작성할 수 있습니다.
<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="www.jkpark.kr"
android:pathPrefix="/birds/" />
</intent-filter>하지만 실제로 www를 운영하지 않는다면 불필요하게 추가할 필요는 없습니다.
가능하면 하나의 대표 도메인을 정해 일관되게 사용하는 것이 좋습니다.
assetlinks.json 설정
8. assetlinks.json이란?
assetlinks.json은 웹사이트와 Android 앱 사이의 신뢰 관계를 선언하는 파일입니다.
이 파일은 다음 주소에 배포합니다.
https://jkpark.kr/.well-known/assetlinks.json중요한 점은 파일 경로가 정해져 있다는 것입니다.
다음 경로는 동일한 역할을 하지 않습니다.
https://jkpark.kr/assetlinks.json반드시 .well-known 디렉터리 아래에 배치해야 합니다.
9. assetlinks.json 기본 구조
다음은 예제입니다.
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.koreanbirds",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}
]각 항목의 의미는 다음과 같습니다.
| 항목 | 설명 |
|---|---|
| relation | 웹 URL 처리를 앱에 위임하는 관계 |
| namespace | Android 앱을 의미 |
| package_name | 앱의 실제 applicationId |
| sha256_cert_fingerprints | 앱 서명 인증서 SHA-256 지문 |
위 SHA-256 값은 설명을 위한 가짜 값입니다.
실제 환경에서는 반드시 앱 서명 인증서에서 확인한 값을 사용해야 합니다.
10. package_name은 namespace와 다를 수 있다
Android 프로젝트에는 다음과 같은 값이 있습니다.
android {
namespace = "com.example.koreanbirds"
defaultConfig {
applicationId = "com.example.koreanbirds"
}
}일반적으로 동일하게 설정할 수 있지만 반드시 같아야 하는 것은 아닙니다.
assetlinks.json의 package_name에는 최종 설치 APK의 applicationId를 사용해야 합니다.
예를 들어 다음과 같이 빌드 변형을 구성했다면 주의해야 합니다.
buildTypes {
debug {
applicationIdSuffix = ".debug"
}
}이 경우 Debug 앱의 applicationId는 다음과 같이 달라집니다.
com.example.koreanbirds.debug따라서 Release 앱과 Debug 앱의 도메인 검증 설정이 서로 다를 수 있습니다.
11. SHA-256 인증서 지문 확인하기
Android App Links 검증에는 앱 서명 인증서의 SHA-256 지문이 필요합니다.
로컬 키스토어 파일이 있다면 keytool로 확인할 수 있습니다.
keytool -list -v \
-keystore release-key.jks \
-alias release실행하면 인증서 정보가 표시됩니다.
Certificate fingerprints:
SHA1: ...
SHA256: ...여기서 SHA-256 값을 복사합니다.
하지만 Google Play App Signing을 사용하는 경우에는 중요한 차이가 있습니다.
12. Google Play App Signing 인증서 주의사항
Google Play를 통해 배포하는 앱은 업로드 키와 실제 배포 앱의 서명 키가 다를 수 있습니다.
예를 들어 다음과 같은 구조입니다.
개발자
│
▼
업로드 키로 AAB 서명
│
▼
Google Play
│
▼
Play App Signing 키로 배포 앱 서명
│
▼
사용자 스마트폰따라서 Google Play에서 설치한 앱의 App Links를 검증하려면 Play App Signing 인증서의 SHA-256 지문을 사용해야 합니다.
Google Play Console의 앱 무결성 관련 화면에서 앱 서명 인증서 정보를 확인할 수 있습니다.
업로드 키의 SHA-256만 등록하면 Play에서 설치한 앱의 검증이 실패할 수 있습니다.
이 부분은 App Links 오류의 대표적인 원인입니다.
13. Debug 앱도 App Links를 테스트할 수 있을까?
가능합니다.
다만 Debug 앱과 Release 앱은 서명 인증서가 다를 수 있습니다.
Debug 인증서의 지문은 다음 명령으로 확인할 수 있습니다.
keytool -list -v \
-keystore ~/.android/debug.keystore \
-alias androiddebugkey \
-storepass android \
-keypass androidDebug 앱까지 같은 도메인에서 검증하려면 해당 Debug applicationId와 인증서 지문을 assetlinks.json에 별도 항목으로 등록해야 할 수 있습니다.
다만 개발용 앱을 운영 도메인과 연결할 필요가 없다면 개발용 도메인을 분리하는 것도 좋은 방법입니다.
14. 여러 앱을 하나의 도메인에 연결하기
예를 들어 다음과 같은 앱이 있다고 가정하겠습니다.
조류 도감 앱
com.example.koreanbirds
어류 도감 앱
com.example.koreanfish두 앱이 같은 웹사이트를 사용한다면 assetlinks.json에 여러 앱을 선언할 수 있습니다.
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.koreanbirds",
"sha256_cert_fingerprints": [
"조류_앱의_실제_SHA256_지문"
]
}
},
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.koreanfish",
"sha256_cert_fingerprints": [
"어류_앱의_실제_SHA256_지문"
]
}
}
]위 코드는 구조 설명용입니다.
실제로 배포할 때는 지문 문자열을 유효한 SHA-256 값으로 교체해야 합니다.
또한 각 앱의 Intent Filter 경로를 분리하는 것이 좋습니다.
/birds/123 → 조류 도감 앱
/fish/456 → 어류 도감 앱같은 URL을 여러 앱이 처리하도록 구성하면 사용자 선택이나 기본 앱 설정에 따라 결과가 달라질 수 있습니다.
Nginx 서버 설정
15. Nginx에 assetlinks.json 배포하기
Spring Boot와 Nginx를 함께 운영하는 서버라면 assetlinks.json을 Nginx에서 정적 파일로 제공할 수 있습니다.
예를 들어 파일을 다음 위치에 배치했다고 가정하겠습니다.
/var/www/jkpark/.well-known/assetlinks.jsonNginx 설정에서는 다음과 같이 구성할 수 있습니다.
server {
listen 443 ssl;
server_name jkpark.kr;
root /var/www/jkpark;
location = /.well-known/assetlinks.json {
default_type application/json;
try_files $uri =404;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}위 설정은 예시입니다.
기존 서버의 SSL 인증서 경로, 다른 location 블록, 리버스 프록시 설정 등을 유지한 상태에서 필요한 부분만 적용해야 합니다.
특히 root 경로와 실제 파일 위치가 일치해야 합니다.
16. Nginx 설정 검사와 반영
설정을 변경한 뒤에는 문법을 확인합니다.
sudo nginx -t정상이라면 다음 명령으로 설정을 다시 읽어들입니다.
sudo systemctl reload nginx파일이 정상적으로 제공되는지 확인합니다.
curl -I \
https://jkpark.kr/.well-known/assetlinks.json정상적인 경우 다음과 같은 응답을 기대할 수 있습니다.
HTTP/2 200
content-type: application/json중요한 점은 다음과 같습니다.
HTTPS 연결이 정상이어야 함
HTTP 상태 코드가 200이어야 함
JSON 내용이 올바르게 제공되어야 함
인증 없이 접근 가능해야 함
리디렉션 없이 해당 주소에서 파일이 제공되어야 함
App Links 검증은 사용자의 브라우저 로그인 상태에 의존해서는 안 됩니다.
17. JSON 파일 유효성 검사
서버에 배포한 파일을 직접 확인합니다.
curl -fsS \
https://jkpark.kr/.well-known/assetlinks.jsonjq가 설치되어 있다면 다음과 같이 JSON 형식을 검사할 수 있습니다.
curl -fsS \
https://jkpark.kr/.well-known/assetlinks.json \
| jq .JSON 문법이 잘못되면 도메인 검증에 실패할 수 있습니다.
특히 다음 사항을 확인합니다.
마지막 항목 뒤의 불필요한 쉼표
큰따옴표 누락
패키지명 오타
SHA-256 지문 누락
잘못된 JSON 배열 구조
Navigation Component 연동
18. Navigation Component에서 Deep Link 처리하기
Navigation Component를 사용하는 앱이라면 Navigation Graph에 Deep Link를 선언할 수 있습니다.
예를 들어 조류 상세 화면의 Fragment가 다음과 같다고 가정하겠습니다.
BirdDetailFragmentNavigation Graph에 다음과 같이 작성합니다.
<fragment
android:id="@+id/birdDetailFragment"
android:name="com.example.koreanbirds.ui.BirdDetailFragment"
android:label="조류 상세">
<argument
android:name="birdId"
app:argType="string" />
<deepLink
app:uri="https://jkpark.kr/birds/{birdId}" />
</fragment>여기서 {birdId}는 URL 경로의 변수입니다.
예를 들어 다음 URL이 전달되었다고 가정하겠습니다.
https://jkpark.kr/birds/123Navigation Component는 경로에 포함된 값을 birdId 인자로 전달할 수 있습니다.
19. Fragment에서 Deep Link 인자 받기
Safe Args를 사용하지 않는 기본 예제는 다음과 같습니다.
class BirdDetailFragment : Fragment(
R.layout.fragment_bird_detail
) {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
val birdId =
arguments?.getString("birdId")
if (birdId.isNullOrBlank()) {
showInvalidBirdMessage()
return
}
loadBirdDetail(birdId)
}
}birdId를 받아 SQLite나 서버 API에서 상세 정보를 조회할 수 있습니다.
예를 들어 다음과 같은 구조입니다.
Deep Link
│
▼
BirdDetailFragment
│
▼
BirdDetailViewModel
│
▼
BirdRepository
│
├── SQLite 조회
│
└── Retrofit API 요청실제 데이터 조회는 메인 스레드를 차단하지 않도록 Coroutine 등을 사용해 처리하는 것이 좋습니다.
20. URL 인자 검증하기
Deep Link는 외부에서 전달되는 입력입니다.
따라서 URL에 포함된 ID를 무조건 신뢰해서는 안 됩니다.
예를 들어 다음과 같은 URL이 전달될 수 있습니다.
https://jkpark.kr/birds/abc또는 다음과 같은 URL도 전달될 수 있습니다.
https://jkpark.kr/birds/999999999앱에서 숫자 ID만 허용한다면 다음과 같이 검증할 수 있습니다.
val birdId = arguments
?.getString("birdId")
?.toLongOrNull()
if (birdId == null || birdId <= 0L) {
showInvalidBirdMessage()
return
}실제 데이터베이스에서 해당 ID가 존재하는지도 확인해야 합니다.
잘못된 ID가 전달되었다는 이유로 앱이 종료되어서는 안 됩니다.
21. Navigation Component의 자동 Deep Link 처리
Navigation Component와 NavHostFragment를 사용하면 Activity가 처음 생성될 때 Deep Link를 자동으로 처리하는 구성이 가능합니다.
따라서 다음 코드를 무조건 추가해서는 안 됩니다.
navController.handleDeepLink(intent)이미 자동 처리된 Intent에 대해 다시 호출하면 중복 이동이나 예상하지 못한 백스택 동작이 발생할 수 있습니다.
일반적으로는 다음 두 상황을 구분해야 합니다.
Activity가 새로 생성되면서 Deep Link를 받는 경우
이미 실행 중인 Activity가
onNewIntent()로 새로운 링크를 받는 경우
특히 singleTop 등의 실행 모드를 사용한다면 두 번째 상황을 테스트해야 합니다.
22. onNewIntent에서 Deep Link 처리하기
이미 실행 중인 Activity가 새 Intent를 받는 경우 다음과 같이 처리할 수 있습니다.
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val navHostFragment =
supportFragmentManager
.findFragmentById(
R.id.nav_host_fragment
) as? NavHostFragment
navHostFragment
?.navController
?.handleDeepLink(intent)
}이 코드는 MainActivity가 NavHostFragment를 사용하고, 새 Intent가 전달되는 실행 모드로 구성된 경우의 예제입니다.
실제 프로젝트에서는 기존 Deep Link 처리 흐름과 중복되지 않도록 해야 합니다.
또한 Activity 실행 모드에 따라 onNewIntent()가 호출되지 않고 새 Activity 인스턴스가 생성될 수 있습니다.
따라서 Manifest의 launchMode와 백스택 동작을 함께 확인해야 합니다.
Navigation Component를 사용하지 않는 경우
23. Activity에서 URI 직접 분석하기
Navigation Component를 사용하지 않는 앱이라면 Intent.data에서 URI를 직접 가져올 수 있습니다.
val uri = intent?.data예를 들어 다음 URL이 전달되었다고 가정하겠습니다.
https://jkpark.kr/birds/123다음과 같이 경로를 분석할 수 있습니다.
val segments = uri?.pathSegments
if (
uri?.scheme == "https" &&
uri.host == "jkpark.kr" &&
segments?.size == 2 &&
segments[0] == "birds"
) {
val birdId = segments[1]
openBirdDetail(birdId)
}여기서 중요한 점은 호스트와 경로를 함께 검증해야 한다는 것입니다.
단순히 URL에 birds라는 문자열이 포함되어 있다는 이유만으로 상세 화면을 열어서는 안 됩니다.
24. 안전한 URI 파서 만들기
여러 화면에서 URL을 처리한다면 파싱 로직을 별도 함수로 분리하는 것이 좋습니다.
fun parseBirdId(uri: Uri?): Long? {
if (uri == null) return null
if (uri.scheme != "https") return null
if (uri.host != "jkpark.kr") return null
val segments = uri.pathSegments
if (segments.size != 2) return null
if (segments[0] != "birds") return null
val id = segments[1].toLongOrNull()
?: return null
return id.takeIf { it > 0L }
}이 함수는 다음 조건을 모두 확인합니다.
HTTPS인지
허용된 도메인인지
경로 구조가 올바른지
숫자 ID인지
유효한 양수인지
이러한 검증은 앱 외부에서 전달되는 데이터를 처리할 때 기본적으로 적용하는 것이 좋습니다.
25. Deep Link를 통한 내부 화면 이동
URI 검증이 완료되었다면 내부 Activity로 이동할 수 있습니다.
val birdId = parseBirdId(intent.data)
if (birdId != null) {
val detailIntent = Intent(
this,
BirdDetailActivity::class.java
).apply {
putExtra("bird_id", birdId)
}
startActivity(detailIntent)
}이때 내부 Activity는 외부에 직접 노출할 필요가 없다면 다음과 같이 설정할 수 있습니다.
<activity
android:name=".BirdDetailActivity"
android:exported="false" />외부 URL을 받는 Activity에서 입력값을 검증한 뒤 내부 Activity로 전달하는 구조입니다.
웹사이트와 앱 연결
26. 앱이 설치되지 않은 경우
App Links의 장점 중 하나는 일반 HTTPS URL을 사용한다는 것입니다.
예를 들어 다음 주소를 공유했다고 가정하겠습니다.
https://jkpark.kr/birds/123앱이 설치되어 있지 않거나 링크가 앱으로 처리되지 않는 상황에서는 웹브라우저에서 해당 URL을 열 수 있습니다.
따라서 웹사이트에서도 같은 콘텐츠를 제공하는 것이 좋습니다.
예를 들어 Spring Boot에서는 다음과 같은 라우팅을 구성할 수 있습니다.
@Controller
public class BirdPageController {
@GetMapping("/birds/{id}")
public String birdDetail(
@PathVariable Long id,
Model model
) {
// 실제 서비스에서는 Repository 조회 및
// 존재하지 않는 ID 처리 필요
model.addAttribute("birdId", id);
return "birds/detail";
}
}이렇게 하면 앱이 없는 사용자도 웹페이지에서 정보를 확인할 수 있습니다.
단, 웹사이트에 실제 콘텐츠 페이지가 존재해야 하며 App Links 설정만으로 웹페이지가 자동 생성되는 것은 아닙니다.
27. 웹과 앱의 URL 구조 통일하기
웹사이트와 Android 앱에서 같은 콘텐츠 ID를 사용하면 링크 관리가 편리해집니다.
예를 들어 다음과 같은 구조입니다.
웹사이트
/birds/123
/birds/124
/birds/125Android 앱
BirdDetailFragment(birdId = 123)
BirdDetailFragment(birdId = 124)
BirdDetailFragment(birdId = 125)이렇게 구성하면 공유 URL을 하나로 통일할 수 있습니다.
다만 앱의 SQLite ID와 서버 데이터베이스 ID가 서로 다르다면 별도의 매핑 전략이 필요합니다.
가능하면 앱과 서버에서 동일하게 식별할 수 있는 안정적인 콘텐츠 ID를 사용하는 것이 좋습니다.
28. 공유 기능 구현하기
앱에서 현재 조류 정보를 다른 사용자에게 공유하려면 ACTION_SEND를 사용할 수 있습니다.
fun shareBird(
context: Context,
birdId: Long
) {
val url =
"https://jkpark.kr/birds/$birdId"
val intent = Intent(
Intent.ACTION_SEND
).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
url
)
}
context.startActivity(
Intent.createChooser(
intent,
"조류 정보 공유"
)
)
}사용자는 카카오톡, 문자, 이메일 등 지원되는 앱을 통해 URL을 공유할 수 있습니다.
링크를 받은 사용자의 Android 기기에서는 App Links 검증 상태와 사용자 설정에 따라 앱 또는 웹브라우저가 열립니다.
보안 고려사항
29. Deep Link는 외부 입력이다
Deep Link는 다른 앱이나 웹브라우저에서 호출할 수 있습니다.
따라서 다음과 같은 위험을 고려해야 합니다.
잘못된 ID 전달
예상하지 못한 URI 형식
비정상적으로 긴 파라미터
권한이 없는 콘텐츠 접근
악의적인 리디렉션
인증 정보 노출
특히 외부에서 전달된 URL을 그대로 WebView에 로드하거나 다른 Intent로 전달하는 구조는 주의해야 합니다.
허용된 도메인과 경로를 확인하고, 필요한 경우 인증 및 접근 권한을 다시 검사해야 합니다.
30. Deep Link에 민감한 정보를 넣지 않기
다음과 같은 URL은 피하는 것이 좋습니다.
https://jkpark.kr/login?password=123456또는 다음과 같은 형태도 신중해야 합니다.
https://jkpark.kr/account?token=SECRET_TOKENURL은 브라우저 기록, 로그, 공유 메시지 등에 남을 수 있습니다.
따라서 비밀번호나 장기간 유효한 인증 토큰을 일반적인 공유 URL에 포함하지 않아야 합니다.
인증을 위한 일회성 링크가 필요한 경우에는 짧은 유효기간, 서버 검증, 재사용 방지 등 별도의 보안 설계가 필요합니다.
31. 로그인 상태가 필요한 Deep Link
일부 화면은 로그인한 사용자만 접근할 수 있습니다.
예를 들어 다음과 같은 링크입니다.
https://jkpark.kr/my/memos/123이 경우 다음 흐름을 고려할 수 있습니다.
Deep Link 수신
│
▼
로그인 상태 확인
│
├── 로그인됨
│ │
│ ▼
│ 접근 권한 검사
│ │
│ ▼
│ 메모 상세 화면
│
└── 로그인 안 됨
│
▼
로그인 화면
│
▼
로그인 성공
│
▼
접근 권한 검사
│
▼
메모 상세 화면중요한 점은 Deep Link에 포함된 ID를 알고 있다는 이유만으로 접근을 허용해서는 안 된다는 것입니다.
사용자의 인증 상태와 해당 콘텐츠에 대한 접근 권한을 별도로 확인해야 합니다.
Android 15 이후 Dynamic App Links
32. Dynamic App Links란?
Android 15(API 35)부터는 서버의 Digital Asset Links 파일에 동적 규칙을 정의해 App Links의 처리 범위를 보다 유연하게 관리할 수 있습니다.
이를 Dynamic App Links라고 합니다.
기존 방식에서는 앱의 Manifest에 경로 규칙을 선언하고 앱 업데이트를 통해 변경하는 경우가 많았습니다.
Dynamic App Links는 지원되는 Android 환경에서 서버 측 규칙을 활용할 수 있습니다.
다만 중요한 제한이 있습니다.
서버 측 규칙만으로 Manifest에 선언되지 않은 새로운 도메인이나 스킴까지 App Links 범위를 확장할 수는 없습니다.
따라서 앱의 기본 도메인 선언은 여전히 중요합니다.
33. Dynamic App Links JSON 예제
동적 규칙은 dynamic_app_link_components를 이용해 정의할 수 있습니다.
예를 들어 다음과 같은 구조입니다.
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.koreanbirds",
"sha256_cert_fingerprints": [
"실제_SHA256_지문"
]
},
"relation_extensions": {
"delegate_permission/common.handle_all_urls": {
"dynamic_app_link_components": [
{
"/": "/birds/*"
},
{
"/": "/privacy",
"exclude": true
}
]
}
}
}
]이 예제는 구조 설명을 위한 것입니다.
실제 배포 시에는 인증서 지문을 교체하고, 동적 규칙의 지원 범위와 Manifest 선언이 일치하는지 확인해야 합니다.
또한 Android 15 미만의 기기에서는 동적 규칙을 동일하게 처리한다고 가정할 수 없습니다.
따라서 기존 Manifest 기반 App Links 동작도 함께 설계해야 합니다.
ADB 테스트와 문제 해결
34. ADB로 Deep Link 실행하기
개발 중에는 ADB로 URL을 직접 실행할 수 있습니다.
adb shell am start \
-a android.intent.action.VIEW \
-d "https://jkpark.kr/birds/123"이 명령은 Android 기기에 VIEW Intent를 전달합니다.
실행 결과는 App Links 검증 상태, 사용자 기본 앱 설정, Android 버전 등에 따라 달라질 수 있습니다.
따라서 앱이 열렸다는 사실만으로 도메인 검증이 완료되었다고 판단해서는 안 됩니다.
35. App Links 검증 상태 확인
Android 12 이상에서는 다음 명령으로 App Links 상태를 확인할 수 있습니다.
adb shell pm get-app-links \
com.example.koreanbirds결과에는 도메인별 검증 상태가 표시될 수 있습니다.
예를 들어 다음과 같은 상태를 확인할 수 있습니다.
Domain verification state:
jkpark.kr: verified실제 출력 형식은 Android 버전에 따라 다를 수 있습니다.
검증 상태를 다시 확인하도록 요청하려면 다음 명령을 사용할 수 있습니다.
adb shell pm verify-app-links \
--re-verify \
com.example.koreanbirds검증은 비동기적으로 진행될 수 있으므로 명령 실행 직후 결과가 바로 바뀌지 않을 수 있습니다.
잠시 후 다시 상태를 확인하는 것이 좋습니다.
36. App Links가 브라우저에서 열리는 이유
App Links를 설정했는데 앱이 열리지 않는다면 다음 사항을 확인해야 합니다.
| 증상 | 가능한 원인 | 확인 방법 |
|---|---|---|
| 브라우저에서 열림 | 도메인 검증 실패 | pm get-app-links |
| 검증 실패 | assetlinks.json 경로 오류 | curl로 직접 확인 |
| 검증 실패 | SHA-256 불일치 | Play App Signing 인증서 확인 |
| 검증 실패 | applicationId 불일치 | 최종 패키지명 확인 |
| 일부 링크만 실패 | Intent Filter 경로 불일치 | pathPrefix 확인 |
| www 링크 실패 | 호스트 미등록 | www 도메인 별도 확인 |
| 앱은 열리지만 화면이 잘못됨 | Navigation Graph 설정 오류 | deepLink 및 인자 확인 |
| 앱이 중복 실행됨 | Intent 중복 처리 | onCreate/onNewIntent 확인 |
| 앱이 종료됨 | 잘못된 URL 인자 | URI 검증 추가 |
| 사용자 기기에서만 실패 | 기본 링크 열기 설정 | 앱의 기본 열기 설정 확인 |
특히 가장 먼저 확인할 것은 assetlinks.json과 SHA-256 인증서 지문입니다.
37. Android 설정에서 지원 링크 확인
Android에서는 사용자가 앱의 기본 링크 열기 동작을 변경할 수 있습니다.
일반적으로 다음과 같은 경로에서 확인할 수 있습니다.
설정
↓
앱
↓
해당 앱
↓
기본으로 열기
↓
지원되는 링크 열기실제 메뉴 이름과 위치는 Android 버전 및 제조사에 따라 다를 수 있습니다.
도메인 검증이 성공했더라도 사용자 설정에 따라 링크 처리 결과가 달라질 수 있습니다.
38. Android 버전별 테스트
App Links는 Android 버전별 동작 차이를 고려해야 합니다.
| Android 버전 | 주요 테스트 |
|---|---|
| Android 7(API 24) | 기본 Deep Link 처리 |
| Android 10(API 29) | 웹 Intent 및 앱 선택 동작 |
| Android 11(API 30) | 기존 도메인 검증 동작 |
| Android 12(API 31) | 검증된 App Links 처리 |
| Android 13(API 33) | 기본 링크 열기 설정 |
| Android 14(API 34) | 도메인별 검증 상태 |
| Android 15(API 35) | Dynamic App Links |
| Android 16(API 36) | 최신 OS에서 링크 처리 및 호환성 |
테스트할 때는 다음 상황을 구분하는 것이 좋습니다.
앱이 설치된 상태
앱이 설치되지 않은 상태
Debug 앱
Google Play에서 설치한 Release 앱
링크 검증 성공
링크 검증 실패
사용자가 앱 열기를 비활성화한 상태
앱이 완전히 종료된 상태
앱이 백그라운드에 있는 상태
앱이 이미 실행 중인 상태
39. 개인 개발자에게 추천하는 App Links 구조
여러 앱과 웹사이트를 운영한다면 URL 구조를 미리 정하는 것이 좋습니다.
예를 들어 다음과 같은 구조입니다.
jkpark.kr
│
├── /birds/{id}
│ └── 조류 도감 앱
│
├── /fish/{id}
│ └── 어류 도감 앱
│
├── /calendar/{id}
│ └── 달력 앱
│
├── /privacy
│ └── 웹사이트
│
└── /.well-known/assetlinks.json
└── Android 앱 도메인 검증이 구조에서는 각 앱이 자신과 관련된 경로만 처리하도록 구성할 수 있습니다.
또한 웹사이트에 같은 콘텐츠를 제공하면 앱 설치 여부와 관계없이 URL을 공유할 수 있습니다.
단, 여러 앱을 동일한 도메인에 연결할 때는 앱별 applicationId와 인증서 지문을 정확하게 관리해야 합니다.
40. App Links 구현 체크리스트
앱과 웹사이트의 URL 구조 정의
HTTPS 도메인 준비
AndroidManifest.xml에 VIEW Intent Filter 추가
DEFAULT 및 BROWSABLE 카테고리 추가
android:autoVerify="true"설정android:exported명시처리할 경로를 적절히 제한
www와 루트 도메인 구분
최종 applicationId 확인
Play App Signing 인증서 SHA-256 확인
Debug 인증서와 Release 인증서 구분
assetlinks.json 생성
.well-known/assetlinks.json에 배포HTTPS 200 응답 확인
JSON 형식 및 Content-Type 확인
불필요한 리디렉션 제거
Nginx 정적 파일 경로 확인
Navigation Graph Deep Link 설정
URI 인자 검증
존재하지 않는 콘텐츠 처리
로그인 및 접근 권한 검사
onNewIntent 처리 여부 확인
중복 화면 이동 방지
웹사이트 대체 페이지 제공
공유 기능 테스트
ADB로 링크 실행 테스트
App Links 도메인 검증 상태 확인
Android 버전별 테스트
Google Play 배포 앱으로 최종 검증
41. 마무리
Android Deep Link와 App Links는 웹사이트와 앱을 연결하는 중요한 기능입니다.
단순히 앱을 실행하는 것을 넘어 사용자가 원하는 콘텐츠로 바로 이동할 수 있도록 만들 수 있습니다.
특히 웹사이트와 Android 앱을 함께 운영한다면 App Links를 이용해 하나의 URL을 웹과 앱에서 공통으로 사용하는 구조를 만들 수 있습니다.
핵심은 다음과 같습니다.
일반 Deep Link와 검증된 App Links의 차이를 이해합니다.
웹사이트와 연결할 때는 HTTPS 기반 App Links를 우선 고려합니다.
Intent Filter에 필요한 호스트와 경로를 명확하게 선언합니다.
assetlinks.json을 정확한 경로에 배포합니다.
Google Play App Signing 인증서의 SHA-256 지문을 사용합니다.
Navigation Component 또는 Activity에서 URI를 안전하게 처리합니다.
Deep Link 인자를 신뢰하지 않고 검증합니다.
앱이 설치되지 않은 경우에도 웹페이지가 정상적으로 열리도록 구성합니다.
Android 12 이후 도메인 검증과 사용자 기본 앱 설정을 확인합니다.
ADB와 실제 배포 앱을 이용해 최종 테스트합니다.
특히 개인 개발자가 여러 앱을 운영한다면 App Links를 공통 URL 설계에 활용할 수 있습니다.
예를 들어 조류 도감, 어류 도감, 달력 앱에서 공유한 링크가 각각의 상세 화면으로 연결되도록 구성할 수 있습니다.
App Links의 핵심은 단순히 URL로 앱을 여는 것이 아니라, 웹과 앱에서 동일한 콘텐츠에 안전하고 일관되게 접근할 수 있도록 만드는 것입니다.
참고 자료
Android Developers — Create Deep Links
https://developer.android.com/training/app-links/deep-linkingAndroid Developers — About App Links
https://developer.android.com/training/app-linksAndroid Developers — Verify App Links
https://developer.android.com/training/app-links/verify-android-applinksAndroid Developers — Configure App Links
https://developer.android.com/training/app-links/configure-assetlinksAndroid Developers — Dynamic App Links
https://developer.android.com/training/app-links/aboutAndroid Developers — Navigation Deep Links
https://developer.android.com/guide/navigation/design/deep-linkAndroid Developers — Intents and Intent Filters
https://developer.android.com/guide/components/intents-filtersGoogle Digital Asset Links
https://developers.google.com/digital-asset-linksGoogle Play — App Signing
https://support.google.com/googleplay/android-developer/answer/9842756
태그: Android, Kotlin, Deep Link, Android App Links, Intent Filter, assetlinks.json, Digital Asset Links, Navigation Component, Nginx, Spring Boot, Google Play App Signing, SHA-256, Android 12, Android 15, Android 16, Android 앱 개발