Dynamic Link와 Kakao Link API 를 이용하여 특정 게시글에 대해 카카오톡으로 공유하는 실습을 진행해보도록 하겠다.
오랜만의 Flutter 실습 예제에 대한 기록을 남기려고 하는데, 남기는 이유는 정말 오랜만에 내가 구현해보지 않은 기능을 배우는데 꽤 오랜시간 삽질을 하게 돼서, 다음 번에는 이런 삽질을 좀 덜 하고자 잘 정리해 두고자 한다.
해당 기능은 특정 게시글에서 공유하기 등의 버튼을 통해 카카오톡으로 메시지를 전달하는 기능으로 요약해보면 아래와 같은 모습이다.
해당 기능이 동작하기 위해선 아래의 작업들이 필요하다.
최대한 상세히 정리해둘테니 참고가 필요한 분들에게 도움이 되었으면 하는 바람이다.
- 카카오톡 링크 사용을 위한 설정 (Link API)
- Firebase 동적 링크 사용을 위한 설정 (Dynamic Link)
- 두가지 기능을 조합한 소스 구현
카카오톡 링크 사용을 위한 설정 (Link API)
카카오톡 링크 사용에 필요한 것은 아래와 같다.
- 신규 애플리케이션
- Android/iOS/Web 플랫폼 추가
- 앱 키 중 네이티브앱키
위의 것들을 차례대로 진행하며 알아가도록 하자.
- Kakao Developers > 내 애플리케이션 > 앱 추가
- 앱 이름, 사업자명 입력 후 저장
- 앱 키에 가보면 네이티브 앱 키 확인이 가능하다. (소스 구현시 활용될 예정이다.)
- 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 확인
이제 카카오 링크 사용을 위한 기본 설정은 끝났다.
Firebase 동적 링크 사용을 위한 설정 (Dynamic Link)
Firebase 동적 링크 사용에 필요한 것은 아래와 같다.
- Firebase project 생성
- Android 앱 추가
- iOS 앱 추가
- Dynamic Link 기능 사용하기
위의 것들을 차례대로 진행하며 알아가도록 하자.
우선 Firebase project 생성 및 Android/iOS 앱 추가는 아래의 링크를 통해 진행하도록 하자.
위의 방법으로 프로젝트 생성 및 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 메뉴로 이동해보자.
- 시작하기
- 이제 URL 을 생성하면 된다. 구글에서 제공하는 도메인은 xxxx.page.link 인데 나는 이 것을 사용했다. (커스텀 도메인을 사용하는 경우 하단에서 추가해줘야 할 코드가 있으니 잘 기억해두자.)
- 이제 각 앱에서 사용을 위해 추가 설정이 필요한데 안드로이드는 추가설정이 필요없고 iOS의 경우 아래의 절차들을 따라가며 설정해주면 된다.
1. Xcode에서 프로젝트 열기 > Signing & Capabilities > Associated Domains 추가
applinks:xxxx.page.link
2. Info 탭으로 이동해서 URL Type을 추가해주자.
3. 만일 본인이 커스텀 도메인을 사용하는 경우 아래의 절차를 진행하자. (Info.plist 에 아래 코드 추가)
두가지 기능을 조합한 소스 구현
이제 설정을 마쳤으니 실제 소스 구현을 해보겠다.
0. 사용되는 패키지 추가 (pubspec.yaml)
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 ...
}
꽤 오랜 시간에 걸쳐 정리가 끝났는데, 중간 중간 빠진 부분이 어딘가 있는 것 같아 찝찝하다.
혹시 진행이 안되는 부분이 있는 경우 댓글로 남겨놓는다면 수정/추가 하도록 하겠다.
댓글