iOS 26 위젯 표면: 하나의 App Intent, 여러 곳에서
수년 동안 위젯은 스냅숏이었습니다. 시스템이 타임라인에 맞춰 뷰를 렌더링하면 사용자는 그것을 들여다보았고, 어디를 탭하든 앱이 열렸습니다. 그 모델은 사라졌습니다. iOS 17부터 위젯은 아무것도 실행하지 않고도 코드를 돌리는 버튼과 토글을 담을 수 있게 되었고, 이때 실행되는 코드가 바로 App Intent입니다1. 제어 센터 컨트롤도, 라이브 액티비티 안의 버튼도 마찬가지입니다. 일단 이 점을 깨닫고 나면, iOS 26의 위젯 표면은 세 개의 별개 API처럼 보이기를 멈추고 하나로 보이기 시작합니다. 시스템이 여러분의 App Intents를 렌더링하는 여러 자리, 각각 차지하는 공간은 다르지만 그 아래의 배관은 동일한 그런 자리들의 집합으로 말입니다.
이 관점의 전환이 이 글의 핵심 전부입니다. App Intent를 한 번 익혀 두면 홈 화면, 잠금 화면, 제어 센터, 동작 버튼, 다이내믹 아일랜드를 한꺼번에 밝힐 수 있고, (똑같은 그 intent로) Siri와 Apple Intelligence까지 켤 수 있습니다. 서로 연결되지 않은 세 가지 기능을 따로 작성하면 네 배의 일을 하고도 더 나쁜 결과를 얻게 됩니다.
요약
- 인터랙티브 위젯 (iOS 17+): 위젯 뷰에
Button(intent:)이나Toggle(isOn:intent:)을 넣으면, 시스템이 앱 밖에서 위젯 익스텐션 안에서 App Intent의perform()을 실행한 뒤 타임라인을 다시 불러옵니다1. - 제어 센터 컨트롤 (iOS 18+):
ControlWidgetButton이나ControlWidgetToggle로 만든ControlWidget은 동일한 App Intents를 기반으로 하며, 제어 센터, 잠금 화면, 동작 버튼에 모습을 드러냅니다2. - 라이브 액티비티 (ActivityKit):
ActivityAttributes와ActivityConfiguration이 잠금 화면과 다이내믹 아일랜드를 구동하고, 서버 주도 업데이트는 푸시로 도착하며, 그 안의 버튼 역시 App Intents입니다3. - iOS 26이 더한 것은 렌더링이지 모델이 아닙니다: 위젯은 Liquid Glass 표현 방식과 강조 렌더링 모드를 받아들이지만, App Intents를 표면으로 삼는 아키텍처는 iOS 18에서 바뀐 것이 없습니다4.
- 모든 것을 하나로 묶는 사실: 위젯 버튼, 컨트롤, Siri 문구, Apple Intelligence 동작이 전부 같은 App Intent일 수 있습니다. 능력은 한 번만 설계하세요.
인터랙티브 위젯: 앱을 열지 않는 버튼
작동 원리는 단순하지만 정확히 짚어 둘 가치가 있습니다. 위젯 뷰에 SwiftUI Button이나 Toggle을 추가하되, 동작 클로저 대신 App Intent를 넘깁니다1:
struct WaterWidgetView: View {
let logged: Int
var body: some View {
VStack {
Text("\(logged) glasses")
Button(intent: LogWaterIntent()) {
Label("Add", systemImage: "plus")
}
}
}
}
사용자가 탭하면 시스템은 위젯 익스텐션 안에서 LogWaterIntent.perform()을 실행합니다. 앱은 실행되지 않습니다. intent는 자신의 작업(공유 저장소에 기록, 카운트 갱신)을 수행하고, 위젯 타임라인이 다시 로드되어 결과를 보여줍니다.
Toggle도 SetValueIntent을 통해 같은 방식으로 작동하며, 이 intent는 사용자가 방금 설정한 새 on/off 값을 받습니다:
struct ToggleReminderIntent: SetValueIntent {
static let title: LocalizedStringResource = "Toggle Reminder"
@Parameter(title: "Enabled")
var value: Bool
func perform() async throws -> some IntentResult {
ReminderStore.shared.enabled = value
return .result()
}
}
위젯은 Toggle(isOn: store.enabled, intent: ToggleReminderIntent())을 바인딩하고, 시스템은 value를 새 상태로 설정한 뒤 perform()을 실행하고 다시 로드합니다. 버튼과 같은 모양에 매개변수 하나가 더 붙었을 뿐입니다1.
여기서 할 수 있는 일을 규정하는 제약이 두 가지 있는데, 둘 다 어기기 쉽습니다. 첫째, perform()은 실행 예산이 빠듯한 제한된 위젯 익스텐션 환경에서 돌아갑니다. 빠른 상태 변경을 위한 것이지, 네트워크 업로드나 무거운 연산을 위한 것이 아닙니다. 둘째, 위젯은 상태를 반영할 뿐 임의의 UI를 애니메이션으로 보여 주지 않습니다. 데이터를 바꾸면 타임라인이 다시 렌더링될 뿐, 직접 만든 전환 효과를 돌리지는 못합니다. 상호작용을 “값을 뒤집고, 새 값을 보여 준다”로 설계하면 잘 작동합니다. 그 이상을 욕심내면 시스템이 맞서 싸웁니다.
제어 센터 컨트롤: 같은 intent, 다른 방
iOS 18은 제어 센터, 잠금 화면, 동작 버튼을 서드파티 컨트롤에 열어 주었고, 그 작동 원리는 위젯과 의도적으로 나란히 맞춰져 있습니다2. 컨트롤은 ControlWidget이고, 그 본문은 App Intent에 연결된 ControlWidgetButton이나 ControlWidgetToggle입니다:
struct LogWaterControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.example.logWater") {
ControlWidgetButton(action: LogWaterIntent()) {
Label("Log Water", systemImage: "drop.fill")
}
}
}
}
LogWaterIntent이 이미 인터랙티브 위젯을 구동하고 있다면, 컨트롤은 거의 공짜로 얻어집니다. 같은 intent에 새 래퍼만 씌우면 되니까요. 바로 이것이 아키텍처가 보상을 돌려주는 지점입니다. 컨트롤은 기능을 두 번째로 구현한 것이 아니라, 이미 작성해 둔 기능을 두 번째로 장착하는 자리입니다. 토글 컨트롤은 위젯 토글과 똑같이 SetValueIntent을 사용하므로, 한 번 만든 “음소거”, “세션 시작”, “다크 모드 전환” 같은 능력이 새로운 로직 없이 제어 센터, 잠금 화면, 동작 버튼에 나타납니다2.
라이브 액티비티: 스스로 갱신되는 표면
라이브 액티비티는 세 번째 자리이자, 움직이는 부품이 가장 많은 자리입니다. ActivityAttributes 타입이 정적·동적 콘텐츠를 정의하고, ActivityConfiguration이 잠금 화면 표현과 다이내믹 아일랜드 영역을 렌더링하며, ActivityKit이 액티비티를 시작하고 갱신하고 종료합니다3. 라이브 액티비티 안의 버튼 역시 App Intents이므로, 다이내믹 아일랜드의 “일시정지”나 “다음” 컨트롤은 위젯 버튼과 같은 종류의 intent를 실행합니다.
라이브 액티비티를 그 자체로 하나의 영역으로 만드는 부분은 업데이트 경로입니다. 로컬에서 직접 갱신하는 타이머는 간단합니다. 하지만 서버에서 일어나는 이벤트(배달 상태 이동, 경기 점수 변화)로 구동되는 액티비티는 푸시로 갱신됩니다. pushTokenUpdates에 등록하고, 백엔드에서 ActivityKit 푸시 페이로드를 보내면, 앱이 전혀 실행되지 않은 상태에서도 시스템이 잠금 화면과 다이내믹 아일랜드를 갱신합니다3. 이것은 정말로 강력하면서도 정말로 잘못되기 쉬운데, 이제 액티비티의 정확성이 로컬 코드만이 아니라 서버 계약, 푸시 신뢰성, 그리고 만료 시각 정책에 달려 있기 때문입니다.
iOS 26이 실제로 바꾼 것
iOS 26 위젯의 표제는 표현 방식이지 아키텍처가 아닙니다. 위젯은 Liquid Glass 소재를 채택하고 강조 렌더링 모드를 갖추어, 시스템의 새 디자인 언어와 어울리고 OS가 콘텐츠 색을 다시 입히는 맥락에서 올바르게 색조가 입혀집니다4. 이것은 홈 화면, 잠금 화면, 대기 화면 전반에서 위젯이 어떻게 보이는지에 영향을 주므로 디자인을 한 번 점검할 가치가 있습니다. 하지만 상호작용이 작동하는 방식은 바꾸지 않습니다. iOS 18에서 인터랙티브 위젯과 컨트롤을 만들어 두었다면, iOS 26으로의 이동은 시각적 새 단장이지 재작성이 아닙니다. iOS 26이 인터랙티브 위젯을 “도입했다”는 주장은 의심하고 봐야 합니다. 상호작용은 iOS 17에서, 컨트롤은 iOS 18에서 출시되었고, iOS 26은 그것들이 시스템의 나머지와 어울려 보이게 만들었을 뿐입니다.
다만 Liquid Glass가 디자인에 미치는 함의는 실재합니다. 위젯은 시스템이 배경화면 위에 합성하고 유리 크롬을 통해 굴절시키는 콘텐츠이므로, 앱에서의 Liquid Glass를 지배하는 것과 같은 규칙이 적용됩니다. 기능 레이어와 콘텐츠 레이어의 구분을 존중하고, 유리가 읽어 낼 수 없는 무거운 커스텀 배경으로 소재와 싸우지 마세요.
아키텍처, 단순하게 말하면
여기 간직할 만한 모델이 있습니다. App Intent는 이름이 붙고, 타입이 정해지고, 설명이 달린 능력입니다. iOS는 그 능력을 장착할 수 있는 자리를 점점 더 많이 내어 줍니다:
- 위젯 안의
Button또는Toggle. - 제어 센터, 잠금 화면, 동작 버튼의
ControlWidget. - 라이브 액티비티와 그 다이내믹 아일랜드 안의 버튼.
- Siri 문구와 단축어 동작.
- Apple Intelligence가 사용자를 대신해 호출할 수 있는 동작5.
이 모든 것은 perform() 메서드를 가진 동일한 AppIntent 타입입니다. 일은 그 능력을 잘 설계하는 데 있습니다. 명확한 이름, 타입이 정해진 매개변수, 빠르고 멱등한 perform(), 그리고 합리적인 결과 말입니다. 이것을 한 번 해 두면 표면들은 래퍼가 됩니다. 이것은 App Intents가 여러분의 앱에 이르는 진짜 API이라는 점, 그리고 이제 iOS 앱이 드러내는 세 가지 표면에 관해 제가 펼쳐 온 것과 같은 논지입니다. 위젯 표면은 여러분이 만들어 내는 기능이 아니라, 능력이 일단 존재하기만 하면 그 능력이 나타나는 자리입니다.
굳이 손대지 않아도 될 때
위젯 표면은 한눈에 볼 만한 상태와 빠른 동작을 진짜로 갖춘 앱에 보상을 줍니다. 트래커, 타이머, 토글, 재생 중 컨트롤 같은 것 말입니다. 모든 것에 보상을 주지는 않습니다.
- 한눈에 볼 만한 상태가 없으면, 위젯도 없습니다. 열지 않고는 보여 줄 만한 것이 앱에 아무것도 없다면, 위젯은 유지보수 비용과 추가 익스텐션 타깃만 떠안기는 장식일 뿐입니다.
- 무겁거나 느린 동작은 여기
perform()에 들어올 자리가 아닙니다. 위젯과 컨트롤의 실행 환경은 제한적입니다. 동작에 실제 시간이나 실제 연산이 필요하다면, 버튼은 익스텐션에서 일을 하는 척하지 말고 준비된 상태로 앱을 열어야 합니다. - 라이브하지 않은 것을 위한 라이브 액티비티. 라이브 액티비티는 시간이 정해져 있고 활발히 변하는 이벤트를 위한 것입니다. 이것을 영구적인 상태 배지로 쓰는 것은 표면을 잘못 읽은 것이며, 액티비티 예산과 관련해 시스템의 블랙리스트에 오르는 지름길입니다.
요령은 세 가지 API를 익히는 데 있지 않습니다. 장착할 가치가 있는 App Intents를 설계하고, 그것을 사용자가 이미 있는 자리에 장착하는 것입니다. 사용자가 들여다보는 홈 화면, 쓸어 올려 여는 제어 센터, 이미 무언가를 보여 주고 있는 다이내믹 아일랜드에 말입니다. 능력을 한 번, 안목 있게 만들어 두면 iOS가 표면들을 공짜로 건네줍니다.
-
Apple Developer, “Adding interactivity to widgets and Live Activities”. 인터랙티브 위젯(iOS 17+)은
AppIntent(토글의 경우SetValueIntent)를 기반으로 한 SwiftUIButton(intent:)과Toggle(isOn:intent:)을 사용합니다. intent의perform()은 앱을 실행하지 않고 위젯 익스텐션 안에서 돌아가며, 타임라인이 다시 로드되어 결과를 반영합니다. ↩↩↩↩ -
Apple Developer, “Creating controls to perform actions across the system” 및
ControlWidget프로토콜. 컨트롤(iOS 18+)은 App Intents에 연결된ControlWidgetButton과ControlWidgetToggle로 만들어지며, 제어 센터, 잠금 화면, 동작 버튼에 모습을 드러냅니다. ↩↩↩ -
Apple Developer, “ActivityKit” 및 “Displaying live data with Live Activities”.
ActivityAttributes와ActivityConfiguration이 잠금 화면과 다이내믹 아일랜드를 정의하고 렌더링하며, ActivityKit이 액티비티를 시작·갱신·종료하고, 서버 주도 업데이트는pushTokenUpdates와 함께 푸시를 사용합니다. ↩↩↩ -
iOS 26 위젯 렌더링: 위젯은 Liquid Glass 소재와 강조 렌더링 모드를 채택하며, 이는
WidgetRenderingMode와\.widgetRenderingMode환경 값으로 제어됩니다. 상호작용 모델(위젯과 컨트롤의 App Intents)은 iOS 17(위젯)과 iOS 18(컨트롤)에서 바뀐 것이 없습니다. Apple, “WWDC 2025: the new software design” 및 WidgetKit 문서. ↩↩ -
Apple Developer, “App Intents”. 동일한
AppIntent타입은 시스템이 Siri, 단축어, Spotlight, 위젯과 컨트롤 표면, 그리고 Apple Intelligence를 통해 드러내는 단위입니다. 교차 표면 모델에 대한 저자의 분석: App Intents는 여러분의 앱에 이르는 Apple의 새로운 API, App Intents 2와 iOS 26의 추가 사항, 그리고 iOS 앱의 세 가지 표면. ↩