Flutter 튜토리얼 19편: 딥 링킹 설정
요약
핵심 요지
문서가 설명하는 범위
- 딥 링킹 기본 개념
- Android App Links 설정
- iOS Universal Links 설정
- go_router로 딥 링크 처리
- 테스트와 검증 방법
읽는 시간: 18분 | 난이도: 중급
참고 자료
- Deep linking - 딥 링킹 개요
- Set up app links - Android 설정
- Set up universal links - iOS 설정
- go_router package - 라우팅 패키지
문제 상황
사용자가 광고, 이메일, 메시지의 링크를 클릭하면 앱의 특정 화면으로 바로 이동해야 합니다. 예를 들어 상품 링크를 클릭하면 앱이 열리고 해당 상품 페이지가 표시되어야 합니다.
딥 링킹 사용 사례
마케팅 캠페인 → 특정 프로모션 페이지이메일 알림 → 주문 상세 페이지SNS 공유 → 콘텐츠 상세 페이지검색 결과 → 앱 내 해당 페이지문제는 다음과 같습니다.
- 플랫폼별로 다른 설정이 필요하다 (Android/iOS).
- 웹 도메인과 앱을 연결해야 한다.
- 앱이 실행 중일 때와 종료 상태일 때 모두 처리해야 한다.
- URL 경로를 앱 화면으로 매핑해야 한다.
해결 방법
Flutter는 Android App Links와 iOS Universal Links를 지원하며, go_router로 쉽게 구현할 수 있습니다.
챕터 1: 딥 링킹 기본 개념
Why
NOTE딥 링킹은 단순히 앱을 여는 것이 아니라, 앱 내부의 특정 위치로 바로 이동합니다.
”딥(deep)“은 앱의 깊은 곳까지 직접 접근한다는 의미입니다.일반 링크: https://example.com/product/123 → 웹 브라우저딥 링크: https://example.com/product/123 → 앱 → 상품 123 화면
What
NOTE딥 링킹에는 두 가지 주요 유형이 있습니다.
유형 설명 예시 URL 스킴 앱 전용 커스텀 스킴 myapp://product/123App Links/Universal Links 도메인 기반 검증된 링크 https://example.com/product/123App Links/Universal Links가 더 안전하고 권장됩니다.
How
TIPFlutter 딥 링킹 흐름
graph TD A[URL 클릭] --> B{앱 설치?} B -->|예| C[앱 열기] B -->|아니오| D[웹페이지 표시] C --> E{앱 상태?} E -->|종료| F[앱 시작 + 라우팅] E -->|실행 중| G[라우팅만] F --> H[특정 화면 표시] G --> H플랫폼별 동작
플랫폼 앱 종료 상태 앱 실행 중 iOS initialRoute + 딜레이 후 라우팅 pushRoute 호출 Android 전체 경로가 initialRoute pushRoute 호출
Watch out
WARNINGURL 스킴(
myapp://)은 다른 앱이 가로챌 수 있어 보안에 취약합니다.
App Links/Universal Links는 도메인 소유권 검증을 통해 이 문제를 해결합니다.// ❌ 보안 취약: 다른 앱도 같은 스킴 등록 가능myapp://product/123// ✅ 보안 강화: 도메인 소유권 검증 필요https://example.com/product/123
결론: App Links(Android)와 Universal Links(iOS)를 사용하면 보안과 사용자 경험이 향상됩니다.
챕터 2: go_router로 라우팅 설정
Why
NOTE딥 링킹을 처리하려면 URL 경로를 앱 화면으로 매핑해야 합니다.
go_router패키지는 선언적 라우팅과 딥 링킹을 쉽게 통합합니다.URL 경로 → go_router → 해당 화면 위젯
What
NOTE
go_router는 Flutter 팀이 추천하는 라우팅 패키지입니다.
URL 기반 네비게이션, 딥 링킹, 리다이렉션 등을 지원합니다.
How
TIPgo_router 설치
dependencies:go_router: ^14.0.0기본 라우터 설정
import 'package:flutter/material.dart';import 'package:go_router/go_router.dart';void main() => runApp(const MyApp());class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp.router(title: '딥링크 데모',routerConfig: _router,);}}final _router = GoRouter(routes: [GoRoute(path: '/',builder: (context, state) => const HomeScreen(),routes: [GoRoute(path: 'product/:id',builder: (context, state) {final productId = state.pathParameters['id']!;return ProductScreen(productId: productId);},),GoRoute(path: 'category/:name',builder: (context, state) {final categoryName = state.pathParameters['name']!;return CategoryScreen(categoryName: categoryName);},),GoRoute(path: 'settings',builder: (context, state) => const SettingsScreen(),),],),],);class HomeScreen extends StatelessWidget {const HomeScreen({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('홈')),body: ListView(children: [ListTile(title: const Text('상품 1'),onTap: () => context.go('/product/1'),),ListTile(title: const Text('전자제품 카테고리'),onTap: () => context.go('/category/electronics'),),ListTile(title: const Text('설정'),onTap: () => context.go('/settings'),),],),);}}class ProductScreen extends StatelessWidget {const ProductScreen({super.key, required this.productId});final String productId;@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('상품 $productId')),body: Center(child: Text('상품 ID: $productId'),),);}}라우트 경로 패턴
패턴 예시 URL 설명 /path/settings정적 경로 /path/:param/product/123경로 파라미터 /path?key=value/search?q=flutter쿼리 파라미터
Watch out
WARNING중첩 라우트에서 전체 경로를 사용해야 합니다.
context.go()는 절대 경로,context.push()는 스택에 추가합니다.// 중첩 라우트 구조GoRoute(path: '/',routes: [GoRoute(path: 'product/:id', ...), // 전체 경로: /product/123],)// ❌ 상대 경로 사용 불가context.go('product/123'); // 에러// ✅ 절대 경로 사용context.go('/product/123'); // OK
결론: go_router로 URL 경로를 화면에 매핑하고, 경로 파라미터로 데이터를 전달합니다.
챕터 3: Android App Links 설정
Why
NOTEAndroid App Links를 설정하면 HTTPS URL이 자동으로 앱을 엽니다.
사용자에게 “앱으로 열기” 선택 대화상자가 표시되지 않습니다.https://example.com/product/123 → 앱이 바로 열림 (대화상자 없음)
What
NOTEApp Links 설정에는 두 가지가 필요합니다.
AndroidManifest.xml에 intent filter 추가- 웹 서버에
assetlinks.json파일 호스팅
How
TIP1. AndroidManifest.xml 수정
android/app/src/main/AndroidManifest.xml파일의<activity>태그 안에 추가:<manifest ...><application ...><activity ...><!-- 기존 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="example.com" /></intent-filter></activity></application></manifest>2. assetlinks.json 생성
웹 서버의
/.well-known/assetlinks.json경로에 파일 생성:[{"relation": ["delegate_permission/common.handle_all_urls"],"target": {"namespace": "android_app","package_name": "com.example.myapp","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"]}}]SHA256 지문 얻기
Terminal window # 디버그 키스토어 (개발용)keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android# 릴리스 키스토어 (배포용)keytool -list -v -keystore your-release-key.keystore -alias your-alias파일 검증
브라우저에서
https://example.com/.well-known/assetlinks.json접근 가능해야 합니다.
Watch out
WARNING
android:autoVerify="true"가 없으면 일반 딥 링크로 작동합니다.
앱 선택 대화상자가 표시될 수 있습니다.<!-- ❌ autoVerify 없음 → 선택 대화상자 표시 가능 --><intent-filter><action android:name="android.intent.action.VIEW" />...</intent-filter><!-- ✅ autoVerify 있음 → 자동으로 앱 열림 --><intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" />...</intent-filter>또한
sha256_cert_fingerprints가 실제 앱 서명과 일치해야 합니다.
결론: AndroidManifest.xml에 intent filter를 추가하고, 웹 서버에 assetlinks.json을 호스팅합니다.
챕터 4: iOS Universal Links 설정
Why
NOTEiOS Universal Links를 설정하면 HTTPS URL이 Safari 대신 앱을 엽니다.
웹사이트와 앱 간의 원활한 전환이 가능합니다.https://example.com/product/123 → 앱이 바로 열림
What
NOTEUniversal Links 설정에는 세 가지가 필요합니다.
- Xcode에서 Associated Domains 설정
Info.plist수정 (Flutter 3.27 이전)- 웹 서버에
apple-app-site-association파일 호스팅
How
TIP1. Xcode에서 Associated Domains 설정
- Xcode에서
ios/Runner.xcworkspace열기- Runner 타겟 선택 → Signing & Capabilities 탭
- ”+ Capability” 클릭 → Associated Domains 추가
applinks:example.com추가2. Info.plist 설정 (Flutter 3.27 이전 버전)
ios/Runner/Info.plist에 추가:<key>FlutterDeepLinkingEnabled</key><true/>3. apple-app-site-association 파일 생성
웹 서버의
/.well-known/apple-app-site-association경로에 파일 생성:{"applinks": {"apps": [],"details": [{"appID": "TEAMID.com.example.myapp","paths": ["/product/*","/category/*","/settings"]}]}}Team ID 찾기
Apple Developer 포털 → Account → Membership → Team ID
앱 ID 형식:
TEAMID.bundleIdentifier예:ABC123XYZ.com.example.myapp파일 검증
- 확장자 없이 저장 (
.json아님)- Content-Type:
application/json- HTTPS로만 접근 가능해야 함
Watch out
WARNING
paths배열이 너무 광범위하면 원하지 않는 URL도 앱으로 열릴 수 있습니다.// ❌ 모든 경로가 앱으로 열림{"paths": ["*"]}// ✅ 필요한 경로만 지정{"paths": ["/product/*","/category/*","/NOT /admin/*" // 제외할 경로]}
/NOT패턴으로 특정 경로를 제외할 수 있습니다.
결론: Xcode에서 Associated Domains를 설정하고, 웹 서버에 apple-app-site-association을 호스팅합니다.
챕터 5: 딥 링크 테스트
Why
NOTE설정이 올바른지 실제 기기에서 테스트해야 합니다.
시뮬레이터/에뮬레이터와 실제 기기에서 동작이 다를 수 있습니다.설정 완료 → 빌드 → 테스트 → 검증
What
NOTE테스트 방법은 플랫폼별로 다릅니다.
ADB 명령어(Android)나 Safari(iOS)를 사용하여 테스트합니다.
How
TIPAndroid 테스트
Terminal window # 앱 먼저 설치flutter run# ADB로 딥 링크 테스트adb shell am start \-a android.intent.action.VIEW \-c android.intent.category.BROWSABLE \-d "https://example.com/product/123" \com.example.myappiOS 테스트
- 실제 기기에 앱 설치
- Safari에서 URL 입력:
https://example.com/product/123- 또는 메모 앱에서 링크 탭
웹 테스트
Terminal window flutter run -d chrome# 브라우저 주소창에 직접 입력http://localhost:port/#/product/123Flutter DevTools로 검증
Terminal window flutter pub global activate devtoolsflutter pub global run devtoolsDevTools → Deep Links 탭에서 설정 검증
Watch out
WARNING앱 링크 검증은 앱이 설치된 후에만 작동합니다.
테스트 전에 반드시 앱을 먼저 설치하세요.Terminal window # ❌ 앱 미설치 상태에서 테스트 → 웹으로 이동adb shell am start -d "https://example.com/product/123"# ✅ 앱 설치 후 테스트flutter run # 먼저 앱 설치adb shell am start -d "https://example.com/product/123" # 앱으로 열림또한 디버그 빌드와 릴리스 빌드의 서명 키가 다르므로 각각 테스트해야 합니다.
결론: 앱 설치 후 플랫폼별 방법으로 딥 링크를 테스트하고, DevTools로 설정을 검증합니다.
챕터 6: 앱 상태별 딥 링크 처리
Why
NOTE앱이 종료된 상태와 실행 중인 상태에서 딥 링크 처리가 다릅니다.
두 가지 경우를 모두 처리해야 완전한 딥 링킹이 구현됩니다.앱 종료 상태: 앱 시작 → 라우팅앱 실행 중: 현재 화면에서 라우팅
What
NOTEgo_router는 두 가지 상태를 자동으로 처리합니다.
추가적인 로직이 필요하면redirect나 리스너를 사용합니다.
How
TIPgo_router의 자동 처리
final _router = GoRouter(routes: [...],// 초기 경로 설정 (앱 시작 시)initialLocation: '/',// 에러 페이지errorBuilder: (context, state) => ErrorScreen(error: state.error),);리다이렉션 추가
final _router = GoRouter(routes: [...],redirect: (context, state) {final isLoggedIn = authService.isLoggedIn;final isLoggingIn = state.matchedLocation == '/login';// 로그인 필요한 페이지 보호if (!isLoggedIn && !isLoggingIn) {return '/login?redirect=${state.matchedLocation}';}// 로그인 후 원래 페이지로if (isLoggedIn && isLoggingIn) {final redirect = state.uri.queryParameters['redirect'];return redirect ?? '/';}return null; // 리다이렉션 없음},);딥 링크 리스너
class MyApp extends StatefulWidget {const MyApp({super.key});@overrideState<MyApp> createState() => _MyAppState();}class _MyAppState extends State<MyApp> {@overridevoid initState() {super.initState();// 앱 실행 중 딥 링크 처리_router.routerDelegate.addListener(_onRouteChanged);}void _onRouteChanged() {final location = _router.routerDelegate.currentConfiguration.fullPath;print('현재 경로: $location');// 분석 이벤트 전송 등}@overridevoid dispose() {_router.routerDelegate.removeListener(_onRouteChanged);super.dispose();}@overrideWidget build(BuildContext context) {return MaterialApp.router(routerConfig: _router);}}딥 링크로 전달된 데이터 처리
GoRoute(path: '/product/:id',builder: (context, state) {final productId = state.pathParameters['id']!;final referrer = state.uri.queryParameters['ref'];// 분석: 어디서 왔는지 추적if (referrer != null) {analytics.logDeepLinkOpen(productId: productId, referrer: referrer);}return ProductScreen(productId: productId);},)
Watch out
WARNING딥 링크로 접근한 화면에서 뒤로가기 동작을 고려해야 합니다.
스택이 비어있으면 앱이 종료될 수 있습니다.// 딥 링크로 직접 접근 시 홈 화면이 스택에 없음// 뒤로가기 → 앱 종료// 해결: ShellRoute로 기본 구조 유지GoRouter(routes: [ShellRoute(builder: (context, state, child) => ScaffoldWithNav(child: child),routes: [GoRoute(path: '/', ...),GoRoute(path: '/product/:id', ...),],),],)// 또는 PopScope로 뒤로가기 제어PopScope(canPop: false,onPopInvoked: (didPop) {if (!didPop) {context.go('/'); // 홈으로 이동}},child: ProductScreen(...),)
결론: go_router가 앱 상태별 딥 링크를 자동 처리하고, redirect로 인증 흐름을 제어합니다.
한계
딥 링킹에는 몇 가지 한계가 있습니다.
- 도메인 소유 필요: App Links/Universal Links는 웹 도메인 소유와 서버 설정이 필요합니다.
- 캐싱 지연: iOS는
apple-app-site-association파일을 캐싱하여 업데이트 반영이 느릴 수 있습니다. - 테스트 어려움: 시뮬레이터에서 완전한 테스트가 어렵고 실제 기기가 필요합니다.
- 플랫폼 제약: 각 플랫폼의 정책과 설정 요구사항이 다릅니다.
Footnotes
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!