이메일 인증 과정 도중 딥링크 적용 문제

한얼·2023년 8월 25일
1
post-thumbnail

1. 딥 링크(deep link)란?

애플리케이션에 직접 연결하는 링크로, 이를 통해 앱의 특정 위치로 유저를 안내할 수 있습니다. 즉, 특정 URL을 딥 링크로 설정하면 유저가 해당 링크를 클릭 했을 때 앱의 특정 위치로 바로 이동하는 것을 가능하게 해줍니다.

딥 링킹 방식에는 크게 두 가지가 존재합니다.

  1. 전통적인 딥 링크
    1. URI Scheme
  2. 디퍼드(Deffered) 딥 링크
    1. App Links (Android)
    2. Universal Links (iOS)
    3. Firebase Dynamic Links

URI Scheme

URI Scheme은 전통적인 딥 링킹 방식으로, 앱에 고유한 스킴(Scheme)를 할당하고, 이를 통해 앱에 연결합니다. 앱의 고유한 을 사용하여 링크를 생성합니다. 예를 들어 다음과 같은 형태로 이루어져 있습니다.

myapp://{path}?param=1234

URI Scheme 방식을 채택할 경우, 개발자가 원하는 스킴을 직접 지정하여 만들 수 있습니다. 또한 앱에서 사용할 수 있는 널리 알려진 스킴들도 존재합니다.

sms:// # 문자
tel:// # 전화
mailto:// # 메일
geo:// # 위치/지도
market://  # 앱마켓

장점

  • 간단하게 구현할 수 있습니다.
  • iOS와 안드로이드에서 모두 지원됩니다.

단점

  • 앱이 설치되어 있지 않다면 URI Scheme은 작동하지 않습니다.
  • 웹 브라우저에서 URI Scheme을 직접 입력할 경우 앱을 연결할 수 없습니다.
  • 스킴이 고유하지 않아 다른 앱과 충돌할 가능성이 존재합니다.
    • 악의적으로 다른 앱에서 사용하는 스킴과 똑같은 스킴을 정의하는 등, 보안 문제가 발생할 수 있습니다.

App Links(Android) & Universal Links(iOS)

App Links와 Universal Links는 항상 웹사이트(http 또는 https) 형태의 URL로 만들어집니다. 따라서 해당 링크를 클릭할 때 앱이 설치되어 있으면 앱의 특정 페이지로 이동하게 되며, 그렇지 않으면 웹 페이지를 열거나 앱 스토어로 리다이렉션 되도록 설정할 수 있습니다. 예를 들어 다음과 같은 형태로 이루어져 있습니다.

이러한 동작 방식 때문에, App Links와 Universal Links 모두 디퍼드 딥 링킹(Deffered Deep Linking) 기능 역시 자동으로 제공하게 됩니다. 디퍼드 딥 링킹의 동작 방식은 다음과 같습니다.

  1. 유저가 앱이 설치되어있지 않을 때 딥 링크를 클릭합니다.
  2. 유저는 앱 스토어로 리다이렉션됩니다.
  3. 유저가 앱을 설치한 후 앱을 처음 실행하면, 원래 딥링크를 통해 액세스하려고 했던 특정 위치로 바로 이동할 수 있습니다.

장점

  • HTTP 및 HTTPS URL을 앱에 직접 연결할 수 있게 해 주기 때문에 사용자 경험이 좋습니다.
  • 앱 설치 여부에 따른 대체 동작(deffered deep linking)이 가능합니다.
  • URI Scheme과 같은 충돌문제가 없으며 특정 도메인과 앱을 연결하기 위한 인증 절차가 필요하므로 보안이 강화됩니다.

단점

  • Android 6.0 / iOS 9.0 이상에서만 지원합니다.
  • 도메인 검증 절차가 필요하며, 구현이 복잡합니다.
  • 자체 브라우저를 사용하는 앱(카카오톡, 페이스북, 인스타그램)들에 따라서 해당 방식의 딥링크가 동작하지 않을 수도 있습니다.

Dynamic Links(Firebase)

Firebase에서 제공하는 딥링크 서비스로, 디퍼드 딥링크의 기능을 포함하고 있습니다. Dynamic Links를 사용하면 플랫폼(Android, iOS, web)에 관계 없이 하나의 URL로 각각의 플랫폼에 맞게 자동으로 앱 내 특정 위치로 이동할 수 있습니다. (기존 Deffered Deep Link는 플랫폼마다 따로 구현해야하며, 구분된 딥 링크가 생성 됨)

장점

  • 기존 Deffered Deep Linking 방식의 복잡한 처리를 Firebase가 처리해주므로, 구현이 간편합니다.
  • 다양한 플랫폼에서의 연결과 통계 기능을 제공합니다.

단점

  • Firebase를 프로젝트에 통합해야하므로, Firebase 플랫폼에 의존성이 생깁니다.
  • 대량의 트래픽 발생으로 Firebase 무료 요금제를 넘어서게 될 경우 추가 비용이 발생할 수 있습니다. ← 확실치 않음

2. URI Scheme 방식을 활용한 이메일 인증 구현

이번 프로젝트에서는 여러 딥 링크 방식 중 가장 전통적인 방식인 URI Scheme 방식을 채택하여, 딥 링크를 공부하는데 초점을 맞추기로 하였습니다. 딥 링크에 대한 기본적인 이해 및 구현을 성공하면 추후 Firebase Dynamic Links로 넘어갈 계획이었습니다.

이메일 인증 프로세스

이번 프로젝트에서의 이메일 인증 프로세스는 다음과 같습니다.

  1. 프론트엔드(Flutter)에서 유저의 이메일을 입력받아 백엔드로 전송합니다.
  2. 백엔드(Spring)는 해당 이메일 주소로 인증 메일을 발송합니다. 인증 메일에는 앱으로 이동할 수 있는 딥링크가 포함되어 있습니다.
  3. 유저가 발송된 인증 메일 내의 딥링크를 클릭하면, Flutter 앱이 자동으로 열리며 딥링크 내에 포함되어있는 토큰을 읽어 이를 이메일 주소와 함께 백엔드로 전송합니다.
  4. 프론트엔드가 백엔드로부터 인증 확인 response를 정상 수신하면 이메일 인증 프로세스가 완료됩니다.

Scheme & URI 설정

우선 백엔드 팀원과의 협업으로 Scheme 및 URI를 지정하였습니다. 이후, Flutter에서 해당 Scheme에 반응할 수 있도록, 다음과 같은 설정을 해두었습니다.

pubspec.yaml

uni_links: ^0.5.1

uni_links | Flutter Package

android/app/src/main/AndroidManifest.xml

<!-- 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>

ios/Runner/Info.plist

<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를 다음과 같이 수정하였습니다.

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로 작성하였습니다.

email_auth.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>

3. 문제 발생

로컬에서 테스트를 해본 결과 URI Scheme 방식 딥링크가 제대로 동작 함을 확인하였고, 이를 백엔드(Spring)에서 메일로 발송하도록 설정 했으나 메일 내 인증 버튼이 클릭 되지 않는 문제가 발생하였습니다.

원인을 분석해보니 메일과 함께 발송된 html 소스가 다음과 같이 a태그의 href 속성이 사라져 있음을 확인할 수 있었습니다.

이와 같은 문제는 Gmail에서만 발견되었고, 다른 메일 앱에서는 문제가 발생하지 않았습니다. 따라서, 해당 문제를 찾아본 결과 다음과 같은 글을 찾아볼 수 있었습니다.

https://github.com/EddyVerbruggen/Custom-URL-scheme/issues/81

일부 온라인 조사에 따르면, Gmail은 보안 프로필에 맞지 않는 링크(일부 잘못된 형식의 http 링크 포함)가 있는 경우 실제로 이러한 링크를 삭제하는 것으로 확인되었습니다.

즉, Gmail이 보안상 강제로 딥 링크를 삭제한 것이었습니다.

4. 해결책

해당 문제를 통해 URI Scheme 방식의 딥링크를 직접적으로 사용하는 것은 불가능함을 알았습니다. 따라서 다음과 같은 해결책을 구상하였습니다.

  1. 딥링크로 리다이렉션 걸기
  2. https 기반의 App Links(또는 Universal Links) 사용하기
  3. Firebase Dynamic Links를 사용하기

구현한 URI Scheme 방식의 딥 링크를 사용해보는 것이 이번 목표였으므로, 2번과 3번은 1번이 불가능하다 판단되면 시도하기로 하였습니다.

딥 링크로 리다이렉션 걸기

https 기반의 URL을 href 속성에 이용하고, 해당 URL 접근 시 딥 링크로 리다이렉션 되도록 설정(HttpServletResponse 활용)하여 문제를 해결하였습니다.

백엔드 코드 추가(spring boot)

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);
}

2개의 댓글

comment-user-thumbnail
2023년 11월 22일

안녕하세요! 해당 기능 구현 중 블로그 보고 도움 받고 있는데요! 혹시 전체 코드 살펴볼 수 있는 github 링크 알려주실 수 있나요?

1개의 답글