안드로이드를 만들며 9 — QR을 찍은 사람이 앱이 있는데도 스토어로 갔어요
공유 그림의 QR이 스토어 주소를 직접 담고 있었습니다. 앱을 이미 깐 사람도 찍으면 스토어로 떨어졌어요. 주소를 우리 도메인으로 바꾸고 App Links를 세우면 링크 하나가 두 경우를 다 받습니다 — 그 구현을 처음부터 끝까지 적었습니다.
물타기 계산기에는 계산 결과를 그림으로 공유하는 기능이 있습니다. 그림 구석에 QR이 붙어 있어요 — 받아 본 사람이 찍으면 앱으로 오라는 문이죠.
그런데 그 QR이 담고 있던 주소가 Play 스토어 직링크였습니다.
앱이 없는 사람에게는 맞는 문입니다. 문제는 앱을 이미 깐 사람이에요. 찍으면 스토어가 열립니다. “설치됨” 버튼이 있는 스토어 페이지가요. 거기서 ‘열기’를 한 번 더 눌러야 앱에 도착합니다. 문을 잘못 단 거예요.
원하는 동작은 이겁니다.
- 앱이 있는 기기: QR을 찍으면 앱이 바로 열린다
- 앱이 없는 기기: 웹 페이지를 거쳐 스토어로 간다
링크 하나가 두 경우를 다 받아야 해요. 이걸 해 주는 게 App Links입니다.
딥링크가 뭔지부터
딥링크(deep link)는 링크를 눌렀을 때 웹 페이지가 아니라 앱의 특정 화면이 열리게 하는 장치입니다. 안드로이드에는 크게 두 세대가 있어요.
1세대 — 커스텀 스킴. mycalc://open 같은 주소를 앱이 받겠다고 선언하는
방식입니다. 간단하지만 약점이 커요. 아무 앱이나 같은 스킴을 선언할 수 있어서
누가 받을지 보장이 없고, 앱이 없는 기기에서는 링크가 그냥 죽습니다.
QR에 담기엔 실격이에요 — 앱 없는 사람이 주 대상인데요.
2세대 — App Links. 그냥 https 주소를 씁니다.
https://twinklelabs.kr/app/stock-calculator 같은 진짜 웹 주소요.
앱이 있으면 OS가 그 주소를 앱에게 주고, 없으면 브라우저가 여는 —
물러설 곳이 웹인 구조입니다. 대신 “이 도메인의 주소는 내가 받겠다”는 주장을
도메인 주인이 보증해야 해요. 그 보증 절차가 App Links의 전부입니다.
구현은 세 조각입니다
1. 매니페스트 — 앱이 ‘이 주소는 내 것’이라고 선언
<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="twinklelabs.kr"
android:pathPrefix="/app/stock-calculator" />
</intent-filter>
autoVerify="true"가 핵심입니다. 이게 있으면 설치 시점에 OS가
도메인에 가서 보증을 확인하고, 통과하면 그 주소를 묻지 않고 앱으로 엽니다.
(없으면 ‘어느 앱으로 열까요’ 선택지가 뜨거나 브라우저로 가요.)
여기서 실수하기 쉬운 게 경로의 폭입니다. pathPrefix 없이 도메인 전체를
잡으면, 소개 화면이든 블로그든 그 도메인의 모든 링크를 앱이 가로챕니다.
블로그 글을 눌렀는데 계산기가 열리는 거예요. 앱이 받아야 할 경로만 좁게 잡습니다.
2. assetlinks.json — 도메인이 ‘맞아, 그 앱 내 것’이라고 보증
도메인 루트의 정해진 자리에 파일을 하나 올립니다.
https://twinklelabs.kr/.well-known/assetlinks.json —
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "kr.twinklelabs.stockcalculator",
"sha256_cert_fingerprints": ["8E:CB:…:03"]
}
}]
패키지 이름과 서명 인증서 지문으로 앱을 특정합니다. 아무 앱이나 “twinklelabs.kr은 내 거”라고 우길 수 없는 이유가 이 지문이에요.
여기 함정이 하나 있습니다. 지문이 두 개예요.
- 업로드 키 지문: 내가 서명해서 올리는 번들의 키
- 앱 서명 키 지문: Play가 사용자에게 다시 서명해서 내보내는 키
Play 앱 서명을 쓰면(요즘은 거의 다 씁니다) 사용자 기기에 도착하는 APK는 Play의 키로 서명돼 있습니다. 그러니 assetlinks에는 앱 서명 키 지문이 서야 해요. 어디서 읽냐면 — Play Console의 ‘앱 무결성 → 앱 서명’이 assetlinks 스니펫째로 보여 줍니다. 짐작하지 말고 거기서 복사하세요. (사이드로드 빌드도 열리게 하려면 업로드 키 지문을 배열에 같이 넣으면 됩니다.)
3. 웹 페이지 — 앱 없는 기기의 물러설 곳
https://twinklelabs.kr/app/stock-calculator는 실제로 열리는 페이지여야
합니다. 앱 없는 기기에서는 브라우저가 여기로 오니까요. 이 페이지가 하는 일은
스토어로 넘기는 것 하나입니다 — 그리고 넘기면서 utm_source=share 같은
꼬리표를 대신 붙여 줍니다.
이게 부수입인데요, QR에 담기는 주소 자체는 짧게 유지하면서 취득 경로 집계는 웹이 이어 주는 구조가 됩니다.
설계에서 정한 것 두 가지
주소의 문법은 저장소가 이미 정해 두고 있었어요. 처음에는
/stock-calculator/app으로 갔다가 /app/stock-calculator로 바로잡았습니다.
저희 저장소는 제품을 ‘어떻게 사용자 손에 가는가’로 바구니에 나눕니다 —
스토어에 올라가는 앱은 app/ 아래에요. 웹 주소도 같은 문법을 따르면,
다음 앱은 규칙을 새로 정할 것 없이 /app/pocket-pdf로 같은 자리에 줄만
서면 됩니다. 실제로 같은 날 Pocket PDF가 두 번째 줄에 섰어요 —
매니페스트에 같은 필터, assetlinks에 항목 하나. 그게 다였습니다.
링크는 유입만 시킵니다. 처음엔 계산 값 전부를 쿼리에 실어서, 받는 쪽이 리포트를 다시 세우는 화면까지 만들었다가 걷었어요. 물리가 반대했거든요 — QR은 담는 바이트가 늘수록 칸이 잘아져서, 공유 그림 구석의 작은 QR에는 밀도 예산이 있습니다(한 칸 4px, 주소 약 106바이트). 종목 이름을 실은 링크는 그 예산에 들어가지 않았어요. 그리고 생각해 보면 공유 그림이 이미 리포트 전체를 보여 주고 있습니다. 링크가 그걸 또 나를 이유가 없어요.
이름도 하나 갈았습니다 — share_store_url → share_app_url.
가리키는 곳이 스토어가 아니게 됐는데 이름이 그대로면, 다음에 읽는 사람에게
거짓말을 하게 되니까요.
마지막 함정 — 배포되기 전엔 서지 않습니다
코드를 다 짜고 머지까지 해도 App Links는 아직 안 섭니다.
.well-known/assetlinks.json이 실제 도메인에서 서빙되고 있어야
설치 시점 검증이 통과하거든요. 저희 웹은 main 푸시에서만 배포되니,
웹이 나가기 전까지 앱 쪽 코드는 완성돼 있어도 링크는 브라우저로 갑니다.
검증이 붙었는지는 기기에서 확인할 수 있어요.
adb shell pm get-app-links kr.twinklelabs.stockcalculator
verified가 떠야 끝난 겁니다.
고치고 나서 적어 둔 것
- 커스텀 스킴이 아니라 App Links입니다. QR의 주 대상은 앱 없는 사람이고, 물러설 곳이 웹이어야 링크가 죽지 않아요.
- 경로는 좁게. 도메인 전체를 잡으면 앱이 웹사이트를 통째로 가로챕니다.
- assetlinks의 지문은 Play 콘솔에서 읽습니다. 업로드 키 지문을 넣고 ‘왜 안 되지’ 하는 게 제일 흔한 함정이에요.
- 링크에 무엇을 실을지는 QR의 밀도 예산과 화면이 이미 하는 일이 정합니다. 실을 수 있다고 싣는 게 아니라요.