현재 똑똑한개발자에서는 웹앱 형태로 사이트를 구현하여 네이티브로 Wrapping 하는 식으로 웹과 네이티브를 동시에 대응하는 방향으로 개발을 하고 있습니다.

지난 주에 제가 앱에서 상세 상품 페이지를 링크로 공유했을 때, 앱이 설치된 유저와 설치되지 않은 유저를 나눠서 처리해주기 위한 로직 구현을 담당했었습니다.

기능

  • 앱이 설치된 유저의 경우 앱 실행 후 링크 이동
  • 앱이 설치되지 않은 유저의 경우 웹 다운로드 페이지로 이동
redirect_page

클라이언트 측에서 전달받은 다운로드 페이지

아이디어

우선 다음 같은 플로우로 처리를 하면 되지않을까 생각..

  1. 공유된 링크로 웹 접근
  2. 네이티브 단에서 웹뷰의 userAgent에 고유값을 추가해 해당 값으로 앱 실행여부를 파악한다.
  3. 앱으로 실행하지 않았을 경우 앱을 실행시키며, 딥링크로 공유된 링크 실행
  4. 딥링크 에러 또는 userAgent에서 고유값이 확인 안되었을 경우 앱에서 실행하지 않았다 판단하여 /download 페이지로 redirect
  5. 딥링크 에러가 없다면 확인 후 url 이동

개발하면서 알게된 부분

URL Schema

toktokhan://path

Universal Link

https://toktokhan.dev/path

링크로 웹을 접근하는 방식이 버튼을 통한 링크 이동인지(a tag의 href), url을 통한 링크 접근인지(웹뷰로 링크 띄우기)에 따라서 인식하는게 다름.

deeplink와 universal link(app links)를 사용해서 2가지 상황에 대해서 동시에 처리해줘야함.

  • url을 읽어 deeplink를 생성한다.
  • 생성한 deeplink로 redirect 시킨다.
  • timeout을 통해 redirect 후 전 페이지에 대한 이벤트 처리를 한다.
  • universal link 설정을 하면, 디바이스에 자동으로 링크를 인식해서 앱을 띄울지 선택한다.
universal_link

네이티브 파트

userAgent 설정

react-native-device-info 라이브러리를 사용해서 userAgent를 가져와 고유값[@toktokhan] 를 넣어줍니다.

...
import DeviceInfo from 'react-native-device-info';

const App = () => {
...
  return (
    <WebView
    ...
	    userAgent={`${DeviceInfo.userAgent}@toktokhan`}/>
  );
};

export default App;

딥링크 설정 [iOS]

01. AppDelegate.m 파일 수정

// iOS 9.x or newer
#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

iOS 버전 9 이하의 경우 아래 코드를 사용하세요!

// iOS 8.x or older
#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
  return [RCTLinkingManager application:application openURL:url
                      sourceApplication:sourceApplication annotation:annotation];
}

Universal Link를 사용할 경우 아래 코드도 함께 추가해주세요.

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

02. Xcode 수정

xcode_1

xcode_2

03. App.js 파일 수정

const ROOT_URL = "http://localhost:3000/" // 배포 전 수정 필요
...
const deepLinkListener = ({url}) => {
  if (url) {
    deepLink(url, 'addEventListener');
  }
};

const deepLink = (url, type) => {
  const DOMAIN = 'toktokhan.dev';
  if (url) {
    const [_, _url] = url.split('://');
    let newUrl = `${notiUrl + '/' + _url}`;
    if (_url.includes(DOMAIN)) {
      newUrl = `https://${_url}`;
    }
    webRef.current.injectJavaScript(`window.location.href = "${newUrl}";`);
  }
};

useEffect(() => {
...
  //IOS && ANDROID : 앱이 딥링크로 처음 실행될때, 앱이 열려있지 않을 때
  Linking.getInitialURL().then((url) => deepLink(url));

  //IOS : 앱이 딥링크로 처음 실행될때, 앱이 열려있지 않을 때 && 앱이 실행 중일 때
  //ANDROID : 앱이 실행 중일 때
  Linking.addEventListener('url', deepLinkListener);
	return () => {
    Linking.removeEventListener('url');
  };
}, []);
...

04. 테스트

xcrun simctl openurl booted toktokhan://product/11654

딥링크 설정 [Android]

01. AndroidManifest.xml 파일 수정

<activity
  android:name=".MainActivity"
  android:launchMode="singleTask">
	<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="*.toktokhan.dev" />
    <data android:scheme="https" android:host="toktokhan.dev" />
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="toktokhan"/>
  </intent-filter>
</activity>

03. 테스트

adb shell am start -W -a android.intent.action.VIEW -d "toktokhan://product/11654" dev.toktokhan

웹 파트

01. _app.tsx

...
const checkUserAgent = () => {
  const userAgent = window?.navigator.userAgent;
  const origin = window?.location.origin;
  const pathname = window?.location.pathname;

  if (userAgent.includes('@toktokhan')) {
    if (pathname !== deepLink) {
      Router.replace(`${origin + pathname}`);
    }
  } else {
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = () => {
      if (xhr.readyState == 4 && xhr.status == 200) {
        window.location.open(`toktokhan:/${pathname}`);
      } else {
        Router.replace(`${origin}/download`);
      }
    };

    xhr.open('head', `toktokhan:/${pathname}`);
    xhr.send(null);
  }
  DeviceStore.deepLink = pathname;
};

/public/.well-known/apple-app-site-association

해당 경로에 확장자 없이 파일을 생성한 다음 아래 코드를 입력합니다.

appID 에는 <TeamID>.<Bundle-Identifier> 을 입력해주세요!

apple_team_id

TeamID는 빨간색 박스 부분에서 확인할 수 있습니다.

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "<TeamID>.dev.toktokhan",
        "paths": ["*"]
      }
    ]
  }
}

다음 명령어를 사용하여 자바 keytool을 통해 지문 파일을 생성할 수 있습니다.

$ keytool -list -v -keystore my-release-key.keystore

/public/.well-known/assetlinks.json

해당 경로에 json 파일을 생성한 다음 아래 코드를 입력합니다. 위에서 생성한 지문 파일을 sha256_cert_fingerprints 에 입력해주세요.

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "dev.toktokhan",
      "sha256_cert_fingerprints": [
        "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
      ]
    }
  }
]

[코드 생성 및 테스트] 에서 테스트를 해볼 수 있습니다!

참고

jangwon.seo's profile image

jangwon.seo

2021-03-20 22:24

Read more posts by this author