[Flutter + Firebase로 북마크 앱 만들기 #7] 스마트 URL 런처 구현 (SNS 앱 연동)

도경원's avatar
Oct 13, 2025
[Flutter + Firebase로 북마크 앱 만들기 #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); }
로직:
  1. URL 타입 감지
  1. 일반 웹사이트 → 바로 실행 (다이얼로그 X)
  1. 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

Gyeongwon's blog