콘솔로 이동

앱에 사용자 초대

신규 사용자가 앱을 설치하도록 유도하는 가장 효과적인 방법 중 하나는 사용자가 앱 콘텐츠를 친구들과 공유하도록 지원하는 것입니다. 동적 링크를 사용하면 편리한 사용자 간 공유 경험을 제공할 수 있습니다. 친구로부터 콘텐츠를 추천받은 사용자는 링크를 클릭하여 앱의 공유 콘텐츠를 바로 열어볼 수 있으며, 우선 App Store 또는 Google Play 스토어로 이동하여 앱을 설치해야 하더라도 링크 정보가 그대로 유지됩니다.

사용자 추천의 밀착도와 동적 링크의 지속성을 결합하여, 앱 콘텐츠로 직접 유도하거나 추천인과 추천을 받은 사용자 모두 혜택을 받는 프로모션을 제공하여 신규 사용자를 유치하는 사용자 간 공유 및 추천 기능을 만들 수 있습니다.

주요 장점

  • 신규 사용자가 앱을 처음으로 열었을 때 보는 환경을 추천인이 신규 사용자에게 공유하려는 내용을 기반으로 상황에 맞게 설정할 수 있습니다. 예를 들어 사용자에게 공유된 콘텐츠를 표시하거나 사용자를 초대한 친구에게 자동으로 연결할 수 있습니다.
  • 다른 플랫폼을 사용하거나 앱을 설치하지 않은 친구와도 손쉽게 콘텐츠를 공유할 수 있습니다.

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

Firebase 및 동적 링크 SDK 설정

새 Firebase 프로젝트를 설정하고 앱에 동적 링크 SDK를 설치합니다.

동적 링크 SDK를 설치하면 Firebase에서 동적 링크와 관련된 데이터를 앱에 전달할 수 있으며, 사용자가 앱을 설치한 후에도 마찬가지입니다.

동적 링크 만들기

이제 사용자가 친구들에게 보낼 수 있는 링크를 설정할 차례입니다. 사용자의 친구가 아직 앱을 설치하지 않았더라도 걱정하지 마세요. 동적 링크가 알아서 처리해 줍니다.

공유할 수 있도록 설정할 콘텐츠 요소마다 동적 링크를 생성합니다.

동적 링크를 만들 때는 공유 대상 콘텐츠를 식별하는 데 사용될 HTTP 또는 HTTPS URL을 link 매개변수로 제공해야 합니다. 동일한 콘텐츠를 갖는 웹사이트가 있으면 웹사이트의 URL을 사용해야 합니다. 이렇게 하면 데스크톱 브라우저와 같이 동적 링크를 지원하지 않는 플랫폼에서도 링크가 올바르게 렌더링됩니다. 예를 들면 다음과 같습니다.

https://example.page.link/?link=https://www.example.com/content?item%3D1234&apn=com.example.android&ibi=com.example.ios&isi=12345

예를 들어 게임 초대와 같이 링크를 특정 사용자용으로 지정하려는 경우 URL 인코딩 매개변수를 추가하여 데이터 페이로드에 정보를 추가할 수도 있습니다.

https://example.page.link/?link=https://www.example.com/invitation?gameid%3D1234%26referrer%3D555&apn=com.example.android&ibi=com.example.ios&isi=12345

이러한 링크를 공유하기 전에 Firebase 동적 링크 URL 축약 API를 사용하여 알아보기 쉬운 URL을 생성할 수도 있습니다. 축약된 동적 링크의 예는 다음과 같습니다.

https://example.page.link/WXYZ

사용하는 링크에 관계없이 사용자가 기기에서 동적 링크를 열면 apn 매개변수(Android) 또는 ibi, isi 매개변수(iOS)에서 지정한 앱이 설치되어 있지 않은 경우 사용자가 앱을 설치하도록 Play 스토어 또는 App Store로 연결됩니다. 그런 다음 앱이 설치되고 열리면 'link' 매개변수에 지정된 URL이 앱에 전달됩니다.

동적 링크를 전송하는 '공유' 버튼 추가

먼저 채팅방에 사용자를 초대하는 링크를 생성하는 행아웃과 같은 채팅방 기반 채팅 앱의 간단한 예를 살펴보겠습니다.

iOS

채팅 앱 스크린샷 공유 시트가 있는 채팅 앱 스크린샷

Android

채팅 앱 스크린샷 공유 시트가 있는 채팅 앱 스크린샷

Swift

func generateContentLink() -> URL {
  let baseURL = URL(string: "https://your-custom-name.page.link")!
  let domain = "https://your-app.page.link"
  let linkBuilder = DynamicLinkComponents(link: baseURL, domainURIPrefix: domain)
  linkBuilder?.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.your.bundleID")
  linkBuilder?.androidParameters =
      DynamicLinkAndroidParameters(packageName: "com.your.packageName")

  // Fall back to the base url if we can't generate a dynamic link.
  return linkBuilder?.link ?? baseURL
}

Objective-C

- (NSURL *)generateContentLink {
  NSURL *baseURL = [NSURL URLWithString:@"https://your-custom-name.page.link"];
  NSString *domain = @"https://your-app.page.link";
  FIRDynamicLinkComponents *builder = [[FIRDynamicLinkComponents alloc] initWithLink:baseURL domainURIPrefix:domain];
  builder.iOSParameters = [FIRDynamicLinkIOSParameters parametersWithBundleID:@"com.your.bundleID"];
  builder.androidParameters = [FIRDynamicLinkAndroidParameters parametersWithPackageName:@"com.your.packageName"];

  // Fall back to the base url if we can't generate a dynamic link.
  return builder.link ?: baseURL;
}

자바
Android

public static Uri generateContentLink() {
    Uri baseUrl = Uri.parse("https://your-custom-name.page.link");
    String domain = "https://your-app.page.link";

    DynamicLink link = FirebaseDynamicLinks.getInstance()
            .createDynamicLink()
            .setLink(baseUrl)
            .setDomainUriPrefix(domain)
            .setIosParameters(new DynamicLink.IosParameters.Builder("com.your.bundleid").build())
            .setAndroidParameters(new DynamicLink.AndroidParameters.Builder("com.your.packageName").build())
            .buildDynamicLink();

    return link.getUri();
}

Kotlin
Android

fun generateContentLink(): Uri {
    val baseUrl = Uri.parse("https://your-custom-name.page.link")
    val domain = "https://your-app.page.link"

    val link = FirebaseDynamicLinks.getInstance()
            .createDynamicLink()
            .setLink(baseUrl)
            .setDomainUriPrefix(domain)
            .setIosParameters(DynamicLink.IosParameters.Builder("com.your.bundleid").build())
            .setAndroidParameters(DynamicLink.AndroidParameters.Builder("com.your.packageName").build())
            .buildDynamicLink()

    return link.uri
}

동적 링크가 있으면 표준 플랫폼 공유 흐름을 시작하는 UI에 공유 버튼을 추가할 수 있습니다.

Swift

lazy private var shareController: UIActivityViewController = {
  let activities: [Any] = [
    "Learn how to share content via Firebase",
    URL(string: "https://firebase.google.com")!
  ]
  let controller = UIActivityViewController(activityItems: activities,
                                            applicationActivities: nil)
  return controller
}()

@IBAction func shareButtonPressed(_ sender: Any) {
  let inviteController = UIStoryboard(name: "Main", bundle: nil)
    .instantiateViewController(withIdentifier: "InviteViewController")
  self.navigationController?.pushViewController(inviteController, animated: true)
}

Objective-C

- (UIActivityViewController *)shareController {
  if (_shareController == nil) {
    NSArray *activities = @[
      @"Learn how to share content via Firebase",
      [NSURL URLWithString:@"https://firebase.google.com"]
    ];
    UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:activities applicationActivities:nil];
    _shareController = controller;
  }
  return _shareController;
}

- (IBAction)shareLinkButtonPressed:(UIView *)sender {
  if (![sender isKindOfClass:[UIView class]]) {
    return;
  }

  self.shareController.popoverPresentationController.sourceView = sender;
  [self presentViewController:self.shareController animated:YES completion:nil];
}

자바
Android

private void onShareClicked() {
    Uri link = DynamicLinksUtil.generateContentLink();

    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("text/plain");
    intent.putExtra(Intent.EXTRA_TEXT, link.toString());

    startActivity(Intent.createChooser(intent, "Share Link"));
}

Kotlin
Android

private fun onShareClicked() {
    val link = DynamicLinksUtil.generateContentLink()

    val intent = Intent(Intent.ACTION_SEND)
    intent.type = "text/plain"
    intent.putExtra(Intent.EXTRA_TEXT, link.toString())

    startActivity(Intent.createChooser(intent, "Share Link"))
}

이 예에서 기본 공유 UI는 링크를 공유할 앱 목록을 자동으로 표시하는데, 이는 앱에서 코드 몇 줄만으로 설정할 수 있습니다.

사용자가 연락처를 선택하고 앱에서 메시지를 작성하도록 하는 대신 이러한 작업을 사용자가 공유 대화상자에서 선택한 앱으로 위임합니다. 또한 다른 앱에 공유 권한을 위임하면 사용자에게 연락처 권한을 요청할 필요가 없으며 사용자가 선택한 앱의 확장된 연락처에서 선택할 수 있습니다. 동적 링크에 주요 소셜 채널의 링크와 함께 표시되는 소셜 미디어 미리보기 메타데이터를 추가하면 소셜 공유를 좀 더 쉽게 할 수 있습니다.

텍스트가 없는 기본적인 링크만 보내면 추천을 받은 사용자의 관심을 끌지 못할 수도 있습니다. 링크를 짧은 메시지와 함께 제공하고 가능한 경우 다채롭게 표현하면 추천을 받은 사용자가 추천의 가치 제안을 이해하기 좋습니다.

iOS

보상 추천 스크린샷 공유 시트가 있는 보상 추천 스크린샷

Android

보상 추천 스크린샷 공유 시트가 있는 보상 추천 스크린샷

이 예가 이전 예보다 좀 더 복잡하지만 접근 방식은 거의 동일합니다. 이 화면에는 초대의 가치 제안을 나타내는 큰 그래픽과 주요 소셜 채널에 공유할 수 있는 버튼이 있습니다. 이 UI 흐름에는 일부 중복성이 있습니다. 일부 공유 채널이 개별적으로 표시되어 이메일 초대에 제목을 추가하는 등 채널별로 메시지를 맞춤설정할 수 있습니다. 이 초대 메뉴에서 다음을 수행합니다.

  • 이메일, SMS, 링크 공유 복사 버튼을 표시하고 메시지를 적절하게 맞춤설정합니다. 이메일에는 제목을 포함하고 줄바꿈, 이미지, 공백이 있는 긴 본문을 포함할 수 있습니다. SMS에는 줄바꿈은 있지만 공백은 거의 없고 이미지는 없는 짧은 본문을 포함해야 합니다. 링크 복사는 링크만 복사해야 합니다.
  • 링크가 포함된 짧은 초대 메시지 등 다른 모든 항목에 시스템 공유 UI를 사용합니다.
  • URL 스키마를 통한 딥 링크 또는 앱 초대를 처리하기 위한 특수 로직이 있는 다른 앱에 대한 범용 링크를 사용합니다. 이 링크는 조직과 다른 앱 사이의 제휴 없이는 작동하지 않으며 소규모 조직에 적당한 옵션이 아닙니다. 그렇지만 일부 앱은 범용/딥 링크 동작을 공개적으로 기록하기도 합니다. 샘플에서는 이 링크의 더미 버전을 구현합니다.

먼저 초대에서 정보만 캡슐화하고 기능을 포함하지 않는 초대 콘텐츠 유형을 정의합니다. 이렇게 하면 데이터 유형부터 시작하고 데이터를 결합하는 방식을 기준으로 코드에 대해 생각해 볼 수 있습니다.

Swift

/// The content within an invite, with optional fields to accommodate all presenters.
/// This type could be modified to also include an image, for sending invites over email.
struct InviteContent {

  /// The subject of the message. Not used for invites without subjects, like text message invites.
  var subject: String?

  /// The body of the message. Indispensable content should go here.
  var body: String?

  /// The URL containing the invite. In link-copy cases, only this field will be used.
  var link: URL

}

Objective-C

/// The content within an invite, with optional fields to accommodate all presenters.
/// This type could be modified to also include an image, for sending invites over email.
@interface InviteContent : NSObject <NSCopying>

/// The subject of the message. Not used for invites without subjects, like text message invites.
@property (nonatomic, readonly, nullable) NSString *subject;

/// The body of the message. Indispensable content should go here.
@property (nonatomic, readonly, nullable) NSString *body;

/// The URL containing the invite. In link-copy cases, only this field will be used.
@property (nonatomic, readonly) NSURL *link;

- (instancetype)initWithSubject:(nullable NSString *)subject
                           body:(nullable NSString *)body
                           link:(NSURL *)link NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;

@end

자바
Android

/**
 * The content of an invitation, with optional fields to accommodate all presenters.
 * This type could be modified to also include an image, for sending invites over email.
 */
public class InviteContent {

    /**
     * The subject of the message. Not used for invites without subjects, like SMS.
     **/
    @Nullable
    public final String subject;

    /**
     * The body of the message. Indispensable content should go here.
     **/
    @Nullable
    public final String body;

    /**
     * The URL containing the link to invite. In link-copy cases, only this field will be used.
     **/
    @NonNull
    public final Uri link;

    public InviteContent(@Nullable String subject, @Nullable String body, @NonNull Uri link) {
        // ...
    }

}

Kotlin
Android

/**
 * The content of an invitation, with optional fields to accommodate all presenters.
 * This type could be modified to also include an image, for sending invites over email.
 */
data class InviteContent(
    /** The subject of the message. Not used for invites without subjects, like SMS.  */
    val subject: String?,
    /** The body of the message. Indispensable content should go here.  */
    val body: String?,
    /** The URL containing the link to invite. In link-copy cases, only this field will be used.  */
    val link: Uri
)

여기에 필요한 유일한 데이터 요소는 URL입니다. 이 URL이 없으면 사용자를 앱에 초대할 수 없습니다. 다른 데이터 요소는 이메일 전송용으로 구성되어 있어 다른 경우에는 다소 어색합니다. 초대를 문자메시지로 보내는 경우 링크가 포함된 문구는 이메일 제목과 비슷하게 보이지만, 소셜 미디어에 링크와 더불어 텍스트를 공유하면 이메일 본문처럼 보일 수 있습니다. 앱에 가장 잘 어울리는 방법을 찾도록 직접 테스트해야 합니다. 확실하지 않으면 원격 구성과 같은 서비스를 사용하여 앱 실행 후 텍스트 값을 변경할 수 있습니다.

Swift

/// A type responsible for presenting an invite given using a specific method
/// given the content of the invite.
protocol InvitePresenter {

  /// The name of the presenter. User-visible.
  var name: String { get }

  /// An icon representing the invite method. User-visible.
  var icon: UIImage? { get }

  /// Whether or not the presenter's method is available. iOS devices that aren't phones
  /// may not be able to send texts, for example.
  var isAvailable: Bool { get }

  /// The content of the invite. Some of the content type's fields may be unused.
  var content: InviteContent { get }

  /// Designated initializer.
  init(content: InviteContent, presentingController: UIViewController)

  /// This method should cause the presenter to present the invite and then handle any actions
  /// required to complete the invite flow.
  func sendInvite()

}

Objective-C

/// A type responsible for presenting an invite given using a specific method
/// given the content of the invite.
@protocol InvitePresenter <NSObject>

/// The name of the presenter. User-visible.
@property (nonatomic, readonly) NSString *name;

/// An icon representing the invite method. User-visible.
@property (nonatomic, readonly, nullable) UIImage *icon;

/// Whether or not the presenter's method is available. iOS devices that aren't phones
/// may not be able to send texts, for example.
@property (nonatomic, readonly) BOOL isAvailable;

/// The content of the invite. Some of the content type's fields may be unused.
@property (nonatomic, readonly) InviteContent *content;

/// Designated initializer.
- (instancetype)initWithContent:(InviteContent *)content presentingViewController:(UIViewController *)controller;

/// This method should cause the presenter to present the invite and then handle any actions
/// required to complete the invite flow.
- (void)sendInvite;

@end

자바
Android

/**
 * Presents the invite using a specific method, such as email or social.
 */
public class InvitePresenter {

    /**
     * The user-visible name of the invite method, like 'Email' or 'SMS'
     **/
    public final String name;

    /**
     * An icon representing the invite method.
     **/
    @DrawableRes
    public final int icon;

    /**
     * Whether or not the method is available on this device. For example, SMS is phone only.
     **/
    public final boolean isAvailable;

    /**
     * The Content of the invitation
     **/
    public final InviteContent content;

    public InvitePresenter(String name, @DrawableRes int icon, boolean isAvailable, InviteContent content) {
        // ...
    }

    /**
     * Send the invitation using the specified method.
     */
    public void sendInvite(Context context) {
        // ...
    }

}

Kotlin
Android

/**
 * Presents the invite using a specific method, such as email or social.
 */
open class InvitePresenter(
    /** The user-visible name of the invite method, like 'Email' or 'SMS'  */
    val name: String,
    /** An icon representing the invite method.  */
    @param:DrawableRes @field:DrawableRes
    val icon: Int,
    /** Whether or not the method is available on this device. For example, SMS is phone only.  */
    val isAvailable: Boolean,
    /** The Content of the invitation  */
    val content: InviteContent
) {
    /**
     * Send the invitation using the specified method.
     */
    open fun sendInvite(context: Context) {
        // ...
    }
}

이제 원하는 UI 구성요소에 이 코드를 연결하기만 하면 됩니다. 이 초대 흐름을 완전히 구현하려면 iOSAndroid용 GitHub 샘플을 참조하세요.

이러한 샘플 예는 사용자가 친구에게 초대를 보내기 위해 사용할 수 있는 모든 메소드로, 가장 간단한 초대 솔루션입니다. 인기 있는 여러 앱도 자체 백엔드를 통해 이메일을 보내 초대를 전송합니다. 이 방법을 사용하려면 메일 전송 서비스를 통합해야 하지만 다른 방법으로는 누릴 수 없는 여러 장점을 제공하며 일부 사소한 단점이 있을 뿐입니다.

장점:

  • 전송하기 전에 사용자가 수정할 수 없는 복잡한 마크업이 있는 이메일 사용
  • 보다 세분화된 추적/분석 가능(예: 백엔드에서 성공 및 실패 전송)

단점:

  • 이메일이 스팸으로 신고될 가능성이 높음
  • 이메일 전송 서비스와 통합 필요
  • 인앱 연락처 권한 필요

일반적으로 자체 이메일 전송 서비스를 통해 초대를 보내면 범용성은 떨어지지만 일관성이 높고 잠재적으로 더 풍부한 초대 환경을 제공합니다.

앱에서 링크된 콘텐츠 열기

마지막으로, 링크된 콘텐츠를 받는 사람에게 표시하려면 앱에 전달된 링크를 수신해야 합니다. 동적 링크 SDK를 사용하면 간단합니다.

iOS

iOS에서는 application:continueUserActivity:restorationHandler: 메소드를 구현하여 동적 링크를 수신합니다. 복원 핸들러에서 handleUniversalLink:completion:을 호출하여 동적 링크를 가져올 수 있습니다. 앱에 동적 링크가 전달되면 FIRDynamicLinkurl 속성에서 해당 링크를 가져올 수 있습니다. 예를 들면 다음과 같습니다.

Objective-C

[[FIRDynamicLinks dynamicLinks]
    handleUniversalLink:userActivity.webpageURL
             completion:^(FIRDynamicLink * _Nullable dynamicLink,
                          NSError * _Nullable error) {
      NSString *link = dynamicLink.url;
      BOOL strongMatch = dynamicLink.matchConfidence == FIRDynamicLinkMatchConfidenceStrong;
      // ...
    }];

Swift

FIRDynamicLinks.dynamicLinks()?.handleUniversalLink(userActivity.webpageURL!) { (dynamiclink, error) in
    let link = dynamicLink.url
    let strongMatch = dynamicLink.matchConfidence == FIRDynamicLinkMatchConfidenceStrong
    // ...
}

또한 application:openURL:options: 메소드에서 dynamicLinkFromCustomSchemeURL:을 호출하여 앱에 맞춤 스키마 URL로 전달된 동적 링크를 수신해야 합니다. 예를 들면 다음과 같습니다.

Objective-C

FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url];
if (dynamicLink) {
  NSString *link = dynamicLink.url;
  BOOL strongMatch = dynamicLink.matchConfidence == FIRDynamicLinkMatchConfidenceStrong;
  // ...
  return YES;
}

Swift

let dynamicLink = FIRDynamicLinks.dynamicLinks()?.dynamicLinkFromCustomSchemeURL(url)
if let dynamicLink = dynamicLink {
  let link = dynamicLink.url
  let strongMatch = dynamicLink.matchConfidence == FIRDynamicLinkMatchConfidenceStrong
  // ...
  return true
}

이제 link 매개변수의 값이 확보되었으므로 링크된 콘텐츠를 받는 사람에게 표시하거나 매개변수에 지정된 데이터를 다른 방식으로 처리할 수 있습니다. JLRoutes 등의 URL 라우팅 라이브러리가 여기에 도움이 될 수 있습니다.

받는 사람이 특정된 링크를 수신한 경우 사용자별 로직을 실행하기 전에 동적 링크의 일치 신뢰도가 strong인지 확인하세요.

Android

Android에서는 getDynamicLink() 메소드를 사용하여 동적 링크에서 데이터를 가져옵니다.

자바
Android

FirebaseDynamicLinks.getInstance()
        .getDynamicLink(getIntent())
        .addOnCompleteListener(new OnCompleteListener<PendingDynamicLinkData>() {
            @Override
            public void onComplete(@NonNull Task<PendingDynamicLinkData> task) {
                if (!task.isSuccessful()) {
                    // Handle error
                    // ...
                }

                FirebaseAppInvite invite = FirebaseAppInvite.getInvitation(task.getResult());
                if (invite != null) {
                    // Handle invite
                    // ...
                }
            }
        });

Kotlin
Android

FirebaseDynamicLinks.getInstance()
        .getDynamicLink(intent)
        .addOnCompleteListener { task ->
            if (!task.isSuccessful) {
                // Handle error
                // ...
            }

            val invite = FirebaseAppInvite.getInvitation(task.result)
            if (invite != null) {
                // Handle invite
                // ...
            }
        }

이제 link 매개변수의 값이 확보되었으므로 링크된 콘텐츠를 받는 사람에게 표시하거나 매개변수에 지정된 데이터를 다른 방식으로 처리할 수 있습니다. URL 라우팅 라이브러리가 여기에 도움이 될 수 있습니다.