이 글은 누구를 위한 것인가
- 카페, 주차장 등 오프라인 매장에 QR/NFC 결제를 붙이려는 팀
- 설치 없이 앱의 핵심 기능을 경험시키고 싶은 마케팅 개발팀
- App Clips와 Instant Apps의 용량 제한과 기능 제약을 이해하려는 개발자
들어가며
App Clips(iOS)와 Instant Apps(Android)는 앱을 설치하지 않고 즉시 실행할 수 있는 경량 앱이다. 카페 QR 스캔 → 결제까지 5초면 완료된다. 앱 설치의 마찰을 없애고 전환율을 높이는 효과적인 수단이다.
이 글은 bluefoxdev.kr의 경량 앱 전략 가이드 를 참고하여 작성했습니다.
1. App Clips vs Instant Apps 비교
[iOS App Clips]
크기 제한: 15MB (다운로드 크기)
실행 방법:
- QR 코드 스캔
- NFC 태그
- Safari 스마트 배너
- 메시지 앱 링크
- Maps 위치 카드
- Apple Watch (watchOS 7+)
특징:
- Apple Pay 사용 가능 (설치 없이 결제)
- Sign in with Apple 지원
- 위치 정보 접근 가능
- 카메라 사용 가능
- 데이터 임시 저장 (최대 1일)
- iCloud 동기화 불가
[Android Instant Apps]
크기 제한: 15MB (각 기능 모듈)
실행 방법:
- URL 클릭 (Try Now 버튼)
- Google Play 스토어
- Google 검색 결과
- Google Assistant
특징:
- 완전한 Android API 접근
- Google Pay 지원
- Play 계정 인증
- 저장소 접근 제한
[적합한 사용 사례]
주차장 결제: QR → App Clip → Apple Pay → 완료
카페 주문: NFC → App Clip → 메뉴 → 결제
이벤트 티켓: URL → Instant App → 티켓 확인
게임 체험: Play 스토어 → 설치 없이 레벨 1 플레이
2. iOS App Clip 구현
// App Clip 타겟 - AppClipApp.swift
import SwiftUI
import AppClip
@main
struct CafeOrderClip: App {
@Environment(\.openURL) private var openURL
var body: some Scene {
WindowGroup {
AppClipContentView()
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
handleURL(activity)
}
}
}
private func handleURL(_ activity: NSUserActivity) {
guard let url = activity.webpageURL else { return }
// URL에서 매장 ID 파싱
// appclip.example.com/cafe?id=12345
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let storeId = components.queryItems?.first(where: { $0.name == "id" })?.value {
NotificationCenter.default.post(
name: .storeIdReceived,
object: storeId
)
}
}
}
// App Clip 메인 뷰
struct AppClipContentView: View {
@StateObject private var viewModel = AppClipViewModel()
@State private var storeId: String?
var body: some View {
NavigationView {
Group {
if let store = viewModel.store {
QuickOrderView(store: store)
} else if viewModel.isLoading {
ProgressView("매장 정보 로딩 중...")
} else {
Text("매장 정보를 불러올 수 없습니다")
}
}
.navigationTitle("빠른 주문")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
// 앱 설치 유도
Button("앱 설치") {
// 앱 스토어 또는 App Clip 배너
viewModel.openFullApp()
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .storeIdReceived)) { notification in
storeId = notification.object as? String
Task { await viewModel.loadStore(id: storeId ?? "") }
}
}
}
// Apple Pay 결제 (App Clip에서 사용 가능)
struct QuickOrderView: View {
let store: Store
@State private var selectedItems: [MenuItem] = []
@StateObject private var paymentManager = PaymentManager()
var totalAmount: Decimal {
selectedItems.reduce(0) { $0 + $1.price }
}
var body: some View {
VStack {
// 메뉴 목록 (경량화)
List(store.popularItems) { item in
MenuItemRow(item: item, isSelected: selectedItems.contains(item)) {
if selectedItems.contains(item) {
selectedItems.removeAll { $0.id == item.id }
} else {
selectedItems.append(item)
}
}
}
// Apple Pay 버튼
if !selectedItems.isEmpty {
ApplePayButton(action: {
paymentManager.requestPayment(
amount: totalAmount,
storeId: store.id,
items: selectedItems
)
})
.frame(height: 50)
.padding()
}
}
}
}
// App Clip 크기 최적화 체크리스트
// 1. 이미지: SF Symbols 최대 활용 (번들 크기 0)
// 2. 네트워크: 메뉴 이미지는 URL 로딩, 번들 미포함
// 3. 코드: 앱과 코드 공유하되 별도 타겟 빌드
// 4. 프레임워크: 꼭 필요한 것만 링크
class AppClipViewModel: ObservableObject {
@Published var store: Store?
@Published var isLoading = false
func loadStore(id: String) async {
await MainActor.run { isLoading = true }
// 경량 API 호출 (필수 데이터만)
let store = try? await APIClient.fetchStoreQuick(id: id)
await MainActor.run {
self.store = store
self.isLoading = false
}
}
func openFullApp() {
// 앱 설치 안 된 경우 App Store, 설치된 경우 앱 열기
let appStoreURL = URL(string: "https://apps.apple.com/app/id123456789")!
UIApplication.shared.open(appStoreURL)
}
}
struct Store { let id: String; let name: String; let popularItems: [MenuItem] }
struct MenuItem: Identifiable { let id: String; let name: String; let price: Decimal }
extension Notification.Name { static let storeIdReceived = Notification.Name("storeIdReceived") }
// MARK: - Xcode 설정
// 1. File > New > Target > App Clip
// 2. Associated Domains: appclips:appclip.example.com
// 3. App Clip Experience 등록 (App Store Connect)
// - URL 패턴 설정
// - 헤더 이미지, 제목, 부제목
// - NFC/QR 태그 연결
// Android - Instant App 모듈 구성
// settings.gradle.kts
// include(":base", ":feature_order", ":instantapp")
// feature_order/build.gradle.kts
plugins {
id("com.android.dynamic-feature")
}
android {
defaultConfig {
// Instant App 지원
}
}
// InstantAppActivity.kt
class QuickOrderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// URL에서 매장 ID 추출
val storeId = intent.data?.getQueryParameter("id")
setContent {
QuickOrderScreen(
storeId = storeId,
onInstallApp = {
// 앱 설치 유도
startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=com.example.app")))
}
)
}
}
}
// AndroidManifest.xml (feature_order)
// <activity android:name=".QuickOrderActivity">
// <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="order.example.com" />
// </intent-filter>
// </activity>
마무리
App Clips는 15MB 제한 내에서 최고의 사용자 경험을 만드는 것이 핵심이다. SF Symbols로 이미지 번들을 최소화하고, 네트워크로 콘텐츠를 로드하며, Apple Pay로 결제 마찰을 없애야 한다. 앱 설치 유도는 경험을 완료한 직후가 가장 효과적이다. "주문 완료! 앱을 설치하면 주문 내역을 확인할 수 있어요"처럼 명확한 가치를 제시해야 전환율이 높아진다.