[React Native] Branch io 도입기

aborile·2023년 4월 27일
5

React Native

목록 보기
2/8
post-thumbnail

도입 배경

현재 개발 중인 서비스에서는 초대 링크를 통한 가입 시 보상을 주는 정책을 시행하고 있다. 이를 구현하기 위해 Firebase의 Dynamic Link를 사용하고 있었는데, 서비스를 운영하고 있던 중 초대 링크 가입이 누락되는 케이스가 빈번하게 발생하고 있었다.

다양한 기기에서 다양한 케이스를 시도해 봤을 때, 문제가 되는 케이스는 '특정 버전 이후의 아이폰'이었다. (정확히 기기 차이라고는 할 수 없으나, 가지고 있던 기기 중 아이폰8에서는 정상작동하였으나, 아이폰12프로맥스, 아이폰13미니, 아이폰14에서는 모두 동작하지 않았다. 아마 Universal Link를 사용하는 기기에서 작동하지 않는 걸로 보인다.)
동작하지 않는 케이스에서는, 앱 설치 유도와 실행까지는 잘 되었으나, 공유 링크의 query parameter로 넘긴 값이 아예 들어오지 않았다.

분명히 ios 관련 어떤 설정을 잘못 했겠거니 하고 이것저것 건드리며 삽질을 계속하고 있던 와중, 다른 팀원이 지인에게 Branch io 도입을 추천 받았다. 그 분의 회사에서도 초대링크 관련 누락 건이 자꾸 발생했는데, Dynamic Link가 아닌 아예 다른 서비스를 사용한 이후로 해당 문제가 아예 사라졌다며. 솔깃한 마음에 '옳다구나!' 하고 바로 적극적으로 도입을 검토했고, 눈물을 머금고 삽질하던 Dynamic Link를 고이 보내 드렸다. 🥲

사실 수많은 react-native-firebase 깃헙의 이슈 글과 stackoverflow 문서를 뒤적이다 지쳐버려서 제3의 대안을 쉽게 받아들인 감도 있다. 때문에 아직 정확한 원인은 못 찾았으며, Dynamic Link 세팅할 때 AppDelegate 파일을 잘못 설정했을 가능성이 가장 높다고 보고 있다.

Branch io

Branch io 공식 홈페이지

사실 Branch io 공식 홈페이지를 확인해 봤을 때에는 Dynamic Link나 Appsflyer 같은 다른 딥링킹 툴과 비슷하다는 느낌뿐이었다. (이전 프로젝트에서는 Appsflyer를 사용해서 아직은 Appsflyer가 내적 친밀감이 높다.) 연동하는 과정에서도, 그 이후에 확인한 대시보드 등에서도 비슷한 느낌이어서 이 글은 Branch io에 대한 추천 글이라기보다는, 그냥 연동 과정을 정리한 정도의 글이라는 점을 알아 주셨으면(?) 한다.
(사족] 사실2 이 글을 남기려고 한 가장 큰 이유는 공식 문서 상 연동 가이드가 생각보다 헷갈리게 되어 있어서... 기록 차원에서...)

궁금해져서 참고 차 npm trends를 한번 확인해 봤는데 전반적으로는 Branch 사용량이 우세한 기간이 종종 있었지만 어찌되었든 큰 수준에서 사용량은 Branch와 Dynamic Link가 엎치락뒤치락하며 엇비슷한 듯 보인다. (Appsflyer가 나름 탑티어라고 생각했었는데 사용량이 이렇게 차이나는 줄은 처음 알았다.)

RN 프로젝트에 Branch io 연동하기

React Native 앱 연동 가이드를 확인해 보면 가장 위에 나오는 내용이 'Default Link Settings를 포함하여 전체적으로 Branch를 구성하기'이다.

대시보드에서 Link Settings를 설정하는 것은 비교적 쉽게 끝났으나, 해당 문서 내에서는 'react native branch 라이브러리 연동'만을 설명하고 있어, 안드로이드/ios 각 기기에서 Branch io의 딥링크를 연결하고 동작하도록 설정하는 것은 일반 안드로이드/ios 연동 문서를 참고해야 했다. 어디까지가 딥링크 연동이고 어디서부터가 Branch 라이브러리 연동인지 모호하게 설명되어 있어서 퍽 애를 먹었다. (이 글을 적게 된 결정적인 원인...) 이 글에서는 공식 문서 페이지의 구분 없이 내가 안드로이드를, 그리고 ios를 어떻게 연동했는지만 구분해서 적으려 한다.

Branch io에 회원가입한 후, 안내에 따라 초기 설정을 구성하였다. 최초에 세팅하는 가이드는 그냥 바로 툭툭 하고 넘겨서 캡처하지 못했는데, 해당 내용은 모두 대시보드 내의 'Configuration' 파트에서 차후에 수정할 수 있다. 위의 공식 문서 상으로도 대시보드 내에서 설정하는 내용을 상정하고 설명되어 있다.
이미 어플이 스토어에 등록되어 있으므로 대부분의 설정을 쉽게 할 수 있었다.

  1. Default Link Behavior
    실제 본인의 어플이나 스토어같은 특정한 redirect가 없을 경우 이동할 fallback URL을 입력해야 한다. 나는 공식 홈페이지 사이트 주소를 적었다.

  2. Android Default Link Behavior
    설정한 어플의 URI Scheme과 패키지 명을 입력해야 한다. 나의 경우 Google Play Search에서 패키지 명을 입력하였을 때 내 어플이 검색되지 않았는데,

    검색된 결과 중에서 선택을 해야만 설정을 저장할 수 있어서 그냥 Custom URL에 어플의 play store 주소와 패키지 명을 입력해서 저장했다. 이후 연동해서 확인해 보니 정상적으로 이동하는 듯 보인다.

    이외에도 App Links를 설정하여 [링크 -> 브라우저 -> 앱]의 순서가 아닌 링크를 통해 바로 앱을 열도록 설정할 수 있는데, 이 경우 SHA256 Cert Fingerprint가 필요하다.

    SHA256 Cert Fingerprint를 얻기 위해서는 앱의 안드로이드 배포용 keystore 파일이 있는 경로에서 keytool -list -v -keystore {my-release-key}.keystore 명령어를 실행한 뒤 keystore 비밀번호를 입력하면 된다.

  3. iOS Default Link Behavior
    안드로이드와 마찬가지로 어플의 URI Scheme과 Bundle ID(패키지 명)를 입력해야 한다.

    또한 마찬가지로, 링크를 통해 바로 앱을 열기 위해 Universal Links 설정을 하려면 Bundle ID와 Apple App Prefix를 입력해야 한다. Apple App Prefix는 보통 앱을 배포한 Apple 개발자 계정의 ID이다.

    이외에도 Branch에서는 iOS에 NativeLink라는 기능을 제공하는데, 클립보드를 사용하여 deferred deep linking을 보장하는 기능이다. 해당 옵션을 활성화한 뒤 링크로 접속해 보면 랜딩 페이지에 CTA와 함께 딥링크를 클립보드에 복사하는 버튼을 표시하고, 사용자가 앱을 설치한 뒤 실행하면 클립보드를 참조하여 복사한 딥링크를 자동으로 수신한다.

  4. Social Media Preview
    Dynamic Link에서는 대시보드 내에서 각 링크 별로 preview 정보를 설정해 줘야했다면, Branch io에서는 모든 링크에 대한 하나의 preview를 설정할 수 있다.

  5. Link Domain
    현재 설정되어 있는 도메인 링크를 보여주고 관리할 수 있다. 기본으로 제공하는 {subdomain}.app.link 도메인이 아닌 자신의 도메인으로 변경하거나, 다른 subdomain으로 변경할 수 있다.

    나는 기본으로 제공하는 app.link 도메인을 사용하였는데, 자신의 도메인으로 변경하려면 CNAME과 CAA 레코드를 변경하면 되는 듯하다. Link Domain 변경에 대한 안내는 공식 문서에서 각 케이스 별로 자세히 확인할 수 있다.
    subdomain 변경은 기본적으로는 단 한 번만 변경할 수 있고, 추가 변경이 필요하면 Support team에 따로 연락해야 한다고 하니 주의.

  6. Advanced Settings
    이외에도 Universal Links나 Android App Links가 실패했을 때의 동작 방식, Redirect Allowlist, UTM 태그 사용 여부 등을 설정할 수 있다.

  7. Desktop App Default Link Behavior
    Desktop에서 링크를 열었을 때, 기본 동작 방식을 설정할 수 있다. Branch에서 자체적으로 제공하는 QR Code 랜딩 페이지를 설정할 수도 있고, 자신이 원하는 링크로 이동시킬 수도 있다.

    이외에도 Mac Desktop이냐 Windows Desktop이냐에 따라 별도로 Redirect를 지정할 수도 있다.

  8. Advanced Mobile Redirects
    Amazon Fire, iPad, Android Tablet, WeChat의 Redirect를 별도로 지정할 수 있다.

react-native-branch 설치하기

RN v0.71.2를 사용하고 있으며, Expo가 아닌 Pure React Native App으로 구성하였다. Expo를 사용하는 경우 Expo 가이드 내용을 확인해 볼 것.

자신이 사용하고 있는 package manager에 따라 npm install react-native-branch 또는 yarn add react-native-branch와 같이 설치하면 된다. (단, react-native-branch 라이브러리는 react-native >= 0.60 이상의 버전만 지원한다고 한다.)

이후, pod install 명령어를 실행하여 Pods dependencies를 설치한다. (공식 문서 상으로는 CocoaPods를 사용한 Native iOS app의 경우 Podfile 내에 pod 'react-native-branch', path: '../node_modules/react-native-branch'를 입력하라고 하는데, 나의 경우에는 Podfile 수정 없이도 정상적으로 설치되었다.)

iOS 연동하기

Branch Dashboard에 입력한 Bundle ID가 프로젝트의 Bundle ID와 동일한지 다시 한번 확인한다.

Associated Domains 설정

대시보드에서 link domian을 확인한 뒤, XCode에서 Signing & Capabilities - Associated Domains에서 해당 link domain을 추가한다.

-alternate 링크는 앱이 설치되지 않은 사용자의 Universal Link 동작과 Deep View를 보장한다고 한다. 테스트 키가 필요한 경우 test- 도메인이 필요하다고 한다.

Entitlements 확인

Entitlements가 target 내에 제대로 있는지 확인 (이 파일은 위의 Capabilities 탭에서 설정을 완료하면 자동으로 구성된다고 한다.)

Info.plist 구성

대시보드에서 확인할 수 있는 keyInfo.plist에 추가해야 한다.

branch_universal_link_domiansbranch_key를 추가하고, URI scheme이 설정되지 않았다면 이 역시 추가해야 한다.

    <key>branch_key</key>
    <dict>
        <key>live</key>
        <string>key_live_{my_branch_key}</string>
        <key>test</key>
        <string>key_test_{my_branch_key}</string>
    </dict>
    <key>branch_universal_link_domains</key>
    <array>
        <string>{my-app}.app.link</string>
        <string>{my-app}-alternate.app.link</string>
        <string>{my-app}.test.app.link</string>
        <string>{my-app}-alternate.test.app.link</string>
    </array>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>{my-app-scheme}</string>
            </array>
            <key>CFBundleURLName</key>
            <string>{com.my-app}</string>
        </dict>
    </array>

AppDelegate file

AppDelegate 파일 내에 Branch 관련 설정을 해주어야 한다. 크게 didFinishLaunchingWithOptions, openURL, continueUserActivity 세 함수에 설정을 추가해 주면 된다.

#import "AppDelegate.h"
#import <RNBranch/RNBranch.h>
  
@implementation AppDelegate
  
// Initialize the Branch Session at the top of existing application:didFinishLaunchingWithOptions:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Uncomment this line to use the test key instead of the live one.
    // [RNBranch useTestInstance];
    [RNBranch initSessionWithLaunchOptions:launchOptions isReferrable:YES];
    // NSURL *jsCodeLocation;
    //...
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    [RNBranch application:app openURL:url options:options];
    //...
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
   [RNBranch continueUserActivity:userActivity];
    //...
}

@end

NSURL *jsCodeLocation; 줄의 경우 공식 문서 상에는 해당 코드까지 적혀 있으나, 나는 이상하게 해당 줄을 넣으면 빌드 오류가 나서 임시 주석처리 해두었다. (아마 import를 제대로 추가 안 한 듯...)

Android 연동하기

AndroidManifest.xml 구성

AndroidManifest.xml에 Branch key 및 링크 설정을 추가해야 한다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.my-app">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="com.google.android.gms.permission.AD_ID"/>

    <application
        android:name="com.my-app"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <!-- Launcher Activity to handle incoming Branch intents -->
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTask"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar"
            android:exported="true">

           <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!-- Branch URI Scheme -->
            <intent-filter>
                <!-- If utilizing $deeplink_path please explicitly declare your hosts, or utilize a wildcard(*) -->
                <data android:scheme="yourapp" android:host="open" />
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>

            <!-- Branch App Links - Live App -->
            <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="{my-app}.app.link" />
                <!-- example-alternate domain is required for App Links when the Journeys/Web SDK and Deepviews are used inside your website.  -->
                <data android:scheme="https" android:host="{my-app}-alternate.app.link" />
            </intent-filter>
           <!-- Branch App Links - Test App -->
           <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="{my-app}.test-app.link" />
                <!-- example-alternate domain is required for App Links when the Journeys/Web SDK and Deepviews are used inside your website.  -->
                <data android:scheme="https" android:host="{my-app}-alternate.test-app.link" />
            </intent-filter>
         </activity>

        <!-- Branch init -->
        <meta-data android:name="io.branch.sdk.BranchKey" android:value="key_live_{my_branch_key}" />
        <meta-data android:name="io.branch.sdk.BranchKey.test" android:value="key_test_{my_branch_key}" />
        <meta-data android:name="io.branch.sdk.TestMode" android:value="false" />     <!-- Set to true to use Branch_Test_Key (useful when simulating installs and/or switching between debug and production flavors) -->

    </application>

    <queries>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="text/plain" />
        </intent>
    </queries>

</manifest>

application 아래 설정이나 activity의 이름 등이 현재 내 프로젝트의 코드와 다른 부분도 있는데, 이는 모두 무시하고 launcher intent-filter가 있는 곳에 Branch URI Scheme과 App Links 관련 intent-filter를 추가하고, root에 queries를 추가하면 된다.

MainApplication.java file

onCreate override 내에 Branch 관련 설정을 해주어야 한다.

// import from RNBranch
import io.branch.rnbranch.RNBranchModule;

public class MainApplication extends Application implements ReactApplication {

// add onCreate() override
@Override
public void onCreate() {
  super.onCreate();
  
  // Branch logging for debugging
  RNBranchModule.enableLogging();
  
  RNBranchModule.getAutoInstance(this);
}

MainActivity.java file

getMainComponentName이 제대로 설정되어 있는지 확인하고, onStart override와 onNewIntent override 내에 Branch 설정을 추가하면 된다.

import android.content.Intent;
import io.branch.rnbranch.*;

public class MainActivity extends ReactActivity {

      @Override
      protected String getMainComponentName() {
          return "my-app";
      }

      // Override onStart:
      @Override
      protected void onStart() {
          super.onStart();
          RNBranchModule.initSession(getIntent().getData(), this);
      }
            
      // Override onNewIntent:
      @Override
      public void onNewIntent(Intent intent) {
          super.onNewIntent(intent);
          RNBranchModule.onNewIntent(intent);
      }

}

Enable 100% Matching

Android SDK 문서 내 Advanced Features를 확인해 보면 Enable 100% Matching이라는 항목이 있다. Chrome Tabs를 사용하여 매칭 성공율을 높이는 방법이라고 한다. android/app/build/gradle 파일 내 dependenciesimplementation 'androidx.browser:browser:1.0.0'를 추가하면 쿠키를 활용하여 매칭 성공을 보장한다고 한다. androidx를 지원하지 않는다면 implementation 'com.android.support:customtabs:28.0.0'을 추가하면 된다고 한다.

react-native-branch 사용하기

import branch from 'react-native-branch'로 import하여 branch 관련 모듈을 사용할 수 있다.

Deep link를 생성하기 위해서는 Branch Universal Object를 먼저 생성한 뒤, 이를 사용하여 deep link url을 생성하면 된다.

import branch from "react-native-branch";

async function generateShortLink() {
  try {
    // Needs a Branch Universal Object
    const buo = await branch.createBranchUniversalObject("identifier", {
      contentMetadata: { customMetadata: { customData: "custom" } },
    });
    // Uses Deep Link Properties
    const { url } = await buo.generateShortUrl();
    return url;
  } catch (error) {
    console.log(error);
  }
};
Branch Universal Object로 넘길 수 있는 option
    
interface BranchUniversalObjectOptions {
  locallyIndex?: boolean;
  publiclyIndex?: boolean;
  canonicalUrl?: string;
  title?: string;
  contentDescription?: string;
  contentImageUrl?: string;
  contentMetadata?: {
    price?: number | string;
    contentSchema?: any; // TODO
    quantity?: number;
    sku?: string;
    productName?: string;
    productBrand?: string;
    productCategory?: any; // TODO
    productVariant?: string;
    condition?: any; // TODO
    currency?: string;
    ratingAverage?: number;
    ratingCount?: number;
    ratingMax?: number;
    addressStreet?: string;
    addressCity?: string;
    addressRegion?: string;
    addressCountry?: string;
    addressPostalCode?: string;
    latitude?: number;
    longitude?: number;
    imageCaptions?: string[];
    customMetadata?: Record(string, string);
  };
}
    
  
contentMetadata: { customMetadata: { ... } } 내에 원하는 파라미터를 넘겨 저장할 수 있다. generateShortUrl에서 설정할 수 있는 option
  	
generateShortUrl: (
  linkProperties?: BranchLinkProperties,
  controlParams?: BranchLinkControlParams
) => Promise { uri: string };
  
interface BranchLinkProperties {
  alias?: string;
  campaign?: string;
  feature?: string;
  channel?: string;
  stage?: string;
  tags?: string[];
}
  
interface BranchLinkControlParams {
  $fallback_url?: string;
  $desktop_url?: string;
  $ios_url?: string;
  $ipad_url?: string;
  $android_url?: string;
  $samsung_url?: string;
}
    
  

branch.subscribe 함수를 사용하여 deep link로부터 data를 받아오는 listener를 만들 수 있다. 단, 이 listner의 경우 앱이 설치되고 난 뒤, 링크를 통해 앱이 열리는 경우를 구독하는 listener이다.

import branch from "react-native-branch";

function subscribeBranch() {
  // listener
  const unsubscribe = branch.subscribe({
    onOpenStart: ({ uri, cachedInitialEvent }) => {
      console.log("subscribe onOpenStart, will open " + uri + " cachedInitialEvent is " + cachedInitialEvent);
    },
    onOpenComplete: ({ error, params, uri }) => {
      if (error) {
        console.error(
          "subscribe onOpenComplete, Error from opening uri: " + uri + " error: " + error);
        return;
      }
      if (params) {
        if (!params["+clicked_branch_link"]) {
          if (params["+non_branch_link"]) {
            console.log("non_branch_link: " + uri);
            // Route based on non-Branch links
            return;
          }
          // handle params
          let deepLinkPath = params.$deeplink_path as string; 
          let canonicalUrl = params.$canonical_url as string;
          // Route based on Branch link data 
        }
      }
    },
  });

  return unsubscribe;
};

공식 문서 상 예제 코드는 이상하게 위와 같이 branch link가 아닌 경우에 대한 핸들링 케이스가 있는데, branch link를 통한 케이스만 핸들링하고 싶다면 params["+clicked_branch_link"]true인 경우를 조건문으로 걸면 된다.

위의 Create deep link 예시에서처럼 customMetadata를 만들어 넘겼다면, params.customData와 같이 호출하여 불러올 수 있다.

BranchParams interface
  	
export interface BranchParams {
  '~channel'?: string;
  '~feature'?: string;
  '~tags'?: string[];
  '~campaign'?: string;
  '~stage'?: string;
  '~creation_source'?: number;
  '~referring_link'?: string;
  '~id'?: string;
  '+match_guaranteed': boolean;
  '+referrer'?: string;
  '+phone_number'?: string;
  '+is_first_session': boolean;
  '+clicked_branch_link': boolean;
  '+click_timestamp'?: number;
  '+url'?: string;
  '+rn_cached_initial_event'?: boolean;
  [data: string]: AnyDataType;
}
    
  

앱이 설치되기 전에 실행되었던 deep link에 대한 정보를 받아오고 싶다면 referring params를 체크하면 된다. getLatestReferringParams는 native SDK에 저장된 가장 최신의 referring link param을 반환한다. getFirstReferringParams는 최초 설치 시의 referring link param을 반환한다.

import branch from "react-native-branch";

async function getBranchReferringParams() {
  const firstReferringParams = await branch.getFirstReferringParams();
  const latestReferringParams = await branch.getLatestReferringParams();
}

References

profile
기록하고 싶은 것을 기록하는 저장소

5개의 댓글

comment-user-thumbnail
2023년 10월 4일

안녕하세요, 저는 브랜치 한국지사에서 일하고 있습니다.
국내에서는 자주 접하기 어려운, 개발자분들의 관점에서 써주신 글이라 매우 새롭고 흥미롭게 읽었습니다.
알고 계실 수도 있지만, Firebase의 Dynamic Link는 이제 10개월 정도 이후에는 서비스를 완전히 종료한다고 하니, 이 또한 글을 읽는 분들께도 도움이 되면 좋겠습니다.

추가로, 글쓴이분과 가벼운 커피챗으로 말씀을 나눠보고 싶습니다. 커피는 제가 대접할게요 :)
공개적으로 이메일 주소를 남기는건 처음인데, 작성자분 연락처를 여기 남겨주실 수는 없으니 제게 메일주시면 꼭 찾아뵐게요. 링크드인으로 연락주셔도 돼요.
changjin.nam@branch.io
https://www.linkedin.com/in/changjin-nam-50434044/

답글 달기
comment-user-thumbnail
2024년 3월 7일

안녕하세요 저도 다이나믹링크가 ios에서 스토어에서 설치후에 url이 소실되어서 지연딥링크가 되지않아서
골머리중입니다.. (안드로이드는 마켓설치후에도 너무 잘됩니다..)
제가 설정을 잘못해서 안되는건지 다이나믹링크의 문제인지를 모르겠네요.
혹시 브랜치io는 현재 잘되고있으신지 궁금합니다 ..

1개의 답글