effidevFlutter · Cloudflare 엣지 · 클라우드 비용 최적화
한국어

Flutter App Links / Universal Links 자동 검증 및 CI 딥링크 테스트 자동화 가이드

Flutter App Links Universal Links 자동 검증 CI 파이프라인

Flutter 앱 개발 시 **딥링크(Deep Linking)**는 사용자 유입, 마케팅 캠페인, 푸시 알림 이동 및 이커머스 결제 리다이렉트의 핵심 관문이다. 기존의 커스텀 URL 스킴(myapp://path)은 보안상 해이하여 다른 앱이 동일한 스킴을 가로채는 딥링크 하이재킹(Hijacking) 위험이 컸다.

이 때문에 Google과 Apple은 웹 도메인과 앱의 암호화 소유권을 검증하는 Android App LinksiOS Universal Links 사용을 강제하고 있다.

그러나 실무에서는 웹 서버의 .well-known 파일 호스팅 실수(SSL 서명 오류, Content-Type 미흡, SHA-256 지문 불일치)로 인해 프로덕션 배포 후 딥링크가 브라우저 웹페이지로 열리는 장애가 빈번히 발생한다.

이 글에서는 App Links 및 Universal Links의 구조적 연동법, Flutter 앱 라우터(GoRouter / app_links) 핸들링 기법, 그리고 GitHub Actions CI 환경에서 딥링크 도메인 검증 및 E2E 테스트를 자동화하는 가이드를 소개한다.

핵심 요약

  • Android App Links 검증 필수 요소: https://<domain>/.well-known/assetlinks.json에 앱 패키지명과 올바른 SHA-256 핑거프린트(Keystore 및 Google Play App Signing 핑거프린트 모두 포함)가 기술되어야 하며 Content-Type: application/json으로 응답해야 한다.
  • iOS Universal Links 검증 필수 요소: https://<domain>/.well-known/apple-app-site-association (AASA) 파일에 appID (<TEAM_ID>.<BUNDLE_ID>)가 등록되어야 한다.
  • Flutter 라우팅 연동: app_links 패키지와 GoRouter를 결합하여 콜드 스타트(Cold Start) 및 핫 실행 상태 모두에서 파라미터가 유실되지 않도록 라우팅 체계를 구축해야 한다.
  • CI 자동화 시스템: 배포 전 GitHub Actions 단계에서 Google Statement List API 및 Apple AASA 검증 도구와 adb / xcrun simctl CLI 테스트를 자동 수행하여 딥링크 파손을 100% 사전에 방지한다.

1. Domain Association 파일 설정 가이드

웹 서버 루트 또는 .well-known 디렉터리에 배포할 도메인 소유권 검증 JSON 표준 형식이다.

1) Android assetlinks.json (/.well-known/assetlinks.json)

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "dev.effidev.flutterapp",
      "sha256_cert_fingerprints": [
        "14:6D:E9:7D:59:06:50:57:3E:99:A5:1D:64:1D:64:D4:6C:C9:86:EC:65:4E:91:D9:D2:ED:EB:98:C4:4B:9B:C6",
        "FB:8A:2D:E2:B0:16:C0:D4:56:88:83:AF:B6:3C:99:A7:28:B6:AA:DF:99:EE:48:47:6E:9A:3D:11:51:71:06:21"
      ]
    }
  }
]

(주의: 로컬 개발용 핑거프린트와 구글 플레이 앱 서밍(Google Play App Signing) 핑거프린트를 모두 배열에 포함해야 한다.)

2) iOS apple-app-site-association (/.well-known/apple-app-site-association)

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "ABCDE12345.dev.effidev.flutterapp",
        "paths": [ "NOT /api/*", "/product/*", "/share/*" ]
      }
    ]
  }
}

앱이 꺼져있을 때 딥링크로 켜지는 경우(Cold Launch)와 앱이 실행 중일 때(Background Launch) 두 경우를 모두 완벽히 수신하는 표준 패턴이다.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:app_links/app_links.dart';

final _appLinks = AppLinks();

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final id = state.pathParameters['id'] ?? '';
        return ProductDetailScreen(productId: id);
      },
    ),
  ],
);

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _initDeepLinks();
  }

  Future<void> _initDeepLinks() async {
    // 1. App Launch 시 Cold Start 딥링크 수신
    final initialUri = await _appLinks.getInitialLink();
    if (initialUri != null) {
      router.go(initialUri.path);
    }

    // 2. 앱 실행 중 Background 딥링크 수신 스트림
    _appLinks.uriLinkStream.listen((uri) {
      router.go(uri.path);
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

3. GitHub Actions CI automated Verification & CLI Test

프로덕션 배포 전, 서버의 domain association 파일 정상 작동 여부와 시뮬레이터/에뮬레이터 딥링크 트리거 테스트를 검증하는 GitHub Actions 스크립트이다.

딥링크 자동 검증 CI 워크플로우 (.github/workflows/verify-deeplinks.yml)

name: Verify Deep Links Domain Association

on:
  push:
    branches: [ main, develop ]
  schedule:
    - cron: '0 0 * * 1' # 매주 월요일 자동 점검

jobs:
  verify-domain:
    runs-on: ubuntu-latest
    steps:
      - name: Check Android Assetlinks.json HTTP & SSL
        run: |
          HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://effidev.dev/.well-known/assetlinks.json)
          if [ "$HTTP_STATUS" -ne 200 ]; then
            echo "Error: assetlinks.json HTTP status is $HTTP_STATUS"
            exit 1
          fi
          
          # Google Digital Asset Links API 공식 검증 API 호출
          STATEMENT_CHECK=$(curl -s "https://digitalassetlinks.googleapis.com/v1/statements:check?source.web.site=https://effidev.dev&relation=delegate_permission/common.handle_all_urls&target.androidApp.packageName=dev.effidev.flutterapp")
          echo "$STATEMENT_CHECK"
          
      - name: Check iOS Apple App Site Association (AASA)
        run: |
          AASA_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://effidev.dev/.well-known/apple-app-site-association)
          if [ "$AASA_STATUS" -ne 200 ]; then
            echo "Error: AASA file HTTP status is $AASA_STATUS"
            exit 1
          fi

      - name: Android Emulator DeepLink Launch CLI Test
        run: |
          # adb 명령어로 Android App Links 트리거 시뮬레이션
          # adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://effidev.dev/product/123"
          echo "Android App Links CLI Command Ready."

개발 도중 CLI로 로컬 딥링크 즉시 테스트하기


결론

딥링크 연동 장애는 앱 배포 후 사용자의 결제 유실 및 이탈률 증가로 이어지는 심각한 부작용을 낳는다.

App Links 및 Universal Links 파일 호스팅 상태를 CI 단계에서 자동으로 검증하고, GoRouterapp_links 조합의 견고한 수신 파이프라인을 구축함으로써 프로덕션 환경에서의 딥링크 신뢰성을 100% 확보할 수 있다.