
모바일 브라우저로 브라우징을 하다보면, 웹 링크를 클릭했을 때 브라우저 대신 네이티브 앱이 바로 열리는 경험을 경험한 적이 있을 것이다. 혹은 앱 안에서 웹사이트 링크로 보이는 부분을 클릭한 경우도 마찬가지이다. 이는 Android App Links를 통해 구현된 것이다. 이 글에서는 도메인 구매부터 실제 화면 구동까지 전체 절차를 단계별로 정리하는 것에 목적이 있다.
목차
1. 도메인 준비 및 웹 서버 설정
1-1. 도메인 구매
App Links를 사용하려면 본인이 소유한 도메인이 필요하다. 가비아, 카페24, Namecheap, Google Domains 등에서 구매할 수 있다. 예: myapp.xyz, example.com
나의 경우에는 https://hosting.kr 에서 구입하였다. 대체적으로 저렴한 도메인이 많고, 한국사람 기준으로는 관리가 편하다. 또한 KRW로 결제가 되어서 환율이 높은 요즘에는 오히려 편하다는 생각도 들어서 이곳을 추천한다. 2026년 3월 기준으로 대충 시세는 아래와 같다. 참고하자.(대체로 1년까지는 저렴이 이벤트를 하지만, 그 이후 연장시에는 비싼 가격 그대로 내야하니, 파일럿 개념이거나 한번 시험삼아 해보는 수준이라면 아래처럼 임시 도메인을 작성해서 비싼 이름도 한번 써봐도 된다.(.life 이런 도메인도 평소엔 6만원인데, 이벤트로 3천원에 쓸 수 있다!)

1-2. 웹 서버 및 DNS 설정
도메인을 구매한 후 다음이 필요하다.
- 웹 호스팅 또는 정적 사이트 호스팅 (GitHub Pages, Vercel, Netlify, Firebase Hosting 등) - 물론 개인 NAS가 있다면 이것도 좋다. 나의 경우에는 집에 Synology 서버가 있어서 이걸로 진행했다.
- SSL 인증서 (HTTPS 필수 — App Links는
httpsscheme만 지원) - 일반적으로는 유료이지만, Let's encrypt를 이용하면 무료로도 충분히 가능하다. 다만 단점이 있다면, 90일마다 갱신이 필수이다. 즉, 이 날짜가 지날 때까지 인증하지 않으면 무효라는 의미이다. Synology에는 자동인증서 발급 기능이 있어서 신경쓸 필요는 없지만, 일반적으로는 좀 귀찮기 때문에 cron과 같은 기능을 이용하거나, 다양한 방법을 이용해서 처리할 수도 있다. 이 부분은 오늘의 핵심은 아니니 여기까지만 정리한다.
App Links는 웹 페이지 자체가 반드시 있어야 하는 것은 아니다. /.well-known/assetlinks.json 경로만 정상적으로 응답하면 된다.
1-3. URL 구조 설계
앱에서 열고 싶은 화면에 맞게 URL 구조를 미리 설계한다.
| URL 패턴 | 열리는 화면 | 예시 |
|---|---|---|
https://example.com/ |
홈 | https://example.com/ |
https://example.com/member/list |
회원 목록 화면 | https://example.com/member/list |
https://example.com/member/detail/{id} |
회원 상세 화면 | https://example.com/member/detail/123 |
2. Digital Asset Links 검증 설정
Android는 해당 도메인이 앱과 정말 연결되어 있는지 Digital Asset Links로 검증합니다.
2-1. SHA256 인증서 지문 확인
앱 서명에 사용된 인증서의 SHA256 지문이 필요하다. 보통은 아래의 세 종류가 필요할 것이다.
로컬 keystore 사용 시(공동작업용, release용):
keytool -list -v -keystore /path/to/keystore.jks -alias your_alias
디버그 키 (개발·테스트용):
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey
출력에서 SHA256: 다음의 값을 복사한다.
형식: AB:CD:EF:12:34:... (콜론 구분, 16진수)
Google Play App Signing 사용 시:
Play Console → 출시 → 설정 → 앱 무결성에서 앱 서명 키 인증서의 SHA-256 지문을 확인한다.
2-2. assetlinks.json 작성
assetlinks.json은 "이 도메인은 이 앱과 연결되어 있다"는 선언이다.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp(실제 앱 패키지네임)",
"sha256_cert_fingerprints": [
"AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34",
"실제 앱 SHA256 서명 지문 2",
"실제 앱 SHA256 서명 지문 3", ...
]
}
}
]
package_name: 앱의 applicationIdsha256_cert_fingerprints: 위에서 확인한 SHA256 지문 (여러 개 가능)- 릴리즈 + 디버그 키 모두 허용하려면 두 지문을 배열에 넣으면 된다.
2-3. 웹 서버에 배포
다음 경로에 assetlinks.json을 배포한다.(실제 .well-known 디렉토리도 만들고 json 파일도 생성해서 넣어놓기만 하면 된다. 물론 200 OK로 나와야 하는 상태여야 한다. 간접링크 금지)
https://yourdomain.com/.well-known/assetlinks.json
중요 요구사항:
| 항목 | 요구사항 |
|---|---|
| HTTP 상태 | 200 OK |
| Content-Type | application/json |
| 리다이렉트 | 301/302 사용 금지 (직접 200 응답) |
2-4. 배포 확인
curl -I https://yourdomain.com/.well-known/assetlinks.json
- 저 수행 명령어로 나온 결과가 HTTP 200이어야 한다.
- Google Digital Asset Links 테스트에서도 검증할 수 있다.
3. Android 앱 구현
3-1. AndroidManifest.xml — intent-filter 선언
앱이 처리할 URL 패턴을 manifest에 등록한다.
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- 기존 LAUNCHER -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- App Links: 목록 화면 -->
<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="yourdomain.com"
android:pathPrefix="/member/list"
android:pathPattern="/member/list" />
</intent-filter>
<!-- App Links: 상세 화면 -->
<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="yourdomain.com"
android:pathPrefix="/member/detail/"
android:pathPattern="/member/detail/*" />
</intent-filter>
</activity>
android:autoVerify="true": assetlinks.json으로 도메인 검증 시도pathPattern="/member/list":/member/list경로만 (정확히 일치)pathPattern="/member/detail/*":/member/detail/123같은 경로 매칭
3-2. 딥링크 처리 로직 — DeepLinkHandler
Intent의 data(Uri)를 파싱해 해당 화면으로 이동하는 핸들러를 만든다. 이 프로젝트는 Navigator라는 별도의 Interface를 만들어서 동작하는 것을 가정하였다. 이 부분에 대해서는 각 프로젝트별 Routing 방법에 의존해서 작성하면 된다.
// DeepLinkHandler.kt
object DeepLinkHandler {
private const val HOST = "yourdomain.com"
private const val PATH_LIST = "/member/list"
private const val PATH_DETAIL_PREFIX = "/member/detail/"
fun handleIntent(intent: Intent?, navigator: Navigator): Boolean {
val data = intent?.data ?: return false
if (data.host != HOST) return false
return when {
// /member/list - 회원 목록
data.path == PATH_LIST || data.path == "$PATH_LIST/" -> {
navigator.openList()
true
}
// /member/list/{member id} - 개별 회원 상세 정보
data.path?.startsWith(PATH_DETAIL_PREFIX) == true -> {
val id = data.path?.removePrefix(PATH_DETAIL_PREFIX)?.trimEnd('/') ?: return false
if (id.isNotEmpty()) {
navigator.openDetail(id)
true
} else false
}
else -> false
}
}
}
3-3. MainActivity — Intent 수신
Activity가 딥링크 intent를 받아 처리한다.
// MainActivity.kt
class MainActivity : ComponentActivity() {
private var pendingDeepLinkState = mutableStateOf<Intent?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 딥링크로 실행된 경우에만 intent.data가 설정되어 있음
intent?.data?.let { pendingDeepLinkState.value = intent }
setContent {
// pendingDeepLinkState가 바뀔 때마다 이 Composable이 다시 그려짐
val pendingDeepLink by pendingDeepLinkState
AppContent(
// data가 있는 Intent만 전달해, 실제 딥링크인 경우만 처리
pendingDeepLinkIntent = pendingDeepLink?.takeIf { it.data != null },
// 딥링크 처리 후 pendingDeepLinkState.value = null로 비워서 중복 처리 방지
onDeepLinkProcessed = { pendingDeepLinkState.value = null }
)
}
}
// 앱이 이미 메모리에 있을 때 같은 Activity로 링크가 들어오면 onNewIntent가 호출됨
// launchMode="singleTask"가 있어서, 링크로 들어올 때 기존 Activity 인스턴스가 재사용되며 onNewIntent가 호출됨
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// 이후 getIntent()가 최신 Intent를 반환하도록 교체
setIntent(intent)
// data가 있을 때만 pendingDeepLinkState.value = intent로 갱신
intent.data?.let { pendingDeepLinkState.value = intent }
}
}
3-4. Compose 화면에서 처리
Compose NavController가 준비된 시점에 딥링크를 처리한다.
@Composable
fun AppContent(
pendingDeepLinkIntent: Intent? = null,
onDeepLinkProcessed: () -> Unit = {}
) {
val navController = rememberNavController()
val navigator = remember { Navigator(navController) }
LaunchedEffect(pendingDeepLinkIntent) {
if (pendingDeepLinkIntent != null) {
// 만약 deeplink 로직으로 들어온 경우 3-2 항목에 작성한 코드의 동작을 수행하도록 함
if (DeepLinkHandler.handleIntent(pendingDeepLinkIntent, navigator)) {
onDeepLinkProcessed()
}
}
}
NavHost(navController = navController, ...) {
// 일반적인 메인 화면 구성
}
}
4. 테스트 및 검증
4-1. ADB로 딥링크 테스트
# 목록 화면
adb shell am start -a android.intent.action.VIEW -d "https://yourdomain.com/list"
# 상세 화면
adb shell am start -a android.intent.action.VIEW -d "https://yourdomain.com/detail/123"
4-2. Verified 상태 확인
설정 → 앱 → [앱 선택] → 기본으로 열기 → 지원되는 웹 링크에서 도메인이 "확인됨"으로 표시되면 검증 성공이다.
4-3. 주의사항 및 트러블슈팅
| 상황 | 원인 | 해결 |
|---|---|---|
| "앱에서 열기" 선택 창 표시 | domain 미검증 | assetlinks.json 경로·내용·헤더 확인 |
| 검증 실패 | SHA256 불일치 | Play App Signing 사용 시 콘솔 지문 사용 |
| 링크 클릭해도 반응 없음 | intent-filter 불일치 | host, pathPrefix, pathPattern 확인 |
| 앱은 열리나 잘못된 화면 | DeepLinkHandler 로직 | path 파싱 및 navigator 호출 로직 점검 |
4-4. 검증 타이밍
App Links 검증은 앱 설치·업데이트 시점에 수행된다. 따라서 assetlinks.json을 수정했다면, 앱을 삭제 후 재설치하면 다시 검증이 시도된다.
마치며
App Links 구현은 다음 네 단계로 정리할 수 있다.
- 도메인 준비 — 소유 도메인·HTTPS 환경 구축
- Digital Asset Links — assetlinks.json 작성·배포 및 검증
- 앱 구현 — intent-filter, DeepLinkHandler, Activity/Compose 연동
- 테스트 — ADB·설정에서 동작 및 Verified 상태 확인
이 흐름을 따르면 웹 링크 클릭 시 원하는 화면으로 바로 연결되는 앱 링크를 안정적으로 구성할 수 있다.
참고 자료
'[Developer] > Android' 카테고리의 다른 글
| [Compose] ScrollIndicator Composable 만들기 (1) | 2026.01.12 |
|---|---|
| [Wear OS] 시계 모드에 컴플리케이션 및 제스처 추가 (1) | 2022.04.11 |
| [Wear OS] 시계 모드 컴플리케이션 개요 (0) | 2022.04.05 |
| scrcpy 제대로 활용하기(2/2) (12) | 2022.02.20 |
| scrcpy 제대로 활용하기(1/2) (2) | 2022.02.19 |
댓글