모바일 추천 트래킹을 자동화하는 방법은 무엇인가요? 자동화된 추천 트래킹을 설정하려면 어트리뷰션 SDK를 연동하고, 웹 공유 링크에 난독화된 추천 토큰(shareCode)을 첨부한 뒤, 랜딩 페이지에서 이를 캡처하고, 플랫폼에서 지원하는 지연된 딥 링크(deferred deep linking)를 활용해 최초 앱 실행 시 파라미터를 복구해야 합니다.
자동화된 추천 트래킹은 기존 사용자의 공유 행동과 모바일 앱 설치를 프로그래밍 방식으로 연결(어트리뷰션)하는 엔지니어링 프레임워크입니다. 웹 랜딩 페이지를 통해 난독화된 초대자 식별자(shareCode)를 전달하고 최초 앱 실행 시 이 컨텍스트를 해결하려고 시도함으로써, 수동으로 프로모션 코드를 입력하는 번거로움을 줄이고 인앱 브라우저 컨테이너 환경 전반에서 트래킹을 지원합니다.
| 용어 | 정의 | 관련 엔티티 | 검색 의도 역할 |
|---|---|---|---|
| 추천 트래킹 (Referral Tracking) | 하위 사용자 유치를 특정 기존 사용자의 공유 세션에 프로그래밍 방식으로 어트리뷰션하는 것. | 앱 파라미터 설치 | 정보성 / 상업성 |
| 추천 트래킹 소프트웨어 | 초대자와 피초대자의 연동을 자동화하도록 설계된 SDK 및 서버 인프라. | 지연된 딥 링크 | 상업성 |
| 앱 추천 프로그램 | 기존 사용자가 새로운 피어(친구)를 초대하여 앱을 설치하도록 인센티브를 제공하는 성장 이니셔티브. | 채널 통계 및 트래킹 | 상업성 |
| 추천 시스템 | 링크 생성, 어트리뷰션, 리워드 발행을 관리하는 엔드투엔드 기술 아키텍처. | 웹 JS SDK | 기술성 |
아키텍처 개요: 자동화된 추천 트래킹이 소셜 공유를 앱 설치로 연결하는 방식
레거시 추천 시스템의 구조적 한계: 수동 프로모션 코드가 퍼널 속도를 저하하는 이유
기존 모바일 추천 프로그램은 수동 영숫자 프로모션 코드에 의존합니다. 이 레거시 워크플로에서는 기존 사용자가 초대 코드(예: INVITE2026)를 생성하여 텍스트를 수동으로 복사한 뒤 SMS나 메신저를 통해 친구에게 보냅니다. 이후 초대받은 사용자는 앱 스토어에서 애플리케이션을 다운로드하고, 회원가입을 완료한 다음, 전용 프로모션 코드 입력란을 찾아 문자열을 정확히 입력해야 합니다.
이러한 수동 프로세스는 사용자 유치 퍼널 전반에 걸쳐 상당한 마찰(friction)을 유발합니다.
- 인지적 및 물리적 이탈: 사용자는 종종 바우처 코드 복사를 잊어버리거나, 복잡한 온보딩 메뉴 속에서 입력란을 찾지 못하거나, 대소문자 또는 오입력으로 인해 코드가 실패할 때 프로세스를 포기합니다.
- 채널 누수 및 코드 스크래핑: 영숫자 코드는 쉽게 추출되어 공개 쿠폰 사이트나 포럼 게시판에 공유될 수 있습니다. 이는 진정한 피어투피어(P2P) 관계를 끊어버리며, 추천을 받지 않은 오가닉 사용자가 리워드를 타갈 수 있게 하여 캠페인 유닛 경제학을 악화시킵니다.
- 측정 사각지대: 수동 프로모션 코드에만 의존하면 별도의 분석 도구가 배포되지 않는 한 중간 퍼널 마일스톤(링크 노출, 소셜 웹뷰 클릭, 스토어 이동률 등)을 측정할 수 없습니다.
프로그래밍 방식의 공유 파이프라인: 초대자 행동, 웹 스테이징, 스토어 핸드오프, 그리고 Day 0 해결

자동화된 추천 트래킹은 수동 입력 방식을 지속적인 프로그래밍 방식의 파라미터 전달 파이프라인으로 대체합니다. 파라미터가 없는 스토어 링크와 연결되지 않은 텍스트 바우처를 함께 배포하는 대신, 애플리케이션은 초대자의 신원을 인터랙티브 웹 공유 URL에 직접 번들링합니다.
기술적 라이프사이클은 4개의 개별 단계로 작동합니다.
[기존 사용자의 친구 초대] ──> shareTelemetry.reportIntent(shareCode, Platform) 호출
│
▼
[동적 웹 링크 생성] ───> 목적지 URL에 ?shareCode=usr_9f82a1b4 추가
│
▼
[WeChat/Line 내에서 공유] ────> 친구가 샌드박스형 웹뷰 내에서 링크 탭
│
▼
[웹 JS SDK 스테이징 및 핸드오프] ─> 컨텍스트 캐시 및 앱 실행 / 스토어 다운로드 실행
│
▼
[기기에서의 최초 앱 실행] ───> 네이티브 SDK가 파라미터 복구 쿼리
│
▼
[애플리케이션 백엔드 바인딩] ──> 백엔드가 토큰을 검증하고 대기 중인 바인딩 생성
│
▼
[검증된 활성화 마일스톤] ─> 마일스톤 완료 시 백엔드에서 리워드 지급
초대받은 사용자가 링크를 열면, 호스팅된 웹 랜딩 페이지가 웹 JS SDK를 실행하여 초대자의 토큰(shareCode)을 추출하고 추천 컨텍스트를 스테이징합니다. 사용자가 네이티브 애플리케이션을 설치하고 열면, 클라이언트 SDK가 최초 실행 중에 스테이징된 페이로드를 가져옵니다. 이를 통해 애플리케이션 백엔드는 두 참가자의 수동 데이터 입력 없이도 추천 관계를 자동으로 바인딩할 수 있습니다.
스토어 설치 후 최초 실행 복구: 플랫폼별 제한 사항
사용자가 앱 스토어에서 애플리케이션을 설치한 후, 추천 파라미터 복구는 플랫폼 및 애플리케이션(또는 어트리뷰션 SDK)에 구현된 메커니즘에 따라 달라집니다. 웹 URL을 통해 shareCode를 전달하는 것만으로는 새로 설치된 애플리케이션에서 해당 파라미터를 사용할 수 있게 된다는 보장이 없습니다.
- Android / Google Play: Google Play 설치 리퍼러(Install Referrer)는 Play 스토어 설치와 연결된 지원 추천 메타데이터를 제공할 수 있습니다. 애플리케이션은 실제 리퍼러 페이로드를 구성 및 테스트하고, 예상 파라미터가 리디렉션 및 설치 흐름 전반에 걸쳐 유지되는지 확인해야 합니다. 타사 어트리뷰션 SDK는 추가적인 메커니즘을 사용할 수 있으며, 이는 별도로 문서화되어야 합니다.
- iOS / App Store: 앱 스토어는 임의의 랜딩 페이지 쿼리 파라미터를 설치된 애플리케이션으로 자동 전송하지 않습니다. 유니버설 링크(Universal Links)는 애플리케이션이 이미 설치되어 있고 연관된 도메인 설정이 유효한 경우 직접 라우팅을 지원하지만, 신규 설치 후의 지연된 파라미터 복구를 자체적으로 보장하지는 않습니다.
- 어트리뷰션 SDK: 배포되는 정확한 SDK 버전 및 플랫폼에 대해 문서화된 복구 메커니즘만 사용하세요. SDK가 모든 브라우저 및 설치 경로에서 모든 추천 파라미터를 복구할 수 있다고 가정하지 말고, 구현, 데이터 수집, 개인정보 보호 영향을 직접 검증하세요.
지원되는 메커니즘을 통해 추천 컨텍스트를 복구할 수 없는 경우, 사용자가 온보딩을 완료하도록 허용하고 해당 설치를 어트리뷰션되지 않은(unattributed) 상태로 기록하세요. 검증되지 않은 일치 항목으로부터 추천 관계를 유추하거나 조작해서는 안 됩니다.

iOS 어트리뷰션과 개인정보 보호 경계
iOS 추천 구현은 애플리케이션이나 웹사이트 간에 사용자나 기기를 식별하기 위해 금지된 기기 핑거프린팅(device fingerprinting)에 의존해서는 안 됩니다. Apple의 개인정보 보호 및 데이터 사용 요구사항은 이 목적으로 기기, 브라우저, 구성, 위치 또는 네트워크 신호에서 고유 식별자를 유추하는 것을 금지합니다. 앱 트래킹 투명성(ATT) 권한 부여는 그렇지 않으면 금지되는 핑거프린팅을 허용 가능하게 만들지 않습니다.
애플리케이션이 설치되어 있을 때는 검증된 직접 앱 라우팅을 위해 유니버설 링크를 사용하세요. 설치 후 추천 복구를 위해서는 배포된 SDK 버전 및 해당 Apple 요구사항에 대해 동작이 검토된 문서화된 메커니즘을 사용하세요. 해당되는 경우 SDK의 개인정보 보호 고지 및 필수 사유 API 선언을 검토하되, 개인정보 보호 매니페스트(Privacy Manifest)의 존재가 어트리뷰션 구현의 준수를 증명하는 것으로 간주하지 마세요.
선택한 SDK가 허용된 것으로 검증할 수 없는 매칭 방법에 의존하는 경우, iOS 배포에 해당 방법을 권장하지 마세요. SDK 제공업체로부터 명확한 설명을 얻고 규정을 준수하는 대안을 선택하세요. 허용된 메커니즘으로 추천 관계를 안정적으로 설정할 수 없는 경우, 일반적인 온보딩 흐름을 유지하고 설치를 어트리뷰션되지 않은 상태로 표시하세요.
참고: Apple 사용자 개인정보 보호 및 데이터 사용.
참고 항목: 추천 트래킹 ──> 앱 파라미터 설치
1단계: 클라이언트 측 공유 보고 API 계측
Android 및 iOS 진입점에서 네이티브 어트리뷰션 SDK 초기화
애플리케이션이 공유 활동을 보고하거나 들어오는 추천 컨텍스트를 가져오기 전에, 네이티브 모바일 SDK는 애플리케이션 라이프사이클의 초기 단계에서 초기화되어야 합니다.
Android에서는 커스텀 Application 서브클래스의 onCreate() 메서드에서 초기화를 수행하여, 포그라운드 UI 컴포넌트가 마운트되기 전에 어트리뷰션 리스너와 코어 메모리 구조가 설정되도록 보장합니다. iOS에서는 application(_:didFinishLaunchingWithOptions:) 실행 델리게이트 내에서 초기화가 이루어집니다.
개발자는 OpoInstall SDK 다운로드 센터에서 공식 모바일 SDK 패키지를 직접 다운로드할 수 있습니다.
네이티브 OS 공유 시트를 트리거하기 전에 공유 보고 메서드 호출
애플리케이션은 플랫폼 공유 인터페이스를 열기 전에 공유 인텐트를 기록해야 합니다. 이는 애플리케이션 소유의 텔레메트리 서비스나 사용 중인 정확한 SDK 버전과 대조하여 검증된 어트리뷰션 SDK 메서드를 통해 구현하세요. 아래 예시는 애플리케이션 소유의 shareTelemetry.reportIntent(...) 추상화를 사용하며, 공식 OpoInstall API가 아닙니다. 어댑터는 배포된 애플리케이션에 대한 실제 네트워크 요청, 오류 처리, 콜백 동작을 구현해야 합니다.
정리된 텔레메트리를 유지하기 위해, 엔지니어링 팀은 공유 라이프사이클 내의 5가지 개별 이벤트를 구분해야 합니다.
- 공유 인텐트 시작됨 (
share_intent_initiated): 클라이언트 앱 UI가 사용자가 초대 액션을 클릭했음을 기록합니다. - 공유 디스패치됨 (
share_dispatched): 클라이언트 앱이 페이로드를 식별된 대상 플랫폼으로 전달하거나(예: 명시적인 소셜 인텐트 실행) 시스템 공유 선택기를 엽니다. - 공유 완료됨 (
share_completed): 타사 소셜 SDK가 메시지가 게시되거나 발송되었음을 확인하는 인증된 완료 콜백을 반환합니다(콜백을 지원하는 전용 타사 플랫폼 SDK를 사용하는 경우에만 해당). - 추천 클릭됨 (
referral_click): 수신자가 웹 랜딩 페이지를 열어 웹 사이드 인그레스 로깅을 트리거합니다. - 추천 바인딩됨 (
referral_bound): 수신자가 계정 등록을 완료하고, 백엔드가 후보shareCode를 검증한 후 추천 관계를 데이터베이스에 기록합니다.
공유 텔레메트리를 기록하면 사용자의 공유 의도가 포착됩니다. 이를 통해 분석 파이프라인은 초대자의 shareCode를 지정된 플랫폼 채널과 인덱싱하여, 후속 웹 랜딩 페이지 방문을 상관관계 분석할 수 있도록 시스템을 준비시킵니다.
플랫폼 식별자 지정: 소셜 채널 매핑
다음 식별자는 예시일 뿐입니다. 배포된 SDK 버전에서 지원하는 정확한 채널 열거형을 사용하고, 각 식별자를 공식 API 레퍼런스와 대조하여 검증하세요.
- 폐쇄형 메시징 네트워크:
WechatSession(WeChat 다이렉트 채팅),WechatTimeline(WeChat 모멘트),Line,WhatsApp,FacebookMessenger. - 소셜 피드 및 공개 마이크로블로그:
Twitter,Facebook,Instagram,Weibo,Douyin,TikTok. - 직접 운영체제 전송:
Sms,Email,Copy(클립보드 링크 복사),Other(시스템 공유 시트).
사용자의 최종 목적지 선택을 사전에 결정할 수 없는 일반 시스템 공유 시트를 애플리케이션이 실행할 때는, 특정 소셜 네트워크를 하드코딩하는 대신 Other로 분류해야 합니다.
초대자 식별자 토큰 구조화: shareCode 포맷팅 모범 사례
shareCode는 신규 설치를 추천한 계정과 다시 연결하는 애플리케이션 레벨의 비즈니스 토큰 역할을 합니다. 개발자가 임의의 영숫자 문자열을 사용할 수 있지만, 구조적 모범 사례를 준수하면 파라미터 손상을 방지할 수 있습니다.
- 난독화된 서버 발행 토큰:
shareCode는 열거 가능한 원시 데이터베이스 자동 증가 ID(1024)가 아니라 난독화된 서버 발행 추천 토큰(예: URL 안전 토큰 또는usr_9f82a1b4같은 UUID)이어야 합니다. 애플리케이션 백엔드는 이 토큰을 초대자 계정으로 해결(해독/식별)하고 만료 및 속도 제한(rate limit)을 적용합니다. - 길이 제약: URL 길이가 늘어나거나 메시징 앱에서 문자열이 잘리는 것을 방지하기 위해 토큰을 간결하게 유지하세요(권장 길이 64자 미만, 엄격하게는
\le 128 자 미만). - 안전한 문자셋: 문자는 URL 안전 영숫자, 하이픈, 언더스코어(
[A-Za-z0-9_-])로 제한하세요. URI 파싱을 변경하는&,=,?또는 공백과 같은 원시 구분 기호는 피하세요.
Android 11(API 수준 30) 이상에서는 타사 소셜 패키지가 설치되어 있는지 쿼리하려면 Android 11의 패키지 가시성에 대한 Google Android 개발자 가이드에 따라 AndroidManifest.xml 내에 해당 <queries> 요소를 선언해야 합니다. 아래 구현 패턴은 패키지 가시성을 선언하고, 논블로킹 공유 보고 타임아웃을 처리하고, 라이프사이클에 안전한 상태 재설정을 관리하고, 클립보드 폴백과 함께 명시적 소셜 인텐트를 디스패치하는 방법을 보여줍니다.
<!-- Android 설정 및 클라이언트 측 공유 구현 -->
<!-- 파트 1: AndroidManifest.xml 패키지 가시성 및 애플리케이션 선언 -->
<!-- 파일: app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- Android 11+ (API 30+)에 필요한 명시적 패키지 가시성 선언 -->
<queries>
<package android:name="com.tencent.mm" />
<package android:name="jp.naver.line.android" />
</queries>
<application
android:name=".CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".InviteActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
// Android 애플리케이션 라이프사이클 진입점
// 파트 2: CustomApplication.kt (SDK 라이프사이클 초기화)
// 파일: app/src/main/java/com/example/app/CustomApplication.kt
package com.example.app
import android.app.Application
import com.opoinstall.api.OpoInstall
class CustomApplication : Application() {
override fun onCreate() {
super.onCreate()
// 액티비티 마운트 전 메인 스레드에서 OpoInstall 코어 클라이언트 초기화
OpoInstall.initialize(this)
}
}
// Android 공유 액티비티 구현
// 파트 3: InviteActivity.kt (클립보드 폴백을 포함한 애플리케이션 텔레메트리 및 타겟 디스패치)
// 파일: app/src/main/java/com/example/app/InviteActivity.kt
package com.example.app
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.util.concurrent.atomic.AtomicBoolean
class InviteActivity : AppCompatActivity() {
companion object {
private const val TAG = "ReferralShareTracking"
// 예시 애플리케이션 소유 리디렉션 엔드포인트; 배포된 HTTPS 라우트로 교체하세요
private const val BASE_INVITE_HOST = "promo.example.com"
private const val BASE_INVITE_PATH = "invite.html"
// 명시적 인텐트 라우팅을 위한 타겟 플랫폼 패키지
private const val PACKAGE_WECHAT = "com.tencent.mm"
private const val PACKAGE_LINE = "jp.naver.line.android"
// 애플리케이션 정의 예시 타임아웃; 실제 텔레메트리 서비스에 맞게 조정 및 테스트하세요
private const val SHARE_TIMEOUT_MS = 800L
}
// 빠른 동시 공유 인텐트 호출을 방지하기 위한 디바운스 가드
private val isSharingInProgress = AtomicBoolean(false)
// 애플리케이션 소유 텔레메트리 어댑터 추상화
private val shareTelemetry = ShareTelemetryService()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_invite)
val btnShareWeChat = findViewById<Button>(R.id.btn_share_wechat)
val btnShareLine = findViewById<Button>(R.id.btn_share_line)
val btnShareGeneric = findViewById<Button>(R.id.btn_share_generic)
// 예시용 모의 토큰: 프로덕션에서는 인증된 세션에서 서버 발행 토큰을 검색하세요
val currentInviterToken = "usr_9f82a1b4"
btnShareWeChat.setOnClickListener {
initiateShareFlow(currentInviterToken, "WechatSession", targetPackage = PACKAGE_WECHAT)
}
btnShareLine.setOnClickListener {
initiateShareFlow(currentInviterToken, "Line", targetPackage = PACKAGE_LINE)
}
btnShareGeneric.setOnClickListener {
// 세분화되지 않은 시스템 공유 시트; 플랫폼은 Other로 분류됨
initiateShareFlow(currentInviterToken, "Other", targetPackage = null)
}
}
/**
* 특정 채널 텔레메트리를 보고하기 전에 타겟 소셜 앱이 설치되어 있는지 확인하는 도우미.
* Android 11+(API 30+)에서는 AndroidManifest.xml에 <queries> 선언이 필요합니다.
*/
private fun isAppInstalled(packageName: String?): Boolean {
if (packageName.isNullOrEmpty()) return false
return try {
packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
true
} catch (e: PackageManager.NameNotFoundException) {
false
} catch (e: Exception) {
Log.w(TAG, "$packageName 패키지 검사 예외 발생: ${e.message}")
false
}
}
/**
* 최종 폴백으로 전체 초대 URL을 시스템 클립보드에 복사합니다.
*/
private fun copyInviteLinkToClipboard(shareUri: String): Boolean {
return try {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("추천 초대 링크", shareUri))
true
} catch (e: Exception) {
Log.e(TAG, "초대 링크 복사 실패: ${e.message}", e)
false
}
}
private fun showShareFailureFallback(shareUri: String) {
val copied = copyInviteLinkToClipboard(shareUri)
val message = if (copied) {
"공유 앱을 열 수 없습니다. 초대 링크가 클립보드에 복사되었습니다."
} else {
"공유 앱을 열거나 초대 링크를 복사할 수 없습니다."
}
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
/**
* 논블로킹 타임아웃 폴백을 통해 애플리케이션 소유 텔레메트리를 통해 공유 인텐트를 기록합니다.
*
* @param shareCode 난독화된 초대자 토큰 (엄격하게 <= 128자)
* @param platformIdentifier 보고 분류와 일치하는 타겟 소셜 채널
* @param targetPackage 특정 애플리케이션 패키지 이름, 또는 일반 시스템 선택기의 경우 null
*/
private fun initiateShareFlow(shareCode: String, platformIdentifier: String, targetPackage: String?) {
// 디바운스 검사 시행
if (!isSharingInProgress.compareAndSet(false, true)) {
Log.d(TAG, "공유 호출이 이미 진행 중입니다. 중복 클릭을 무시합니다.")
return
}
val isTargetInstalled = isAppInstalled(targetPackage)
val effectivePlatform = if (isTargetInstalled) platformIdentifier else "Other"
val effectivePackage = if (isTargetInstalled) targetPackage else null
val isDispatched = AtomicBoolean(false)
val mainHandler = Handler(Looper.getMainLooper())
// 논블로킹 타임아웃 실행 가능: 네트워크 핸드셰이크가 지연되더라도 UI 공유 시트가 열리도록 보장
val timeoutRunnable = Runnable {
if (isDispatched.compareAndSet(false, true)) {
Log.w(TAG, "사전 공유 보고가 ${SHARE_TIMEOUT_MS}ms 후 타임아웃되었습니다. 디스패치를 진행합니다.")
dispatchShareSafely(shareCode, effectivePlatform, effectivePackage)
}
}
mainHandler.postDelayed(timeoutRunnable, SHARE_TIMEOUT_MS)
// 애플리케이션 소유 텔레메트리 어댑터를 통해 비동기적으로 공유 인텐트 보고
shareTelemetry.reportIntent(shareCode, effectivePlatform) { result ->
mainHandler.removeCallbacks(timeoutRunnable)
if (isDispatched.compareAndSet(false, true)) {
if (result.isFailure) {
Log.w(TAG, "공유 텔레메트리 실패: ${result.exceptionOrNull()?.message}")
} else {
Log.d(TAG, "$effectivePlatform 에 대한 공유 인텐트 기록됨")
}
mainHandler.post {
dispatchShareSafely(shareCode, effectivePlatform, effectivePackage)
}
}
}
}
/**
* 라이프사이클 검사 및 finally 블록의 보장된 상태 리셋과 함께 인텐트 디스패치를 실행합니다.
*/
private fun dispatchShareSafely(shareCode: String, platform: String, targetPackage: String?) {
try {
if (isFinishing || isDestroyed) {
Log.d(TAG, "액티비티가 종료되었거나 파괴되었습니다. 공유 디스패치를 건너뜁니다.")
return
}
dispatchShareAction(shareCode, platform, targetPackage)
} catch (e: Exception) {
Log.e(TAG, "공유 디스패치 중 처리되지 않은 예외 발생: ${e.message}", e)
} finally {
isSharingInProgress.set(false)
}
}
private fun dispatchShareAction(shareCode: String, platform: String, targetPackage: String?) {
// RFC 3986 인코딩을 강제하기 위해 Uri.Builder를 사용하여 목적지 URL을 안전하게 구성
val shareUri = Uri.Builder()
.scheme("https")
.authority(BASE_INVITE_HOST)
.appendEncodedPath(BASE_INVITE_PATH)
.appendQueryParameter("shareCode", shareCode)
.appendQueryParameter("platform", platform)
.build()
.toString()
val shareMessage = "앱에 가입하고 웰컴 바우처를 받으세요! 여기서 다운로드: $shareUri"
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, shareMessage)
type = "text/plain"
}
if (targetPackage != null) {
sendIntent.setPackage(targetPackage)
try {
startActivity(sendIntent)
return
} catch (e: Exception) {
Log.w(TAG, "타겟 앱 실행 실패: ${e.message}", e)
}
}
// 타겟 앱을 사용할 수 없거나 타겟 공유 실행에 실패한 경우 시스템 공유 선택기 사용
try {
sendIntent.setPackage(null)
val fallbackChooser = Intent.createChooser(sendIntent, "다음으로 친구 초대:")
startActivity(fallbackChooser)
} catch (e: Exception) {
Log.e(TAG, "시스템 공유 선택기 실패: ${e.message}", e)
showShareFailureFallback(shareUri)
}
}
}
// 예시 애플리케이션 소유 텔레메트리 서비스 인터페이스
class ShareTelemetryService {
fun reportIntent(shareCode: String, platform: String, callback: (Result<Unit>) -> Unit) {
// 구현: 애플리케이션 텔레메트리 백엔드로 네트워크 요청 디스패치
callback(Result.success(Unit))
}
}
애플리케이션 프로필을 등록하고 공유 트래킹에 필요한 설정 키를 검색하려면 팀에서 OpoInstall 개발자 콘솔에 액세스할 수 있습니다.
2단계: 동적 공유 파라미터를 캡처하기 위한 웹 JS SDK 구성
버전이 검증된 어댑터를 통한 웹 SDK 연동
아래 랜딩 페이지 구현은 검증된 드롭인 OpoInstall SDK 연동이 아닌 예시 연동 패턴입니다. 배포하기 전에 정확한 JavaScript 번들 URL, 초기화 시그니처, 준비 콜백, 웨이크업 또는 설치 메서드, 그리고 파라미터 스키마를 배포 중인 SDK 버전의 공식 문서와 대조하여 확인하세요.
SDK 관련 동작은 소형 애플리케이션 어댑터 뒤에 유지하세요. 어댑터는 문서화된 초기화 결과와 단일 웨이크업 또는 설치 작업을 랜딩 페이지에 노출해야 합니다. 해당 메서드 이름, 콜백 동작, 구성 요구사항을 확인한 후에만 이러한 작업을 실제 벤더 API에 매핑하세요.
랜딩 페이지는 수신된 shareCode를 계속 검증하고, 승인된 폴백 경로에서 추천 파라미터를 유지하며, 중복 내비게이션을 방지해야 합니다. SDK 초기화가 실패하거나 문서화된 진입점을 사용할 수 없는 경우 구성된 폴백 경로를 사용하세요. 백엔드 및 플랫폼 연동이 지원하도록 테스트되지 않은 경우 폴백이 지연된 어트리뷰션을 복구할 수 있다고 주장하지 마세요.

브라우저 URLSearchParams를 통한 shareCode 쿼리 문자열 추출 자동화
공유 URL이 생성될 때 애플리케이션은 초대자의 토큰을 쿼리 파라미터로 추가합니다(예: https://promo.example.com/invite.html?shareCode=usr_9f82a1b4&platform=WechatSession).
랜딩 페이지에서 클라이언트 측 스크립트는 URLSearchParams와 같은 표준 브라우저 인터페이스를 사용하여 이 값을 추출합니다. 악의적인 입력이나 크로스 사이트 스크립팅(XSS)으로부터 방어하기 위해, 스크립트는 SDK를 초기화하기 전에 허용 목록(allowlist) 및 포맷 패턴과 토큰을 비교하여 검증해야 합니다.
const query = new URLSearchParams(window.location.search);
const rawShareCode = query.get("shareCode");
const safePattern = /^[A-Za-z0-9_-]{1,128}$/;
const sanitizedShareCode = (rawShareCode && safePattern.test(rawShareCode)) ? rawShareCode : "";
추천 컨텍스트를 임시 메모리 및 시스템 버퍼에 스테이징
웹 JS SDK가 초기화되면, 정화된 shareCode를 보조 캠페인 식별자(channelCode)와 함께 수집합니다. 사용자가 기본 유도 버튼(CTA)을 탭하면 SDK가 컨텍스트 스테이징을 조정합니다.
- 어트리뷰션 서버 등록: 스크립트는 현재 웹 세션과 클라이언트 컨텍스트를
shareCode에 연결하는 비동기 네트워크 요청을 전송합니다. - 시스템 페이보드(클립보드) 스테이징: 클립보드 액세스는 브라우저 권한, 사용자 활성화 요구사항, 구현별 보안 정책의 적용을 받습니다. 웹 구현은 클립보드 스테이징을 선택적 개선 사항으로 취급하고 액세스가 거부되거나 사용할 수 없는 경우 폴백을 제공해야 합니다.
- 폴백 조정: 페이보드 액세스가 제한되거나, 사용자 프롬프트에 의해 거부되거나, 후속 사용자 작업에 의해 지워진 경우, 어트리뷰션 아키텍처는 서버 측 세션 매칭 또는 플랫폼 지원 설치 리퍼러 채널로 폴백합니다.
다운로드 CTA 버튼을 바인딩하여 지연된 파라미터 등록 실행
랜딩 페이지는 기본 다운로드 버튼을 SDK의 실행 메서드(wakeupOrInstall())에 직접 바인딩합니다.
웹 JS SDK는 리디렉션 라이프사이클을 내부적으로 관리합니다. 구성된 스킴 또는 유니버설 링크를 통해 앱 웨이크업 시도를 시작하고, 앱이 설치되어 있지 않은 경우 구성된 앱 스토어로의 지연된 리디렉션을 실행합니다. 사용자 영역 코드는 정상 실행 중에 SDK의 내부 내비게이션 관리에 의존해야 하며, SDK 스크립트 로드에 실패하거나 동기식 예외를 발생시키거나 브라우저 보안 제어에 의해 차단되는 시나리오를 위해 직접 폴백 내비게이션을 예약해 둡니다.
아래 구현 패턴은 웹 JS SDK를 연동하여 수신된 shareCode 쿼리 파라미터를 캡처하고, 인앱 소셜 컨테이너를 감지하고, 클릭 대기열 상태를 관리하고, 중복 키나 잘못된 형식의 URI 조각 없이 폴백 경로 전반에서 파라미터를 유지하는 방법을 보여줍니다.
<!-- 웹 JS SDK 동적 공유 파라미터 수집 및 라우팅 -->
<!-- 파일: /var/www/html/invite.html -->
<!--
연동 참고:
이 예시는 배포된 SDK 버전과 대조하여 검증된 어댑터가 필요합니다.
예시 스니펫에 표시된 생성자, CDN 경로, 콜백 또는 메서드 이름이 현재 벤더 SDK와 일치한다고 가정하지 마세요.
-->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>추천 초대 수락하기</title>
<!-- 공식 CDN에서 검증된 OpoInstall 웹 JS SDK 포함 -->
<script src="https://web.cdn.opoinstallcloud.com/opoinstall.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 20px;
text-align: center;
background-color: #f8fafc;
}
.card {
background: #ffffff;
border-radius: 12px;
padding: 32px 24px;
max-width: 420px;
margin: 40px auto;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
}
button {
background-color: #2563eb;
color: #ffffff;
border: none;
padding: 14px 28px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
width: 100%;
margin-top: 20px;
}
button:disabled {
background-color: #94a3b8;
cursor: not-allowed;
}
/* 제한된 인앱 웹뷰 컨테이너를 위한 시각적 가이드 오버레이 */
.webview-guide-mask {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
color: #ffffff;
z-index: 9999;
padding-top: 40px;
box-sizing: border-box;
}
.guide-arrow {
font-size: 32px;
text-align: right;
padding-right: 30px;
}
.guide-text {
font-size: 18px;
line-height: 1.6;
margin-top: 20px;
padding: 0 30px;
}
</style>
</head>
<body>
<main class="card">
<h1>앱에 오신 것을 환영합니다</h1>
<p id="invite-desc">친구의 초대를 받았습니다. 아래를 탭하여 설치하고 웰컴 크레딧을 받으세요.</p>
<!-- 기본 전환 버튼 -->
<button id="btn-install" type="button">다운로드 및 보너스 받기</button>
<!-- 예시 애플리케이션 소유 리디렉션 엔드포인트; 배포된 HTTPS 라우트로 교체하세요 -->
<p style="margin-top: 16px; font-size: 14px;">
<a id="fallback-store-link" href="https://promo.example.com/app-redirect">다이렉트 스토어 링크</a>
</p>
</main>
<!-- 제한된 메시징 웹뷰(WeChat, Line) 내에 표시되는 오버레이 프롬프트 -->
<div id="webview-guide" class="webview-guide-mask">
<div class="guide-arrow">↗</div>
<div class="guide-text">
우측 상단 메뉴(•••)를 탭하고 <strong>"브라우저에서 열기"</strong>(Safari / Chrome)를 선택하여 앱을 다운로드하세요.
</div>
</div>
<script>
/**
* window.location에서 shareCode 쿼리 파라미터를 추출하고 정화합니다.
* 사양에 따라 최대 128자의 URL 안전 영숫자 범위를 강제합니다.
*/
function getSanitizedShareCode() {
try {
const query = new URLSearchParams(window.location.search);
const rawCode = query.get("shareCode");
const pattern = /^[A-Za-z0-9_-]{1,128}$/;
return (rawCode && pattern.test(rawCode)) ? rawCode : "";
} catch (e) {
console.error("검색 파라미터 구문 분석 실패:", e);
return "";
}
}
/**
* platform 쿼리 파라미터를 추출하고 정화합니다.
*/
function getSanitizedPlatform() {
try {
const query = new URLSearchParams(window.location.search);
const rawPlatform = query.get("platform");
const pattern = /^[A-Za-z0-9_]{1,32}$/;
return (rawPlatform && pattern.test(rawPlatform)) ? rawPlatform : "";
} catch (e) {
return "";
}
}
/**
* 제한된 인앱 브라우저 컨테이너(WeChat, Line, WhatsApp, Facebook)를 감지합니다.
*/
function detectInAppBrowser() {
const ua = navigator.userAgent.toLowerCase();
return {
isWeChat: ua.indexOf("micromessenger") !== -1,
isLine: ua.indexOf("line/") !== -1,
isWhatsApp: ua.indexOf("whatsapp") !== -1,
isFacebook: ua.indexOf("fbav") !== -1 || ua.indexOf("instagram") !== -1
};
}
/**
* URI 컴포넌트를 안전하게 디코딩하며, 잘못된 형식의 퍼센트 인코딩 시 원시 문자열로 폴백합니다.
*/
function safeDecode(str) {
if (!str) return "";
try {
return decodeURIComponent(str.replace(/\+/g, " "));
} catch (e) {
return str;
}
}
/**
* URL 조각을 손상시키지 않고 URL 구문 분석 및 문자열 폴백 경로 전반에서
* shareCode 파라미터를 대체하거나 업데이트하여 폴백 내비게이션을 실행합니다.
*/
function executeFallbackNavigation(fallbackLink, shareCode, platform) {
if (!fallbackLink || !fallbackLink.href) {
console.error("폴백 목적지 링크가 정의되지 않았습니다.");
return;
}
let destinationUrl = fallbackLink.href;
try {
const url = new URL(destinationUrl, window.location.origin);
if (shareCode) {
url.searchParams.set("shareCode", shareCode);
}
if (platform) {
url.searchParams.set("platform", platform);
}
destinationUrl = url.toString();
} catch (err) {
console.warn("URL API 구문 분석 실패, 안정적인 문자열 폴백 적용:", err);
const hashIndex = destinationUrl.indexOf("#");
const fragment = hashIndex !== -1 ? destinationUrl.substring(hashIndex) : "";
const rawBase = hashIndex !== -1 ? destinationUrl.substring(0, hashIndex) : destinationUrl;
const qIndex = rawBase.indexOf("?");
const pathname = qIndex !== -1 ? rawBase.substring(0, qIndex) : rawBase;
const queryString = qIndex !== -1 ? rawBase.substring(qIndex + 1) : "";
const params = {};
if (queryString) {
const pairs = queryString.split("&");
for (let i = 0; i < pairs.length; i++) {
if (!pairs[i]) continue;
const parts = pairs[i].split("=");
const key = safeDecode(parts[0]);
const value = parts.length > 1 ? safeDecode(parts.slice(1).join("=")) : "";
if (key) {
params[key] = value;
}
}
}
if (shareCode) params["shareCode"] = shareCode;
if (platform) params["platform"] = platform;
const serializedPairs = [];
for (const k in params) {
if (Object.prototype.hasOwnProperty.call(params, k)) {
serializedPairs.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
}
}
const newQuery = serializedPairs.length > 0 ? "?" + serializedPairs.join("&") : "";
destinationUrl = pathname + newQuery + fragment;
}
window.location.href = destinationUrl;
}
document.addEventListener("DOMContentLoaded", function() {
const shareCode = getSanitizedShareCode();
const platform = getSanitizedPlatform();
const inApp = detectInAppBrowser();
const installBtn = document.getElementById("btn-install");
const guideOverlay = document.getElementById("webview-guide");
const fallbackLink = document.getElementById("fallback-store-link");
// 다운로드를 가로채는 제한된 컨테이너에서 열린 경우 시각적 가이드 마스크 표시
if (inApp.isWeChat || inApp.isLine) {
if (guideOverlay) guideOverlay.style.display = "block";
}
// 직접 탭해도 파라미터가 유지되도록 DOM 로드 시 정적 폴백 링크의 href를 즉시 업데이트
if (fallbackLink && fallbackLink.href) {
try {
const initialUrl = new URL(fallbackLink.href, window.location.origin);
if (shareCode) initialUrl.searchParams.set("shareCode", shareCode);
if (platform) initialUrl.searchParams.set("platform", platform);
fallbackLink.href = initialUrl.toString();
} catch (e) {
// 클릭 시점에 폴백 구문 분석 처리
}
}
// OpoInstall 수집을 위한 동적 파라미터 패키징
const dynamicData = {
shareCode: shareCode,
platform: platform
};
// SDK 준비 상태 및 일회성 작업 상태
let isSdkReady = false;
let isSdkBlocked = false;
let isClickQueued = false;
let actionStarted = false;
let installerInstance = null;
let clickTimeoutId = null;
// 검증된 벤더 기본값이 아닌, 애플리케이션 정의 초기화 폴백 임계값
const SDK_CLICK_TIMEOUT_MS = 1500;
function clearClickTimeout() {
if (clickTimeoutId !== null) {
clearTimeout(clickTimeoutId);
clickTimeoutId = null;
}
}
function restoreInstallButton() {
if (!installBtn) return;
installBtn.disabled = false;
installBtn.textContent = "다운로드 및 보너스 받기";
}
function triggerSdkWakeup() {
try {
if (installerInstance && typeof installerInstance.wakeupOrInstall === "function") {
installerInstance.wakeupOrInstall({
data: dynamicData,
channelCode: "viral_social_share"
});
} else {
executeFallbackNavigation(fallbackLink, shareCode, platform);
}
} catch (err) {
console.error("SDK 웨이크업 실패:", err);
executeFallbackNavigation(fallbackLink, shareCode, platform);
}
}
// 모든 내비게이션 경로는 이 일회성 가드를 통과해야 함
function runOnce(action) {
if (actionStarted) return;
actionStarted = true;
isClickQueued = false;
clearClickTimeout();
if (installBtn) {
installBtn.disabled = true;
installBtn.textContent = "여는 중...";
}
action();
}
function handleInstallClick() {
if (actionStarted) return;
if (isSdkReady) {
runOnce(triggerSdkWakeup);
return;
}
if (isSdkBlocked) {
runOnce(function() {
executeFallbackNavigation(fallbackLink, shareCode, platform);
});
return;
}
// SDK가 초기화되는 동안 클릭 대기
isClickQueued = true;
if (installBtn) {
installBtn.disabled = true;
installBtn.textContent = "앱 스토어에 연결 중...";
}
clearClickTimeout();
clickTimeoutId = setTimeout(function() {
if (actionStarted || isSdkReady || !isClickQueued) {
return;
}
isSdkBlocked = true;
runOnce(function() {
executeFallbackNavigation(fallbackLink, shareCode, platform);
});
}, SDK_CLICK_TIMEOUT_MS);
}
if (installBtn) {
installBtn.addEventListener("click", handleInstallClick);
}
// 웹 JS SDK 초기화
if (typeof OpoInstall === "function") {
try {
// 구성된 벤더 콘솔에서 얻은 앱 키로 교체하세요;
// 프로덕션 시크릿을 공개 소스 제어에 절대 커밋하지 마세요.
installerInstance = new OpoInstall({
appKey: "OPOINSTALL_APPKEY_PLACEHOLDER",
onready: function() {
if (this && typeof this.wakeupOrInstall === "function") {
installerInstance = this;
}
isSdkReady = true;
clearClickTimeout();
// 늦은 콜백은 완료된 작업을 절대 반복해서는 안 됨
if (actionStarted) return;
if (isClickQueued) {
runOnce(triggerSdkWakeup);
} else {
restoreInstallButton();
}
}
});
} catch (err) {
console.error("OpoInstall 초기화 실패:", err);
isSdkBlocked = true;
clearClickTimeout();
if (isClickQueued) {
runOnce(function() {
executeFallbackNavigation(fallbackLink, shareCode, platform);
});
} else {
restoreInstallButton();
}
}
} else {
console.warn("OpoInstall SDK를 사용할 수 없음; 폴백 내비게이션 활성화됨.");
isSdkBlocked = true;
restoreInstallButton();
}
});
</script>
</body>
</html>
3단계: 추천 파라미터 복구 및 계정 바인딩
최초 앱 실행 중 추천 컨텍스트 검색
설치 후 애플리케이션은 플랫폼과 배포된 어트리뷰션 SDK에서 지원하는 메커니즘을 사용하여 추천 컨텍스트를 검색해야 합니다.
웹 랜딩 페이지 URL에 있는 파라미터가 앱 스토어 설치 후 자동으로 유지된다고 가정하지 마세요.
Android에서는 설치 경로와 구성된 리퍼러 페이로드가 지원되는 경우 애플리케이션이 Google Play 설치 리퍼러를 사용할 수 있습니다. 타사 어트리뷰션 SDK가 추가 검색 API를 제공하는 경우, 사용 중인 정확한 SDK 버전에 대해 메서드 시그니처, 콜백 동작, 페이로드 스키마를 확인하세요.
iOS에서는 직접 유니버설 링크 처리와 설치 후 추천 복구를 별도의 흐름으로 취급해야 합니다. 애플리케이션은 지연된 복구를 위해 문서화된 플랫폼 허용 메커니즘을 사용해야 합니다. 메커니즘을 확인할 수 없거나 추천 컨텍스트를 복구할 수 없는 경우, 설치를 초대자에게 어트리뷰션하지 않고 온보딩을 계속 진행하세요.
논블로킹 검색 및 재시도 동작
추천 검색은 메인 UI 스레드를 차단하거나 사용자가 등록을 완료하는 것을 방해해서는 안 됩니다. 검색 작업을 비동기식으로 취급하고, 실제 SDK 또는 애플리케이션 서비스에 적절한 타임아웃을 적용하며, 성공적인 복구, 빈 결과, 타임아웃, 실패에 대해 각각 다른 결과를 기록하세요.
문서화된 SDK 동작 및 애플리케이션의 온보딩 요구사항에 따라 타임아웃을 구성하세요. 이 예시에 표시된 타임아웃 값은 관련 SDK 문서에서 명시적으로 확인하지 않는 한 벤더 기본값이 아니라 애플리케이션 레벨의 구성입니다.
문서화된 API가 지원하고 애플리케이션에 정의된 재시도 정책이 있는 경우 재시도를 시도할 수 있습니다. 등록이 완료된 후에 늦은 콜백이 도착했을 때 중복 계정 바인딩 요청을 방지하고 리워드가 반복 발행되는 것을 피하세요.
서버 측 추천 검증
클라이언트가 보고한 모든 추천 값은 신뢰할 수 없는 입력으로 취급하세요. 애플리케이션 백엔드는 후보 초대자 토큰을 검증하고, 피초대자의 자격을 확인하며, 바인딩을 생성하기 전에 추천 프로그램의 비즈니스 규칙을 시행해야 합니다.
데이터베이스 수준의 고유성 제약 조건(uniqueness constraint) 또는 동등한 트랜잭션 제어를 사용하여 동일한 피초대자에 대한 다중 초대자 바인딩을 방지하세요. 추천 바인딩을 리워드 발행과 분리하고, 서버에서 필요한 활성화 마일스톤이 검증된 후에만 리워드를 지급하세요.

WeChat, Line, WhatsApp에서의 인앱 브라우저 제한 관리
컨테이너 샌드박스 장벽: 메시징 앱이 커스텀 스킴과 직접 APK 다운로드를 가로채는 방식
바이럴 추천 공유의 대다수는 모바일 메시징 애플리케이션 내에서 발생합니다. 그러나 WeChat, Line, WhatsApp과 같은 플랫폼은 운영체제 버전 및 컨테이너 보안 정책에 따라 직접 바이너리 다운로드와 딥 링크 스킴을 자주 가로채고, 제한하거나 수정하는 샌드박스형 인앱 웹뷰 내에서 외부 웹 링크를 렌더링합니다.
초대받은 사용자가 인앱 브라우저 내에서 추천 링크를 탭할 때:
- 직접 APK 다운로드 가로채기됨: Android에서 웹 랜딩 페이지에서 직접
.apk바이너리 파일을 다운로드하려는 시도는 웹뷰 컨테이너에 의해 자주 가로채기되거나 차단됩니다. - 커스텀 URL 스킴 차단됨: 개인 URL 스킴 호출(
myapp://invite?code=123)이 실행되지 못하는 경우가 많아, 이미 앱을 설치한 사용자의 경우 직접적인 네이티브 앱 실행이 방지됩니다. - 유니버설 링크 억제될 수 있음: 특정 메시징 애플리케이션은 아웃바운드 내비게이션을 가로채어 표준 iOS 유니버설 링크가 운영체제로 바깥쪽으로 해결되는 것을 방지합니다.
인앱 사용자 에이전트(User-Agent) 헤더 감지
이러한 컨테이너 제한을 원활하게 처리하기 위해, 공유 랜딩 페이지는 초기 DOM 로딩 중에 들어오는 브라우저 User-Agent 문자열을 검사해야 합니다.
- WeChat 감지:
navigator.userAgent에서MicroMessenger검사. - Line 감지:
navigator.userAgent에서Line/검사. - WhatsApp 감지:
navigator.userAgent에서WhatsApp검사. - Facebook / Messenger / Instagram:
navigator.userAgent에서FBAV또는Instagram검사.
리디렉션 전술 평가: 직접 앱 핸드오프 vs 시스템 브라우저 전환
운영체제와 소셜 애플리케이션은 고유한 내비게이션 동작을 보입니다.
- WeChat 및 Line 환경: 이러한 컨테이너는 직접 바이너리 다운로드 및 외부 스킴 실행을 제한합니다. 자동 리디렉션을 시도하면 종종 플랫폼 경고 화면이 트리거됩니다. 랜딩 페이지는 사용자에게 기기의 기본 시스템 브라우저(iOS의 경우 Safari, Android의 경우 Chrome)에서 링크를 열도록 유도하는 명시적인 시각적 가이드를 표시합니다.
- WhatsApp 환경: 최신 iOS 및 Android 구현에서 WhatsApp은 일반적으로 시스템에서 제공하는 인앱 뷰포트(iOS의 SFSafariViewController 또는 Android의 Chrome Custom Tabs)에서 웹 링크를 엽니다. 이러한 환경은 표준 웹 내비게이션을 허용하고 플랫폼 앱 스토어로의 전환을 지원하지만, 커스텀 URL 스킴에는 여전히 명시적인 사용자 탭이 필요할 수 있습니다.
원활한 사용자 가이드 구현: 직접 리디렉션 실패 시 네이티브 메뉴 프롬프트 오버레이
인앱 웹뷰가 프로그래밍 방식의 외부 핸드오프를 방지할 때, 랜딩 페이지는 시각적 사용자 가이드를 배포합니다.
- 조건부 마스크 오버레이:
MicroMessenger또는Line/이 감지되면 화면 우측 상단을 가리키는 고대비 시각적 오버레이를 표시합니다. - 명시적인 액션 문구: 프롬프트는 사용자에게 다음과 같이 지시합니다. “우측 상단 메뉴(•••)를 탭하고 ‘브라우저에서 열기’(Safari / Chrome)를 선택하여 앱을 다운로드하세요.”
- 파라미터 상태 유지: 사용자가 동일한 URL을 로드하여 외부 브라우저로 전환할 때, 중간 리디렉션이 제거하지 않는 한 주소 표시줄에 쿼리 파라미터(
?shareCode=...)가 그대로 유지됩니다. Safari 또는 Chrome에서 랜딩 페이지가 다시 로드되면 웹 JS SDK가 깨끗하게 실행되고 추천 컨텍스트를 스테이징합니다.
소셜 퍼널 전반의 추천 파라미터 누락 트러블슈팅
공격적인 인앱 링크 시밍 프록시 전반의 파라미터 스트리핑 진단
특정 소셜 플랫폼은 중간 링크 래핑 또는 시밍 도메인(예: Facebook의 l.facebook.com 또는 Twitter의 t.co)을 통해 공유된 외부 링크를 라우팅합니다. 경우에 따라 이러한 리디렉션 홉(hop)은 비표준 쿼리 파라미터를 제거하거나 충돌하는 내부 클릭 식별자(fbclid)를 추가합니다.
다중 홉 소셜 리디렉션 전반에서 파라미터 손실을 방지하려면:
- 표준 파라미터 네이밍: IETF RFC 3986에 따라
shareCode및channelCode와 같은 표준 영숫자 쿼리 키를 고수하세요. - 사전 비행(Pre-Flight) 링크 검증: 타겟 메시징 네트워크의 라이브 채팅 세션 전반에서 링크 해결을 테스트하고, 최종 랜딩 페이지에서
window.location.search를 검사하여shareCode파라미터가 모든 중간 리디렉션을 거쳐 살아남는지 확인하세요.
데스크톱 웹 공유 처리: 사용자가 PC 브라우저에서 모바일 추천 링크를 여는 경우
추천 캠페인은 모바일 기기를 위해 설계되었지만, 사용자는 종종 데스크톱 컴퓨터(예: WhatsApp 웹, Mac/Windows용 WeChat, 또는 Slack)에서 채팅 링크를 엽니다. 데스크톱 사용자가 모바일 다운로드 랜딩 페이지에 액세스하면 모바일 스토어 리디렉션이 실패합니다.
엔터프라이즈 랜딩 페이지는 반응형 저하(responsive degradation)를 통해 데스크톱 트래픽을 처리합니다.
- 데스크톱 환경 감지: 웹 JS SDK가 데스크톱 운영체제(
Windows,macOS,Linux)를 감지합니다. - 동적 QR 코드 생성: 클릭할 수 없는 모바일 스토어 버튼을 표시하는 대신, 반응형 템플릿은 데스크톱 뷰포트에 QR 코드 컨테이너를 조건부로 표시할 수 있습니다.
- 크로스 기기 스캔: 사용자가 모바일 기기로 데스크톱 화면의 QR 코드를 스캔합니다. QR 코드는 원본
shareCode가 포함된 목적지 URL을 인코딩하여 추천 세션을 데스크톱에서 모바일로 이전합니다.
운영체제 클립보드 액세스 제한 완화
운영체제 개인정보 보호 제어는 클립보드 액세스를 관리합니다.
- Android 포그라운드 포커스 규칙: Android 10(API 수준 29) 이상에서는 백그라운드 애플리케이션이 Android ClipboardManager를 통해 클립보드 데이터를 읽을 수 없습니다. 네이티브 SDK는 애플리케이션의 메인 액티비티가 활성 창 포커스를 얻을 때까지 클립보드 검사를 지연시켜야 합니다.
- iOS 클립보드 프롬프트: iOS 16 이상에서는 Apple UIPasteboard에 대한 프로그래밍 방식 액세스가 소스 브라우저에서 붙여넣기를 허용할지 묻는 시스템 알림이나 프롬프트를 트리거할 수 있습니다.
- 네트워크 폴백 조정: 어트리뷰션 아키텍처는 로컬 클립보드 버퍼를 기회주의적 개선 사항으로 취급하고, 클립보드 읽기가 거부되거나 사용할 수 없는 경우 서버 측 세션 매칭으로 폴백하여 이러한 제한을 보완합니다.
물리적 기기 전반의 소셜 추천 퍼널을 위한 사전 비행 검증 프로토콜
고객 추천 캠페인을 시작하기 전에 성장 엔지니어링 팀은 다음 체계적인 검증 체크리스트를 실행해야 합니다.
- Android 타겟 공유 폴백: WeChat이나 Line이 설치되어 있지 않을 때 앱이 시스템 공유 선택기로 정상적으로 폴백하는지 확인합니다.
- Android 클립보드 링크 복사 폴백: 시스템 공유 선택기가 실패하거나 예외를 발생시키는 경우 전체 추천 링크가 클립보드에 복사되고 토스트 알림이 표시되는지 확인합니다.
- H5 SDK 지연 및 일회성 내비게이션: SDK initialization(초기화) 지연이 1500ms를 초과하는 상황을 시뮬레이션하여 폴백 내비게이션이 깔끔하게 실행되고 늦은
onready이벤트가 중복 내비게이션을 트리거하지 않는지 확인합니다. - H5 파라미터 및 조각(fragment) 유지: 기존 쿼리 파라미터, 중복 키,
#fragments가 포함된 URL로 랜딩 페이지 폴백 내비게이션을 테스트하여 깔끔한 파라미터 교체를 확인합니다. - iOS 유니버설 링크 및 스토어 핸드오프: 기존 사용자에 대한 직접 앱 열기를 테스트하고 Apple의 안티 핑거프린팅 규칙과 대조하여 설치 후 파라미터 복구를 평가합니다.
- 라이브 소셜 메시징 디스패치: 활성 WeChat 및 Line 채팅 세션으로 링크를 디스패치하여 인앱 브라우저 감지 오버레이가
shareCode를 누락하지 않고 사용자를 Safari나 Chrome으로 유도하는지 확인합니다. - 백엔드 바인딩 및 리워드 멱등성(Idempotency): 애플리케이션 백엔드가 후보 토큰을 수락하고, 중복 바인딩을 거부하며, 요구되는 마일스톤이 완료될 때까지 리워드를 대기 상태로 유지하는지 확인합니다.
추천 트래킹 아키텍처 비교 분석
추천 트래킹 아키텍처 비교
| 아키텍처 차원 | 수동 프로모션 코드 | 기기 핑거프린팅 | 플랫폼 지원 복구가 포함된 명시적 추천 파라미터 |
|---|---|---|---|
| 사용자 온보딩 마찰 | 수동 입력 필요 | 매칭 성공 시 낮을 수 있음 | 추천 컨텍스트가 성공적으로 복구될 때 낮음 |
| 어트리뷰션 기반 | 백엔드에서 검증된 사용자가 입력한 코드 | 구현별 신호에 기반한 확률적 매칭 | 지원되는 복구 메커니즘과 결합된 명시적 추천 컨텍스트 |
| 어트리뷰션 신뢰성 | 정확한 코드 입력 및 검증에 의존 | 신호 변경, 플랫폼 제한, 개인정보 보호 요구사항의 영향을 받을 수 있음 | 파라미터 연속성, 플랫폼 기능, 선택된 복구 메커니즘에 의존 |
| 개인정보 보호 및 규정 준수 | 안전한 토큰 처리 및 어뷰징 방지 제어 필요 | 해당 플랫폼 정책과 대조하여 평가해야 함; 금지된 핑거프린팅을 사용해서는 안 됨 | SDK 구현 및 해당 플랫폼 규칙 검토 필요 |
| 운영 복잡성 | 비교적 간단한 초기 구현; 수동 리뎀션(사용)이 추가 마찰 유발 | 매칭 로직, 모니터링, 정책 검토 필요 | 링크 라우팅, 플랫폼별 연동, 백엔드 검증, 폴백 처리 필요 |
구현 복잡성, 전환 마찰, 어트리뷰션 정확도의 트레이드오프
수동 프로모션 코드는 구현하기가 비교적 간단하지만, 사용자가 코드를 직접 입력하고 사용해야 합니다. 이는 마찰을 유발할 수 있으며 의도된 추천 관계 외부에서 코드가 공유되는 기회를 제공합니다.
기기 핑거프린팅은 사용자 상호작용을 줄여주는 것처럼 보일 수 있지만, 그 신뢰성은 매칭 구현과 사용 가능한 신호에 따라 달라집니다. Apple의 금지된 핑거프린팅 제한을 포함하여 적용 가능한 개인정보 보호 요구사항과 충돌하는 곳에서는 사용해서는 안 됩니다.
명시적 추천 파라미터는 의도된 초대자에 대한 명확한 비즈니스 수준의 표현을 제공합니다. 그러나 웹 URL에 토큰을 담고 다니는 것은 앱 스토어 설치 후 해당 토큰을 복구하는 것과 같지 않습니다. 전체 솔루션은 파라미터 연속성, 플랫폼 지원 복구 메커니즘, 서버 측 검증을 결합해야 합니다. 복구에 실패한 경우, 검증되지 않은 초대자에게 할당되는 대신 설치가 어트리뷰션되지 않은 상태로 유지되어야 합니다.
자주 묻는 질문 (FAQ)
자동화된 추천 트래킹은 프로모션 코드 없이 어떻게 앱 스토어를 통해 파라미터를 전달하나요?
WeChat이나 Line 안에서 링크를 공유할 때 추천 파라미터가 유실되는 이유는 무엇인가요?
애플리케이션 백엔드는 사기성 추천 루프를 어떻게 방지하나요?
요약 및 의사결정 프레임워크
자동화된 모바일 추천 트래킹은 웹 공유 흐름을 통해 명시적인 초대자 토큰을 전달하고 애플리케이션 백엔드에서 결과 추천 관계를 검증함으로써 수동 추천 코드 입력을 줄일 수 있습니다.
안정적인 구현에는 shareCode 쿼리 파라미터를 추가하는 것 이상의 작업이 필요합니다. 랜딩 페이지는 지원되는 리디렉션 전반에서 토큰을 유지해야 하고, 애플리케이션은 관련 플랫폼에 대해 검증된 복구 메커니즘을 사용해야 하며, 백엔드는 계정 자격을 검증하고 멱등적 바인딩 및 리워드 발행을 시행해야 합니다.
Android와 iOS는 별도로 평가해야 합니다. Google Play 설치 리퍼러는 Android에서 지원되는 설치 메타데이터를 제공할 수 있습니다. iOS에서는 애플리케이션이 설치되어 있을 때 유니버설 링크가 직접 라우팅을 지원하지만, 앱 스토어 설치 후 지연된 파라미터 복구를 자동으로 보장하지는 않습니다. 추가 복구 메커니즘은 해당 플랫폼 개인정보 보호 요구사항에 대해 문서화되고, 테스트되며, 검토되어야 합니다.
프로덕션 배포의 경우, 통합 중인 정확한 버전과 대조하여 모든 SDK 메서드 이름, 구성 값, 채널 식별자, 콜백 스키마를 확인하세요. 깨끗한 기기와 타겟 메시징 애플리케이션에서 전체 여정을 테스트하세요. 지원되는 메커니즘을 통해 추천을 복구할 수 없는 경우, 검증되지 않은 추천 관계를 생성하는 대신 온보딩이 진행되도록 허용하고 결과를 어트리뷰션되지 않은 상태로 기록하세요.
OpoInstall의 SDK 및 개발자 문서는 이 아키텍처를 구현하기 위한 컴포넌트를 제공할 수 있습니다. 프로덕션 추천 캠페인에서 신뢰하기 전에 배포된 릴리스에서 지원하는 특정 기능과 플랫폼 동작을 확인하세요.
연동 세부정보는 OpoInstall 개발자 문서, OpoInstall SDK 다운로드 센터, OpoInstall 개발자 콘솔을 참조하세요. 해당 공식 Android 및 Apple 플랫폼 문서를 사용하여 애플리케이션과 관련된 설치 및 개인정보 보호 동작을 검증하세요.
관련 자료
-
개념: 자동화된 추천 트래킹, 초대자-피초대자 바인딩, 지연된 딥 링크, 인앱 웹뷰 샌드박싱
-
기술: 모바일 어트리뷰션 SDK, 웹 JS SDK, 유니버설 링크, Android 앱 링크, S2S 포스트백
-
표준: IETF RFC 3986 URI(Uniform Resource Identifier) 일반 구문, W3C 클립보드 API 및 이벤트 사양
-
연동 인터페이스: 애플리케이션 소유 공유 인텐트 텔레메트리, 배포된 SDK 버전에 대한 문서화된 설치-리퍼러 또는 어트리뷰션 API, 플랫폼별 딥 링크 라우팅, 서버 측 추천 검증.
-
공식 문서 및 레퍼런스:
Share this article



