# App Intents와 Siri Shortcuts: iOS 2026 고급 자동화 > iOS 18+를 위한 App Intents와 Siri Shortcuts 완전 가이드입니다. 맞춤형 Siri 액션 구축, Apple Intelligence 통합, 2026년 Swift 앱 자동화를 다룹니다. - Published: 2026-03-08 - Updated: 2026-04-29 - Author: SharpSkill - Tags: app-intents, siri-shortcuts, ios, swift, apple-intelligence - Reading time: 15 min --- 2026년, Apple Intelligence와 App Intents 프레임워크 덕분에 iOS 앱은 사용자 의도가 그래픽 인터페이스보다 더 중요한 시대로 진입합니다. intent를 노출하지 않는 앱은 AI 중심 OS에서 보이지 않게 됩니다. App Intents는 Siri, Spotlight, 위젯, Action Button이 앱 기능과 상호작용하도록 하는 토대를 형성합니다. > **이 글에서 다루는 내용** > > 이 글은 iOS 18+를 위한 App Intents와 Siri Shortcuts의 생성을 기본 개념부터 Apple Intelligence 및 App Intent Domains와의 통합까지 완전히 소개합니다. ## App Intents 프레임워크 이해하기 iOS 16에서 도입된 App Intents 프레임워크는 기존의 SiriKit Intents 프레임워크를 대체하면서 Swift에서 intent 구성을 현대화합니다. 이 선언적 아키텍처는 시스템에서 발견 가능한 액션(Spotlight, 단축어 앱, Siri, Action Button)을 만들 수 있게 합니다. ```swift // TaskIntent.swift import AppIntents // AppIntent는 사용자가 수행할 수 있는 액션을 나타냅니다 struct CreateTaskIntent: AppIntent { // 단축어와 Siri에 표시되는 제목 static var title: LocalizedStringResource = "작업 만들기" // 접근성과 제안을 위한 설명 static var description = IntentDescription( "애플리케이션에 새 작업을 만듭니다." ) // 자동 검증을 갖춘 매개변수 @Parameter(title: "작업 제목") var taskTitle: String // 기본값을 가진 선택적 매개변수 @Parameter(title: "우선순위", default: .medium) var priority: TaskPriority // 액션 실행 func perform() async throws -> some IntentResult & ReturnsValue { // 서비스를 통해 작업 생성 let task = TaskService.shared.createTask( title: taskTitle, priority: priority ) // 체이닝을 위해 생성된 entity 반환 return .result(value: TaskEntity(task: task)) } } ``` intent는 `@Parameter` 프로퍼티 래퍼로 매개변수를 선언하며, 이를 통해 Siri는 누락된 값을 요청할 수 있습니다. `perform()` 메서드는 비즈니스 로직을 실행하고 타입 결과를 반환합니다. ## 데이터를 위한 App Entities 정의하기 App Entities는 애플리케이션의 "명사"를 나타냅니다: intent가 작동하는 객체입니다. 이를 통해 Siri는 앱 데이터를 이해하고 조작할 수 있습니다. ```swift // TaskEntity.swift import AppIntents // 내부 데이터 모델 struct Task: Identifiable, Codable { let id: UUID var title: String var priority: TaskPriority var isCompleted: Bool var dueDate: Date? } // 시스템에 노출된 entity struct TaskEntity: AppEntity { // 필수 고유 식별자 var id: UUID // 표시 가능한 속성 var title: String var priority: TaskPriority var isCompleted: Bool // 시스템에서의 표시 구성 static var typeDisplayRepresentation: TypeDisplayRepresentation = "작업" // 인스턴스의 시각적 표현 var displayRepresentation: DisplayRepresentation { DisplayRepresentation( title: "\(title)", subtitle: "\(priority.rawValue)", image: .init(systemName: isCompleted ? "checkmark.circle.fill" : "circle") ) } // entity 검색을 위한 기본 쿼리 static var defaultQuery = TaskEntityQuery() // 내부 모델로부터의 이니셜라이저 init(task: Task) { self.id = task.id self.title = task.title self.priority = task.priority self.isCompleted = task.isCompleted } } // entity를 검색하고 필터링하는 쿼리 struct TaskEntityQuery: EntityQuery { // 식별자로 검색 func entities(for identifiers: [UUID]) async throws -> [TaskEntity] { TaskService.shared.fetchTasks() .filter { identifiers.contains($0.id) } .map { TaskEntity(task: $0) } } // 인터페이스에 표시되는 제안 func suggestedEntities() async throws -> [TaskEntity] { TaskService.shared.fetchTasks() .filter { !$0.isCompleted } .prefix(5) .map { TaskEntity(task: $0) } } } ``` `EntityQuery`는 시스템이 entity를 검색하고 제안하는 방식을 정의합니다. `entities(for:)`와 `suggestedEntities()` 메서드는 Siri와 단축어 인터페이스에 데이터를 공급합니다. > **App Enum과 App Entity** > > 고정된 값 집합을 가진 타입(우선순위, 상태)에는 `AppEnum`을, 사용자가 생성하는 동적 타입(작업, 메모, 연락처)에는 `AppEntity`를 사용합니다. ## 고정 값을 위한 App Enums 만들기 App Enums는 열거형 타입을 시스템에 노출하여 Siri가 문맥에 맞는 선택지를 제공할 수 있도록 합니다. ```swift // TaskPriority.swift import AppIntents // 시스템에 노출된 enum enum TaskPriority: String, AppEnum, Codable { case low case medium case high // 표시되는 타입 이름 static var typeDisplayRepresentation: TypeDisplayRepresentation = "우선순위" // 각 케이스의 표현 static var caseDisplayRepresentations: [TaskPriority: DisplayRepresentation] = [ .low: DisplayRepresentation( title: "낮음", image: .init(systemName: "arrow.down.circle") ), .medium: DisplayRepresentation( title: "중간", image: .init(systemName: "minus.circle") ), .high: DisplayRepresentation( title: "높음", image: .init(systemName: "exclamationmark.circle") ) ] } // 작업 상태를 위한 enum enum TaskStatus: String, AppEnum, Codable { case pending case inProgress case completed static var typeDisplayRepresentation: TypeDisplayRepresentation = "상태" static var caseDisplayRepresentations: [TaskStatus: DisplayRepresentation] = [ .pending: "대기 중", .inProgress: "진행 중", .completed: "완료됨" ] } ``` 시각적 표현(SF Symbols 아이콘)은 단축어와 Siri 제안에서의 표시를 풍부하게 합니다. ## AppShortcutsProvider 구현하기 `AppShortcutsProvider`는 App Shortcuts를 시스템에 노출하여 사용자 설정 없이 즉시 사용 가능하게 합니다. 이 단축어들은 Spotlight, Siri, Action Button에 나타납니다. ```swift // ShortcutsProvider.swift import AppIntents // 앱의 모든 단축어를 선언하는 프로바이더 struct TaskAppShortcutsProvider: AppShortcutsProvider { // 애플리케이션당 최대 10개의 단축어 @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { // 작업을 만드는 단축어 AppShortcut( intent: CreateTaskIntent(), phrases: [ // .applicationName 플레이스홀더는 필수입니다 "\(.applicationName)에서 작업 만들기", "\(.applicationName)에 새 작업", "\(.applicationName)에 작업 추가" ], shortTitle: "작업 만들기", systemImageName: "plus.circle" ) // 작업을 나열하는 단축어 AppShortcut( intent: ListTasksIntent(), phrases: [ "\(.applicationName)에서 내 작업 보기", "\(.applicationName)의 작업 표시", "\(.applicationName)에 내 작업이 뭐예요" ], shortTitle: "내 작업", systemImageName: "list.bullet" ) // 동적 매개변수가 있는 단축어 AppShortcut( intent: CompleteTaskIntent(), phrases: [ "\(.applicationName)에서 \(\.$taskName) 완료하기", "\(.applicationName)에서 \(\.$taskName)을(를) 완료로 표시" ], shortTitle: "작업 완료", systemImageName: "checkmark.circle" ) } } ``` 음성 문구에는 Siri가 대상 앱을 식별하도록 `\(.applicationName)` 플레이스홀더가 포함되어야 합니다. `\(\.$taskName)` 같은 동적 매개변수는 문맥적인 명령을 가능하게 합니다. > **단축어 10개 제한** > > 한 애플리케이션은 최대 10개의 App Shortcuts를 선언할 수 있습니다. 사용자에게 가장 자주 사용되고 유용한 액션을 우선시해야 합니다. ## 복잡한 매개변수를 가진 intents intents는 entity와 구성 옵션을 포함한 복잡한 매개변수를 받을 수 있습니다. ```swift // CompleteTaskIntent.swift import AppIntents struct CompleteTaskIntent: AppIntent { static var title: LocalizedStringResource = "작업 완료" static var description = IntentDescription( "작업을 완료된 것으로 표시합니다." ) // 자동 검색이 있는 entity 매개변수 @Parameter(title: "작업") var task: TaskEntity // 날짜를 가진 선택적 매개변수 @Parameter(title: "완료 날짜") var completionDate: Date? // Siri 대화 구성 static var parameterSummary: some ParameterSummary { Summary("\(\.$task) 완료") { \.$completionDate } } func perform() async throws -> some IntentResult & ProvidesDialog { // 작업 업데이트 TaskService.shared.completeTask( id: task.id, completionDate: completionDate ?? Date() ) // Siri를 위한 음성 피드백 return .result( dialog: "\(task.title) 작업이 완료로 표시되었습니다." ) } } // entity 목록을 반환하는 intent struct ListTasksIntent: AppIntent { static var title: LocalizedStringResource = "작업 나열" // 선택적 필터 @Parameter(title: "상태", default: nil) var statusFilter: TaskStatus? @Parameter(title: "우선순위", default: nil) var priorityFilter: TaskPriority? func perform() async throws -> some IntentResult & ReturnsValue<[TaskEntity]> { var tasks = TaskService.shared.fetchTasks() // 필터 적용 if let status = statusFilter { tasks = tasks.filter { switch status { case .completed: return $0.isCompleted case .pending, .inProgress: return !$0.isCompleted } } } if let priority = priorityFilter { tasks = tasks.filter { $0.priority == priority } } let entities = tasks.map { TaskEntity(task: $0) } return .result(value: entities) } } ``` `ParameterSummary`는 Siri가 음성 실행 중 intent를 제시하는 방식을 정의하여 자연스러운 피드백을 제공합니다. ## Apple Intelligence 통합 iOS 18은 특정 기능을 위해 설계된 API 모음인 App Intent Domains를 도입합니다. 이 도메인들은 Apple Intelligence가 액션을 더 높은 정확도로 이해하고 실행할 수 있도록 합니다. ```swift // BookmarkDomainIntents.swift import AppIntents // Apple Intelligence 통합을 위해 Bookmarks 도메인을 준수 struct SaveBookmarkIntent: AppIntent { static var title: LocalizedStringResource = "북마크 저장" // 자동 검증되는 URL 매개변수 @Parameter(title: "URL") var url: URL @Parameter(title: "제목", default: nil) var title: String? @Parameter(title: "폴더", default: nil) var folder: BookmarkFolderEntity? // 필요 시 앱 열기 static var openAppWhenRun: Bool = false func perform() async throws -> some IntentResult & ProvidesDialog { let bookmark = BookmarkService.shared.save( url: url, title: title, folder: folder?.id ) return .result( dialog: "북마크 저장됨: \(bookmark.title)" ) } } // 화면 콘텐츠 인식을 가진 intent (iOS 18.4+) struct AnalyzeScreenContentIntent: AppIntent { static var title: LocalizedStringResource = "콘텐츠 분석" // Apple Intelligence를 통한 화면 컨텍스트 접근 @Parameter(title: "컨텍스트") var screenContext: String? func perform() async throws -> some IntentResult & ProvidesDialog { guard let context = screenContext else { return .result(dialog: "분석할 콘텐츠가 없습니다.") } // Apple Intelligence가 추출한 콘텐츠 처리 let analysis = ContentAnalyzer.analyze(context) return .result(dialog: analysis.summary) } } ``` 사전 정의된 도메인(Books, Camera, Spreadsheets)은 이러한 특정 작업에 맞게 학습된 모델 덕분에 Siri가 요청에 정확하게 응답하도록 합니다. ## 사용자 확인이 필요한 Siri 액션 민감한 액션의 경우 시스템은 실행 전에 확인을 요청할 수 있습니다. ```swift // DeleteTaskIntent.swift import AppIntents struct DeleteTaskIntent: AppIntent { static var title: LocalizedStringResource = "작업 삭제" @Parameter(title: "작업") var task: TaskEntity // 사용자 확인 필요 static var isDiscoverable: Bool = true func perform() async throws -> some IntentResult & ProvidesDialog { // 대화를 통해 확인 요청 try await requestConfirmation( result: .result( dialog: "\(task.title)을(를) 정말 삭제하시겠습니까?" ) ) // 확인 후 삭제 TaskService.shared.deleteTask(id: task.id) return .result( dialog: "\(task.title) 작업이 삭제되었습니다." ) } } // 여러 대화 단계를 가진 intent struct ScheduleTaskIntent: AppIntent { static var title: LocalizedStringResource = "작업 예약" @Parameter(title: "작업") var task: TaskEntity @Parameter(title: "날짜") var scheduledDate: Date @Parameter(title: "리마인더", default: true) var setReminder: Bool static var parameterSummary: some ParameterSummary { When(\.$setReminder, .equalTo, true) { Summary("\(\.$task)을(를) \(\.$scheduledDate)에 리마인더와 함께 예약") } otherwise: { Summary("\(\.$task)을(를) \(\.$scheduledDate)에 예약") } } func perform() async throws -> some IntentResult & ProvidesDialog { TaskService.shared.schedule( taskId: task.id, date: scheduledDate, reminder: setReminder ) let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .short let formattedDate = dateFormatter.string(from: scheduledDate) return .result( dialog: "작업이 \(formattedDate)에 예약되었습니다." ) } } ``` `requestConfirmation` 메서드는 사용자가 검증할 때까지 실행을 일시 정지하여 우발적인 액션으로부터 보호합니다. ## 인터랙티브 위젯에서의 intents App Intents는 iOS 17+의 인터랙티브 위젯을 만들기 위해 WidgetKit과 자연스럽게 통합됩니다. ```swift // TaskWidgetIntents.swift import AppIntents import WidgetKit // 위젯에 최적화된 intent (빠른 실행) struct ToggleTaskFromWidgetIntent: AppIntent { static var title: LocalizedStringResource = "작업 전환" @Parameter(title: "작업 ID") var taskID: String init() {} init(taskID: UUID) { self.taskID = taskID.uuidString } // 위젯에는 대화 없음 func perform() async throws -> some IntentResult { guard let uuid = UUID(uuidString: taskID) else { return .result() } TaskService.shared.toggleCompletion(taskId: uuid) // 위젯 즉시 새로고침 WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result() } } // 인터랙티브 버튼이 있는 위젯 뷰 import SwiftUI struct TaskWidgetView: View { let task: Task var body: some View { Button(intent: ToggleTaskFromWidgetIntent(taskID: task.id)) { HStack { Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .foregroundStyle(task.isCompleted ? .green : .secondary) Text(task.title) .strikethrough(task.isCompleted) } .padding() } .buttonStyle(.plain) } } ``` 위젯은 `Button(intent:)` 구문을 사용하여 앱을 열지 않고도 인터랙션을 App Intent에 직접 연결합니다. ## Action Button 구성 iPhone 15 Pro 이상의 Action Button은 App Shortcuts를 직접 트리거할 수 있습니다. ```swift // ActionButtonIntent.swift import AppIntents // Action Button에 최적화된 intent struct QuickCaptureIntent: AppIntent { static var title: LocalizedStringResource = "빠른 캡처" static var description = IntentDescription( "제목이 있는 작업을 빠르게 만듭니다." ) // 입력을 위해 앱 열기 static var openAppWhenRun: Bool = true func perform() async throws -> some IntentResult & OpensIntent { // 빠른 캡처 화면을 열기 위한 알림 NotificationCenter.default.post( name: .quickCaptureTriggered, object: nil ) return .result(opensIntent: ShowQuickCaptureViewIntent()) } } // Action Button을 위해 AppShortcutsProvider에서 선언 extension TaskAppShortcutsProvider { @AppShortcutsBuilder static var appShortcuts: [AppShortcut] { // ... 다른 단축어 AppShortcut( intent: QuickCaptureIntent(), phrases: [ "\(.applicationName) 빠른 캡처", "\(.applicationName) 빠른 메모" ], shortTitle: "캡처", systemImageName: "bolt.circle" ) } } ``` 사용자는 설정 > 액션 버튼에서 이 단축어를 트리거하도록 Action Button을 구성할 수 있습니다. > **iOS 18+ 매크로** > > iOS 18은 보일러플레이트를 줄이는 `@DeferredProperty`와 `@ComputedProperty` 매크로를 도입합니다. App Intents는 또한 크로스 플랫폼 재사용을 위해 Swift Packages에 위치할 수 있습니다. ## Swift Packages에서의 intents App Intents는 iOS, macOS, watchOS 간 공유를 위해 Swift Packages에서 정의될 수 있습니다. ```swift // Package.swift // swift-tools-version: 5.9 import PackageDescription let package = Package( name: "TaskIntents", platforms: [ .iOS(.v17), .macOS(.v14), .watchOS(.v10) ], products: [ .library(name: "TaskIntents", targets: ["TaskIntents"]) ], dependencies: [], targets: [ .target( name: "TaskIntents", dependencies: [] ) ] ) // Sources/TaskIntents/SharedIntents.swift import AppIntents // 크로스 플랫폼 공유 intent public struct SharedCreateTaskIntent: AppIntent { public static var title: LocalizedStringResource = "작업 만들기" @Parameter(title: "제목") public var taskTitle: String public init() {} public func perform() async throws -> some IntentResult { // 공유 로직 await TaskRepository.shared.create(title: taskTitle) return .result() } } // 앱 내 플랫폼별 확장 #if os(iOS) extension SharedCreateTaskIntent { // iOS 전용 동작 static var openAppWhenRun: Bool = false } #endif ``` 이 아키텍처는 intents를 위한 단일 코드베이스를 유지하면서 플랫폼별로 동작을 조정합니다. ## App Intents 테스트 App Intents는 다른 Swift 코드처럼 단위 테스트할 수 있습니다. ```swift // TaskIntentTests.swift import XCTest import AppIntents @testable import TaskApp final class TaskIntentTests: XCTestCase { override func setUp() { super.setUp() // 격리된 테스트를 위해 서비스 초기화 TaskService.shared.reset() } func testCreateTaskIntent() async throws { // Given var intent = CreateTaskIntent() intent.taskTitle = "테스트 작업" intent.priority = .high // When let result = try await intent.perform() // Then let tasks = TaskService.shared.fetchTasks() XCTAssertEqual(tasks.count, 1) XCTAssertEqual(tasks.first?.title, "테스트 작업") XCTAssertEqual(tasks.first?.priority, .high) } func testCompleteTaskIntent() async throws { // Given let task = TaskService.shared.createTask( title: "완료할 작업", priority: .medium ) var intent = CompleteTaskIntent() intent.task = TaskEntity(task: task) // When _ = try await intent.perform() // Then let updatedTask = TaskService.shared.fetchTask(id: task.id) XCTAssertTrue(updatedTask?.isCompleted ?? false) } func testEntityQuery() async throws { // Given let task1 = TaskService.shared.createTask(title: "작업 1", priority: .low) let task2 = TaskService.shared.createTask(title: "작업 2", priority: .high) let query = TaskEntityQuery() // When let entities = try await query.entities(for: [task1.id, task2.id]) // Then XCTAssertEqual(entities.count, 2) } } ``` 테스트는 시스템 인터페이스와 독립적으로 intents의 동작을 검증합니다. ## 모범 사례와 최적화 여러 패턴이 성능 좋은 App Intents와 최적의 사용자 경험을 보장합니다. ```swift // BestPractices.swift import AppIntents // 1. 현지화된 문구 struct LocalizedTaskIntent: AppIntent { static var title: LocalizedStringResource = "task.create.title" static var description = IntentDescription( "task.create.description", categoryName: "task.category" ) @Parameter(title: "task.parameter.title") var taskTitle: String func perform() async throws -> some IntentResult { // ... return .result() } } // 2. 적절한 오류 처리 enum TaskIntentError: Error, CustomLocalizedStringResourceConvertible { case taskNotFound case invalidInput case serviceUnavailable var localizedStringResource: LocalizedStringResource { switch self { case .taskNotFound: return "작업을 찾을 수 없습니다." case .invalidInput: return "유효하지 않은 데이터입니다." case .serviceUnavailable: return "서비스를 일시적으로 사용할 수 없습니다." } } } struct RobustTaskIntent: AppIntent { static var title: LocalizedStringResource = "견고한 intent" @Parameter(title: "ID") var taskId: String func perform() async throws -> some IntentResult & ProvidesDialog { guard let uuid = UUID(uuidString: taskId) else { throw TaskIntentError.invalidInput } guard let task = TaskService.shared.fetchTask(id: uuid) else { throw TaskIntentError.taskNotFound } return .result(dialog: "작업을 찾았습니다: \(task.title)") } } // 3. EntityQuery 최적화 struct OptimizedTaskQuery: EntityStringQuery { // 최적화된 텍스트 검색 func entities(matching string: String) async throws -> [TaskEntity] { // 제한이 있는 서비스 측 검색 TaskService.shared.search(query: string, limit: 10) .map { TaskEntity(task: $0) } } // 성능을 위한 제한된 제안 func suggestedEntities() async throws -> [TaskEntity] { TaskService.shared.fetchRecentTasks(limit: 5) .map { TaskEntity(task: $0) } } } // 4. 집중 모드를 위한 Focus Filter struct TaskFocusFilter: SetFocusFilterIntent { static var title: LocalizedStringResource = "작업 필터" @Parameter(title: "높은 우선순위만 표시") var showHighPriorityOnly: Bool func perform() async throws -> some IntentResult { TaskService.shared.setFocusFilter(highPriorityOnly: showHighPriorityOnly) return .result() } } ``` 이 패턴들은 시스템과의 매끄러운 통합을 보장하면서 최적의 성능을 유지합니다. ## 결론 App Intents와 Siri Shortcuts는 사용자가 iOS 앱과 상호작용하는 방식을 변화시킵니다. 2026년, Apple Intelligence와 함께 intents를 노출하는 것은 더 이상 선택이 아니라 현대적이고 직관적인 경험을 제공하기 위해 필수적입니다. ### App Intents iOS 18+ 체크리스트 - ✅ 앱의 주요 액션을 위한 AppIntents 만들기 - ✅ 조작 가능한 데이터를 위한 AppEntities 정의 - ✅ 열거형 타입에 AppEnum 사용 - ✅ 음성 문구가 포함된 AppShortcutsProvider 구현 - ✅ 최대 10개의 App Shortcuts 제한 준수 - ✅ 모든 문구에 `\(.applicationName)` 포함 - ✅ Siri 피드백을 위한 ParameterSummary 구성 - ✅ 해당하는 경우 Apple Intelligence 도메인 통합 - ✅ 단위 테스트로 intents 테스트 - ✅ 제목과 설명 현지화 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/ios/app-intents-siri-shortcuts-advanced-ios-automation