Flutter SDK - 딥 링크 지원

문서

딥링킹 지원 추가

딥링크는 사용자를 앱 내의 특정 콘텐츠로 연결합니다. 사용자가 앱이 설치된 기기에서 딥링크를 탭하면 앱이 제품 페이지나 특정 경험 등 의도한 콘텐츠로 바로 연결됩니다.

Singular 추적 링크는 표준 딥링킹(설치된 앱의 경우)과 디퍼드 딥링킹(신규 설치의 경우)을 모두 지원합니다. 자세한 내용은 딥링킹 FAQSingular 링크 FAQ를 참조하세요.


요구 사항

전제 조건

앱에 딥링킹을 사용하려면 Singular 링크 사전 요구 사항을 완료하세요.

참고:

  • 이 문서에서는 귀하의 조직이 Singular의 추적 링크 기술인 Singular 링크를 사용하고 있다고 가정합니다. 기존 고객은 레거시 트래킹 링크를 사용하고 있을 수 있습니다.
  • 앱의 딥링크 대상은 Singular의 페이지에서 구성해야 합니다( 어트리뷰션 추적을 위한 앱 구성하기 참조).

사용 가능한 파라미터

Singular링크 핸들러는 앱이 열릴 때 Singular 트래킹 링크의 딥링크, 디퍼드 딥링크, 패스스루 파라미터에 대한 액세스를 제공합니다.

  • 딥링크(_dl): 링크를 클릭하는 사용자의 앱 내 목적지 URL
  • 디퍼드 딥링크(_ddl): 링크를 클릭한 후 앱을 설치하는 사용자를 위한 목적지 URL
  • 패스스루(_p): 추가 컨텍스트를 위해 추적 링크를 통해 전달되는 사용자 지정 데이터

플랫폼 구성

iOS 플랫폼 구성

iOS 앱디렉티브 업데이트

launchOptionsuserActivity 개체를 AppDelegate 파일에서 Singular SDK에 전달하여 Singular SDK가 실행 관련 데이터를 처리하고 딥링크를 처리할 수 있도록 설정합니다.

이러한 개체에는 앱이 실행된 방법과 이유에 대한 중요한 정보가 포함되어 있으며, Singular는 어트리뷰션 추적 및 딥링크 탐색에 사용합니다.

Objective-C 구현

AppDelegate.m
// Top of AppDelegate.m
#import "SingularAppDelegate.h"

- (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self];
    [SingularAppDelegate shared].launchOptions = launchOptions;

    return [super application:application 
        didFinishLaunchingWithOptions:launchOptions];
}

- (BOOL)application:(UIApplication *)application 
    continueUserActivity:(NSUserActivity *)userActivity 
    restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> 
        *restorableObjects))restorationHandler {
    [[SingularAppDelegate shared] continueUserActivity:userActivity 
        restorationHandler:restorationHandler];

    return [super application:application 
        continueUserActivity:userActivity 
        restorationHandler:restorationHandler];
}

- (BOOL)application:(UIApplication *)app 
    openURL:(NSURL *)url 
    options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    [[SingularAppDelegate shared] handleOpenUrl:url options:options];

    return [super application:app openURL:url options:options];
}

신속한 구현

AppDelegate.swift
import singular_flutter_sdk

override func application(_ application: UIApplication, 
    didFinishLaunchingWithOptions 
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.launchOptions = launchOptions
    }

    return super.application(application, 
        didFinishLaunchingWithOptions:launchOptions)
}

override func application(_ application: UIApplication, 
    continue userActivity: NSUserActivity, 
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.continueUserActivity(userActivity, 
            restorationHandler: nil)
    }

    return super.application(application, 
        continue: userActivity, 
        restorationHandler: restorationHandler)
}

override func application(_ app: UIApplication, 
    open url: URL, 
    options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.handleOpen(url, options: options)
    }

    return super.application(app, open: url, options: options)
}

안드로이드 플랫폼 구성

안드로이드 메인 액티비티 업데이트

MainActivity 파일을 수정하여 Intent 오브젝트를 Singular SDK에 전달함으로써 Singular SDK가 실행 관련 데이터를 처리하고 딥링크를 처리할 수 있도록 합니다.

Intent 객체에는 앱이 실행된 방법과 이유에 대한 정보가 포함되어 있으며, Singular는 어트리뷰션 추적 및 딥링크 탐색에 사용합니다.

Java 구현

MainActivity.java
// Add as part of the imports at the top of the class
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;

// Add to the MainActivity class
@Override
public void onNewIntent(Intent intent) {
    if(intent.getData() != null) {
        setIntent(intent);
        super.onNewIntent(intent);
        SingularBridge.onNewIntent(intent);
    }
}

Kotlin 구현

MainActivity.kt
// Add as part of the imports at the top of the class
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge

// Add to the MainActivity class
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    if (intent.data != null) {
        setIntent(intent)
        SingularBridge.onNewIntent(intent)
    }
}

SDK 구성

Singular 링크 핸들러 구현

SDK 초기화 중에 들어오는 딥링크 및 디퍼드된 딥링크 데이터를 처리하도록 singularLinksHandler 콜백을 구성합니다.

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    initializeSDK();
  }

  void initializeSDK() {
    // Create SDK configuration
    SingularConfig config = SingularConfig(
      'YOUR_SDK_KEY',
      'YOUR_SDK_SECRET'
    );

    // Configure deep link handler
    config.singularLinksHandler = (SingularLinkParams params) {
      print('=== Singular Link Resolved ===');
      print('Deep link: ${params.deeplink}');
      print('Passthrough: ${params.passthrough}');
      print('Is deferred: ${params.isDeferred}');

      // Handle deep link navigation
      if (params.deeplink != null) {
        handleDeepLink(params.deeplink!, params.isDeferred);
      }

      // Handle passthrough data
      if (params.passthrough != null) {
        handlePassthroughData(params.passthrough!);
      }
    };

    // Initialize SDK
    Singular.start(config);
  }

  void handleDeepLink(String url, bool isDeferred) {
    print('Routing to: $url (Deferred: $isDeferred)');

    // Parse URL and navigate to appropriate screen
    // Example: myapp://product/123
    if (url.contains('product')) {
      final productId = url.split('/').last;
      navigateToProduct(productId);
    } else if (url.contains('promo')) {
      navigateToPromo(url);
    }
  }

  void handlePassthroughData(String passthrough) {
    print('Processing passthrough data: $passthrough');
    // Parse and use passthrough data as needed
  }

  void navigateToProduct(String productId) {
    // Your navigation logic
    print('Navigating to product: $productId');
  }

  void navigateToPromo(String url) {
    // Your navigation logic
    print('Navigating to promo: $url');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

구성 속성:

void Function(SingularLinkParams)? singularLinksHandler

참고: singularLinksHandler 콜백은 앱이 Singular 링크를 통해 열릴 때만 트리거됩니다. 자세한 내용은 Singular 링크 FAQ를 참조하세요.

전체 문서는 Singular링크 핸들러 참조를 참조하세요.


핸들러 동작

링크 해상도 이해

singularLinksHandler 은 앱이 새로 설치되었는지 또는 이미 설치되었는지에 따라 다르게 동작합니다.

새로 설치(디퍼드 딥링크)

새로 설치한 경우 앱이 실행될 때 열기 URL이 존재하지 않습니다. Singular는 추적 링크에 딥링크 또는 디퍼드 딥링크 값이 포함되어 있는지 확인하기 위해 어트리뷰션을 완료합니다.

디퍼드 딥링크 흐름:

  1. 사용자가 딥링크 값으로 구성된 Singular 트래킹 링크를 클릭합니다.
  2. 사용자가 앱을 처음 설치하고 실행합니다.
  3. Singular SDK가 첫 번째 세션을 Singular 서버로 전송합니다.
  4. 어트리뷰션이 트래킹 링크에서 딥링크를 완성하고 식별합니다.
  5. 딥링크 값이 deeplink 파라미터의 singularLinksHandler 콜백에 isDeferred = true로 반환됩니다.

디퍼드 딥링크 테스트:

  1. 테스트 기기에서 앱을 제거합니다(현재 앱이 설치되어 있는 경우).
  2. iOS: IDFA를 재설정합니다. Android: Google 광고 ID(GAID)를 재설정합니다.
  3. 기기에서 Singular 추적 링크를 클릭합니다(딥링크 값으로 구성되었는지 확인).
  4. 앱을 설치하고 엽니다.

어트리뷰션이 성공적으로 완료되고 디퍼드 딥링크 값이 singularLinksHandler 핸들러로 전달됩니다.

프로 팁: 다른 패키지 이름(예: com.example.prod 대신 com.example.dev )을 사용하는 개발 빌드로 딥링크를 테스트하는 경우, 개발 앱의 패키지 이름에 맞게 추적 링크를 구성하세요. 테스트 링크를 클릭한 후 앱 스토어에서 프로덕션 앱을 다운로드하지 않고 Android Studio 또는 Xcode를 통해 기기에 직접 개발 빌드를 설치합니다.


이미 설치됨(즉시 딥 링크)

앱이 이미 설치되어 있는 경우 Singular 링크를 클릭하면 유니버설 링크(iOS) 또는 Android 앱 링크 기술을 사용하여 앱이 즉시 열립니다.

즉시 딥링크 흐름:

  1. 사용자가 Singular 추적 링크를 클릭합니다.
  2. 운영 체제는 전체 Singular 추적 링크가 포함된 오픈 URL을 제공합니다.
  3. SDK 초기화 중에 Singular가 URL을 구문 분석합니다.
  4. Singular가 deeplinkpassthrough 값을 추출합니다.
  5. 값은 singularLinksHandler 핸들러를 통해 isDeferred = false으로 반환됩니다.

고급 기능

패스스루 매개변수

패스스루 매개변수를 사용하여 추적 링크 클릭에서 추가 데이터를 캡처합니다.

추적 링크에 passthrough (_p) 파라미터가 포함된 경우 singularLinksHandler 핸들러의 passthrough 파라미터에 해당 데이터가 포함됩니다. 이를 사용하여 캠페인 메타데이터, 사용자 세분화 데이터 또는 앱에서 필요한 사용자 지정 정보를 캡처할 수 있습니다.

Dart
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
import 'dart:convert';

SingularConfig config = SingularConfig('API_KEY', 'SECRET');

config.singularLinksHandler = (SingularLinkParams params) {
  // Extract passthrough data
  final passthroughData = params.passthrough;

  if (passthroughData != null) {
    try {
      // Parse JSON passthrough data
      final jsonData = jsonDecode(passthroughData);

      print('Campaign ID: ${jsonData['campaign_id']}');
      print('User Segment: ${jsonData['segment']}');
      print('Promo Code: ${jsonData['promo_code']}');

      // Apply campaign-specific settings
      applyCampaignSettings(jsonData);
    } catch (error) {
      print('Error parsing passthrough data: $error');
    }
  }
};

void applyCampaignSettings(Map<String, dynamic> data) {
  // Your campaign logic here
  print('Applying campaign settings: $data');
}

모든 쿼리 파라미터 전달

추적 링크에 _forward_params=2 파라미터를 추가하여 추적 링크 URL에서 모든 쿼리 파라미터를 캡처합니다.

_forward_params=2 을 추적 링크에 추가하면 모든 쿼리 파라미터가 singularLinksHandler 핸들러의 deeplink 파라미터에 포함되므로 모든 파라미터가 포함된 전체 URL에 액세스할 수 있습니다.

추적 링크 예시:
https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_forward_params=2&utm_source=facebook&promo=SALE2024

singularLinksHandler 핸들러가 수신합니다:
deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"


전체 구현 예시

Flutter 앱의 탐색, 패스스루 처리 및 매개변수 전달을 통한 포괄적인 딥링킹 구현.

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
import 'dart:convert';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  void initState() {
    super.initState();
    initializeSDK();
  }

  void initializeSDK() {
    SingularConfig config = SingularConfig(
      'YOUR_SDK_KEY',
      'YOUR_SDK_SECRET'
    );

    config.singularLinksHandler = (SingularLinkParams params) {
      print('=== Singular Link Resolved ===');
      print('Deep link: ${params.deeplink}');
      print('Passthrough: ${params.passthrough}');
      print('Is deferred: ${params.isDeferred}');

      // Handle passthrough data
      if (params.passthrough != null) {
        handlePassthroughData(params.passthrough!);
      }

      // Handle deep link navigation
      if (params.deeplink != null) {
        handleDeepLinkNavigation(params.deeplink!, params.isDeferred);
      }
    };

    Singular.start(config);
  }

  void handlePassthroughData(String passthroughString) {
    try {
      final data = jsonDecode(passthroughString);

      // Apply promo code if present
      if (data.containsKey('promo_code')) {
        applyPromoCode(data['promo_code']);
      }

      // Set user segment
      if (data.containsKey('segment')) {
        setUserSegment(data['segment']);
      }

      // Track campaign
      if (data.containsKey('campaign_id')) {
        trackCampaign(data['campaign_id']);
      }
    } catch (error) {
      print('Error parsing passthrough: $error');
    }
  }

  void handleDeepLinkNavigation(String url, bool isDeferred) {
    // Parse URL to extract route and parameters
    final urlObj = parseDeepLink(url);

    print('Navigating to: ${urlObj['route']}');
    print('Parameters: ${urlObj['params']}');
    print('Deferred install: $isDeferred');

    // Route based on deep link structure
    switch (urlObj['route']) {
      case 'product':
        navigateToProduct(urlObj['params']['id']);
        break;
      case 'promo':
        navigateToPromo(urlObj['params']['code']);
        break;
      case 'category':
        navigateToCategory(urlObj['params']['name']);
        break;
      default:
        navigateToHome();
    }
  }

  Map<String, dynamic> parseDeepLink(String url) {
    // Parse myapp://product/123?variant=blue
    final parts = url.split('?');
    final path = parts[0];
    final queryString = parts.length > 1 ? parts[1] : null;

    // Extract path components
    final pathParts = path.replaceFirst(RegExp(r'^[^:]+://'), '').split('/');
    final route = pathParts[0];

    // Parse parameters
    final params = <String, String>{};

    // Add path parameters
    if (pathParts.length > 1) {
      params['id'] = pathParts[1];
    }

    // Add query parameters
    if (queryString != null) {
      queryString.split('&').forEach((pair) {
        final keyValue = pair.split('=');
        if (keyValue.length == 2) {
          params[keyValue[0]] = Uri.decodeComponent(keyValue[1]);
        }
      });
    }

    return {
      'route': route,
      'params': params
    };
  }

  // Navigation functions
  void navigateToProduct(String? productId) {
    if (productId != null) {
      print('Navigating to product: $productId');
      // Use your navigation framework (Navigator, GoRouter, etc.)
      navigatorKey.currentState?.pushNamed('/product/$productId');
    }
  }

  void navigateToPromo(String? promoCode) {
    if (promoCode != null) {
      print('Navigating to promo: $promoCode');
      navigatorKey.currentState?.pushNamed('/promo/$promoCode');
    }
  }

  void navigateToCategory(String? categoryName) {
    if (categoryName != null) {
      print('Navigating to category: $categoryName');
      navigatorKey.currentState?.pushNamed('/category/$categoryName');
    }
  }

  void navigateToHome() {
    print('Navigating to home');
    navigatorKey.currentState?.pushNamed('/');
  }

  // Utility functions
  void applyPromoCode(String code) {
    print('Applying promo code: $code');
    // Your promo code logic
  }

  void setUserSegment(String segment) {
    print('Setting user segment: $segment');
    // Your user segmentation logic
  }

  void trackCampaign(String campaignId) {
    print('Tracking campaign: $campaignId');
    // Your campaign tracking logic
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      navigatorKey: navigatorKey,
      initialRoute: '/',
      routes: {
        '/': (context) => HomeScreen(),
        '/product': (context) => ProductScreen(),
        '/promo': (context) => PromoScreen(),
        '/category': (context) => CategoryScreen(),
      },
    );
  }
}

// Placeholder screen classes
class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(child: Text('Home Screen')),
    );
  }
}

class ProductScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product')),
      body: Center(child: Text('Product Screen')),
    );
  }
}

class PromoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Promo')),
      body: Center(child: Text('Promo Screen')),
    );
  }
}

class CategoryScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Category')),
      body: Center(child: Text('Category Screen')),
    );
  }
}

모범 사례:

  • URL을 안전하게 파싱하기: 보안 취약점을 방지하기 위해 내비게이션 전에 항상 딥링크 URL의 유효성을 검사하고 위생 처리합니다.
  • 탐색 상태 처리: 탐색 상태 처리: MaterialApp이 완전히 초기화되기 전에 탐색을 활성화하려면 탐색 상태용 GlobalKey를 사용합니다.
  • 두 시나리오를 모두 테스트합니다: 개발 중에 즉시 딥링크(앱 설치)와 디퍼드 딥링크(새로 설치)를 모두 테스트합니다.
  • 디버깅을 위한 로그: 개발 중 포괄적인 로깅을 활성화하여 딥링크 해상도 및 탐색 흐름을 추적합니다.
  • 오류 처리: JSON 구문 분석 및 탐색 작업을 위한 try-catch 블록을 구현하여 잘못된 데이터를 정상적으로 처리합니다.