App Intents와 Siri Shortcuts: iOS 2026 고급 자동화
iOS 18+를 위한 App Intents와 Siri Shortcuts 완전 가이드입니다. 맞춤형 Siri 액션 구축, Apple Intelligence 통합, 2026년 Swift 앱 자동화를 다룹니다.

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)을 만들 수 있게 합니다.
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<TaskEntity> {
// 서비스를 통해 작업 생성
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는 앱 데이터를 이해하고 조작할 수 있습니다.
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와 단축어 인터페이스에 데이터를 공급합니다.
고정된 값 집합을 가진 타입(우선순위, 상태)에는 AppEnum을, 사용자가 생성하는 동적 타입(작업, 메모, 연락처)에는 AppEntity를 사용합니다.
고정 값을 위한 App Enums 만들기
App Enums는 열거형 타입을 시스템에 노출하여 Siri가 문맥에 맞는 선택지를 제공할 수 있도록 합니다.
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에 나타납니다.
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개의 App Shortcuts를 선언할 수 있습니다. 사용자에게 가장 자주 사용되고 유용한 액션을 우선시해야 합니다.
복잡한 매개변수를 가진 intents
intents는 entity와 구성 옵션을 포함한 복잡한 매개변수를 받을 수 있습니다.
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를 제시하는 방식을 정의하여 자연스러운 피드백을 제공합니다.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
Apple Intelligence 통합
iOS 18은 특정 기능을 위해 설계된 API 모음인 App Intent Domains를 도입합니다. 이 도메인들은 Apple Intelligence가 액션을 더 높은 정확도로 이해하고 실행할 수 있도록 합니다.
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 액션
민감한 액션의 경우 시스템은 실행 전에 확인을 요청할 수 있습니다.
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과 자연스럽게 통합됩니다.
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를 직접 트리거할 수 있습니다.
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은 보일러플레이트를 줄이는 @DeferredProperty와 @ComputedProperty 매크로를 도입합니다. App Intents는 또한 크로스 플랫폼 재사용을 위해 Swift Packages에 위치할 수 있습니다.
Swift Packages에서의 intents
App Intents는 iOS, macOS, watchOS 간 공유를 위해 Swift Packages에서 정의될 수 있습니다.
// 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 코드처럼 단위 테스트할 수 있습니다.
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와 최적의 사용자 경험을 보장합니다.
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 테스트
- ✅ 제목과 설명 현지화
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

WidgetKit iOS 17+: App Intents로 만드는 인터랙티브 위젯
WidgetKit과 App Intents로 인터랙티브 iOS 위젯을 만드는 완전한 가이드입니다. 2026년 iOS 17+를 위한 버튼, 토글, 애니메이션 및 모범 사례.

Swift에서 Combine vs async/await: 점진적 마이그레이션 패턴
Swift에서 Combine에서 async/await로 마이그레이션하는 완전한 가이드: 점진적 전략, 브리징 패턴, iOS 코드베이스의 패러다임 공존.

2026년 iOS 접근성 면접 질문: VoiceOver와 Dynamic Type
iOS 면접 대비를 위한 핵심 접근성 질문: VoiceOver, Dynamic Type, 시맨틱 traits, 접근성 감사.