사용자 추천에 대한 보상을 제공하여 신규 사용자 확보

신규 사용자를 확보하는 가장 효과적인 방법 중 하나는 사용자 추천을 활용하는 것입니다. 실시간 데이터베이스Firebase용 Cloud Functions와 함께 동적 링크를 사용하면 추천이 성공한 경우 추천인과 추천을 받은 사용자 모두에게 인앱 보상을 지급함으로써 사용자가 친구를 초대하도록 유도할 수 있습니다.

주요 이점

  • 사용자가 친구를 초대하도록 유도하는 인센티브를 제공하여 성장 속도를 높일 수 있습니다.
  • 초대 링크는 여러 플랫폼에서 작동합니다.
  • 신규 사용자가 앱을 처음으로 열었을 때 보는 환경을 맞춤설정할 수 있습니다. 예를 들어 사용자를 초대한 친구에게 자동으로 연결할 수 있습니다.
  • 필요에 따라 신규 사용자가 튜토리얼 완수와 같은 초기 태스크 일부를 완료할 때까지 보상 제공을 미룰 수 있습니다.

이제 시작하는 방법을 안내해 드리겠습니다.

Firebase 및 동적 링크 SDK 설정

새 Firebase 프로젝트를 설정하고 앱에 동적 링크 SDK를 설치합니다(iOS, Android, C++, Unity). 동적 링크 SDK를 설치하면 Firebase에서 동적 링크와 관련된 데이터를 앱에 전달할 수 있으며, 사용자가 앱을 설치한 후에도 마찬가지입니다. SDK가 없으면 설치 전 클릭 정보를 설치 후 사용자 정보에 연결할 방법이 없습니다.

초대를 만들려면 우선 수신자가 열어 초대를 수락할 수 있는 링크를 만듭니다. 나중에 이 링크를 초대 텍스트에 포함합니다. 초대를 받은 사용자가 링크를 열어 앱을 설치하면 인앱 보상과 함께 맞춤설정된 최초 실행 환경이 제공됩니다.

이 초대 링크는 기존 사용자로부터 전달되었음을 알리는 link 매개변수 값을 가진 동적 링크입니다.

이러한 link 매개변수 페이로드의 형식을 지정하고 앱에 연결하는 방법에는 여러 가지가 있습니다. 간단한 방법은 다음 예시와 같이 쿼리 매개변수에 보낸 사람의 사용자 계정 ID를 지정하는 것입니다.

https://mygame.example.com/?invitedby=SENDER_UID

그런 다음 초대에 포함하기에 적합한 동적 링크를 만들려면 Dynamic Link Builder API를 사용하면 됩니다.

Swift

참고: 이 Firebase 제품은 macOS, Mac Catalyst, tvOS 또는 watchOS 대상에서는 사용할 수 없습니다.
guard let uid = Auth.auth().currentUser?.uid else { return }
let link = URL(string: "https://mygame.example.com/?invitedby=\(uid)")
let referralLink = DynamicLinkComponents(link: link!, domain: "example.page.link")

referralLink.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.example.ios")
referralLink.iOSParameters?.minimumAppVersion = "1.0.1"
referralLink.iOSParameters?.appStoreID = "123456789"

referralLink.androidParameters = DynamicLinkAndroidParameters(packageName: "com.example.android")
referralLink.androidParameters?.minimumVersion = 125

referralLink.shorten { (shortURL, warnings, error) in
  if let error = error {
    print(error.localizedDescription)
    return
  }
  self.invitationUrl = shortURL
}

Kotlin+KTX

val user = Firebase.auth.currentUser!!
val uid = user.uid
val invitationLink = "https://mygame.example.com/?invitedby=$uid"
Firebase.dynamicLinks.shortLinkAsync {
    link = Uri.parse(invitationLink)
    domainUriPrefix = "https://example.page.link"
    androidParameters("com.example.android") {
        minimumVersion = 125
    }
    iosParameters("com.example.ios") {
        appStoreId = "123456789"
        minimumVersion = "1.0.1"
    }
}.addOnSuccessListener { shortDynamicLink ->
    mInvitationUrl = shortDynamicLink.shortLink
    // ...
}

Java

FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
String uid = user.getUid();
String link = "https://mygame.example.com/?invitedby=" + uid;
FirebaseDynamicLinks.getInstance().createDynamicLink()
        .setLink(Uri.parse(link))
        .setDomainUriPrefix("https://example.page.link")
        .setAndroidParameters(
                new DynamicLink.AndroidParameters.Builder("com.example.android")
                        .setMinimumVersion(125)
                        .build())
        .setIosParameters(
                new DynamicLink.IosParameters.Builder("com.example.ios")
                        .setAppStoreId("123456789")
                        .setMinimumVersion("1.0.1")
                        .build())
        .buildShortDynamicLink()
        .addOnSuccessListener(new OnSuccessListener<ShortDynamicLink>() {
            @Override
            public void onSuccess(ShortDynamicLink shortDynamicLink) {
                mInvitationUrl = shortDynamicLink.getShortLink();
                // ...
            }
        });

초대 보내기

링크를 만들었다면 이제 초대에 링크를 포함시킵니다. 초대는 앱과 잠재고객에 따라 이메일, SMS 메시지 또는 기타 다른 매체의 형태로 전달될 수 있습니다.

이메일 초대를 보내는 방법의 예는 다음과 같습니다.

Swift

참고: 이 Firebase 제품은 macOS, Mac Catalyst, tvOS 또는 watchOS 대상에서는 사용할 수 없습니다.
guard let referrerName = Auth.auth().currentUser?.displayName else { return }
let subject = "\(referrerName) wants you to play MyExampleGame!"
let invitationLink = invitationUrl?.absoluteString
let msg = "<p>Let's play MyExampleGame together! Use my <a href=\"\(invitationLink)\">referrer link</a>!</p>"

if !MFMailComposeViewController.canSendMail() {
  // Device can't send email
  return
}
let mailer = MFMailComposeViewController()
mailer.mailComposeDelegate = self
mailer.setSubject(subject)
mailer.setMessageBody(msg, isHTML: true)
myView.present(mailer, animated: true, completion: nil)

Kotlin+KTX

val referrerName = Firebase.auth.currentUser?.displayName
val subject = String.format("%s wants you to play MyExampleGame!", referrerName)
val invitationLink = mInvitationUrl.toString()
val msg = "Let's play MyExampleGame together! Use my referrer link: $invitationLink"
val msgHtml = String.format("<p>Let's play MyExampleGame together! Use my " +
        "<a href=\"%s\">referrer link</a>!</p>", invitationLink)

val intent = Intent(Intent.ACTION_SENDTO).apply {
    data = Uri.parse("mailto:") // only email apps should handle this
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, msg)
    putExtra(Intent.EXTRA_HTML_TEXT, msgHtml)
}
intent.resolveActivity(packageManager)?.let {
    startActivity(intent)
}

Java

String referrerName = FirebaseAuth.getInstance().getCurrentUser().getDisplayName();
String subject = String.format("%s wants you to play MyExampleGame!", referrerName);
String invitationLink = mInvitationUrl.toString();
String msg = "Let's play MyExampleGame together! Use my referrer link: "
        + invitationLink;
String msgHtml = String.format("<p>Let's play MyExampleGame together! Use my "
        + "<a href=\"%s\">referrer link</a>!</p>", invitationLink);

Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:")); // only email apps should handle this
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, msg);
intent.putExtra(Intent.EXTRA_HTML_TEXT, msgHtml);
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

앱에서 추천 정보 가져오기

아직 앱이 설치되지 않은 경우 초대를 받은 사용자가 추천 링크를 열면 앱 설치를 위해 App Store 또는 Play 스토어로 연결됩니다. 설치 후 사용자가 앱을 처음으로 열면 동적 링크에 포함된 추천 정보를 가져와 이를 바탕으로 보상을 적용할 수 있습니다.

초대를 받은 사용자가 가입을 하거나 신규 사용자가 특정 작업을 완료해야만 추천 보상을 제공하는 것이 일반적입니다. 보상 기준이 충족될 때까지 동적 링크에서 얻은 보상 정보를 추적해야 합니다.

이 정보를 추적하는 방법 중 하나는 익명으로 사용자를 로그인 처리하고 익명 계정의 실시간 데이터베이스 레코드에 데이터를 저장하는 것입니다. 초대를 받은 사용자가 가입하여 익명 계정이 영구 계정으로 변환되면 새 계정에 익명 계정과 동일한 UID가 제공되고 보상 정보를 볼 수 있게 됩니다.

초대를 받은 사용자가 앱을 연 후 추천인의 UID를 저장하는 방법의 예는 다음과 같습니다.

Swift

참고: 이 Firebase 제품은 macOS, Mac Catalyst, tvOS 또는 watchOS 대상에서는 사용할 수 없습니다.
struct MyApplication: App {

  var body: some Scene {
    WindowGroup {
      VStack {
        Text("Example text")
      }
      .onOpenURL { url in
        if DynamicLinks.dynamicLinks()?.shouldHandleDynamicLink(fromCustomSchemeURL: url) ?? false {
        let dynamicLink = DynamicLinks.dynamicLinks()?.dynamicLink(fromCustomSchemeURL: url)
        handleDynamicLink(dynamicLink)
      }
      // Handle incoming URL with other methods as necessary
      // ...
      }
    }
  }
}

func handleDynamicLink(_ dynamicLink: DynamicLink?) {
  guard let dynamicLink = dynamicLink else { return false }
  guard let deepLink = dynamicLink.url else { return false }
  let queryItems = URLComponents(url: deepLink, resolvingAgainstBaseURL: true)?.queryItems
  let invitedBy = queryItems?.filter({(item) in item.name == "invitedby"}).first?.value
  let user = Auth.auth().currentUser
  // If the user isn't signed in and the app was opened via an invitation
  // link, sign in the user anonymously and record the referrer UID in the
  // user's RTDB record.
  if user == nil && invitedBy != nil {
    Auth.auth().signInAnonymously() { (user, error) in
      if let user = user {
        let userRecord = Database.database().reference().child("users").child(user.uid)
        userRecord.child("referred_by").setValue(invitedBy)
        if dynamicLink.matchConfidence == .weak {
          // If the Dynamic Link has a weak match confidence, it is possible
          // that the current device isn't the same device on which the invitation
          // link was originally opened. The way you handle this situation
          // depends on your app, but in general, you should avoid exposing
          // personal information, such as the referrer's email address, to
          // the user.
        }
      }
    }
  }
}

Kotlin+KTX

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // ...

    Firebase.dynamicLinks
            .getDynamicLink(intent)
            .addOnSuccessListener(this) { pendingDynamicLinkData ->
                // Get deep link from result (may be null if no link is found)
                var deepLink: Uri? = null
                if (pendingDynamicLinkData != null) {
                    deepLink = pendingDynamicLinkData.link
                }
                //
                // If the user isn't signed in and the pending Dynamic Link is
                // an invitation, sign in the user anonymously, and record the
                // referrer's UID.
                //
                val user = Firebase.auth.currentUser
                if (user == null &&
                        deepLink != null &&
                        deepLink.getBooleanQueryParameter("invitedby", false)) {
                    val referrerUid = deepLink.getQueryParameter("invitedby")
                    createAnonymousAccountWithReferrerInfo(referrerUid)
                }
            }
}

private fun createAnonymousAccountWithReferrerInfo(referrerUid: String?) {
    Firebase.auth
            .signInAnonymously()
            .addOnSuccessListener {
                // Keep track of the referrer in the RTDB. Database calls
                // will depend on the structure of your app's RTDB.
                val user = Firebase.auth.currentUser
                val userRecord = Firebase.database.reference
                        .child("users")
                        .child(user!!.uid)
                userRecord.child("referred_by").setValue(referrerUid)
            }
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // ...

    FirebaseDynamicLinks.getInstance()
            .getDynamicLink(getIntent())
            .addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {
                @Override
                public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
                    // Get deep link from result (may be null if no link is found)
                    Uri deepLink = null;
                    if (pendingDynamicLinkData != null) {
                        deepLink = pendingDynamicLinkData.getLink();
                    }
                    //
                    // If the user isn't signed in and the pending Dynamic Link is
                    // an invitation, sign in the user anonymously, and record the
                    // referrer's UID.
                    //
                    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                    if (user == null
                            && deepLink != null
                            && deepLink.getBooleanQueryParameter("invitedby", false)) {
                        String referrerUid = deepLink.getQueryParameter("invitedby");
                        createAnonymousAccountWithReferrerInfo(referrerUid);
                    }
                }
            });
}

private void createAnonymousAccountWithReferrerInfo(final String referrerUid) {
    FirebaseAuth.getInstance()
            .signInAnonymously()
            .addOnSuccessListener(new OnSuccessListener<AuthResult>() {
                @Override
                public void onSuccess(AuthResult authResult) {
                    // Keep track of the referrer in the RTDB. Database calls
                    // will depend on the structure of your app's RTDB.
                    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                    DatabaseReference userRecord =
                            FirebaseDatabase.getInstance().getReference()
                                    .child("users")
                                    .child(user.getUid());
                    userRecord.child("referred_by").setValue(referrerUid);
                }
            });
}

이후 초대를 받은 사용자가 계정을 만들기로 결정하면 익명 계정의 추천 정보를 초대를 받은 사용자의 새 계정으로 옮길 수 있습니다.

먼저 초대 대상자가 원하는 로그인 방법을 사용해 AuthCredential 객체를 가져옵니다. 이메일 주소와 비밀번호로 로그인하는 방법의 예는 다음과 같습니다.

Swift

참고: 이 Firebase 제품은 macOS, Mac Catalyst, tvOS 또는 watchOS 대상에서는 사용할 수 없습니다.
let credential = EmailAuthProvider.credential(withEmail: email, password: password)

Kotlin+KTX

val credential = EmailAuthProvider.getCredential(email, password)

Java

AuthCredential credential = EmailAuthProvider.getCredential(email, password);

그런 다음 이 사용자 인증 정보를 익명 계정에 연결합니다.

Swift

참고: 이 Firebase 제품은 macOS, Mac Catalyst, tvOS 또는 watchOS 대상에서는 사용할 수 없습니다.
if let user = Auth.auth().currentUser {
  user.link(with: credential) { (user, error) in
    // Complete any post sign-up tasks here.
  }
}

Kotlin+KTX

Firebase.auth.currentUser!!
        .linkWithCredential(credential)
        .addOnSuccessListener {
            // Complete any post sign-up tasks here.
        }

Java

FirebaseAuth.getInstance().getCurrentUser()
        .linkWithCredential(credential)
        .addOnSuccessListener(new OnSuccessListener<AuthResult>() {
            @Override
            public void onSuccess(AuthResult authResult) {
                // Complete any post sign-up tasks here.
            }
        });

새 영구 계정에서 익명 계정에 추가된 모든 보상 데이터를 볼 수 있게 됩니다.

추천인과 초대를 받은 사용자에게 보상 제공

동적 링크에서 초대 데이터를 가져와 저장했으므로 이제 요구된 기준을 충족할 때마다 추천인과 초대를 받은 사용자에게 추천 보상을 제공할 수 있습니다.

클라이언트 앱에서 실시간 데이터베이스에 데이터를 쓸 수는 있지만 앱의 인앱 화폐 같은 데이터에 대한 읽기 액세스만 허용하고 쓰기 작업은 백엔드에서만 수행하려는 경우가 많습니다. Firebase Admin SDK를 실행할 수 있는 시스템이라면 무엇이든 백엔드가 될 수 있지만 Cloud Functions를 사용하면 이러한 작업이 훨씬 쉬워집니다.

예를 들어 게임 앱에서 초대를 받은 사용자가 가입하면 해당 사용자에게 인게임 화폐를 보상으로 제공하고 추천인에게는 초대를 받은 사용자가 레벨 5에 도달하면 보상을 제공한다고 가정해 봅시다.

가입 시 보상은 특정 실시간 데이터베이스 키가 생성되는지 관찰하다가 키를 발견하면 보상을 제공하는 함수를 배포하여 구현합니다. 예를 들면 다음과 같습니다.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.grantSignupReward = functions.database.ref('/users/{uid}/last_signin_at')
    .onCreate(event => {
      var uid = event.params.uid;
      admin.database().ref(`users/${uid}/referred_by`)
        .once('value').then(function(data) {
          var referred_by_somebody = data.val();
          if (referred_by_somebody) {
            var moneyRef = admin.database()
                .ref(`/users/${uid}/inventory/pieces_of_eight`);
            moneyRef.transaction(function (current_value) {
              return (current_value || 0) + 50;
            });
          }
        });
    });

그런 다음 신규 사용자가 가입할 때 실시간 데이터베이스 키를 만들어 이 함수를 트리거합니다. 예를 들면 이전 단계에서 만든 linkWithCredential의 성공 리스너에서 함수를 트리거합니다.

Swift

참고: 이 Firebase 제품은 macOS, Mac Catalyst, tvOS 또는 watchOS 대상에서는 사용할 수 없습니다.
if let user = Auth.auth().currentUser {
  user.link(with: credential) { (user, error) in
    // Complete any post sign-up tasks here.

    // Trigger the sign-up reward function by creating the "last_signin_at" field.
    // (If this is a value you want to track, you would also update this field in
    // the success listeners of your Firebase Authentication signIn calls.)
    if let user = user {
      let userRecord = Database.database().reference().child("users").child(user.uid)
      userRecord.child("last_signin_at").setValue(ServerValue.timestamp())
    }
  }
}

Kotlin+KTX

Firebase.auth.currentUser!!
        .linkWithCredential(credential)
        .addOnSuccessListener {
            // Complete any post sign-up tasks here.

            // Trigger the sign-up reward function by creating the
            // "last_signin_at" field. (If this is a value you want to track,
            // you would also update this field in the success listeners of
            // your Firebase Authentication signIn calls.)
            val user = Firebase.auth.currentUser!!
            val userRecord = Firebase.database.reference
                    .child("users")
                    .child(user.uid)
            userRecord.child("last_signin_at").setValue(ServerValue.TIMESTAMP)
        }

Java

FirebaseAuth.getInstance().getCurrentUser()
        .linkWithCredential(credential)
        .addOnSuccessListener(new OnSuccessListener<AuthResult>() {
            @Override
            public void onSuccess(AuthResult authResult) {
                // Complete any post sign-up tasks here.

                // Trigger the sign-up reward function by creating the
                // "last_signin_at" field. (If this is a value you want to track,
                // you would also update this field in the success listeners of
                // your Firebase Authentication signIn calls.)
                FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
                DatabaseReference userRecord =
                        FirebaseDatabase.getInstance().getReference()
                                .child("users")
                                .child(user.getUid());
                userRecord.child("last_signin_at").setValue(ServerValue.TIMESTAMP);
            }
        });

초대를 받은 사용자가 레벨 5에 도달했을 때 추천인에게 보상을 제공하려면 사용자 레코드의 level 필드에 변경사항이 있는지 관찰하는 함수를 배포합니다. 사용자가 레벨 4에서 레벨 5로 이동했을 때 기록된 추천인이 있다면 보상을 제공합니다.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

exports.rewardReferrals = functions.database.ref('/users/{uid}/level')
    .onUpdate(event => {
      var level = event.data.val();
      var prev_level = event.data.previous.val();
      if (prev_level == 4 && level == 5) {
        var referrerRef = event.data.ref.parent.child('referred_by');
        return referrerRef.once('value').then(function(data) {
          var referrerUid = data.val();
          if (referrerUid) {
            var moneyRef = admin.database()
                .ref(`/users/${referrerUid}/inventory/pieces_of_eight`);
            return moneyRef.transaction(function (current_value) {
              return (current_value || 0) + 50;
            });
          }
        });
      }
    });

이제 추천인과 신규 사용자 모두 보상을 받게 되었습니다.