Android Deep Link와 App Links 완벽 가이드 — Intent Filter, Navigation Component, assetlinks.json과 웹사이트 연동
Android 앱을 개발하다 보면 외부에서 앱의 특정 화면을 바로 열어야 하는 상황이 발생합니다.
예를 들어 조류 정보 앱에서 참새 상세 화면을 공유한다고 가정하겠습니다.
사용자가 다음 주소를 전달받습니다.
https://example.com/birds/1001이 링크를 눌렀을 때 앱이 설치되어 있다면 조류 상세 화면으로 이동하고, 앱이 없다면 웹사이트의 상세 페이지를 표시하도록 만들 수 있습니다.
이러한 기능을 구현할 때 사용하는 것이 Deep Link와 Android App Links입니다.
처음에는 AndroidManifest.xml에 Intent Filter만 추가하면 될 것처럼 보입니다.
하지만 실제로는 Android 버전에 따른 링크 처리 방식, 도메인 소유권 인증, 앱 서명 인증서, Navigation Component, 웹서버 설정 등을 함께 고려해야 합니다.
이번 글에서는 Kotlin + XML + Navigation Component 기반 Android 앱과 Spring Boot + Nginx 웹사이트를 연결하는 과정을 단계별로 살펴보겠습니다.
작성 기준: 2026년 10월. 예제 도메인 example.com과 패키지 이름 com.example.birds는 설명용입니다. 실제 적용 시 운영 도메인, Application ID, 서명 인증서 SHA-256 값으로 변경해야 합니다.
1. Deep Link란 무엇인가?
Deep Link는 앱의 특정 화면이나 기능으로 직접 이동할 수 있는 링크입니다.
일반적으로 앱을 실행하면 메인 화면이 표시됩니다.
하지만 Deep Link를 사용하면 다음과 같이 특정 상세 화면으로 바로 이동할 수 있습니다.
일반 실행
앱 아이콘 선택
↓
메인 화면
↓
검색 화면
↓
조류 상세 화면Deep Link 실행
상세 페이지 링크 선택
↓
조류 상세 화면이 기능은 다음과 같은 상황에서 유용합니다.
웹사이트에서 앱의 상세 화면 열기
카카오톡이나 문자로 공유한 콘텐츠 열기
이메일에 포함된 링크로 앱 실행
QR코드를 이용한 특정 화면 이동
알림을 눌러 관련 화면 열기
앱 내부 콘텐츠 공유
특히 앱과 웹사이트가 동일한 콘텐츠를 제공한다면 Deep Link를 활용할 가치가 큽니다.
2. Deep Link와 App Links의 차이
Android에서 사용하는 링크 방식은 크게 다음과 같이 구분할 수 있습니다.
| 구분 | 일반 Deep Link | Android App Links |
|---|---|---|
| URL 형태 | 사용자 정의 스킴 또는 HTTP(S) | HTTP(S) |
| 예시 | birdapp://birds/1001 | https://example.com/birds/1001 |
| 도메인 인증 | 필요하지 않을 수 있음 | Digital Asset Links 인증 |
| 웹사이트 연결 | 별도 구현 필요 | 동일한 웹 URL 사용 가능 |
| 앱 미설치 시 | 일반적으로 처리 앱이 없을 수 있음 | 웹사이트로 이동 가능 |
| 다른 앱과 충돌 | 가능 | 인증된 도메인 연결로 완화 |
| 권장 용도 | 내부 연동, 특정 프로토콜 | 웹사이트와 앱의 공식 연결 |
웹사이트와 Android 앱을 함께 운영한다면 Android App Links를 우선 검토하는 것이 좋습니다.
다만 App Links도 사용자의 기본 링크 열기 설정, 도메인 인증 상태, Android 버전에 따라 실제 동작이 달라질 수 있습니다.
3. Android App Links 동작 구조
App Links는 Android 앱과 웹사이트가 서로 연결되어 있다는 사실을 검증하는 방식입니다.
전체 구조는 다음과 같습니다.
사용자가 링크 선택
https://example.com/birds/1001
│
▼
Android 시스템
│
▼
도메인 인증 확인
│
┌──────┴──────┐
│ │
인증됨 인증되지 않음
│ │
▼ ▼
앱 연결 처리 브라우저 또는
│ 사용자 선택 흐름
▼
MainActivity
│
▼
BirdDetailFragment
│
▼
KTSN = 1001 조회도메인 인증은 웹사이트의 assetlinks.json 파일을 통해 이루어집니다.
Android 시스템은 해당 파일에 등록된 앱 패키지와 서명 인증서 정보를 이용해 웹사이트와 앱의 관계를 검증합니다.
4. 구현 전 준비사항
App Links를 구현하려면 다음 항목이 필요합니다.
Android 앱의 Application ID
앱 서명 인증서 SHA-256 지문
HTTPS를 사용하는 웹사이트
웹서버에 파일을 배포할 수 있는 권한
AndroidManifest.xml 수정
링크를 처리할 Activity 또는 Fragment
실제 기기에서 링크 인증 테스트
예제에서는 다음 구성을 사용하겠습니다.
웹사이트
https://example.com
Android Application ID
com.example.birds
상세 화면 URL
https://example.com/birds/{id}5. AndroidManifest.xml에 Intent Filter 추가하기
App Links를 처리할 Activity에 Intent Filter를 추가합니다.
<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="example.com"
android:pathPrefix="/birds/" />
</intent-filter>
</activity>각 설정의 역할은 다음과 같습니다.
| 속성 | 설명 |
|---|---|
ACTION_VIEW | URL을 여는 Intent 처리 |
CATEGORY_DEFAULT | 일반적인 Intent 처리 허용 |
CATEGORY_BROWSABLE | 브라우저 등 외부에서 실행 허용 |
android:autoVerify | App Links 도메인 인증 요청 |
android:scheme | HTTPS 지정 |
android:host | 연결할 도메인 |
android:pathPrefix | 처리할 URL 경로 |
이 예제에서는 /birds/로 시작하는 경로를 처리합니다.
예를 들어 다음 주소가 대상이 됩니다.
https://example.com/birds/1001
https://example.com/birds/2002
https://example.com/birds/3003단, pathPrefix는 단순한 접두어 조건이므로 상세 URL의 ID 형식은 앱 내부에서 별도로 검증해야 합니다.
6. Intent Filter를 너무 넓게 설정하지 않기
다음과 같이 도메인 전체를 대상으로 설정할 수도 있습니다.
<data
android:scheme="https"
android:host="example.com" />하지만 이렇게 하면 웹사이트의 다양한 URL이 앱으로 전달될 수 있습니다.
예를 들어 다음과 같은 페이지까지 앱이 처리하려고 할 수 있습니다.
https://example.com/privacy
https://example.com/about
https://example.com/blog
https://example.com/birds/1001앱에서 모든 페이지를 처리할 수 없다면 사용자 경험이 나빠질 수 있습니다.
따라서 실제로 앱에서 지원하는 경로를 기준으로 Intent Filter를 설계하는 것이 좋습니다.
Android 15 이상에서는 Dynamic App Links를 통해 서버 측에서 경로 규칙을 세부적으로 관리하는 기능도 제공됩니다.
다만 동적 규칙이 Manifest에 선언한 기본 URL 범위를 임의로 확장하는 것은 아니므로 기본 범위 설계가 중요합니다.
7. assetlinks.json이란?
assetlinks.json은 웹사이트와 Android 앱의 관계를 선언하는 파일입니다.
이 파일은 다음 위치에서 제공해야 합니다.
https://example.com/.well-known/assetlinks.json파일에는 앱의 패키지 이름과 서명 인증서 SHA-256 지문이 포함됩니다.
예를 들어 다음과 같은 구조입니다.
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.birds",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:..."
]
}
}
]위 지문은 형식을 설명하기 위한 자리표시자이며 유효한 SHA-256 인증서 지문이 아닙니다.
실제로는 32바이트 SHA-256 값을 콜론으로 구분한 전체 지문을 입력해야 합니다.
8. assetlinks.json의 주요 항목
| 항목 | 설명 |
|---|---|
relation | 웹사이트와 앱 사이의 관계 |
namespace | 대상 유형 |
package_name | Android Application ID |
sha256_cert_fingerprints | 앱 서명 인증서 SHA-256 지문 |
여기서 가장 중요한 것은 패키지 이름과 서명 인증서입니다.
둘 중 하나라도 실제 설치된 앱과 일치하지 않으면 도메인 인증에 실패할 수 있습니다.
9. 앱 서명 인증서 SHA-256 확인하기
Android 앱은 서명 인증서로 식별됩니다.
개발 환경에서는 Debug 인증서를 사용하고, 배포 환경에서는 Release 인증서를 사용합니다.
Gradle의 signingReport로 인증서 정보를 확인할 수 있습니다.
./gradlew signingReport출력 결과에서 다음과 같은 항목을 찾습니다.
Variant: debug
SHA1: ...
SHA-256: ...하지만 Google Play App Signing을 사용하는 앱이라면 주의해야 합니다.
Google Play에서 사용자에게 배포하는 앱은 업로드 키가 아니라 Google Play의 앱 서명 키로 서명될 수 있습니다.
따라서 실제 배포 앱의 App Links 인증에는 Play Console에 표시된 앱 서명 인증서 SHA-256 지문을 사용해야 합니다.
10. Google Play Console에서 인증서 확인하기
Google Play Console에서 앱 서명 관련 페이지를 열면 앱 서명 인증서 정보를 확인할 수 있습니다.
일반적으로 다음 정보를 확인합니다.
앱 서명 인증서
SHA-1 인증서 지문
SHA-256 인증서 지문
업로드 키 인증서
App Links의 assetlinks.json에는 실제 설치되는 앱의 서명 인증서 지문이 필요합니다.
내부 테스트, 직접 설치한 APK, Google Play 배포 앱은 서명 인증서가 다를 수 있습니다.
따라서 테스트 환경별 인증서도 구분해야 합니다.
11. 여러 인증서 등록하기
개발 환경과 운영 환경에서 같은 도메인을 사용한다면 여러 인증서 지문을 등록할 수 있습니다.
예를 들어 다음과 같습니다.
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.birds",
"sha256_cert_fingerprints": [
"REPLACE_WITH_RELEASE_SHA256",
"REPLACE_WITH_DEBUG_SHA256"
]
}
}
]이 예제의 지문 문자열은 실제 값으로 교체해야 합니다.
다만 운영 도메인에 개발용 인증서를 추가하면 해당 개발 인증서로 서명한 앱도 도메인 관계 검증 대상이 될 수 있습니다.
따라서 보안과 운영 정책을 고려해 필요한 인증서만 등록하는 것이 좋습니다.
가능하다면 개발·스테이징 환경은 별도 도메인으로 분리하는 것도 방법입니다.
12. Nginx 서버에 assetlinks.json 배포하기
Spring Boot와 Nginx를 함께 사용하는 서버라면 Nginx에서 정적 파일을 직접 제공할 수 있습니다.
예를 들어 다음 경로에 파일을 배치합니다.
/var/www/example.com/.well-known/assetlinks.jsonNginx 설정 예제입니다.
server {
listen 443 ssl;
server_name example.com;
# SSL 인증서 설정 등은 생략
location = /.well-known/assetlinks.json {
alias /var/www/example.com/.well-known/assetlinks.json;
default_type application/json;
}
location / {
proxy_pass http://127.0.0.1:8080;
}
}이렇게 구성하면 App Links 인증 파일은 Nginx가 직접 제공하고, 일반 웹 요청은 Spring Boot로 전달할 수 있습니다.
다만 기존 Nginx 설정과 충돌하지 않도록 location 우선순위와 SSL 설정을 확인해야 합니다.
13. Nginx 설정 검증하기
설정을 수정한 뒤에는 먼저 문법을 검사합니다.
sudo nginx -t정상이라면 Nginx 설정을 다시 불러옵니다.
sudo systemctl reload nginx이후 다음 명령으로 파일 접근을 확인합니다.
curl -i \
https://example.com/.well-known/assetlinks.json정상적인 경우 다음 사항을 확인할 수 있어야 합니다.
HTTP/2 200
content-type: application/json실제 응답은 서버 환경에 따라 HTTP/1.1 또는 HTTP/2 등으로 달라질 수 있습니다.
중요한 것은 HTTPS를 통해 파일이 정상적으로 제공되고, JSON 내용이 올바르며, 인증 과정에서 리디렉션 문제가 발생하지 않는 것입니다.
14. assetlinks.json 배포 시 주의사항
다음 항목을 확인해야 합니다.
HTTPS 인증서가 정상인지
파일 경로가 정확한지
JSON 문법이 올바른지
Content-Type이 적절한지외부에서 인증 없이 접근 가능한지
리디렉션 없이 직접 제공되는지
패키지 이름이 정확한지
SHA-256 지문이 정확한지
특히 다음 경로는 서로 다릅니다.
/.well-known/assetlinks.json
/well-known/assetlinks.json반드시 앞에 점이 포함된 .well-known 경로를 사용해야 합니다.
15. MainActivity에서 Deep Link 수신하기
Manifest 설정이 완료되면 Android는 해당 URL을 처리할 수 있는 Activity로 Intent를 전달합니다.
가장 기본적인 방식은 intent.data를 확인하는 것입니다.
override fun onCreate(
savedInstanceState: Bundle?
) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
handleDeepLink(intent)
}URL 처리 함수는 다음과 같이 구현할 수 있습니다.
private fun handleDeepLink(
intent: Intent?
) {
val uri = intent?.data ?: return
if (uri.scheme != "https") return
if (uri.host != "example.com") return
val segments = uri.pathSegments
if (
segments.size == 2 &&
segments[0] == "birds"
) {
val birdId = segments[1].toLongOrNull()
?: return
openBirdDetail(birdId)
}
}외부에서 전달된 URL은 신뢰할 수 없는 입력으로 취급해야 합니다.
따라서 경로 형식과 ID 값 등을 검증한 뒤 화면을 이동시키는 것이 좋습니다.
16. 앱이 이미 실행 중일 때 링크 처리하기
앱이 실행 중인 상태에서 새 Intent가 전달되는 경우에는 onNewIntent()를 처리해야 할 수 있습니다.
override fun onNewIntent(
intent: Intent
) {
super.onNewIntent(intent)
setIntent(intent)
handleDeepLink(intent)
}다만 onNewIntent()가 호출되는지는 Activity의 launchMode와 기존 Activity 인스턴스 상태에 따라 달라집니다.
따라서 앱이 종료된 상태, 백그라운드 상태, 이미 실행 중인 상태를 각각 테스트해야 합니다.
또한 Navigation Component가 자동으로 Deep Link를 처리하도록 구성한 경우에는 수동 처리와 중복되지 않도록 설계해야 합니다.
17. Navigation Component와 Deep Link 연결하기
Navigation Component를 사용한다면 navigation graph에 Deep Link를 선언할 수 있습니다.
예를 들어 nav_graph.xml에 다음과 같이 추가합니다.
<fragment
android:id="@+id/birdDetailFragment"
android:name="com.example.birds.ui.detail.BirdDetailFragment"
android:label="@string/bird_detail_title">
<argument
android:name="birdId"
app:argType="long" />
<deepLink
app:uri="https://example.com/birds/{birdId}" />
</fragment>위 XML은 app 네임스페이스가 선언된 Navigation Graph 내부에 작성해야 합니다.
예를 들어 루트 요소에 다음 선언이 필요합니다.
xmlns:app="http://schemas.android.com/apk/res-auto"이렇게 구성하면 URL의 {birdId} 부분을 Fragment 인자로 전달할 수 있습니다.
18. Fragment에서 Deep Link 인자 읽기
Navigation Component가 전달한 인자를 다음과 같이 읽을 수 있습니다.
val birdId = requireArguments().getLong("birdId")하지만 실제로는 인자가 없거나 잘못 전달된 상황도 고려하는 것이 좋습니다.
Safe Args를 사용한다면 타입이 지정된 인자를 더 안전하게 다룰 수 있습니다.
val args by navArgs<BirdDetailFragmentArgs>()
val birdId = args.birdIdSafe Args를 사용하려면 프로젝트에 Navigation Safe Args Gradle Plugin을 별도로 구성해야 합니다.
19. Navigation Component 자동 처리와 수동 처리 구분
Deep Link 구현에서 자주 발생하는 실수 중 하나는 동일한 링크를 두 번 처리하는 것입니다.
예를 들어 다음 두 가지를 동시에 사용하는 경우입니다.
Navigation Graph의
<deepLink>선언MainActivity의
handleDeepLink()직접 호출
두 방식이 모두 화면 이동을 실행하면 상세 화면이 중복으로 열릴 수 있습니다.
따라서 다음 중 하나를 선택하는 것이 좋습니다.
방법 A. Navigation Component 중심 처리
Navigation Graph에서 Deep Link를 선언하고 NavController가 처리하도록 구성합니다.
방법 B. MainActivity 직접 처리
URL을 직접 분석한 뒤 NavController로 원하는 화면을 이동합니다.
일반적인 단일 Activity + Navigation Component 구조라면 방법 A가 관리하기 편리할 수 있습니다.
다만 앱의 초기화 상태나 인증 여부에 따라 복잡한 분기가 필요하다면 방법 B가 더 적합할 수도 있습니다.
20. Deep Link로 상세 화면을 열 때 데이터 조회하기
URL에서 전달받은 ID를 이용해 SQLite에서 상세 데이터를 조회할 수 있습니다.
예를 들어 다음과 같은 URL입니다.
https://example.com/birds/1001여기서 1001은 조류 데이터의 KTSN이라고 가정하겠습니다.
Repository에서는 다음과 같이 조회할 수 있습니다.
suspend fun getBirdById(
birdId: Long
): Bird? = withContext(Dispatchers.IO) {
database.rawQuery(
"""
SELECT
KTSN,
SpeciesKor,
Species
FROM korean_birds
WHERE KTSN = ?
LIMIT 1
""".trimIndent(),
arrayOf(birdId.toString())
).use { cursor ->
if (cursor.moveToFirst()) {
Bird(
id = cursor.getLong(0),
nameKo = cursor.getString(1),
scientificName = cursor.getString(2)
)
} else {
null
}
}
}여기서 database는 Repository가 관리하는 읽기 가능한 SQLiteDatabase 인스턴스라고 가정합니다.
실제 프로젝트에서는 SQLiteOpenHelper를 통해 데이터베이스를 열고 적절한 생명주기로 관리해야 합니다.
21. 데이터가 없는 경우 처리하기
외부 URL에서 전달받은 ID가 항상 유효하다고 가정해서는 안 됩니다.
예를 들어 다음과 같은 상황이 발생할 수 있습니다.
데이터베이스에 없는 ID
삭제된 콘텐츠
앱 버전이 오래되어 포함되지 않은 데이터
잘못된 URL
숫자가 아닌 ID
이 경우 앱이 강제 종료되지 않도록 처리해야 합니다.
예를 들어 다음과 같은 안내 화면을 제공할 수 있습니다.
해당 정보를 찾을 수 없습니다.
앱에 저장된 데이터에 없는 항목이거나
현재 버전에서 지원하지 않는 콘텐츠입니다.
[목록으로 이동]필요하다면 최신 데이터 업데이트를 안내할 수도 있습니다.
22. 웹사이트와 앱의 URL 구조 통일하기
Android App Links를 운영할 때는 웹사이트와 앱이 동일한 URL 구조를 사용하는 것이 좋습니다.
예를 들어 다음과 같습니다.
| 콘텐츠 | 웹 URL | 앱 화면 |
|---|---|---|
| 조류 상세 | /birds/1001 | BirdDetailFragment |
| 어류 상세 | /fish/2001 | FishDetailFragment |
| 장소 상세 | /places/3001 | PlaceDetailFragment |
| 공지사항 | /notice/4001 | NoticeDetailFragment |
이렇게 하면 콘텐츠 공유와 링크 관리가 쉬워집니다.
다만 하나의 앱이 모든 경로를 처리하는지, 여러 앱이 서로 다른 경로를 처리하는지에 따라 App Links 설정을 구분해야 합니다.
특히 Product Flavor를 사용하는 프로젝트에서는 Application ID와 URL 경로가 올바르게 연결되도록 주의해야 합니다.
23. Product Flavor별 App Links 구성
예를 들어 하나의 프로젝트에서 조류 앱과 어류 앱을 만든다고 가정하겠습니다.
bird Flavor
Application ID: com.example.birds
fish Flavor
Application ID: com.example.fish웹사이트의 URL은 다음과 같이 구분할 수 있습니다.
https://example.com/birds/1001
https://example.com/fish/2001각 Flavor에서 서로 다른 Intent Filter를 사용하도록 구성할 수 있습니다.
예를 들어 다음과 같은 디렉터리 구조입니다.
app/src/
main/
AndroidManifest.xml
bird/
AndroidManifest.xml
fish/
AndroidManifest.xmlFlavor별 Manifest에 해당 앱이 처리할 링크를 선언하면 됩니다.
단, Manifest 병합 결과를 확인해 의도하지 않은 Intent Filter가 다른 Flavor에 포함되지 않도록 해야 합니다.
24. 하나의 도메인에 여러 Android 앱 연결하기
하나의 도메인에서 여러 앱을 운영한다면 assetlinks.json에 여러 앱을 등록할 수 있습니다.
예를 들어 다음과 같은 구조입니다.
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.birds",
"sha256_cert_fingerprints": [
"REPLACE_WITH_BIRD_APP_SHA256"
]
}
},
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.example.fish",
"sha256_cert_fingerprints": [
"REPLACE_WITH_FISH_APP_SHA256"
]
}
}
]이때 각 앱의 Manifest에서 처리할 경로를 구분하는 것이 중요합니다.
두 앱이 동일한 URL 범위를 처리하도록 설정하면 사용자의 링크 열기 설정이나 시스템의 앱 선택 동작에 영향을 받을 수 있습니다.
25. 앱이 설치되지 않았을 때 웹사이트로 이동하기
Android App Links의 장점 중 하나는 일반 HTTPS URL을 사용한다는 것입니다.
예를 들어 사용자가 다음 링크를 누릅니다.
https://example.com/birds/1001앱이 설치되어 있고 도메인 인증 및 사용자 설정이 적절하다면 앱에서 열릴 수 있습니다.
앱이 설치되어 있지 않다면 일반 웹 URL로 처리되어 브라우저에서 페이지를 열 수 있습니다.
다만 이것은 해당 주소에 실제 웹페이지가 존재한다는 전제입니다.
따라서 웹사이트에도 대응되는 상세 페이지를 제공해야 합니다.
26. Spring Boot에서 상세 페이지 제공하기
Spring Boot에서는 다음과 같은 컨트롤러를 구성할 수 있습니다.
@Controller
@RequestMapping("/birds")
public class BirdController {
private final BirdService birdService;
public BirdController(
BirdService birdService
) {
this.birdService = birdService;
}
@GetMapping("/{id}")
public String detail(
@PathVariable Long id,
Model model
) {
Bird bird = birdService.findById(id);
if (bird == null) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND
);
}
model.addAttribute("bird", bird);
return "birds/detail";
}
}이 예제는 Thymeleaf와 같은 서버 템플릿을 사용한다고 가정한 코드입니다.
웹사이트에서 해당 URL을 열면 조류 상세 정보를 표시하고, Android에서는 동일한 URL을 App Links로 처리할 수 있습니다.
27. 공유 버튼 구현하기
앱 내부에서 콘텐츠를 공유할 때도 App Links URL을 사용할 수 있습니다.
예를 들어 다음과 같이 구현합니다.
fun shareBird(
context: Context,
birdId: Long
) {
val shareUrl =
"https://example.com/birds/$birdId"
val intent = Intent(
Intent.ACTION_SEND
).apply {
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
shareUrl
)
}
context.startActivity(
Intent.createChooser(
intent,
"공유하기"
)
)
}이렇게 하면 카카오톡, 문자, 이메일 등 공유 기능을 지원하는 앱으로 링크를 전달할 수 있습니다.
수신자는 링크를 통해 앱 또는 웹사이트에서 콘텐츠를 열 수 있습니다.
28. 알림과 Deep Link 연결하기
알림을 눌렀을 때도 특정 상세 화면으로 이동하도록 만들 수 있습니다.
예를 들어 다음과 같은 알림입니다.
새로운 조류 정보가 등록되었습니다.
참새 상세 정보를 확인해 보세요.
[알림 선택]
↓
BirdDetailFragment이 경우 알림의 PendingIntent에 Deep Link URL을 지정할 수 있습니다.
val uri = Uri.parse(
"https://example.com/birds/1001"
)
val intent = Intent(
Intent.ACTION_VIEW,
uri,
context,
MainActivity::class.java
)
val pendingIntent = PendingIntent.getActivity(
context,
1001,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_IMMUTABLE
)이 방식은 명시적인 Activity Intent에 URL 데이터를 전달합니다.
앱 내부 알림이므로 외부 URL 해석 과정을 거치지 않고 대상 Activity를 지정할 수 있습니다.
다만 실제 Navigation 백스택 구성과 Activity launchMode에 따라 뒤로 가기 동작을 별도로 검증해야 합니다.
29. Deep Link 보안 문제
Deep Link는 외부에서 앱으로 데이터를 전달하는 통로입니다.
따라서 외부에서 전달된 URL을 신뢰해서는 안 됩니다.
예를 들어 다음과 같은 URL이 전달될 수 있습니다.
https://example.com/birds/abc
https://example.com/birds/-1
https://example.com/birds/999999999999앱에서는 입력값을 검증해야 합니다.
val birdId = uri.lastPathSegment
?.toLongOrNull()
if (birdId == null || birdId <= 0L) {
return
}또한 다음과 같은 작업은 주의해야 합니다.
URL 파라미터를 SQL 문자열에 직접 연결
URL에 포함된 파일 경로를 그대로 사용
외부 URL을 검증 없이 WebView에서 실행
Deep Link만으로 인증이 필요한 화면에 접근 허용
민감한 정보를 URL에 포함
Deep Link는 화면 이동을 위한 입력 경로일 뿐, 사용자 인증이나 접근 권한을 대신하지 않습니다.
30. 인증이 필요한 화면 처리하기
로그인이 필요한 화면을 Deep Link로 열어야 한다면 인증 상태를 먼저 확인해야 합니다.
예를 들어 다음과 같은 흐름입니다.
Deep Link 수신
│
▼
로그인 상태 확인
│
├── 로그인됨
│ └── 상세 화면 이동
│
└── 로그인되지 않음
│
▼
로그인 화면
│
▼
로그인 완료
│
▼
원래 상세 화면이 경우 원래 Deep Link 목적지를 안전하게 보관했다가 로그인 완료 후 이동할 수 있습니다.
단, 외부에서 전달된 URL만으로 인증이 필요한 데이터를 조회하거나 수정할 수 있도록 구현해서는 안 됩니다.
서버에서도 사용자 인증과 권한 검증을 수행해야 합니다.
31. ADB로 Deep Link 테스트하기
Deep Link를 구현한 뒤에는 ADB로 테스트할 수 있습니다.
예를 들어 다음 명령을 실행합니다.
adb shell am start \
-a android.intent.action.VIEW \
-d "https://example.com/birds/1001"이 명령은 지정한 URL을 처리할 수 있는 Activity를 실행하도록 요청합니다.
다만 브라우저나 다른 앱이 링크를 처리할 수도 있으므로, 실행 결과만으로 App Links 도메인 인증이 성공했다고 판단해서는 안 됩니다.
앱 내부 라우팅을 별도로 테스트하려면 패키지를 지정해 실행할 수도 있습니다.
adb shell am start \
-a android.intent.action.VIEW \
-d "https://example.com/birds/1001" \
-p com.example.birds하지만 패키지를 강제로 지정하는 테스트는 실제 사용자 환경의 링크 선택 동작을 완전히 재현하지 않습니다.
따라서 도메인 인증 테스트와 앱 내부 라우팅 테스트를 구분해야 합니다.
32. App Links 도메인 인증 상태 확인하기
Android 12 이상에서는 다음 명령을 사용할 수 있습니다.
adb shell pm get-app-links \
com.example.birds이 명령으로 앱의 도메인 인증 상태를 확인할 수 있습니다.
테스트 환경에서 재검증이 필요하다면 다음 명령을 사용할 수 있습니다.
adb shell pm verify-app-links \
--re-verify com.example.birds검증 결과가 즉시 반영되지 않을 수 있으므로 일정 시간 후 상태를 다시 확인해야 할 수 있습니다.
또한 사용자 설정으로 링크 열기가 변경되어 있다면 도메인 인증 상태와 실제 링크 실행 결과가 다를 수 있습니다.
33. Android 설정에서 링크 열기 확인하기
기기 설정에서도 앱의 링크 열기 상태를 확인할 수 있습니다.
일반적으로 다음과 같은 항목을 찾을 수 있습니다.
설정
↓
앱
↓
대상 앱
↓
기본으로 열기
↓
지원되는 링크 열기메뉴 이름과 위치는 Android 버전 및 제조사 UI에 따라 달라질 수 있습니다.
여기서 해당 도메인이 앱과 연결되어 있는지 확인할 수 있습니다.
34. App Links가 동작하지 않는 대표적인 원인
원인 1. assetlinks.json 경로 오류
다음 경로가 정확한지 확인합니다.
https://example.com/.well-known/assetlinks.json원인 2. SHA-256 지문 불일치
Debug 인증서와 Google Play 앱 서명 인증서를 혼동했을 수 있습니다.
원인 3. Application ID 불일치
Manifest 패키지 구조가 아니라 실제 설치된 앱의 Application ID를 기준으로 확인해야 합니다.
원인 4. HTTPS 인증서 문제
SSL 인증서가 올바르지 않으면 도메인 인증에 실패할 수 있습니다.
원인 5. URL 경로 불일치
Manifest의 pathPrefix와 실제 URL이 일치하는지 확인해야 합니다.
원인 6. 앱의 링크 열기 설정 비활성화
사용자가 시스템 설정에서 지원 링크 열기를 비활성화했을 수 있습니다.
원인 7. 여러 앱의 URL 범위 중복
두 앱이 동일한 도메인과 경로를 처리하도록 설정되어 있을 수 있습니다.
35. Android 15 Dynamic App Links
Android 15에서는 App Links의 서버 측 규칙을 더 유연하게 관리할 수 있는 Dynamic App Links 기능이 도입되었습니다.
기존에는 Manifest에 선언한 URL 범위를 변경하려면 앱 업데이트가 필요한 경우가 많았습니다.
Dynamic App Links를 사용하면 지원 환경에서 assetlinks.json의 동적 규칙을 이용해 URL 처리 범위를 세부적으로 조정할 수 있습니다.
다만 다음 사항에 주의해야 합니다.
Android 버전에 따른 지원 차이
Manifest에 선언한 기본 범위
서버 측 규칙의 적용 범위
이전 Android 버전에서의 동작
사용자 링크 열기 설정
따라서 모든 Android 버전에서 동일하게 동작한다고 가정해서는 안 됩니다.
운영 환경에서는 기존 Manifest 기반 App Links와의 호환성을 함께 고려해야 합니다.
36. 웹사이트 SEO와 App Links
App Links를 사용한다고 해서 웹사이트 SEO가 자동으로 개선되는 것은 아닙니다.
하지만 앱과 웹사이트에서 동일한 콘텐츠 URL을 사용하면 콘텐츠 공유와 관리 측면에서 장점이 있습니다.
예를 들어 다음과 같은 구조를 사용할 수 있습니다.
https://example.com/birds/1001웹사이트에서는 검색 엔진이 접근할 수 있는 상세 페이지를 제공하고, Android에서는 같은 URL을 App Links로 처리합니다.
웹페이지에는 다음과 같은 SEO 요소를 적용할 수 있습니다.
적절한 title
meta description
Open Graph 이미지
구조화된 데이터
canonical URL
검색 엔진이 접근 가능한 본문 콘텐츠
App Links는 이러한 SEO 설정을 대체하는 기능이 아닙니다.
웹사이트의 검색 노출과 앱 연결 기능은 각각 관리해야 합니다.
37. 실무에서 추천하는 URL 설계
개인 개발자가 여러 앱과 웹사이트를 운영한다면 URL 구조를 처음부터 일관되게 설계하는 것이 좋습니다.
예를 들어 다음과 같습니다.
https://example.com/birds/{id}
https://example.com/fish/{id}
https://example.com/places/{id}
https://example.com/articles/{id}이 구조의 장점은 다음과 같습니다.
콘텐츠 유형별 경로 구분
App Links Intent Filter 관리 용이
Spring Boot Controller 구성 용이
공유 URL 일관성
향후 iOS Universal Links 확장 가능
앱 미설치 사용자에게 웹페이지 제공 가능
다만 ID를 변경하거나 콘텐츠를 삭제하는 경우에는 기존 링크가 깨지지 않도록 리디렉션 또는 오류 페이지 정책을 마련해야 합니다.
38. Android App Links 테스트 체크리스트
HTTPS 도메인 준비
실제 Application ID 확인
앱 서명 인증서 SHA-256 확인
Google Play 앱 서명 인증서 확인
assetlinks.json 생성
.well-known경로에 배포Nginx에서 JSON 응답 확인
HTTPS 인증서 확인
불필요한 리디렉션 제거
Manifest Intent Filter 추가
android:autoVerify="true"설정BROWSABLE카테고리 선언URL 경로 범위 확인
Deep Link 입력값 검증
Navigation Component 연결
중복 화면 이동 방지
SQLite 상세 데이터 조회
존재하지 않는 ID 처리
앱 종료 상태에서 링크 실행
앱 백그라운드 상태에서 링크 실행
앱 실행 중 링크 실행
뒤로 가기 동작 확인
ADB로 링크 실행 테스트
pm get-app-links인증 상태 확인앱 미설치 상태에서 웹페이지 확인
Product Flavor별 링크 분리
Google Play 배포 앱으로 최종 검증
39. 개인 개발자에게 추천하는 구현 순서
처음부터 Dynamic App Links와 복잡한 라우팅을 모두 구현할 필요는 없습니다.
다음 순서로 진행하는 것이 현실적입니다.
1단계. URL 구조 설계
앱의 어떤 화면을 외부 URL로 열 것인지 결정합니다.
2단계. 웹 상세 페이지 구현
앱이 설치되지 않아도 동일한 URL에서 콘텐츠를 볼 수 있도록 합니다.
3단계. Manifest Intent Filter 추가
지원할 도메인과 경로를 선언합니다.
4단계. assetlinks.json 배포
패키지 이름과 서명 인증서를 등록합니다.
5단계. Deep Link 라우팅 구현
URL에서 ID를 추출해 상세 화면으로 이동합니다.
6단계. Navigation Component 연결
기존 앱의 화면 이동 구조와 통합합니다.
7단계. 공유 기능 구현
앱에서 콘텐츠 URL을 외부로 공유할 수 있도록 합니다.
8단계. 실제 배포 앱 테스트
Google Play 앱 서명 인증서로 설치된 앱에서 도메인 인증을 확인합니다.
40. 마무리
Android Deep Link와 App Links는 앱과 웹사이트를 연결하는 중요한 기능입니다.
특히 콘텐츠 중심 앱에서는 사용자가 공유한 URL을 통해 원하는 상세 화면으로 바로 이동할 수 있다는 장점이 있습니다.
구현할 때는 다음 원칙을 기억하는 것이 좋습니다.
웹사이트와 앱에서 동일한 HTTPS URL을 사용합니다.
Android App Links를 이용해 도메인 소유권을 인증합니다.
assetlinks.json에는 정확한 Application ID와 SHA-256 지문을 등록합니다.
Google Play App Signing의 실제 앱 서명 인증서를 확인합니다.
Manifest에는 앱이 처리할 URL 범위를 명확하게 선언합니다.
Navigation Component와 수동 라우팅이 중복되지 않도록 합니다.
외부 URL의 ID와 파라미터를 반드시 검증합니다.
앱이 설치되지 않아도 웹페이지를 제공하도록 설계합니다.
Product Flavor별 URL과 앱 연결을 구분합니다.
ADB와 실제 배포 앱에서 도메인 인증을 테스트합니다.
Android 앱과 Spring Boot 웹사이트를 함께 운영한다면 App Links는 단순한 화면 이동 기능을 넘어 콘텐츠 공유와 서비스 연결 구조를 개선하는 데 도움이 됩니다.
특히 조류·어류 도감, 장소 정보, 지역 정보처럼 개별 콘텐츠에 고유한 ID가 있는 앱이라면 비교적 명확한 URL 구조를 만들 수 있습니다.
App Links의 핵심은 앱을 강제로 실행하는 것이 아니라, 사용자가 하나의 URL을 통해 앱과 웹 어디에서든 같은 콘텐츠에 접근할 수 있도록 만드는 것입니다.
참고 자료
Android Developers — Create Deep Links
https://developer.android.com/training/app-links/deep-linkingAndroid Developers — Android App Links
https://developer.android.com/training/app-linksAndroid Developers — Verify Android App Links
https://developer.android.com/training/app-links/verify-android-applinksAndroid Developers — Dynamic App Links
https://developer.android.com/training/app-links/configure-assetlinksAndroid Developers — Navigation Deep Links
https://developer.android.com/guide/navigation/design/deep-linkAndroid Developers — Intent Filters
https://developer.android.com/guide/components/intents-filtersGoogle Digital Asset Links
https://developers.google.com/digital-asset-linksAndroid Developers — App Signing
https://developer.android.com/studio/publish/app-signing
태그: Android, Kotlin, Deep Link, App Links, Android App Links, Intent Filter, Navigation Component, assetlinks.json, Digital Asset Links, SHA-256, Google Play App Signing, Nginx, Spring Boot, Product Flavor, Android 앱 웹사이트 연동