애플리케이션에 직접 연결하는 링크로, 이를 통해 앱의 특정 위치로 유저를 안내할 수 있습니다. 즉, 특정 URL을 딥 링크로 설정하면 유저가 해당 링크를 클릭 했을 때 앱의 특정 위치로 바로 이동하는 것을 가능하게 해줍니다.
딥 링킹 방식에는 크게 두 가지가 존재합니다.
URI Scheme은 전통적인 딥 링킹 방식으로, 앱에 고유한 스킴(Scheme)를 할당하고, 이를 통해 앱에 연결합니다. 앱의 고유한 을 사용하여 링크를 생성합니다. 예를 들어 다음과 같은 형태로 이루어져 있습니다.
myapp://{path}?param=1234
URI Scheme 방식을 채택할 경우, 개발자가 원하는 스킴을 직접 지정하여 만들 수 있습니다. 또한 앱에서 사용할 수 있는 널리 알려진 스킴들도 존재합니다.
sms:// # 문자
tel:// # 전화
mailto:// # 메일
geo:// # 위치/지도
market://  # 앱마켓
App Links와 Universal Links는 항상 웹사이트(http 또는 https) 형태의 URL로 만들어집니다. 따라서 해당 링크를 클릭할 때 앱이 설치되어 있으면 앱의 특정 페이지로 이동하게 되며, 그렇지 않으면 웹 페이지를 열거나 앱 스토어로 리다이렉션 되도록 설정할 수 있습니다. 예를 들어 다음과 같은 형태로 이루어져 있습니다.
이러한 동작 방식 때문에, App Links와 Universal Links 모두 디퍼드 딥 링킹(Deffered Deep Linking) 기능 역시 자동으로 제공하게 됩니다. 디퍼드 딥 링킹의 동작 방식은 다음과 같습니다.
- 유저가 앱이 설치되어있지 않을 때 딥 링크를 클릭합니다.
 - 유저는 앱 스토어로 리다이렉션됩니다.
 - 유저가 앱을 설치한 후 앱을 처음 실행하면, 원래 딥링크를 통해 액세스하려고 했던 특정 위치로 바로 이동할 수 있습니다.
 

Firebase에서 제공하는 딥링크 서비스로, 디퍼드 딥링크의 기능을 포함하고 있습니다. Dynamic Links를 사용하면 플랫폼(Android, iOS, web)에 관계 없이 하나의 URL로 각각의 플랫폼에 맞게 자동으로 앱 내 특정 위치로 이동할 수 있습니다. (기존 Deffered Deep Link는 플랫폼마다 따로 구현해야하며, 구분된 딥 링크가 생성 됨)
이번 프로젝트에서는 여러 딥 링크 방식 중 가장 전통적인 방식인 URI Scheme 방식을 채택하여, 딥 링크를 공부하는데 초점을 맞추기로 하였습니다. 딥 링크에 대한 기본적인 이해 및 구현을 성공하면 추후 Firebase Dynamic Links로 넘어갈 계획이었습니다.
이번 프로젝트에서의 이메일 인증 프로세스는 다음과 같습니다.
우선 백엔드 팀원과의 협업으로 Scheme 및 URI를 지정하였습니다. 이후, Flutter에서 해당 Scheme에 반응할 수 있도록, 다음과 같은 설정을 해두었습니다.
uni_links: ^0.5.1
<!-- Deep Links -->
		<intent-filter>
		    <action android:name="android.intent.action.VIEW" />
		
		    <category android:name="android.intent.category.DEFAULT" />
		    <category android:name="android.intent.category.BROWSABLE" />
		    <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
		    <data android:scheme="[YOUR_SCHEME]" />
		</intent-filter>
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>[ANY_URL_NAME]</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>[YOUR_SCHEME]</string>
    </array>
  </dict>
</array>
이와 같이 설정을 추가하면, 지정한 [YOUR_SCHEME] 스킴을 통해 딥링크 연결설정이 완료됩니다. 즉, 미리 지정해둔 URI 링크를 클릭 했을 때 플러터 앱이 실행됩니다.
여기서 추가적으로 해당 딥 링크를 클릭 했을 때 앱의 특정 위치로 이동하게 하기 위해서, main.dart를 다음과 같이 수정하였습니다.
...
class _MyAppState extends State<MyApp> with WidgetsBindingObserver{
  bool? _isEmailAuth;
  
  void initState() {
    super.initState();
    
		// 앱이 꺼져있을 때
		_initUniLinks();
    // 앱이 백그라운드에서 포그라운드로 전환될 때
		WidgetsBinding.instance.addObserver(this);
  }
  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.resumed) {
      _initUniLinks();
    }
  }
  _initUniLinks() async {
    final initialLink = await getInitialLink();
    if (initialLink != null) {
      _handleIncomingLink(initialLink);
    }
  }
...
유저가 딥 링크를 클릭 할 때, 앱이 꺼져있었을 때와 백그라운드에서 동작하고 있을 때 모두 딥링크를 통해 반응해야하므로, 두 가지 상태에 따른 딥링크 인식 코드를 작성하였습니다.
마지막으로 딥 링크를 실어 보낼 메일 양식을 html로 작성하였습니다.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Email Authentication</title>
    <link
      rel="stylesheet"
      type="text/css"
      href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"
    />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body style="font-family: Pretendard, Arial, sans-serif; font-size: 14px">
    <!-- OUTERMOST CONTAINER TABLE -->
    <table
      border="0"
      cellpadding="0"
      cellspacing="0"
      width="100%"
      id="bodyTable"
    >
      <tr>
        <td align="center">
          <!-- 600px CONTENTS CONTAINER TABLE -->
          <table border="0" cellpadding="0" cellspacing="0" width="600">
            <tr>
              <td bgcolor="#9B4FFB" height="7"></td>
            </tr>            
...
            <tr>
              <td
                style="
                  padding-left: 40px;
                  padding-right: 40px;
                  padding-top: 24px;
                  text-align: center;
                "
              >
                <a
                  href="[YOUR_SCHEME]://email_auth?token=a1s2d3f4g5h6j7k8"
                  class="button"
                  style="
                    display: inline-block;
                    background-color: #9b4ffb;
                    color: #ffffff;
                    font-weight: bold;
                    padding: 20px;
                    text-align: center;
                    width: 313px;
                    height: 24px;
                    line-height: 24px;
                    border-radius: 32px;
                    cursor: pointer;
                    text-decoration: none;
                    font-size: 18px;
                  "
                  >인증하기</a
                >
              </td>
            </tr>
...
          </table>
        </td>
      </tr>
    </table>
  </body>
</html>
로컬에서 테스트를 해본 결과 URI Scheme 방식 딥링크가 제대로 동작 함을 확인하였고, 이를 백엔드(Spring)에서 메일로 발송하도록 설정 했으나 메일 내 인증 버튼이 클릭 되지 않는 문제가 발생하였습니다.
원인을 분석해보니 메일과 함께 발송된 html 소스가 다음과 같이 a태그의 href 속성이 사라져 있음을 확인할 수 있었습니다.

이와 같은 문제는 Gmail에서만 발견되었고, 다른 메일 앱에서는 문제가 발생하지 않았습니다. 따라서, 해당 문제를 찾아본 결과 다음과 같은 글을 찾아볼 수 있었습니다.
https://github.com/EddyVerbruggen/Custom-URL-scheme/issues/81
일부 온라인 조사에 따르면, Gmail은 보안 프로필에 맞지 않는 링크(일부 잘못된 형식의 http 링크 포함)가 있는 경우 실제로 이러한 링크를 삭제하는 것으로 확인되었습니다.
즉, Gmail이 보안상 강제로 딥 링크를 삭제한 것이었습니다.
해당 문제를 통해 URI Scheme 방식의 딥링크를 직접적으로 사용하는 것은 불가능함을 알았습니다. 따라서 다음과 같은 해결책을 구상하였습니다.
구현한 URI Scheme 방식의 딥 링크를 사용해보는 것이 이번 목표였으므로, 2번과 3번은 1번이 불가능하다 판단되면 시도하기로 하였습니다.
https 기반의 URL을 href 속성에 이용하고, 해당 URL 접근 시 딥 링크로 리다이렉션 되도록 설정(HttpServletResponse 활용)하여 문제를 해결하였습니다.
import javax.servlet.http.HttpServletResponse;
@GetMapping("/redirect")
public void redirectToDeepLink(@RequestParam String auth, HttpServletResponse response) {
    String email = auth;
    String token = emailAuthService.findAuthTokenByEmail(email);
    String redirectUrl = "[YOUR_SCHEME]://email_auth?token=" + token;
    response.setHeader("Location", redirectUrl);
    response.setStatus(302);
}
안녕하세요! 해당 기능 구현 중 블로그 보고 도움 받고 있는데요! 혹시 전체 코드 살펴볼 수 있는 github 링크 알려주실 수 있나요?