구글 파이어베이스 딥링크 서비스인 다이나믹 링크가 2025년 8월 25일을 끝으로 종료되어서 다른 딥링크 서비스로 대체했다.
AppsFlyer | Unified Marketing Measurement Powered by AI
Stop drowning in fragmented marketing data. AppsFlyer's AI-powered platform delivers unified insights and automated actions that accelerate growth. Join 15K+ brands - request a demo today.
www.appsflyer.com
1. AppsFlyer 세팅
나는 아래 유튜브 링크를 참고해서 세팅했다.
위 링크처럼 안드로이드와 iOS 두 앱을 등록해놓으면 된다.
2. 프로젝트 설정
처음에 설정하고 안 되어서 이것저것 많이 추가했다. 그래서 뭐가 필수인지는 모르겠긴한데 나는 아래처럼 추가했다.
명령어를 통해서 `pubspec.yml`에 패키지를 추가한다. (패키지 문서)
flutter pub add appsflyer_sdk
나는 아래 버전을 추가하였다.
appsflyer_sdk: ^6.16.21
android
`android/app/build.gradle` 에 다음 코드를 추가한다.
dependencies {
implementation ("com.appsflyer:af-android-sdk:6.15.2")
}
`AndroidManifest.xml` 에 다음 코드를 추가한다.
<!-- Universal Link (앱 링크) -->
<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" />
<!-- OneLink 도메인과 서브도메인 ID -->
<data
android:scheme="https"
android:host="YOUR_ONELINK_DOMAIN" <!-- 예: myapp.onelink.me -->
android:pathPrefix="/YOUR_ONELINK_ID" <!-- 예: /AbCd -->
/>
</intent-filter>
<!-- (선택) 커스텀 스킴: Universal Link 실패 시 백업용 -->
<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="YOUR_CUSTOM_SCHEME" /> <!-- 예: myapp -->
</intent-filter>
<!-- AppsFlyer 개발자 키 (대시보드에서 발급받은 Dev Key) -->
<meta-data
android:name="com.appsflyer.AF_DEV_KEY"
android:value="YOUR_APPSFLYER_DEV_KEY" />
<!-- OneLink ID (AppsFlyer OneLink Management에서 생성한 서브도메인 ID) -->
<meta-data
android:name="com.appsflyer.ONELINK_ID"
android:value="YOUR_ONELINK_ID" />
<!-- Google Play Install Referrer 수신 (권장) -->
<receiver
android:name="com.appsflyer.MultipleInstallBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.android.vending.INSTALL_REFERRER" />
</intent-filter>
</receiver>
iOS
`Info.plist`에 다음 코드를 추가한다.
<!-- Flutter 딥링크는 비활성화 -->
<key>FlutterDeepLinkingEnabled</key>
<false/>
<!-- Universal Link (OneLink 도메인 등록) -->
<key>com.apple.developer.associated-domains</key>
<array>
<!-- AppsFlyer OneLink 도메인 -->
<string>applinks:YOUR_ONELINK_DOMAIN</string> <!-- 예: applinks:myapp.onelink.me -->
</array>
<!-- (선택) 커스텀 스킴: Universal Link 실패 시 백업용 -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>your.custom.scheme</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- 앱에서 정의하는 스킴 (자유롭게 지정 가능) -->
<string>YOUR_CUSTOM_SCHEME</string> <!-- 예: myapp -->
</array>
</dict>
</array>
3. 코드
나는 setting과 code에 공식 깃허브를 많이 참고했다. 기본적인 설명이 정말 잘 나와있고, `issues`를 보면 자주 발생하는 오류와 해결 방법도 알 수 있다.
SDK 초기화
앱의 시작점인 `main` 파일에서 다음과 같이 AppsFlyer SDK를 초기화한다.
late AppsflyerSdk appsFlyerSdk;
AppsFlyerOptions appsFlyerOptions = AppsFlyerOptions(
// AppsFlyer 대시보드 > App Settings에서 확인
afDevKey: "YOUR_AF_DEV_KEY",
// iOS App Store ID (iOS만 필요)
appId: "YOUR_IOS_APP_ID"
showDebug: false,
manualStart: true,
);
appsFlyerSdk = AppsflyerSdk(appsFlyerOptions);
딥링크 생성
아래는 초대 딥링크를 생성하는 예시 코드이다.
/// OneLink 링크 생성
/// 그룹 ID, 역할(role), 초대한 사용자 이름, 문서/레코드 ID
String getOneLink(String groupId, String role, String inviterName, String recordId) {
// AppsFlyer에서 발급받은 OneLink 도메인 (대시보드에서 확인 가능)
String oneLinkDomain = 'https://example.onelink.me/abcd';
// 쿼리 파라미터를 붙여 딥링크 URL 생성
Uri oneLink = Uri.parse(oneLinkDomain).replace(queryParameters: {
// 앱에서 라우팅할 때 사용할 값
'deep_link_value': 'inviteByGroup',
// 초대 링크에 담고 싶은 커스텀 데이터들
'groupId': groupId,
'role': role,
'inviter': inviterName,
'recordId': recordId,
// 앱이 설치되어 있으면 바로 열릴 딥링크 주소
'af_dp': 'myapp://inviteByGroup',
// 앱이 반드시 열리도록 강제하는 옵션
'af_force_deeplink': 'true',
});
return oneLink.toString();
}
- 초대 링크에 커스텀 데이터를 담아, 초대한 사람과 수신자의 역할을 확인하고 docId를 통해 초대장의 유효성을 검증하도록 했다.
- 생성된 초대 링크는 `SharePlus` 등의 라이브러리를 통해 전달하면 된다.
딥링크 수신
/// AppsFlyer OneLink 리스너
void initOneLinkListener() {
appsFlyerSdk.onDeepLinking((DeepLinkResult dp) async {
try {
if (dp.status == Status.FOUND && dp.deepLink != null) {
DeepLink deepLink = dp.deepLink!;
String deepLinkValue = deepLink.deepLinkValue ?? "";
log("딥링크 수신: $deepLinkValue"); // inviteByGroup
log("전체 데이터: ${deepLink.toString()}");
if (deepLinkValue == "inviteByGroup") {
await _handleDeepLink(deepLink);
}
} else {
log("딥링크 없음 또는 에러: ${dp.error}");
}
} catch (e, s) {
log("딥링크 리스너 예외: $e\n$s");
}
});
}
- 위 코드는 공식 깃허브를 참고하여 작성하였다.
/// 딥링크 처리 로직
Future<void> _handleDeepLink(DeepLink deepLink) async {
try {
String groupId = deepLink.getStringValue("groupId") ?? "";
String role = deepLink.getStringValue("role") ?? "";
String recordId = deepLink.getStringValue("recordId") ?? "";
if (recordId.isEmpty || groupId.isEmpty || role.isEmpty) {
return;
}
// 초대장 유효성 확인 (예: 7일 이내)
DocumentSnapshot<Map<String, dynamic>> docSnapshot =
await FirebaseFirestore.instance.collection('INVITE').doc(recordId).get();
bool isValidInvite = docSnapshot.exists &&
DateTime.now()
.difference(docSnapshot.data()!['CreatedAt'].toDate())
.inDays <= 7;
if (!isValidInvite) {
Get.snackbar('알림', '만료되었거나 유효하지 않은 초대장입니다.', backgroundColor: Colors.white);
return;
}
// 여기서 로그인 여부, 역할(role)에 따라 라우팅이나 처리 로직 작성
// 예: Get.toNamed('/sign_up'); 또는 Get.offAllNamed('/member_main');
} catch (e, s) {
log("딥링크 처리 예외: $e\n$s");
Get.snackbar(
'알림',
'초대 링크를 열 수 없습니다.\n링크가 만료되었거나 올바르지 않습니다.',
backgroundColor: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
Future<void> _initAndStartAppsFlyerSafely() async {
try {
await Future<void>.delayed(const Duration(milliseconds: 0));
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
// SDK 초기화
await appsFlyerSdk.initSdk(
registerConversionDataCallback: true,
registerOnDeepLinkingCallback: true,
);
// SDK 시작
appsFlyerSdk.startSDK(
onSuccess: () => log('AppsFlyer SDK 시작 성공'),
onError: (code, msg) => log('AppsFlyer SDK 시작 실패: 코드 $code, 메시지 $msg'),
);
} catch (e, s) {
log('AppsFlyer 초기화/시작 중 오류: $e\n$s');
}
});
} catch (e, s) {
log('postFrame 스케줄링 오류: $e\n$s');
}
}
함수 호출 순서는 아래와 같이 구성했다.
initOneLinkListener();
await _initAndStartAppsFlyerSafely();
- `initOneLinkListener()`: OneLink 딥링크 리스너를 먼저 등록한다.
- `await _initAndStartAppsFlyerSafely()`: 이후에 AppsFlyer SDK를 초기화하고 실행한다.
트러블 슈팅
AppsFlyer 딥링크를 적용하면서 나는 크게 2가지 오류를 겪었다.
딥링크를 라우팅으로 잘못 인식하는 문제
증상
딥링크가 앱 내부 라우터 경로로 잘못 인식되어 `[GETX] GOING TO ROUTE / '딥링크'...` 형태로 로그에 찍히는 현상이 있었다.
해결 방법
`manualStart`를 `true`로 설정한 뒤, OneLink 리스너를 등록하고 나서 `appsFlyerSdk`를 실행하도록 변경했다. 위에 정리한 함수 호출 순서를 그대로 지켜주면 된다.
회고
이 부분에 대해서 정말 많은 시간을 쏟았다. 현재 프로젝트에서는 GetX를 사용하고 있었는데, 공식 깃허브 이슈를 보니 GetX를 아예 안 쓰는 방향으로 수정했더니 해결했다는 사람이 있어서 그렇게 바꾸어야하나 생각했는데, 그러면 바꾸어야할 부분이 너무 많아서 최후의 수단으로 미뤄두었다.
또, 단순히 GetX 버전을 올려보는 방법도 있었지만 시도해보니 효과는 없었다.
어찌 됐든 문제를 해결하고 나니 너무 좋았다. 당시에는 너무 골치 아팠는데 막상 원인을 찾고 나니 의외로 단순한 문제였다.
앱이 설치되어 있는데도 스토어로 연결되는 문제
증상
앱이 기기에 설치되어 있음에도 불구하고, 딥링크를 실행 시 앱이 아닌 스토어로 연결되는 문제가 발생했다. 딥링크 스킴 등 기본 설정을 모두 확인했지만 해결되지 않았다.
해결 방법
딥링크가 반드시 앱으로 열리도록 `af_force_deeplink`를 `true`로 설정하여 문제를 해결했다.
'Flutter' 카테고리의 다른 글
[Flutter] 사용자 접속 여부 (1) | 2025.03.06 |
---|---|
[Flutter] 네이버 로그인 (0) | 2024.10.03 |
[Flutter Error] invalid android_key_hash or ios_bundle_id or web_site_url 에러 (0) | 2024.09.29 |
[Flutter] 카카오 로그인 (0) | 2024.09.29 |
[Flutter] Stack 잘리는 경우 (0) | 2024.09.04 |