윈도우를 넘어선 visionOS 공간 패턴
visionOS에 출시되는 대부분의 앱은 Apple의 “Designed for iPad” 호환성 경로를 통해 플랫폼에 접근합니다. 기존 iPad 바이너리가 3D 공간에 떠 있는 평면 패널로 실행되고, 개발자는 visionOS 네이티브 경험을 구축하는 대신 체크박스 하나만 선택합니다. 이 경로는 사용자 입장에서는 괜찮습니다(앱이 동작하니까요). 하지만 플랫폼의 가치를 충분히 살리지 못합니다. visionOS의 네이티브 표면은 개발자에게 세 가지 표현 방식(Windows, Volumes, Immersive Spaces)과 구조적 UI 프리미티브(Ornaments, Attachments)를 제공합니다. iPad SDK는 이러한 요소를 갖추고 있지 않습니다. 이를 채택한 앱은 네이티브하게 느껴지고, 그렇지 않은 앱은 Vision 위에 올라간 iPad처럼 읽힙니다.
이 글은 Apple의 공식 문서를 기준으로 공간 어휘를 짚어봅니다. 프레임은 visionOS 입문서가 아니라 “플랫폼이 SwiftUI 앱에 실제로 무엇을 제공하는가”입니다. 클러스터의 RealityKit and the Spatial Mental Model 글은 3D 콘텐츠 레이어를 다루며, 이 글은 그것을 담는 SwiftUI 표면을 다룹니다.
TL;DR
- visionOS 앱은 세 가지 scene 타입을 조합합니다.
WindowGroup(Windows),.windowStyle(.volumetric)을 적용한WindowGroup(Volumes), 그리고ImmersiveSpace(Immersive Spaces)입니다1. - Window는 2D 평면, Volume은 3D 경계 영역, Immersive Space는 사용자를 둘러싸는 공간입니다. 각각 규칙이 다릅니다. Volume은 생성 후 크기가 변경 불가능하고, Immersive Space는 명시적인 open/dismiss가 필요하며, Window는 iPad와 가장 비슷하게 동작합니다.
- 몰입(Immersion)은 세 가지 스타일로 제공됩니다.
.mixed(콘텐츠가 방과 공존),.full(방이 가상 환경으로 대체됨),.progressive(주변부 인지를 유지하는 중간 단계)입니다2. - Ornament는 Window와 평행하고 z축 방향으로 앞쪽에 위치한 UI 평면입니다. visionOS에서 toolbar와 tab bar를 구현하는 방식입니다3. Attachment는 RealityView 내부의 3D 콘텐츠 안에 SwiftUI 뷰를 임베드하는 방식이며, 평면 UI와 공간 기하 구조 사이의 다리 역할을 합니다.
- “패널 앱(panel app)” 안티패턴: iPad UI를 Window 하나로 출시하면서 Volume, Space, Ornament를 채택하지 않는 경우입니다. 사용자는 앱을 사용할 수 있지만, 플랫폼의 진짜 가치는 활용되지 않은 채 남습니다.
세 가지 Scene 타입
visionOS 앱의 App body는 세 가지 클래스의 scene을 조합합니다. 각각은 사용자에게 분명히 구분되는 멘탈 모델을 가집니다.
Windows: 2D 평면
WindowGroup은 기본적으로 visionOS 글래스 프레임이 적용된 2D Window를 생성합니다. Window는 공간에 배치되고(시스템이 사용자가 보고 있는 위치 앞에 배치합니다) 표준 시스템 제스처를 통해 사용자가 이동하거나 크기를 변경합니다. SwiftUI 관점에서 Window는 macOS 윈도우의 visionOS 대응물입니다. 깊이를 인지하는 글래스 머티리얼이 적용된 평면 콘텐츠 표면입니다.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
기본 Window는 콘텐츠 주변에 글래스 머티리얼을 가집니다. 완전히 투명한 표면을 원하는 앱은 .windowStyle(.plain)을 사용합니다.
WindowGroup {
ContentView()
}
.windowStyle(.plain)
Plain 스타일 Window는 시스템 글래스 프레임이 사라집니다. 콘텐츠 자체가 시각적 컨테이너를 제공할 때 사용하고, 그 외에는 기본값이 옳습니다.
Volumes: 3D 경계 영역
Volume은 깊이를 인지하는 콘텐츠를 담는 3D 영역입니다(모델, 여러 객체로 구성된 scene, 세 번째 축의 이점을 누리는 UI 등). volume scene 역시 WindowGroup이며, 다른 스타일을 사용합니다.
WindowGroup(id: "globe") {
GlobeView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)
.defaultSize(width:height:depth:in:) 모디파이어는 실제 단위(미터) 기준으로 volume의 경계를 지정합니다. 기본적으로 경계는 열린 시점에 고정되며, 사용자는 volume을 이동할 수는 있지만 크기를 변경할 수는 없습니다. visionOS 2 이상에서는 사용자가 크기를 조절할 수 있는 volume을 원하는 앱을 위해 .windowResizability(.contentSize) 및 관련 API를 통한 옵트인 경로가 추가되었습니다. 그러나 고정 크기 기본값이 여전히 가장 일반적인 경우입니다. 시사점은 명확합니다. 대부분의 volume은 개발자가 명시적으로 옵트인하지 않는 한 크기를 변경할 수 없으므로, 기본 크기를 신중하게 선택해야 합니다.
Volume에 적합한 후보는 공간 경계가 경험의 일부인 앱입니다. 사용자가 주위를 걸어다니는 가상 조각, 실제 벽에 고정된 줄자, 깊이별로 배치된 타깃이 있는 운동 scene 등입니다. 단순히 더 넓은 캔버스를 원하는 앱은 Volume에서 얻는 것이 없습니다. 더 큰 Window가 정답입니다.
Immersive Spaces: 둘러싸는 공간
ImmersiveSpace는 사용자의 주변 환경을 차지하는 scene입니다. Window나 Volume(둘 다 Shared Space에서 다른 앱과 함께 보임)과 달리, Immersive Space는 사용자의 주변을 장악하고 다른 앱 윈도우의 동시 사용을 차단합니다.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "training") {
TrainingScene()
}
.immersionStyle(selection: .constant(.mixed), in: .mixed, .progressive, .full)
}
}
.immersionStyle(...) 모디파이어는 경험의 단계를 선택합니다.
.mixed. 가상 콘텐츠가 실제 방과 함께 나타납니다. 사용자가 두 가지 맥락 모두에서 이점을 얻는 앱에 사용합니다..progressive. Digital Crown으로 강도를 조절하는 부분 몰입입니다. 중심 시야는 가상이지만 사용자는 방에 대한 주변 인지를 유지합니다..full. 방이 가상 환경으로 대체됩니다. 완전 몰입 경험(명상, 트레이닝 시뮬레이션, 게임)에 사용합니다.
Immersive Space를 여는 것은 명시적입니다. 앱은 space의 id와 함께 @Environment(\.openImmersiveSpace)를 호출하고, 시스템이 전환 애니메이션과 충돌하는 space의 해제를 처리합니다.
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
Button("Start Session") {
Task {
await openImmersiveSpace(id: "training")
}
}
앱당 한 번에 하나의 Immersive Space만 활성화될 수 있습니다. Space 간 전환(예: .mixed에서 .full로)은 기존 Space의 명시적 해제와 새 Space의 열기를 요구합니다.
Ornaments: Window 주변의 UI 평면
Ornament는 Window 가장자리에 부착되어 z축 상에서 Window 평면보다 약간 앞에 위치하는 SwiftUI 뷰입니다. visionOS가 toolbar, tab bar, 보조 컨트롤을 구현하는 방식입니다. 시스템은 ornament를 광범위하게 사용합니다. TV의 재생 컨트롤, Music의 segmented control, Mail의 toolbar 등이 그 예입니다.
ContentView()
.ornament(
attachmentAnchor: .scene(.bottom),
contentAlignment: .center
) {
HStack {
Button("Previous", systemImage: "backward.fill") { ... }
Button("Play", systemImage: "play.fill") { ... }
Button("Next", systemImage: "forward.fill") { ... }
}
.padding()
.glassBackgroundEffect()
}
attachmentAnchor: 매개변수는 Window를 기준으로 ornament가 위치할 곳을 지정합니다. .scene(.top), .scene(.bottom), .scene(.leading), .scene(.trailing)이 가능합니다. Ornament의 시각적 처리는 개발자의 책임이며, .glassBackgroundEffect()는 Window 프레임과 어울리는 visionOS 네이티브 글래스 머티리얼을 생성합니다.
Ornament는 visionOS의 실제 문제를 해결합니다. 컨트롤을 Window 안에 넣으면 콘텐츠가 답답해지고, 별도 Window에 두면 사용자가 시선을 다시 맞춰야 합니다. Ornament는 사용자의 주변 시야에 떠 있어 시선으로 타깃팅할 수 있으면서도, 중앙 시야의 메인 콘텐츠와 경쟁하지 않습니다.
RealityView Attachments: 3D 공간 안의 SwiftUI
3D scene 안에 SwiftUI 뷰가 필요할 때(3D 모델 위의 라벨, 가상 객체 옆에 떠 있는 버튼, 실제 표면에 고정된 측정값 표시 등), 다리 역할을 하는 것이 RealityView의 attachments 메커니즘입니다.
RealityView { content, attachments in
let model = ModelEntity(...)
content.add(model)
if let label = attachments.entity(for: "label") {
label.position = [0, 0.5, 0]
model.addChild(label)
}
} attachments: {
Attachment(id: "label") {
Text("Vintage Globe, 1872")
.padding()
.glassBackgroundEffect()
}
}
attachments: 클로저는 안정적인 식별자와 함께 SwiftUI 뷰를 선언합니다. 메인 RealityView 클로저 안에서 attachments.entity(for:)는 scene의 좌표 공간에서 위치 지정이 가능한 3D Entity로 뷰를 가져옵니다. 뷰는 SwiftUI의 업데이트 사이클에 참여하면서(상태 변경 시 뷰가 다시 그려집니다) 3D scene에서는 텍스처가 적용된 평면으로 렌더링됩니다.
이 메커니즘은 모든 in-world UI에 적합한 방식입니다. 움직이는 객체를 따라가는 라벨, 측정 어노테이션, 컨텍스트 버튼 등이 그렇습니다. SwiftUI 뷰 작성 방식은 동일하게 유지되고, 3D 위치 지정은 RealityView 레이어에서 일어납니다.
“패널 앱” 안티패턴
가장 흔한 visionOS 출시 실수는 패널 앱입니다. “Designed for iPad” 호환성을 통해 visionOS에 도착한 iPad 앱이 Volume도, Immersive Space도, Ornament도 없이 단일 Window로 출시되는 경우입니다. 앱은 동작하지만, 플랫폼의 가치를 얻지는 못합니다.
앱이 패널 앱임을 보여주는 세 가지 신호입니다.
단일 Window scene. .windowStyle(.volumetric)도 없고, 선언된 ImmersiveSpace도 없습니다. 앱은 평면 표면이고 그게 전부입니다.
Ornament 미채택. 앱의 tab bar가 Window 바깥이 아니라 Window 콘텐츠 안에 자리잡습니다. 결과적으로 같은 콘텐츠 밀도의 visionOS 네이티브 앱보다 더 답답하게 보입니다.
공간 전용 기능 부재. 앱이 세 번째 축을 어떤 식으로도 사용하지 않습니다. Volume에 3D 모델도 없고, Space에 환경 scene도 없으며, attachment를 통한 z축 위치 지정 UI도 없습니다. 앱은 iPad에서 하던 것을 그대로 하면서 단지 떠 있을 뿐입니다.
패널 앱이 곧 실패는 아닙니다. 공간 컴퓨팅의 이점이 없는 콘텐츠 카테고리(채팅 앱, 노트 앱, 설정 유틸리티 등)에는 적절한 선택입니다. 실패 모드는 패널 앱을 출시하면서 그것을 visionOS 네이티브로 자처하는 것입니다. 클러스터의 Apple Platform Matrix 글은 플랫폼 포함 여부가 제품 결정이라고 주장합니다. visionOS의 경우 그 결정은 “이 앱이 공간 표면을 얻을 만한가, 아니면 패널로 충분한가?”입니다.
흔한 실패
좋지 않은 visionOS UX를 만드는 세 가지 패턴입니다.
실제로는 깊이 패딩이 추가된 2D 콘텐츠인 Volume. Volume을 채우지만 그 안에 평면을 렌더링하는 “3D” UI는 공간을 낭비합니다. Volume은 3D 콘텐츠를 위한 것이고, 평면 콘텐츠는 Window에 속합니다.
사용 사례와 충돌하는 몰입 스타일. .full 몰입만 출시하는 명상 앱은 짧은 세션에서도 사용자를 환경 밖으로 끌어냅니다. .mixed만 출시하는 트레이닝 앱은 완전 집중이 필요한 운동에 충분히 멀리 가지 못합니다. 사용자의 실제 세션에 맞춰 몰입 스타일을 선택해야 합니다.
콘텐츠와 경쟁하는 Ornament. Ornament는 의도적으로 주변부에 위치합니다. 중앙 주의를 요구하는 ornament(번쩍이는 색, 애니메이션 동작)는 그 목적을 무너뜨립니다. Ornament는 안정적이고 흘끗 볼 수 있는 컨트롤에 사용해야 합니다.
이 패턴이 visionOS 앱에 의미하는 것
세 가지 시사점입니다.
-
Scene 타입은 쉬운 것이 아니라 사용자의 멘탈 모델에 따라 선택하세요. 평면 항목 목록은 Window입니다. 사용자가 살펴보는 3D 모델은 Volume입니다. 둘러싸는 환경은 Immersive Space입니다. 한 앱 안에서 이를 혼합하는 것(요구 시 Volume이 열리는 Window, Window의 버튼에서 접근 가능한 Immersive Space)이 visionOS 네이티브 패턴입니다.
-
Toolbar와 보조 UI에는 ornament를 채택하세요. Ornament는 visionOS가 “이 UI는 보조적이다”라고 전달하는 방식입니다. Window 콘텐츠 안에 toolbar를 두면 Vision 위에 올라간 iPad처럼 읽힙니다. 통합 비용은 작고 시각적 차이는 큽니다.
-
RealityView의 in-world UI에는 attachment를 사용하세요. 3D 객체 위의 라벨, 가상 콘텐츠 옆의 버튼, 컨텍스트 표시값 등입니다. SwiftUI와 3D 공간 사이의 다리는 이미 해결되어 있으며, 실패 모드는 그것을 사용하지 않고 임시방편의 3D 텍스트 렌더링으로 끝나는 것입니다.
전체 Apple 생태계 클러스터: 타입 기반 App Intents; MCP 서버; 라우팅 문제; Foundation Models; 런타임 vs 툴링 LLM 구분; 세 가지 표면; single source of truth 패턴; 두 개의 MCP 서버; Apple 개발용 hooks; Live Activities; watchOS 런타임; SwiftUI 내부 구조; RealityKit의 공간 멘탈 모델; SwiftData 스키마 규율; Liquid Glass 패턴; 멀티플랫폼 출시; 플랫폼 매트릭스; Vision 프레임워크; Symbol Effects; Core ML 추론; Writing Tools API; Swift Testing; Privacy Manifest; 플랫폼으로서의 접근성; SF Pro 타이포그래피; 내가 쓰지 않기로 한 것들. 허브는 Apple Ecosystem Series에 있습니다. AI 에이전트와 함께하는 iOS의 더 넓은 맥락은 iOS Agent Development guide를 참고하세요.
FAQ
Volume과 Immersive Space의 차이는 무엇인가요?
Volume은 다른 앱과 함께 Shared Space에 존재하는 경계가 있는 3D 영역입니다. 사용자는 그 주위를 걸어다닐 수 있고, 시스템이 프레임을 잡아주며, 다른 앱의 Window는 여전히 보입니다. Immersive Space는 사용자를 둘러싸고 환경을 장악하며 다른 앱의 동시 사용을 막습니다. Volume은 “이 3D 사물을 보세요”이고, Space는 “이 환경 안에 있으세요”입니다.
동시에 여러 Volume을 열 수 있나요?
가능합니다. .volumetric 스타일의 여러 WindowGroup scene이 각자의 크기와 콘텐츠를 가지고 동시에 열릴 수 있습니다. 시스템은 이들을 공간에서 독립적으로 배치합니다.
동시에 여러 Immersive Space를 열 수 있나요?
불가능합니다. 앱당 한 번에 하나의 Immersive Space만 활성화될 수 있습니다. Space 간 전환은 @Environment(\.openImmersiveSpace)와 @Environment(\.dismissImmersiveSpace)를 통해 현재 Space를 명시적으로 해제하고 새 Space를 여는 작업을 요구합니다.
Volume 크기가 정말 변경 불가능한가요?
Volume 경계는 기본적으로 열리는 시점에 고정됩니다. visionOS HIG의 프레이밍은 Volume이 의도된 경계를 가진 특정 3D 콘텐츠를 표현하며, 임의의 사용자 크기 조정은 콘텐츠의 의도된 스케일을 왜곡한다는 것입니다. visionOS 2 이상에서는 .windowResizability(.contentSize)와 관련 API를 통해 크기 조절이 가능한 volume을 위한 개발자 옵트인이 추가되었기에, 사용자가 크기를 조절할 수 있는 공간 컨테이너가 필요한 앱은 이를 요청할 수 있습니다. 대부분의 volume은 고정 기본값으로 출시되며, HIG는 특정 스케일을 가진 콘텐츠(가상 조각, 실제 크기의 모델)에 대해 이 방식을 계속 권장합니다.
visionOS Window에 tab bar를 어떻게 추가하나요?
콘텐츠 내부 탭(iPad 스타일 패턴)을 위해서는 Window 안에서 TabView를 사용하거나, visionOS 네이티브의 주변부 tab UI를 위해서는 커스텀 버튼 행이 있는 ornament를 사용하세요. Ornament 경로는 Apple의 자체 앱(Music, Mail)이 사용하는 방식이며 visionOS 사용자에게 가장 네이티브하게 느껴지는 방식입니다.
RealityView attachment가 hand tracking과 상호작용할 수 있나요?
가능합니다. Attachment는 위치가 지정되면 3D 엔티티이며, 다른 RealityKit 엔티티와 동일한 제스처 및 hit-testing 시스템에 참여합니다. Tap, drag, hover 제스처는 SwiftUI의 표준 제스처 모디파이어를 통해 부착됩니다. 클러스터의 RealityKit 글이 hand tracking 통합 패턴을 다룹니다.
References
-
Apple Developer: Meet SwiftUI for spatial computing (WWDC 2023 session 10109). visionOS의 세 가지 scene 타입으로서 WindowGroup, volumetric WindowGroup, ImmersiveSpace를 소개합니다. ↩
-
Apple Developer Documentation:
ImmersionStyle. 세 가지 몰입 스타일(.mixed,.progressive,.full)과.immersionStyle(selection:in:)모디파이어 API. ↩ -
Apple Developer Documentation:
ornament(visibility:attachmentAnchor:contentAlignment:ornament:). 지정된 anchor와 함께 Window에 ornament UI 평면을 추가하는 SwiftUI view 모디파이어입니다. ↩ -
Apple Developer: Go beyond the window with SwiftUI (WWDC 2023 session 10111). Volume, Immersive Space, 그리고 visionOS에서 평면 패널 UI를 넘어서기 위한 패턴을 다루는 세션입니다. ↩
-
Apple Developer Documentation: Creating an immersive space in visionOS with SwiftUI. Immersive space 정의와 열기를 다루는 엔드 투 엔드 가이드입니다. ↩