이 글은 누구를 위한 것인가
- "시리야, 주문 확인해줘"처럼 Siri로 앱 기능을 열고 싶은 팀
- App Shortcuts를 구현해서 Spotlight 검색과 연동하고 싶은 개발자
- NSUserActivity 기반 구현에서 App Intents로 마이그레이션하려는 엔지니어
들어가며
iOS 16에서 App Intents 프레임워크로 App Shortcuts가 크게 개선됐다. 코드로 Siri 명령을 정의하면 사용자가 별도 설정 없이 바로 사용할 수 있다.
이 글은 bluefoxdev.kr의 iOS 시스템 연동 가이드 를 참고하여 작성했습니다.
1. App Shortcuts 아키텍처
[App Intents vs NSUserActivity]
NSUserActivity (구식):
사용자가 Siri에서 직접 바로 가기 추가 필요
매개변수 처리 복잡
Swift UI 통합 제한
App Intents (iOS 16+):
설치 즉시 Siri에서 사용 가능
코드로 모든 정의
SwiftUI PreviewProvider 지원
Spotlight, Shortcuts 앱 자동 연동
[App Shortcut 구성요소]
AppIntent 프로토콜:
perform() → 실제 작업 수행
title, description
매개변수 정의
AppShortcutsProvider:
앱의 모든 Shortcut 목록
Siri 구절(phrase) 정의
@Parameter:
사용자 입력값
EntityQuery로 목록 제공
[Siri 구절 규칙]
"주문 확인해줘 \(applicationName)"
applicationName: 앱 이름 (필수 포함)
여러 구절 지원 가능
다국어: appShortcutsSystemImageName
2. App Shortcuts 구현
import AppIntents
// 1. AppIntent 정의: 주문 확인
struct CheckOrderStatusIntent: AppIntent {
static var title: LocalizedStringResource = "주문 확인"
static var description = IntentDescription("최근 주문 상태를 확인합니다")
// 매개변수: 주문 번호 (선택적)
@Parameter(title: "주문 번호", optionsProvider: OrderOptionsProvider())
var orderId: String?
func perform() async throws -> some IntentResult & ProvidesDialog {
let orders = try await OrderService.shared.fetchRecentOrders()
if let orderId = orderId,
let order = orders.first(where: { $0.id == orderId }) {
return .result(
dialog: IntentDialog("주문 \(orderId)는 \(order.status.description) 상태입니다.")
)
}
guard let latest = orders.first else {
return .result(dialog: IntentDialog("최근 주문이 없습니다."))
}
return .result(
dialog: IntentDialog("최근 주문은 \(latest.status.description) 상태입니다.")
)
}
}
// 주문 옵션 제공자
struct OrderOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
let orders = try await OrderService.shared.fetchRecentOrders()
return orders.map { $0.id }
}
}
// 2. AppIntent: 장바구니 추가
struct AddToCartIntent: AppIntent {
static var title: LocalizedStringResource = "장바구니에 추가"
@Parameter(title: "상품명")
var productName: String
@Parameter(title: "수량", default: 1)
var quantity: Int
func perform() async throws -> some IntentResult & ProvidesDialog {
guard let product = try await ProductService.shared.search(name: productName).first else {
return .result(dialog: IntentDialog("'\(productName)' 상품을 찾을 수 없습니다."))
}
try await CartService.shared.addItem(productId: product.id, quantity: quantity)
return .result(
dialog: IntentDialog("\(productName) \(quantity)개를 장바구니에 추가했습니다.")
)
}
static var parameterSummary: some ParameterSummary {
Summary("장바구니에 \(\.$productName) \(\.$quantity)개 추가")
}
}
// 3. AppShortcutsProvider: Siri 구절 등록
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CheckOrderStatusIntent(),
phrases: [
"주문 확인해줘 \(.applicationName)",
"내 주문 어떻게 됐어 \(.applicationName)",
"배달 언제 와 \(.applicationName)",
],
shortTitle: "주문 확인",
systemImageName: "shippingbox",
)
AppShortcut(
intent: AddToCartIntent(),
phrases: [
"\(.applicationName)에서 \(\.$productName) 장바구니에 담아",
"\(\.$productName) \(.applicationName) 장바구니에 넣어줘",
],
shortTitle: "장바구니 추가",
systemImageName: "cart.badge.plus",
)
}
}
// 4. 앱 업데이트 시 Shortcuts 갱신
@main
struct ShoppingApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
init() {
// AppShortcuts 기부 (자동으로 처리되지만 명시적 호출 가능)
MyAppShortcuts.updateAppShortcutParameters()
}
}
// 5. Spotlight 검색 통합
struct ProductEntity: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "상품"
static var defaultQuery = ProductQuery()
var id: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)", subtitle: "\(price)원")
}
let name: String
let price: Int
}
struct ProductQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [ProductEntity] {
try await ProductService.shared.fetchProducts(ids: identifiers)
.map { ProductEntity(id: $0.id, name: $0.name, price: $0.price) }
}
func suggestedEntities() async throws -> [ProductEntity] {
try await ProductService.shared.fetchPopularProducts()
.prefix(5)
.map { ProductEntity(id: $0.id, name: $0.name, price: $0.price) }
}
}
// 서비스 스텁 (실제 구현 대체)
struct Order { let id: String; var status: OrderStatus }
enum OrderStatus {
case preparing, shipped, delivered
var description: String {
switch self {
case .preparing: return "준비 중"
case .shipped: return "배송 중"
case .delivered: return "배달 완료"
}
}
}
struct Product { let id: String; let name: String; let price: Int }
class OrderService {
static let shared = OrderService()
func fetchRecentOrders() async throws -> [Order] { [] }
}
class CartService {
static let shared = CartService()
func addItem(productId: String, quantity: Int) async throws {}
}
class ProductService {
static let shared = ProductService()
func search(name: String) async throws -> [Product] { [] }
func fetchProducts(ids: [String]) async throws -> [Product] { [] }
func fetchPopularProducts() async throws -> [Product] { [] }
}
마무리
App Shortcuts의 핵심 가치는 "설치 즉시 Siri에서 작동"이다. NSUserActivity와 달리 사용자가 Shortcuts 앱에서 따로 설정할 필요가 없다. Siri 구절에 앱 이름을 반드시 포함해야 하며, 자주 쓰는 3-5개 기능만 Shortcut으로 만드는 것이 좋다. 너무 많으면 Siri가 혼란스러워한다.