본문 바로가기
프로그래밍/Flutter & Dart

Flutter 동적 링크(Dynamic Link) 및 카카오톡 공유(Link API) 조합하기

by 어느덧중반 2021. 9. 15.
반응형

 

 

Dynamic Link와 Kakao Link API 를 이용하여 특정 게시글에 대해 카카오톡으로 공유하는 실습을 진행해보도록 하겠다.

 

오랜만의 Flutter 실습 예제에 대한 기록을 남기려고 하는데, 남기는 이유는 정말 오랜만에 내가 구현해보지 않은 기능을 배우는데 꽤 오랜시간 삽질을 하게 돼서, 다음 번에는 이런 삽질을 좀 덜 하고자 잘 정리해 두고자 한다.

해당 기능은 특정 게시글에서 공유하기 등의 버튼을 통해 카카오톡으로 메시지를 전달하는 기능으로 요약해보면 아래와 같은 모습이다.

게시글에서 공유하기 버튼 클릭(좌측 스크린샷)을 하면 카카오톡으로 공유(중간 스크린샷)가 되고 해당 메시지를 클릭하면 동적링크 페이지(우측 스크린샷)를 통해 앱 설치가 안되어 있는 사용자는 앱 설치를 위한 스토어로 연결되며 앱 설치가 되어 있는 사용자는 해당 게시글로 이동하게 된다.

 

해당 기능이 동작하기 위해선 아래의 작업들이 필요하다.

최대한 상세히 정리해둘테니 참고가 필요한 분들에게 도움이 되었으면 하는 바람이다.

 

  • 카카오톡 링크 사용을 위한 설정 (Link API)
  • Firebase 동적 링크 사용을 위한 설정 (Dynamic Link)
  • 두가지 기능을 조합한 소스 구현

 

카카오톡 링크 사용을 위한 설정 (Link API)

카카오톡 링크 사용에 필요한 것은 아래와 같다.

  • 신규 애플리케이션
  • Android/iOS/Web 플랫폼 추가
  • 앱 키 중 네이티브앱키

위의 것들을 차례대로 진행하며 알아가도록 하자.

 

- Kakao Developers > 내 애플리케이션 > 앱 추가

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

- 앱 이름, 사업자명 입력 후 저장

새 애플리케이션을 추가한다.

- 앱 키에 가보면 네이티브 앱 키 확인이 가능하다. (소스 구현시 활용될 예정이다.)

 

- Android, iOS 플랫폼 추가가 필요하며, 이따가 Dynamic link로 만든 URL 사용을 위해 Web 플랫폼 추가도 필요하다.
우선은 Android, iOS 플랫폼 추가 먼저 진행하도록 하자.

  • Android 플랫폼 추가 : 패키지명과 키 해시 값 확인이 필요하다.
    패키지명, 키 해시값을 아래의 방법으로 확인 후 입력하자.

    • 패키지명 확인 : Flutter Project > android > app > src > main > AndroidManifest.xml 상단의 패키지명 확인Flutter Project > android > app > src > main > AndroidManifest.xml 상단의 패키지명 확인
    •  키 해시 값 확인 : Terminal 에서 확인 (debug, release 각각 다르게 입력된다.
      // debug
      keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64
      // release
      keytool -exportcert -alias androiddebugkey -keystore ~/.android/release.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64​
       
  • iOS 플랫폼 추가 : bundle identifier 확인이 필요하다.
    bundle identifier를 아래의 방법으로 확인 후 입력하자.

    • bundle identifier 확인
      Flutter Project > ios 폴더 우클릭 > Flutter > Open iOS module in Xcode > General > Bundle identifier

이제 카카오 링크 사용을 위한 기본 설정은 끝났다.



Firebase 동적 링크 사용을 위한 설정 (Dynamic Link)

Firebase 동적 링크 사용에 필요한 것은 아래와 같다.

  • Firebase project 생성
  • Android 앱 추가
  • iOS 앱 추가
  • Dynamic Link 기능 사용하기

위의 것들을 차례대로 진행하며 알아가도록 하자.

우선 Firebase project 생성 및 Android/iOS 앱 추가는 아래의 링크를 통해 진행하도록 하자.

 

 

Firebase project 생성 및 Android, iOS 앱 추가하기

Flutter를 이용하여 앱을 개발하게 되면 Firebase는 기본적으로 사용 방법을 알아두는 것이 좋다. Project 생성부터 Android, iOS 앱 추가하는 간단한 방법을 짚고 넘어가도록 하겠다. Firebase project 생성 And

kyungsnim.net

 

위의 방법으로 프로젝트 생성 및 Android, iOS앱 추가가 되었다면 아래부터 진행하면 된다.

- 우선 Android에서는 SHA-1 추가가 필요하다. (경로 : Firebase project 설정 > Android 앱 설정 > 디지털 지문 추가)

디지털 지문 추가

- SHA1 확인하는 방법은 아래의 코드를 Terminal에서 실행하면 된다.

// 키 생성하기
keytool -genkey -v -keystore debug.keystore -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000

// 키 조회하기 (PW : android)
keytool -list -v -keystore debug.keystore -alias androiddebugkey

- 조회된 디지털 지문키를 추가해주면 된다.

- 이제 Dynamic Link 메뉴로 이동해보자.

Dynamic Links 메뉴 클릭

- 시작하기

시작하기 클릭

- 이제 URL 을 생성하면 된다. 구글에서 제공하는 도메인은 xxxx.page.link 인데 나는 이 것을 사용했다. (커스텀 도메인을 사용하는 경우 하단에서 추가해줘야 할 코드가 있으니 잘 기억해두자.)

구글에서 제공하는 도메인이 있고 커스텀 도메인을 사용해도 된다.

- 이제 각 앱에서 사용을 위해 추가 설정이 필요한데 안드로이드는 추가설정이 필요없고 iOS의 경우 아래의 절차들을 따라가며 설정해주면 된다.

1. Xcode에서 프로젝트 열기 > Signing & Capabilities > Associated Domains 추가

applinks:xxxx.page.link

Associated Domains 추가 후 Domains에 본인이 만들었던 Dynamic Links의 URL을 입력하자

2. Info 탭으로 이동해서 URL Type을 추가해주자.

identifier에는 본인의 Bundle identifier를 넣고 URL Schemes에는 Dynamic Link에서 만든 URL을 넣어주자.

3. 만일 본인이 커스텀 도메인을 사용하는 경우 아래의 절차를 진행하자. (Info.plist 에 아래 코드 추가)

pub.dev/packages/firebase_dynamic.links 에서 확인 가능

 

두가지 기능을 조합한 소스 구현

이제 설정을 마쳤으니 실제 소스 구현을 해보겠다.

 

0. 사용되는 패키지 추가 (pubspec.yaml)

 

kakao_flutter_sdk | Flutter Package

A flutter plugin for Kakao API, which supports Kakao login, KakaoLink, User API, KakaoTalk API, KakaoStory API, and Push API.

pub.dev

 

firebase_dynamic_links | Flutter Package

Flutter plugin for Google Dynamic Links for Firebase, an app solution for creating and handling links across multiple platforms.

pub.dev

firebase_dynamic_links 및 kakao Link API 사용을 위한 패키지 6개 추가

0-1. iOS Info.plist에 카카오 링크 관련 소스 추가

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
	<key>LSApplicationQueriesSchemes</key>
	<array>
		<string>kakaob6cb50231a2306a68657a2a6e07a1d3b</string>
		<string>kakaokompassauth</string>
		<string>storykompassauth</string>
		<string>kakaolink</string>
		<string>kakaotalk-5.9.7</string>
	</array>
</dict>
</plist>

1. main.dart : 다른 소스는 볼 것 없고 아래 소스를 추가해주자.

KakaoContext.clientId = '카카오 개발자사이트에서 만든 애플리케이션 네이티브앱키';

 

2. StatefulWidget인 Home widget에 WidgetsBindingObserver 추가하기

* 추가하는 이유 : 앱이 완전 종료되었을 때 링크를 클릭해서 앱이 실행되는 경우와 앱이 백그라운드상태에 있다가 링크를 클릭해서 앱으로 돌아오는 경우 모두 작동해야 하는데, 백그라운드 상태에서 앱으로 돌아오게 하려면 Observer가 필요하다. 이 때문에 추가해줘야 함

 

- 먼저 동적 링크를 생성해서 카카오톡으로 보내는 부분이다.

  • 공유하기 등의 버튼을 클릭했을 때 동적링크 생성 후 카카오톡 공유 (특정 게시판에서 공유하기 버튼 부분)

InkWell(
  onTap: () async {
    /// Make dynamic links
    String link = await KakaoLinkWithDynamicLink()
        .buildDynamicLink('HowToBeRichDetail',
            widget.detailNotice.id);

    /// Kakao Link share
    KakaoLinkWithDynamicLink()
        .isKakaotalkInstalled()
        .then((installed) {
      if (installed) {
        KakaoLinkWithDynamicLink()
            .shareMyCode(widget.detailNotice, link);
      } else {
        // show alert
        Share.share(link);
      }
    });
  },
  child: Padding(
    padding: const EdgeInsets.all(5.0),
    child: Row(
      children: [
        Icon(Icons.share),
        SizedBox(width: 5),
        Text('공유하기',
            style: TextStyle(
                fontFamily: 'Binggrae',
                color: Colors.black,
                fontSize: 20)),
      ],
    ),
  ),
),
  • 동적링크를 통해 카카오톡 링크 만드는 과정 (kakao_link_with_dynamic_link.dart 별도 파일로 구성)

import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:kakao_flutter_sdk/link.dart';
import 'package:make_ten_billion/models/models.dart';

class KakaoLinkWithDynamicLink {
  static final KakaoLinkWithDynamicLink _manager =
      KakaoLinkWithDynamicLink._internal();

  factory KakaoLinkWithDynamicLink() {
    return _manager;
  }

  KakaoLinkWithDynamicLink._internal() {
    // 초기화 코드
  }

  Future<bool> isKakaotalkInstalled() async {
    bool installed = await isKakaoTalkInstalled();
    return installed;
  }

  void shareMyCode(NoticeModel notice, String link) async {
    try {
      /// 카카오톡 FeedTemplate으로 공유메시지 만들기
      var template = getTemplate(notice, link);
      var uri = await LinkClient.instance.defaultWithTalk(template);
      /// 카카오톡 링크 공유
      await LinkClient.instance.launchKakaoTalk(uri);
    } catch (error) {
      print(error.toString());
    }
  }

  FeedTemplate getTemplate(NoticeModel notice, String link) {
    /// 카카오톡 공유를 위한 콘텐츠 구성
    Content content = Content(
        // title
        notice.title,
        // image url
        Uri.parse(notice.imgUrl),
        // 메시지 클릭시 이동되는 link
        Link(
          mobileWebUrl: Uri.parse(link),
        ),
        // description
        description: notice.description);
    
    /// 위에서 만든 콘텐츠를 이용해 카카오톡 공유 FeedTemplate 만들기
    FeedTemplate template = FeedTemplate(content, buttons: [
      Button("앱에서보기", Link(mobileWebUrl: Uri.parse(link))),
    ]);

    return template;
  }

  Future<String> buildDynamicLink(String whereNotice, String noticeId) async {
    /// Dynamic Links 에서 만든 URL Prefix
    String url = "https://maketenbillion.page.link";
    
    /// Dynamic Links 소스를 이용해 만들기
    final DynamicLinkParameters parameters = DynamicLinkParameters(
        uriPrefix: url,
        /// 딥링크 사용을 위한 특정 게시판-게시글ID 구성
        link: Uri.parse('$url/$whereNotice/$noticeId'),
        /// 안드로이드의 경우 packageName 추가
        androidParameters: AndroidParameters(
          packageName: "com.kyungsnim.make_ten_billion",
        ),
        /// iOS의 경우 bundleId 추가 (appStoreId와 TeamID가 필요)
        iosParameters: IosParameters(
            bundleId: "com.kyungsnim.makeTenBillion",
            appStoreId: '1554807824'));

    final ShortDynamicLink dynamicUrl = await parameters.buildShortLink();
    return dynamicUrl.shortUrl.toString();
  }
}

- 다음은 동적 링크를 눌러서 내부적으로 특정 게시글로 이동되는 부분이다.

  • 앱이 실행될 때 2가지 중 하나로 실행된다.
    • 앱이 종료된 상태에서 링크를 클릭해서 앱이 최초 실행되는 경우 [initState -> initDynamicLinks() 를 통해 확인]
    • 앱이 백그라운드 상태일 때 링크를 클릭해서 앱으로 돌아오는 경우 [didChangeAppLifecycleState -> AppLifecycleState.resumed -> initDynamicLinks()를 통해 확인]
  • 2가지 중 하나로 앱이 실행될 때 동적링크를 통해 들어온 경우 딥링크 여부를 파악 (특정 게시물로 이동하고싶은 경우 딥링크를 사용한다.)
  • (나는 딥링크를 사용했으므로) 딥링크 정보 분석 (어떤 게시판의 어떤 게시글인지)
  • 해당 게시글로 페이지 push

위의 절차대로 진행될 것이고 코드는 아래와 같다.

class Home extends StatefulWidget {
  int index;

  Home(this.index);

  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> with WidgetsBindingObserver {
  int? currentIndex;

  @override
  void initState() {
    super.initState();
    currentIndex = widget.index;

    /// with WidgetsBindingObserver 과 함께 foreground 동적링크를 위한 추가
    WidgetsBinding.instance!.addObserver(this);

    initDynamicLinks();
  }

  @override
  void dispose() {
    super.dispose();

    /// with WidgetsBindingObserver 과 함께 foreground 동적링크를 위한 추가
    WidgetsBinding.instance!.removeObserver(this);
  }
  
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    /// 백그라운드에서 링크 클릭을 통해 앱으로 돌아온 경우
    if (state == AppLifecycleState.resumed) {
      initDynamicLinks();
    }
  }

  void initDynamicLinks() async {
    /// 동적링크 확인
    final PendingDynamicLinkData? data =
        await FirebaseDynamicLinks.instance.getInitialLink();
    /// 딥링크가 있는 경우 체크 (딥링크는 단순히 앱만 실행하는 것이 아닌 특정 게시물로의 이동이 필요한 경우 사용)
    final Uri? deepLink = data?.link;

    if (deepLink != null) {
      /// 딥링크가 있는 경우 URL을 분석해서 어느 게시물로 이동할지 알아내야 함
      handleDynamicLink(deepLink);
    }
    FirebaseDynamicLinks.instance.onLink(
        onSuccess: (PendingDynamicLinkData? dynamicLink) async {
      final Uri? deepLink = dynamicLink?.link;

      if (deepLink != null) {
        handleDynamicLink(deepLink);
      }
    }, onError: (OnLinkErrorException e) async {
      print(e.message);
    });
  }

  handleDynamicLink(Uri url) {
    /// 나의 경우 동적링크(+딥링크) 형식을 xxx.page.link/게시판이름/게시글ID 형태로 만들어 두었다.
    /// 때문에 separatedString[1] 에는 어떤 게시판으로 이동할지
    /// separatedString[2] 에는 어떤 게시글인지의 정보가 담겨져 있다.

    List<String> separatedString = [];
    separatedString.addAll(url.path.split('/'));

    /// ex) https://maketenbillion.page.link/HowToBeRichDetail/167812785917
    /// separatedString[0] => https://maketenbillion.page.link (동적링크 URL Prefix)
    /// separatedString[1] => HowToBeRichDetail (게시판 종류)
    /// separatedString[2] => 167812785917 (게시글 ID)

    Future.microtask(() async {
      Timer(Duration(milliseconds: 700), () async {
        /// 게시판 종류에 따라 분기처리
        switch (separatedString[1]) {
          case 'HowToBeRichDetail':
            /// 게시글 ID로 해당 문서를 조회한 후 해당 게시글로 이동
            FirebaseFirestore.instance
                .collection('HowToBeRich')
                .doc(separatedString[2])
                .get()
                .then((data) {
              NoticeModel detailNotice = NoticeModel.fromSnapshot(data);
              Get.to(() => HowToBeRichDetail(detailNotice));
            });
            break;
          case 'Motivation':
          /// 게시글 ID로 해당 문서를 조회한 후 해당 게시글로 이동
            FirebaseFirestore.instance
                .collection('Motivation')
                .doc(separatedString[2])
                .get()
                .then((data) {
              NoticeModel detailNotice = NoticeModel.fromSnapshot(data);
              Get.to(() => MotivationDetail(detailNotice));
            });
            break;
          case 'ThinkAboutRich':
          /// 게시글 ID로 해당 문서를 조회한 후 해당 게시글로 이동
            FirebaseFirestore.instance
                .collection('ThinkAboutRich')
                .doc(separatedString[2])
                .get()
                .then((data) {
              NoticeModel detailNotice = NoticeModel.fromSnapshot(data);
              Get.to(() => ThinkAboutRichDetail(detailNotice));
            });
            break;
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
  	return ...
  }

 

꽤 오랜 시간에 걸쳐 정리가 끝났는데, 중간 중간 빠진 부분이 어딘가 있는 것 같아 찝찝하다.

혹시 진행이 안되는 부분이 있는 경우 댓글로 남겨놓는다면 수정/추가 하도록 하겠다.

 

반응형

댓글5

  • Favicon of https://kyungsnim.net BlogIcon 어느덧중반 2021.09.15 17:27 신고

    추가로 설정이 필요한 부분은 카카오 개발자 사이트에서 만든 애플리케이션 > Web 플랫폼 추가에 본인이 Dynamic link로 만든 URL을 꼭 추가해줘야 한다.
    답글

  • 왕덕원 2022.01.02 17:17

    혹시 model.dart에 사용하신 코드가 어떤것인지 여쭤봐도 될까요..??
    답글

    • BlogIcon kyungsnim 2022.01.02 17:20

      아래 코드 참고하세요-!

      import 'package:cloud_firestore/cloud_firestore.dart';

      class NoticeModel {
      String id;
      String writer;
      String title;
      String description;
      String imgUrl;
      late int read;
      int like;
      int share;
      DateTime createdAt;
      List<dynamic> likeList;

      NoticeModel(
      {required this.id,
      required this.writer,
      required this.title,
      required this.description,
      required this.imgUrl,
      required this.read,
      required this.like,
      required this.share,
      required this.likeList,
      required this.createdAt});

      // factory NoticeModel.fromMap(Map data) {
      // return NoticeModel(id: data['id'], writer: data['writer'], createdAt: data['createdAt'].toDate());
      // }

      Map<String, dynamic> toJson() => {
      "id": id,
      "writer": writer,
      "createdAt": createdAt,
      "title": title,
      "description": description,
      "imgUrl": imgUrl,
      "read": read,
      "like": like,
      "share": share,
      "likeList": likeList,
      };

      NoticeModel.fromMap(var data)
      : id = data['id'],
      writer = data['writer'] ?? '',
      title = data['title'],
      description = data['description'],
      read = data['read'] ?? 0,
      like = data['like'] ?? 0,
      share = data['share'] ?? 0,
      likeList = data['likeList'] ?? [],
      imgUrl = data['imgUrl'] ?? '',
      createdAt = data['createdAt'].toDate();

      NoticeModel.fromSnapshot(DocumentSnapshot snapshot)
      : this.fromMap(snapshot.data());
      }

  • linecut 2022.07.07 16:35

    도움이 되었습니다 감사합니다
    답글