[Flutter] 플러터에 다이나믹 링크를 도입했을 때 iOS에서 발생하는 문제점들 고치기

우재윤·2023년 3월 15일
0

플러터로 앱을 빌드하고 배포하게 되면 자연스럽게 dynamic link 적용을 고려하게 된다.
flutter도 dynamic link도 구글에서 서비스하는 제품들이기에 공식 가이드에서 flutter의 사례를 볼 수 있고 유저들이 만들어 둔 문서들도 많기 때문인데,

그러나 둘 다 구글의 서비스여서 그런지 실제로 서비스에 적용했을 때 안드로이드에선 공식 가이드 대로만 진행해도 문제없이 연결된 콘텐츠로 이동하는 반면, iOS에선 두 가지의 문제점이 있었다. *2023년 3월 기준

문제점

dynamic link로 앱을 여는 케이스는 4가지가 있다.
앱이 foreground(실행 중)일 때, background(백그라운드)일 때, terminated(종료)일 때, uninstall(설치 안됨)일 때

foregroundbackground에선 안드로이드, iOS 모두 정상적으로 작동하는 데 terminated, uninstall 상태 일 때 iOS에선 연결된 콘텐츠로 이동하지 않는 현상이 발생하고 있었다.

debug 빌드에선 앱을 종료하면 console과의 연결이 끊기고 설치 안된 상태에선 dynamic link로 debug 빌드를 설치할 수 없었기에 문제를 분석하고 해결하기 힘들었는데,
급한 문제라 빠르게 해결하고 배포 후 충분히 안정적으로 동작하는걸 테스트 후 해결방법을 공유합니다.

Terminated - 앱이 종료되었을 때

우선 dynamic link의 공식 가이드를 모두 따라한 상태를 기준으로 합니다.
https://firebase.google.com/docs/dynamic-links/flutter/receive

문서 가장 하단을 보면

// Check if you received the link via `getInitialLink` first
  final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();

  if (initialLink != null) {
    final Uri deepLink = initialLink.link;
    // Example of using the dynamic link to push the user to a different screen
    Navigator.pushNamed(context, deepLink.path);
  }

  FirebaseDynamicLinks.instance.onLink.listen(
        (pendingDynamicLinkData) {
          // Set up the `onLink` event listener next as it may be received here
          if (pendingDynamicLinkData != null) {
            final Uri deepLink = pendingDynamicLinkData.link;
            // Example of using the dynamic link to push the user to a different screen
            Navigator.pushNamed(context, deepLink.path);
          }
        },
      );

이 파트에서 앱이 종료된 상태에서 dynamic link로 앱을 실행했을 때 처리를 볼 수 있는데
여기서 추가적인 처리를 할 것이다.

pub에 app_links를 추가해준다.

flutter pub add app_links

추가적인 처리를 해준다

// 다이나믹 링크로 앱 오픈 - 종료되었을 때
Future<void> didOpenAppByLink() async {
  final _appLinks = AppLinks();
  final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink();

  if (initialLink != null) {
    final Uri deepLink = initialLink.link;

    // 링크 처리
    onListenLink(deepLink.path);
  } else {
    final Uri? uri = await _appLinks.getInitialAppLink();
    if (uri != null) {
      final PendingDynamicLinkData? appLinkData =
          await FirebaseDynamicLinks.instance.getDynamicLink(uri);
      if (appLinkData != null) {
        // 링크 처리
    	onListenLink(appLinkData.link.path);
      }
    }
  }
}

코드 설명

우선 해당 파트를 함수로 빼준 후 FirebaseDynamicLinks.instance.getInitialLink()에서 받아 온 링크가 null 일 경우 _appLinks.getInitialAppLink()를 통해 다시 앱을 실행하는데 사용한 링크를 받아온다. app_links에서 받아온 링크가 있을 경우 링크 처리.

이렇게 적용하면 terminated 상태에서 작동하지 않는 문제는 해결된다. 이제 남은 건 uninstall

추가

initialLink를 받아오기 전에 딜레이를 줘서 해결 했다는 글도 보긴 했으나 안정적인 실행을 보장할 정확한 딜레이 타임을 구하기도 힘들고 앱의 초기 실행 속도를 늦춰 사용자 경험을 저해시킬 것 같아서 사용하진 않았다. 아래 참고

// Duration
await Future.delayed();

참고 출처) https://github.com/firebase/flutterfire/issues/6242

Uninstall - 앱이 설치되지 않았을 때

app_links로 여기까지 해결되었다면 좋았겠지만 해결되지 않았다.
하나 의문이었던 점은 분명 앱을 설치하고 열기 버튼을 클릭하면 클립보드에서 붙여넣기를 허용할 것인지 물어보는 창이 나온다는 점. 그러나 연결된 콘텐츠로 이동하진 않았다.

해결 아이디어

클립보드에서 붙여넣기 허용 팝업이 뜬다는 건 링크를 가져오기 위해 클립보드 읽기를 요청했다는 것.
열기 버튼을 눌렀을 때 무언가가 클립보드에 저장될 것이라 추측하고 열기 버튼을 누르자 마자 앱을 종료하고 클립보드에 복사된 걸 붙여 넣어봤다.

결과는

https://ypetapp.page.link/?link=https://ypet.co.kr/shopping/viewer/1674020844589&apn=com.ypet.ypetapp&isi=1598707883&ibi=com.ypet.app&st=%EB%82%B4+%EC%86%90%EC%9C%BC%EB%A1%9C+%EC%A7%81%EC%A0%91+%EB%A7%8C%EB%93%9C%EB%8A%94!+%EB%82%B4%EC%83%88%EA%BE%B8+%EC%BA%98%EB%A6%B0%EB%8D%94&sd=%EB%AA%A8%EB%91%90%EA%B0%80+%EA%B8%B0%EB%8B%A4%EB%A6%B0+2023%EB%82%B4%EC%83%88%EA%BE%B8+%EB%8B%AC%EB%A0%A5!+%EA%B7%80%EC%97%AC%EC%9A%B4+%EB%82%B4%EC%83%88%EA%BE%B8%EA%B0%80+%EA%B0%80%EB%93%9D+%EB%8B%B4%EA%B8%B4+%EC%83%88%ED%95%B4+%EB%8B%AC%EB%A0%A5%EC%9D%84+%EC%A7%81%EC%A0%91+%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%84%B8%EC%9A%94!&si=https://api.ypet.co.kr/download-file/temp_1773757_1674633857156000.jpeg&cid=4821790542107258486&_osl=https://ypetapp.page.link/xYfP&_icp=1

매우 긴 어떤 링큰데 dynamic link를 조금 만지작 해봤다면 바로 알 수 있다.
이건 내 dynamic link다. 긴 버전의.

떠올렸다. 공식 가이드 가장 하단에 조그맣게 적힌 문구를

결국 앱을 실행할 때 클립보드에서 텍스트를 불러오고 텍스트를 FirebaseDynamicLinks.instance.getDynamicLink()를 통해 분석해서 사용했고
해당 버전을 배포한지 3주 가량 지났지만 잘 작동하고 있다.

아래에 코드 첨부.
link.text!.startWith() 부분은 본인의 다이나믹 url을 입력하면 된다.
실행 시 매번 클립보드를 요청하면 매번 붙여넣기 허용 팝업이 떠서 앱 첫 실행 시에만 실행하도록 해두었다. 귀찮으면 is_first_run을 사용해도 되고 직접 구현한다면 shared_preferences 등의 로컬 저장소를 사용해도 좋다. 기회가 되면 Hive라는 지금 사용 중인 로컬 저장소의 사용법도 올리는 걸로.

// 다이나믹 링크로 앱 오픈 - 첫 실행일 때 클립보드에서 받아옴
Future<void> didOpenAppByLinkOnClipboard() async {
  ClipboardData? link = await Clipboard.getData(Clipboard.kTextPlain);
  if (link != null && link.text != null) {
    if (link.text!.startsWith('https://ypetapp.page.link')) {
      final Uri uri = Uri.parse(link.text!);
      final PendingDynamicLinkData? appLinkData =
          await FirebaseDynamicLinks.instance.getDynamicLink(uri);
      if (appLinkData != null) {
        onListenLink(appLinkData.link.path);
      }
    }
  }
}

여담

flutterWeb으로 서비스 중이던 사내 프로젝트를 앱으로 전환하고 출시까지 마쳤습니다.
서비스 고도화와 앱 전환을 동시에 하느라 정신 없는 시간을 보냈는데
이걸 두 달 만에 할 수 있다는 flutter의 대단함과 그럼에도 역시 web과 android, iOS 등 모든 플랫폼을 아우르는 건 시기상조 임을 느끼며, 그 과정에서 겪은 일들도 공유할만 한 것들은 글로 올리겠습니다.

질문과 더 좋은 해결방법은 댓글로 남겨주세요!

Flutter로 빌드한 프로덕트가 궁금하시다면 와이펫을 검색해주세요

profile
프론트엔드 개발자/Flutter/와이펫

1개의 댓글

comment-user-thumbnail
2023년 4월 18일

감사합니다 덕분에 퇴근합니다.

답글 달기