![[Flutter + Firebase로 북마크 앱 만들기 #7] 스마트 URL 런처 구현 (SNS 앱 연동)](https://image.inblog.dev?url=https%3A%2F%2Finblog.ai%2Fapi%2Fog%3Ftitle%3D%255BFlutter%2520%252B%2520Firebase%25EB%25A1%259C%2520%25EB%25B6%2581%25EB%25A7%2588%25ED%2581%25AC%2520%25EC%2595%25B1%2520%25EB%25A7%258C%25EB%2593%25A4%25EA%25B8%25B0%2520%25237%255D%2520%25EC%258A%25A4%25EB%25A7%2588%25ED%258A%25B8%2520URL%2520%25EB%259F%25B0%25EC%25B2%2598%2520%25EA%25B5%25AC%25ED%2598%2584%2520%28SNS%2520%25EC%2595%25B1%2520%25EC%2597%25B0%25EB%258F%2599%29%26logoUrl%3Dhttps%253A%252F%252Finblog.ai%252Finblog_logo.png%26blogTitle%3DGyeongwon%27s%2520blog&w=2048&q=75)
📌 시리즈 목차
- #7 스마트 URL 런처 구현 (SNS 앱 연동) ← 현재 글
🎯 이번 글에서 다룬 내용
문제 상황
북마크 앱에서 링크를 클릭하면 무조건 브라우저로 열렸어요. 하지만:
- YouTube 링크는 YouTube 앱으로 보고 싶고
- Instagram 링크는 Instagram 앱으로 보고 싶어요
- 하지만 때로는 브라우저에서 보고 싶을 때도 있죠!
구현 목표
사용자에게 선택권 주기!
"YouTube 앱으로 열기" vs "브라우저에서 열기"
💡 핵심 아이디어
URL 타입 감지 → 선택지 제공
1. URL 분석
├─ youtube.com? → YouTube 앱/브라우저 선택
├─ instagram.com? → Instagram 앱/브라우저 선택
└─ 일반 웹사이트? → 바로 브라우저 열기
2. SNS 플랫폼 감지
├─ 플랫폼별 아이콘 표시
├─ 플랫폼 컬러로 시각화
└─ 앱 설치 여부 체크
3. 사용자 선택
├─ 앱으로 열기 (LaunchMode.externalApplication)
└─ 브라우저로 열기 (LaunchMode.inAppBrowserView)
🏗️ 1단계: URL 타입 정의
UrlType enum 생성
위치:
lib/core/utils/url_launcher_helper.dart
enum UrlType {
youtube,
instagram,
twitter,
facebook,
tiktok,
general,
}
왜 이렇게?
- 타입 안전성 (String보다 enum이 안전)
- 확장 가능 (나중에 TikTok, LinkedIn 추가 가능)
- 가독성 (코드만 봐도 의도 명확)
🔍 2단계: URL 타입 감지
detectUrlType() 함수
static UrlType detectUrlType(String url) {
try {
final uri = Uri.parse(url);
final host = uri.host.toLowerCase();
if (host.contains('youtube.com') || host.contains('youtu.be')) {
return UrlType.youtube;
} else if (host.contains('instagram.com')) {
return UrlType.instagram;
} else if (host.contains('twitter.com') || host.contains('x.com')) {
return UrlType.twitter;
} else if (host.contains('facebook.com') || host.contains('fb.com')) {
return UrlType.facebook;
} else if (host.contains('tiktok.com')) {
return UrlType.tiktok;
}
return UrlType.general;
} catch (e) {
return UrlType.general;
}
}
처리하는 도메인:
youtube.com
,youtu.be
→ YouTube
instagram.com
→ Instagram
twitter.com
,x.com
→ Twitter/X
facebook.com
,fb.com
→ Facebook
tiktok.com
→ TikTok
🎨 3단계: 플랫폼별 UI 요소
아이콘, 색상, 이름 정의
// 플랫폼 이름
static String getAppName(UrlType type) {
switch (type) {
case UrlType.youtube: return 'YouTube';
case UrlType.instagram: return 'Instagram';
case UrlType.twitter: return 'X';
case UrlType.facebook: return 'Facebook';
case UrlType.tiktok: return 'TikTok';
case UrlType.general: return '브라우저';
}
}
// 플랫폼 아이콘
static IconData getIcon(UrlType type) {
switch (type) {
case UrlType.youtube: return Icons.play_circle_outline;
case UrlType.instagram: return Icons.camera_alt_outlined;
case UrlType.twitter: return Icons.alternate_email;
case UrlType.facebook: return Icons.facebook;
case UrlType.tiktok: return Icons.music_note;
case UrlType.general: return Icons.language;
}
}
// 플랫폼 색상
static Color? getColor(UrlType type) {
switch (type) {
case UrlType.youtube: return Colors.red[700];
case UrlType.instagram: return Colors.purple[400];
case UrlType.twitter: return Colors.blue[400];
case UrlType.facebook: return Colors.blue[800];
case UrlType.tiktok: return Colors.black;
case UrlType.general: return null;
}
}
디자인 시스템:
- YouTube → 빨간색 재생 아이콘
- Instagram → 보라색 카메라 아이콘
- Twitter/X → 파란색 @ 아이콘
- 각 플랫폼의 브랜드 컬러 사용
🚀 4단계: 스마트 URL 열기
openUrl() - 메인 진입점
static Future<void> openUrl(String url, BuildContext context) async {
final urlType = detectUrlType(url);
// 일반 웹사이트면 바로 인앱 브라우저로
if (urlType == UrlType.general) {
await _launchInApp(url, context);
return;
}
// SNS 링크면 선택 다이얼로그
_showLaunchOptions(url, urlType, context);
}
로직:
- URL 타입 감지
- 일반 웹사이트 → 바로 실행 (다이얼로그 X)
- SNS 링크 → 선택 다이얼로그 표시
📱 5단계: 선택 다이얼로그 UI
BottomSheet로 선택지 제공
static void _showLaunchOptions(
String url,
UrlType urlType,
BuildContext context,
) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 헤더
Text('링크 열기', ...),
Text(url, ...), // URL 표시
// 앱으로 열기
ElevatedButton.icon(
icon: Icon(getIcon(urlType)),
label: Text('${getAppName(urlType)} 앱으로 열기'),
onPressed: () {
Navigator.pop(context);
_launchInExternalApp(url, context);
},
),
// 브라우저로 열기
OutlinedButton.icon(
icon: const Icon(Icons.language),
label: const Text('브라우저에서 열기'),
onPressed: () {
Navigator.pop(context);
_launchInApp(url, context);
},
),
// 취소
TextButton(
child: const Text('취소'),
onPressed: () => Navigator.pop(context),
),
],
),
),
),
);
}
🎨 6단계: 북마크 카드에 아이콘 표시
BookmarkCard에 플랫폼 아이콘 추가
@override
Widget build(BuildContext context) {
final urlType = UrlLauncherHelper.detectUrlType(bookmark.url);
return Card(
child: InkWell(
child: Padding(
child: Column(
children: [
Row(
children: [
// SNS 플랫폼 아이콘 (새로 추가!)
if (urlType != UrlType.general) ...[
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: UrlLauncherHelper.getColor(urlType)
?.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
UrlLauncherHelper.getIcon(urlType),
size: 20,
color: UrlLauncherHelper.getColor(urlType),
),
),
const SizedBox(width: 12),
],
// 제목
Expanded(child: Text(bookmark.title)),
// 메뉴 버튼
PopupMenuButton(...),
],
),
// URL (context 전달!)
InkWell(
onTap: () => UrlLauncherHelper.openUrl(
bookmark.url,
context, // ← 중요!
),
child: Text(bookmark.url, ...),
),
],
),
),
),
);
}
변경 사항:
- ✅ URL 타입 감지
- ✅ SNS 플랫폼 아이콘 표시
- ✅
openUrl()
에context
전달 (다이얼로그용)
⚙️ 7단계: Android 설정
AndroidManifest.xml - queries 추가
위치:
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<!-- URL Launcher를 위한 queries -->
<queries>
<!-- 브라우저 -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- SNS 앱들 -->
<package android:name="com.google.android.youtube" />
<package android:name="com.instagram.android" />
<package android:name="com.twitter.android" />
<package android:name="com.facebook.katana" />
<package android:name="com.zhiliaoapp.musically" />
</queries>
<application ...>
...
</application>
</manifest>
왜 필요한가?
Android 11(API 30)부터 패키지 가시성 제한이 생겼어요:
- 앱이 다른 앱과 통신하려면 명시적으로 선언 필요
<queries>
로 "어떤 앱과 통신할지" 선언
- 없으면 YouTube 앱을 찾지 못함!
🎬 사용자 경험 플로우
YouTube 링크 클릭 시
1. 사용자가 YouTube 북마크 카드 터치
↓
2. URL 타입 감지: UrlType.youtube
↓
3. BottomSheet 표시:
┌─────────────────────────┐
│ 링크 열기 │
│ youtube.com/watch... │
│ │
│ [▶ YouTube 앱으로 열기] │
│ [🌐 브라우저에서 열기] │
│ 취소 │
└─────────────────────────┘
↓
4. 사용자 선택
├─ "YouTube 앱으로 열기"
│ → YouTube 앱 실행
│ → 비디오 재생
│
└─ "브라우저에서 열기"
→ 인앱 브라우저 열림
→ YouTube 웹에서 재생
일반 웹사이트 (naver.com) 클릭 시
1. 사용자가 네이버 북마크 카드 터치
↓
2. URL 타입 감지: UrlType.general
↓
3. 바로 인앱 브라우저 실행 (다이얼로그 X)
↓
4. 네이버 웹페이지 표시
🐛 트러블슈팅
문제 1: "component name is null"
에러:
I/UrlLauncher: component name for https://www.youtube.com is null
원인:
<queries>
태그 누락해결:
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
문제 2: YouTube가 브라우저에서도 앱으로 전환됨
원인: Android의 Deep Link 시스템
설명:
- Android가 youtube.com 링크를 감지
- "YouTube 앱이 이 링크를 처리할 수 있어!" 판단
- 자동으로 YouTube 앱 실행
해결 (선택):
// m.youtube.com으로 변경하면 웹에서만 열림
static String convertToWebVersion(String url, UrlType type) {
if (type == UrlType.youtube) {
return url.replaceFirst('youtube.com', 'm.youtube.com');
}
return url;
}
하지만: 대부분 사용자는 앱 전환을 선호하므로 그대로 두는 것도 좋아요!
문제 3: google-services.json에 여러 패키지명
에러:
No matching client found for package name 'com.doythan.bookmarkmanager'
원인: 이전
com.example
패키지가 남아있음해결:
# google-services.json 확인
cat android/app/google-services.json | grep package_name
# Firebase Console에서 구 앱 삭제
# 새 google-services.json 다운로드
📊 Before & After
Before (기존)
모든 링크 → 무조건 브라우저
- YouTube 링크도 브라우저
- Instagram 링크도 브라우저
- 사용자 선택권 없음
After (개선)
URL 타입 감지 → 스마트한 선택
- YouTube → 앱/브라우저 선택
- Instagram → 앱/브라우저 선택
- 일반 웹 → 바로 브라우저
- 플랫폼 아이콘 표시
- 사용자 친화적 UX
🎨 UI 스크린샷
북마크 카드 - YouTube
┌──────────────────────────────────┐
│ [🔴] Flutter 튜토리얼 ⋮ │
│ │
│ youtube.com/watch?v=abc123 │
│ Flutter 기초부터 배우기 │
│ │
│ [개발] 2시간 전 │
└──────────────────────────────────┘
북마크 카드 - Instagram
┌──────────────────────────────────┐
│ [📷] 인스타 릴스 ⋮ │
│ │
│ instagram.com/p/abc123 │
│ 재미있는 릴스 영상 │
│ │
│ [SNS] 1일 전 │
└──────────────────────────────────┘
선택 다이얼로그
┌──────────────────────────────────┐
│ 링크 열기 │
│ youtube.com/watch?v=abc │
│ │
│ [🔴 YouTube 앱으로 열기] │
│ [🌐 브라우저에서 열기] │
│ 취소 │
└──────────────────────────────────┘
💡 코드 구조
파일 구조
lib/
├── core/
│ └── utils/
│ └── url_launcher_helper.dart ← 핵심 로직
└── features/
└── bookmarks/
└── widgets/
└── bookmark_card.dart ← UI 통합
url_launcher_helper.dart 구조
// 1. 타입 정의
enum UrlType { ... }
// 2. URL 분석
detectUrlType()
convertToWebVersion()
// 3. UI 정보
getAppName()
getIcon()
getColor()
// 4. 실행 로직
openUrl() // 메인 진입점
_showLaunchOptions() // 다이얼로그
_launchInApp() // 인앱 브라우저
_launchInExternalApp() // 외부 앱
// 5. 기존 함수 (호환성)
openUrlInNewTab()
openUrlInApp()
isValidUrl()
🧪 테스트
테스트 케이스
// 1. YouTube 링크
https://youtube.com/watch?v=abc123
https://youtu.be/abc123
// 2. Instagram 링크
https://instagram.com/p/abc123
// 3. Twitter 링크
https://twitter.com/user/status/123
https://x.com/user/status/123
// 4. Facebook 링크
https://facebook.com/post/123
// 5. 일반 웹사이트
https://naver.com
https://google.com
테스트 결과
✅ URL 타입 감지 정확도 100%
✅ 플랫폼 아이콘 정상 표시
✅ 다이얼로그 UX 직관적
✅ 앱 실행 성공 (YouTube, Instagram)
✅ 인앱 브라우저 정상 작동
✅ 일반 웹사이트 바로 실행
🚀 성능 최적화
URL 파싱 캐싱 (선택적 개선)
현재는 매번
Uri.parse()
를 호출하는데, 성능이 중요하다면:// Bookmark 모델에 캐싱 추가
class Bookmark {
final String url;
UrlType? _cachedUrlType; // 캐시
UrlType get urlType {
_cachedUrlType ??= UrlLauncherHelper.detectUrlType(url);
return _cachedUrlType!;
}
}
하지만 현재 성능으로도 충분히 빠르므로 불필요!
📈 향후 개선 방향
1. 더 많은 플랫폼 지원
enum UrlType {
youtube, instagram, twitter, facebook, tiktok,
linkedin, // ← 추가
github, // ← 추가
notion, // ← 추가
general,
}
2. 사용자 기본 설정
// SharedPreferences로 저장
enum DefaultLaunchMode {
alwaysAsk, // 항상 물어보기 (현재)
alwaysApp, // 항상 앱으로
alwaysBrowser, // 항상 브라우저로
}
3. 최근 선택 기억
// 최근에 "YouTube 앱으로 열기"를 선택했다면
// 다음에는 자동으로 앱으로 열기
Map<UrlType, LaunchMode> recentChoices;
🎓 배운 점
기술적 학습
Android Deep Link:
- 앱이 특정 URL 패턴 처리 가능
- OS 레벨에서 자동 라우팅
<queries>
태그로 명시적 선언 필요
url_launcher 패키지:
LaunchMode.externalApplication
→ 외부 앱
LaunchMode.inAppBrowserView
→ Chrome Custom Tabs
LaunchMode.inAppWebView
→ WebView (덜 사용)
UX 디자인:
- 사용자에게 선택권 제공
- 플랫폼 브랜드 컬러 활용
- 명확한 라벨링 (솔직한 안내)
🔗 관련 자료
공식 문서
✅ 완료!
이번 글에서 구현한 것
✅ URL 타입 감지 시스템
✅ SNS 플랫폼별 아이콘/컬러
✅ 선택 다이얼로그 UI
✅ 인앱 브라우저 / 외부 앱 선택
✅ Android queries 설정
✅ 직관적인 사용자 경험
Share article