앱이 안 열려요 (딥링크, 유니버설 링크 완전 정복)
1. "링크를 눌렀는데 왜 사파리가 뜨죠?"
마케팅 팀에서 이벤트를 한다고 문자를 돌렸습니다.
https://myapp.com/event/123
이 링크를 누르면 멋지게 우리 앱의 이벤트 페이지로 이동해야 합니다.
그런데 현실은? 그냥 사파리(웹브라우저)가 열리고 맙니다. 심지어 앱이 설치되어 있는데도요. 사용자는 "뭐야, 앱 있는데 왜 로그인을 또 새로 하래?"라며 이탈합니다.
2. 원리 이해: Custom Scheme vs Universal Link
옛날에는 myapp://event/123 같은 Custom Scheme을 썼습니다.
하지만 이건 보안 문제(중복 가능)가 있어서, 요즘은 표준인 App Link (안드로이드) / Universal Link (iOS)를 써야 합니다.
핵심 원리는 "웹사이트가 앱을 보증한다"입니다. 앱만 설정해서는 안 되고, 반드시 도메인(서버)에도 "이 앱이 내꺼 맞다"라는 증명 파일을 올려둬야 합니다.
3. 해결책 1 - 서버 설정 (증명 파일 업로드)
이걸 안 하면 아무리 앱을 고쳐도 동작 안 합니다.
안드로이드 (assetlinks.json)
도메인의 /.well-known/assetlinks.json 경로에 파일이 있어야 합니다.
SHA-256 지문이 앱 서명 키와 정확히 일치해야 합니다. (Play Console 서명 키 주의!)
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": ["YOUR_SHA256_HASH"]
}
}]
iOS (apple-app-site-association)
도메인의 /.well-known/apple-app-site-association (확장자 없음!) 경로에 있어야 합니다.
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.myapp",
"paths": [ "/event/*", "/user/*" ]
}
]
}
}
주의: 이 파일들은 반드시 Content-Type: application/json으로 서빙되어야 하며, 리다이렉트 없이 200 OK가 떠야 합니다.
4. 해결책 2 - 앱 설정 (플러터)
이제 앱에게 "내가 저 도메인 주인이야"라고 알려줘야 합니다.
안드로이드 (AndroidManifest.xml)
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="myapp.com" />
</intent-filter>
iOS (Xcode)
Signing & Capabilities -> Associated Domains 추가.
applinks:myapp.com 입력.
(주의: https://를 붙이면 안 됩니다. 그냥 도메인만.)
5. 해결책 3 - 라우팅 처리 (go_router)
앱이 켜지는 것까진 성공했습니다. 이제 해당 페이지로 이동시켜야 합니다.
go_router를 쓰면 아주 쉽습니다.
final goRouter = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
routes: [
GoRoute(
path: 'event/:id',
builder: (context, state) {
final id = state.pathParameters['id'];
return EventPage(id: id);
},
),
],
),
],
);
이제 https://myapp.com/event/123으로 들어오면 자동으로 EventPage(id: 123)을 띄워줍니다.
6. Deferred Deep Link (지연된 딥링크) 한 걸음 더
앱이 설치 안 된 사용자가 링크를 누르면 어떻게 될까요? 앱스토어로 이동시킵니다. 사용자가 앱을 설치하고 처음 실행했을 때, 방금 눌렀던 그 링크(event/123)로 보내주고 싶다면?
이걸 Deferred Deep Link라고 합니다. Firebase Dynamic Links가 2025년에 종료되므로, AppsFlyer나 Branch 같은 유료 솔루션을 쓰거나 직접 클립보드 매칭 로직을 구현해야 합니다.
GoRouter ShellRoute 통합 제대로 이해하기
딥링크로 들어왔을 때, 단순히 페이지만 띡 보여주는 게 아니라 Bottom Navigation Bar (Scaffold)가 유지된 상태로 보여야 할 때가 있습니다.
이럴 땐 ShellRoute를 씁니다.
ShellRoute(
builder: (context, state, child) {
return Scaffold(
body: child,
bottomNavigationBar: MyBottomBar(),
);
},
routes: [
GoRoute(path: '/event/:id', ...),
],
)
이렇게 하면 외부에서 링크 타고 들어와도 앱의 네비게이션 구조가 안 깨집니다.
Flavor & Environment Management 파헤치기
실제로는 Dev, Staging, Prod 환경이 나뉩니다. 딥링크 도메인도 달라야 합니다.
- Dev:
dev.myapp.com - Prod:
myapp.com
iOS (XCConfig 사용):
Associated Domains 값에 변수를 사용하세요. applinks:$(DEEP_LINK_DOMAIN).
그리고 Debug.xcconfig, Release.xcconfig에서 값을 다르게 설정합니다.
Android (Manifest Placeholders):
build.gradle에서 manifestPlaceholders를 사용합니다.
// build.gradle
productFlavors {
dev {
manifestPlaceholders = [hostName: "dev.myapp.com"]
}
prod {
manifestPlaceholders = [hostName: "myapp.com"]
}
}
<!-- AndroidManifest.xml -->
<data android:scheme="https" android:host="${hostName}" />
이렇게 해야 개발 중 테스트할 때 실제 운영 서버로 넘어가는 사고를 막을 수 있습니다.
테스트 방법 (터미널) 더 알아보기
매번 문자 보내서 테스트할 순 없습니다. 터미널 명령어로 시뮬레이션 하세요.
Android:
adb shell am start -W -a android.intent.action.VIEW -d "https://myapp.com/event/123" com.example.myapp
iOS (Simulator):
xcrun simctl openurl booted "https://myapp.com/event/123"
iOS 메모 앱이나 사파리에 링크를 적어두고 클릭해보는 것이 가장 확실합니다. (주소창에 치면 검색으로 넘어가버리는 경우가 많음)
10. 요약
- 서버 증명 파일:
assetlinks.json과apple-app-site-association없이는 절대 안 됨. autoVerify: 안드로이드에서 이거 빼먹으면 브라우저/앱 선택창 뜸.Associated Domains: iOS는applinks:접두어 필수.- GoRouter: URL 파싱 로직 짜지 말고 그냥 이거 써라.
- Flavor 분리: Dev와 Prod 도메인을 섞지 마라.