ghdtjgus

웹뷰 스터디를 진행하면서 딥링크도 구현해 볼 수 있는 기회를 얻었다.
이전부터 웹뷰 앱을 만든다면 정말 중요하게 생각할만한 포인트 중 하나가 딥링크라고 들었다.
이번에 딥링크를 공부해보니 신기하고 재밌었다.
근데 레퍼런스가 너무 없어서 정신 나갈 뻔..
 

딥링크

일단 딥링크에 대해서 알아보자.
딥링크란 특정 앱으로 이동시키는 동작을 의미한다.
우리가 무신사 앱에서 결제 버튼을 누르면 토스 앱으로 넘어가는 동작 같은 걸 상상하면 될 거 같다.
물론 앱뿐만 아니라 웹에서도 가능하다.
모바일 기기 브라우저에서 특정 버튼을 누르면 앱이 열리고 특정 앱 화면으로 전환이 되는 동작 같은 거 말이다.
 
이번에 내가 구현해본 동작은 웹페이지에서 앱으로 전환하는 동작이다.
 
아래 화면을 살펴보자.
오늘의 집 클론코딩한 페이지인데 아직 UI가 미흡하니 그건 실눈 뜨고 보도록 하자.
 
안드로이드 에뮬레이터 크롬 창에서 기록 챌린지 페이지에 더 편하게 앱으로 보기라는 버튼을 누르면 딥링크를 통해서 우리가 만든 오늘의 집 클론 앱이 띄워지고, 해당 앱의 기록 챌린지 탭으로 전환이 된다.
물론 앱에서는 더 편하게 앱으로 보기 버튼이 안 보이게 처리해 놨다.

 
지금부터 딥링크 방식에 대한 이해, 그리고 딥링크 구현 방식에 대해서 조금 더 알아보도록 하자.
 

딥링크 방식

딥링크 구현 방식은 여러 가지가 있다.
 
다음 표는 토스페이먼츠의 딥링크 관련 레퍼런스에서 가져온 자료이다.

딥링크 유형AndroidiOS특징예시
url 스킴OO널리 사용되지만 다른 앱에서 같은 스킴을 사용할 수 있음my-app://host/path
App LinkOXGoogle에서 출시한 Android 전용 딥링크https://host/path
Universal LinkXOApple에서 출시한 iOS 전용 딥링크이며 macOS, watchOS에서도 사용 가능https://host/path
Intent 스킴OX스킴, 패키지 등 많은 정보를 담고 있음intent://path#intent;scheme...

 
url 스킴 방식은 이전부터 널리 사용되던 방식이다.
하지만, 여러 앱에서 동일한 스킴을 가질 수 있어서 문제가 발생했다.
이때 스킴이란 my-app://host/path에서 my-app을 나타낸다.
Android의 경우 여러 앱이 동일한 스킴을 가지고 있다면 선택할 수 있게 해 주지만 iOS에서는 이 문제를 해결할 수 없다고 한다.
 
그래서 이 중복 스킴 문제를 해결하기 위해서 iOS는 Universal Link, Android는 App Link 방식을 출시했고 가장 추천하는 구현 방식이라고 한다.
 
intent 스킴의 경우 Android에서만 사용할 수 있는 딥링크 방식인데, 앱이 설치되지 않았는데 실행하는 등 예외 처리와 관련된 복잡한 동작 같은 부분을 설정을 통해 지정해 줄 수 있다고 한다.
 
나는 현재 가장 추천하는 딥링크 구현 방식인 App Link 방식을 통해서 구현하려고 여러 가지 세팅을 해보았다.
 

App Link 세팅

먼저 AndroidManifest.xml 파일에서 intent-filter 부분을 추가해 주었다.

// AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">

      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        
        <!-- 앱 실행을 위한 기본 intent-filter -->
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <!-- App Link를 위한 intent-filter -->
        <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="ohouse-web.vercel.app" />
            <data android:scheme="https"
                  android:host="*.ohouse-web.vercel.app" />
        </intent-filter>

      </activity>
    </application>
</manifest>

 
주목해야 할 부분은 App Link를 위한 intent-filter 부분이다.
어쨌든 App Link라는 것이 url 이벤트를 웹이 아니라 앱에서 처리해 주기 위한 동작이기 때문에 이를 위한 설정들을 추가해 주었다.
우리는 https://ohouse-web.vercel.app이라는 도메인에서 url 관련 처리를 해주어야 하기 때문에 이 부분 또한 설정에 추가해 주었다.
 
그리고 App Link를 설정하기 위해서는 웹사이트 서버에 assetlinks.json 파일을 배포해야 한다.
이 파일을 통해서 안드로이드가 해당 url을 앱과 연결할 수 있는지 확인하는 과정을 거치게 된다.

// public/.well-known/assetlinks.json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.team-warrr.ohouseApp",
      "sha256_cert_fingerprints": [
        // ...
      ]
    }
  }
]

sha256_cert_fingerprints 부분은 각자 생성한 키 부분과 관련되어 있으니 따로 생성해서 넣어주면 된다.
 
이렇게만 하면 우리가 생각한 것처럼 이 딥링크가 작동할 때 앱에서 잘 열릴 거 같지만 아니다.
앱에서 해당 url을 처리하지 않고 크롬 창에서 열린다.
 
이는 안드로이드 에뮬레이터 설정과 관련된 부분을 수정해줘야 하는데,
Settings > Apps > Default Apps > Opening links > ohouseApp 부분에서 add link를 해줘야 우리가 원하는 대로 ohouse-web.vercel.app 도메인을 가진 링크를 앱에서 열 수 있게 된다.

 
App Link 관련 설정이 잘 되어 있는지 확인하기 위해서는 아래 명령어를 통해서 살펴보면 된다.
Selection state 부분의 Enabled에 우리가 설정해주고 싶은 도메인이 뜨면 잘 된 거다.

adb shell pm get-app-links --user 0 com.ohouseapp

//  com.ohouseapp:
//    ID: // ...
//    Signatures: // ...
//    Domain verification state:
//      ohouse-web.vercel.app: 1024
//      *.ohouse-web.vercel.app: 1024
//    User 0:
//      Verification link handling allowed: true
//      Selection state:
//        Enabled:
//          ohouse-web.vercel.app
//          *.ohouse-web.vercel.app

 
안드로이드 에뮬레이터를 실행한 상태로 아래 명령어를 터미널에 입력해 주면 딥링크가 잘 동작하는지 확인할 수 있다.
간단하게 설명해 보자면, 안드로이드 에뮬레이터에서 인텐트를 시작하게 해주는 명령어인데, 입력한 url이 앱에서 잘 열리는지 테스트한다고 보면 된다.

adb shell am start -a android.intent.action.VIEW -d "https://ohouse-web.vercel.app/record-challenge"

 
이렇게까지 했을 때 우리가 원하는 것처럼 앱에서 기록 챌린지 페이지가 잘 뜰 것이다.
 
이제 App Link를 사용하기 위한 기본적인 설정은 다 해주었으니 남은 건 두 가지다.
1. 환경(os, 브라우저 여부)에 따라서 동작 분기 처리
2. 딥링크 연결
 
위에서 우리가 딥링크가 잘 연결되었는지 확인하기 위한 명령어는 단순 테스트를 위해서 인텐트를 수동으로 동작시킨 거다.
그래서 웹페이지에서 앱으로 전환하기 위해서는 별도의 코드 작성이 필요하다.
 
이 작업을 지금부터 해보자.
 
우리가 기록 챌린지에 있는 버튼을 클릭했을 때 앱으로 연결되게 하기 위해서는 window.location.replace를 통해서 destination url을 전달해 주면 된다.
여기서 path로는 record-challenge를 전달해주면 되겠다.

const handleClick = () => {
    const url = `https://ohouse-web.vercel.app/${path}`;

    window.location.replace(url);
  };

 
그리고 앱 상에서도 한 가지 변경해주어야 하는 부분이 있다.
App.tsx 부분인데, url 관련 이벤트를 앱의 진입점에서 처리해 준다.
react-native의 Linking을 활용해서 url 관련 이벤트 발생 시 deepLink 함수에서 처리해 주는데, 여기서 경로 부분을 파싱 해서 내비게이션을 해준다고 보면 된다.
물론 이 코드는 조금 더 다듬을 필요가 있지만, 1차적인 구현으론 그렇다.

// App.tsx
const App = () => {
  const deepLink = useCallback((url: string) => {
    if (url) {
      try {
        const urlObject = new URL(url);
        let route: string | null = null;

        route = urlObject.pathname.split('/')[1];

        if (route) {
          const routeName = urlMap[route];
          if (routeName) {
            navigate(routeName as keyof RootParamList);
          } else {
            Alert.alert('urlMap에서 경로를 찾을 수 없습니다:', route);
          }
        } else {
          Alert.alert('경로가 null이거나 navigationRef를 사용할 수 없습니다');
        }
      } catch (error) {
        Alert.alert(
          '링크 처리 오류',
          '링크를 처리하는 도중 오류가 발생했습니다.',
        );
      }
    }
  }, []);

  useEffect(() => {
    Linking.getInitialURL().then(url => {
      if (url) {
        deepLink(url);
      }
    });

    const urlListener = Linking.addEventListener('url', ({url}) => {
      if (url) {
        deepLink(url);
      }
    });q

    return () => {
      urlListener.remove();
    };
  }, [deepLink]);

  return <NavigationProvider />;
};

export default App;

 
이렇게만 하면 분명 웹 기록 챌린지 페이지에서 앱 기록 챌린지 페이지가 떠야 한다.
 
하지만 웹페이지 상에서 앱으로 안 넘어간다.
일단 App Link 설정은 잘해놨고, 인텐트 명령어를 활용해서 확인까지 했기 때문에 설정에는 문제가 없다.
 
그래서 원인을 생각해 보았는데, 우리 앱은 웹뷰 앱이고, 앱에서도 기록 챌린지 탭에서 https://ohouse-web.vercel.app/record-challenge 페이지를 띄워주고, 웹 기록 챌린지 페이지에서도 동일한 url로 띄워주게 된다.
이렇게 동일한 url 상에서 띄워주다 보니 브라우저가 계속 웹에서 처리하는 듯했다.
 
이를 위한 해결 방안으로는 두 가지 정도가 있을 거 같다. (물론 아직 한 가지밖에 안 해봐서 예상 해결 방안이다.)
1. url 스킴 방식으로 해결 
2. App Link 방식으로 하되 url에 웹과 앱 링크를 구분하기 위한 특정 매개변수 추가
 
일단 1번 방식에서는 url 스킴 방식은 웹 기록 챌린지 페이지(https://ohouse-web.vercel.app/record-challenge)에서 앱 기록 챌린지 탭(ohouseapp://record-challenge)으로 전환되는데 url이 다르기 때문에 문제가 없을 거라 생각했다.
2번 방식은 웹 기록 챌린지 페이지(https://ohouse-web.vercel.app/record-challenge), 앱 기록 챌린지 탭(https://ohouse-web.vercel.app/record-challenge?app_link=true) 이렇게 구분을 하게 되면 구분을 할 수 있을 거라 생각했다.
 
일단은 1번 방식으로 도전한 후, 2번 방식으로까지 수정해보려고 했다.
왜냐하면 1번 방식으로 하더라도 기존 App Link 설정에 url 스킴 설정을 추가만 하면 되기 때문에 1번 방식으로 하더라도 추후 2번으로 수정하는데 큰 코드 변경이 필요하지 않았기 때문에 url 스킴 방식도 구현해보고 App Link 방식도 구현해보려 했다.
 
아까 App Link를 설정하면서 intent-filter를 설정해준 적이 있었다.
여기에 url 스킴을 사용하기 위한 설정 몇 가지가 추가돼야 한다.

// AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">

      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        
        <!-- 앱 실행을 위한 기본 intent-filter -->
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <!-- App Links를 위한 intent-filter -->
        <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="ohouse-web.vercel.app" />
            <data android:scheme="https"
                  android:host="*.ohouse-web.vercel.app" />
        </intent-filter>

        <!-- URL Scheme을 위한 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" />
            
            <!-- URL Scheme 설정 -->
            <data android:scheme="ohouseapp" />
        </intent-filter>

      </activity>
    </application>
</manifest>

이전에 설정해준 부분과 상당히 비슷하다.
url 이벤트를 웹이 아닌 앱에서 처리하기 위한 intent-filter 설정을 넣어주고 url 스킴도 설정해준다.
우리는 ohouseapp으로 설정해주었기 때문에 앱 기록 챌린지 탭은 ohouseapp://record-challenge이 된다.
 
이제, 기록 챌린지 버튼 url 부분을 변경해주면 된다.
아까와 다르게 url 스킴 방식을 활용하기 위해서 ohouseapp으로 변경해준 것을 볼 수 있다.

const handleClick = () => {
    const url = `ohouseapp://${path}`;

    window.location.replace(url);
  };

 
마지막으로 앱의 App.tsx 부분도 변경이 필요한데, route 부분 파싱하는 부분이 추가되었다.
기존 App Link 방식과 url 스킴 방식 둘 다 경로를 파싱할 수 있도록 분기 처리해주었다.

// App.tsx
const App = () => {
  const deepLink = useCallback((url: string) => {
    if (url) {
      try {
        const urlObject = new URL(url);
        let route: string | null = null;

        if (urlObject.protocol === 'https:') {
          route = urlObject.pathname.split('/')[1];
        } else if (urlObject.protocol === 'ohouseapp:') {
          route = url.split('://')[1];
        }

        if (route) {
          const routeName = urlMap[route];
          if (routeName) {
            navigate(routeName as keyof RootParamList);
          } else {
            Alert.alert('urlMap에서 경로를 찾을 수 없습니다:', route);
          }
        } else {
          Alert.alert('경로가 null이거나 navigationRef를 사용할 수 없습니다');
        }
      } catch (error) {
        Alert.alert(
          '링크 처리 오류',
          '링크를 처리하는 도중 오류가 발생했습니다.',
        );
      }
    }
  }, []);

  useEffect(() => {
    Linking.getInitialURL().then(url => {
      if (url) {
        deepLink(url);
      }
    });

    const urlListener = Linking.addEventListener('url', ({url}) => {
      if (url) {
        deepLink(url);
      }
    });q

    return () => {
      urlListener.remove();
    };
  }, [deepLink]);

  return <NavigationProvider />;
};

export default App;

 
이렇게만 하면 url 스킴 방식으로 문제 없이 동작한다.
아까 보여줬던 영상처럼 잘 동작하는 것을 볼 수 있다.

 
하지만 이전에도 언급했다시피 url 스킴 방식은 중복 스킴 이슈가 있기 때문에 안드로이드에서는 App Link 방식이 권장된다고 했다.
추후에는 App Link 방식으로 처리해보려고 한다.

왜냐면 진짜 오늘의 집 앱에서도 url 스킴으로 ohouseapp을 사용하는 거 같기 때문..
iOS 부분은 아직 딥링크 처리가 불완전한 상태이기는 하나, 내 폰 브라우저에서 위 버튼을 누르면 진짜 오늘의 집 앱이 열린다..

위에서 언급했듯이, iOS에선 중복 url 스킴을 해결할 수 있는 방법이 없다.
앱을 선택할 기회 자체를 안 줌..
이게 url 스킴을 사용하지 말라는 이유임을 몸소 느껴버렸다..ㅋㅋ
 

 
 
뿐만 아니라 이 과정에서 알게 된 것들이 많았지만 이번 글은 철저히 구현 위주였어서 다른 글로 다시 알아가보도록 하자.
이번에 작성한 코드들을 기반으로 여러 예외 처리도 추가하고 디벨롭해보려고 한다.
 

레퍼런스

https://velog.io/@tosspayments/%EB%94%A5%EB%A7%81%ED%81%AC-%EC%8B%A4%EC%A0%84%EC%97%90%EC%84%9C-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
https://velog.io/@tosspayments/Android-iOS-%EC%9B%B9%EB%B7%B0%EC%97%90%EC%84%9C-%EB%94%A5%EB%A7%81%ED%81%AC-%EC%97%B4%EA%B8%B0

profile

ghdtjgus

@gugu76

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그