Flutter 튜토리얼 19편: 딥 링킹 설정

요약#

핵심 요지#

  • 문제 정의: URL을 클릭했을 때 앱의 특정 화면으로 바로 이동하는 기능이 필요하다.
  • 핵심 주장: Flutter의 딥 링킹1으로 URL과 앱 화면을 연결할 수 있다.
  • 주요 근거: Android App Links2와 iOS Universal Links3 설정으로 플랫폼별 딥 링킹을 구현한다.
  • 실무 기준: go_router 패키지와 함께 사용하면 라우팅 관리가 편리하다.
  • 한계: 웹 도메인 소유와 서버 설정이 필요하다.

문서가 설명하는 범위#

  • 딥 링킹 기본 개념
  • Android App Links 설정
  • iOS Universal Links 설정
  • go_router로 딥 링크 처리
  • 테스트와 검증 방법

읽는 시간: 18분 | 난이도: 중급


참고 자료#


문제 상황#

사용자가 광고, 이메일, 메시지의 링크를 클릭하면 앱의 특정 화면으로 바로 이동해야 합니다. 예를 들어 상품 링크를 클릭하면 앱이 열리고 해당 상품 페이지가 표시되어야 합니다.

딥 링킹 사용 사례#

마케팅 캠페인 → 특정 프로모션 페이지
이메일 알림 → 주문 상세 페이지
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/123
App Links/Universal Links도메인 기반 검증된 링크https://example.com/product/123

App Links/Universal Links가 더 안전하고 권장됩니다.

How#

TIP

Flutter 딥 링킹 흐름

graph TD A[URL 클릭] --> B{앱 설치?} B -->|예| C[앱 열기] B -->|아니오| D[웹페이지 표시] C --> E{앱 상태?} E -->|종료| F[앱 시작 + 라우팅] E -->|실행 중| G[라우팅만] F --> H[특정 화면 표시] G --> H

플랫폼별 동작

플랫폼앱 종료 상태앱 실행 중
iOSinitialRoute + 딜레이 후 라우팅pushRoute 호출
Android전체 경로가 initialRoutepushRoute 호출

Watch out#

WARNING

URL 스킴(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#

TIP

go_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});
@override
Widget 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});
@override
Widget 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;
@override
Widget 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 경로를 화면에 매핑하고, 경로 파라미터로 데이터를 전달합니다.


Why#

NOTE

Android App Links를 설정하면 HTTPS URL이 자동으로 앱을 엽니다.
사용자에게 “앱으로 열기” 선택 대화상자가 표시되지 않습니다.

https://example.com/product/123 → 앱이 바로 열림 (대화상자 없음)

What#

NOTE

App Links 설정에는 두 가지가 필요합니다.

  1. AndroidManifest.xml에 intent filter 추가
  2. 웹 서버에 assetlinks.json 파일 호스팅

How#

TIP

1. 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을 호스팅합니다.


Why#

NOTE

iOS Universal Links를 설정하면 HTTPS URL이 Safari 대신 앱을 엽니다.
웹사이트와 앱 간의 원활한 전환이 가능합니다.

https://example.com/product/123 → 앱이 바로 열림

What#

NOTE

Universal Links 설정에는 세 가지가 필요합니다.

  1. Xcode에서 Associated Domains 설정
  2. Info.plist 수정 (Flutter 3.27 이전)
  3. 웹 서버에 apple-app-site-association 파일 호스팅

How#

TIP

1. Xcode에서 Associated Domains 설정

  1. Xcode에서 ios/Runner.xcworkspace 열기
  2. Runner 타겟 선택 → Signing & Capabilities 탭
  3. ”+ Capability” 클릭 → Associated Domains 추가
  4. 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#

TIP

Android 테스트

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

iOS 테스트

  1. 실제 기기에 앱 설치
  2. Safari에서 URL 입력: https://example.com/product/123
  3. 또는 메모 앱에서 링크 탭

웹 테스트

Terminal window
flutter run -d chrome
# 브라우저 주소창에 직접 입력
http://localhost:port/#/product/123

Flutter DevTools로 검증

Terminal window
flutter pub global activate devtools
flutter pub global run devtools

DevTools → 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#

NOTE

go_router는 두 가지 상태를 자동으로 처리합니다.
추가적인 로직이 필요하면 redirect나 리스너를 사용합니다.

How#

TIP

go_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});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
// 앱 실행 중 딥 링크 처리
_router.routerDelegate.addListener(_onRouteChanged);
}
void _onRouteChanged() {
final location = _router.routerDelegate.currentConfiguration.fullPath;
print('현재 경로: $location');
// 분석 이벤트 전송 등
}
@override
void dispose() {
_router.routerDelegate.removeListener(_onRouteChanged);
super.dispose();
}
@override
Widget 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#

  1. Deep Linking(딥 링킹): URL을 통해 앱의 특정 화면으로 직접 이동하는 기능이다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 19편: 딥 링킹 설정
https://moodturnpost.net/posts/flutter/flutter-deep-linking/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차