← 모든 글

iOS 27의 SwiftUI 성능과 상호 운용성

LazyVStack은 자신의 높이가 얼마인지 알지 못합니다. 이미 배치한 뷰들의 평균 크기와 앞으로 남았다고 예상하는 뷰의 개수를 바탕으로 자신의 높이를 추정한 다음, 스크롤하는 동안 그 추정값을 실시간으로 보정합니다.1 UI Frameworks 팀의 WWDC26 세션 세 개는 이 한 가지 사실(그리고 그래픽과 상호 운용에서의 유사한 사실들)을 가져다가, iOS 27에서 SwiftUI가 부하 상태에서 어떻게 동작하는지에 대한 작동 모델로 바꿔 놓습니다. 스크롤이 어떻게 매끄럽게 유지되는지, GPU 효과가 어떻게 합성되는지, 그리고 이미 출시한 AppKit이나 UIKit 앱에 SwiftUI가 어떻게 끼워 맞춰지는지를 다룹니다.

세 세션은 하나의 주장을 세 가지 방식으로 펼친 것처럼 읽힙니다. lazy stack은 그 추정 방식과 싸우기를 멈출 때 좋은 성능을 냅니다. 셰이더 효과는 각 modifier를 파이프라인의 한 단계로 다룰 때 합성됩니다. 그리고 상호 운용은 @Observable과 representable 프로토콜이 그 이음새를 짊어지도록 맡길 때 작동합니다. 셋 중 어느 것도 기능 발표가 아닙니다. 각각은 추측에 기대지 않고 프레임워크를 예측할 수 있을 만큼 충분히 잘 설명된 메커니즘입니다.

TL;DR / 핵심 요약

  • LazyVStack은 보이는 영역(visible rect)을 채우는 뷰만 평가합니다. 화면 밖의 높이와 콘텐츠 오프셋은 추정되므로, 절대 콘텐츠 오프셋 값을 읽는 것은 불안정하며, .onScrollTargetVisibilityChange처럼 상대적 가시성 기반 API를 선호해야 합니다.12
  • 프리페치(prefetching)는 뷰가 나타나기 전에 그 뷰를 보여주는 작업을 여러 프레임에 걸쳐 나눕니다. 프리페치된 작업이 버려지지 않도록, onAppear가 아니라 뷰의 이니셜라이저에서 뷰를 설정하세요.1
  • ForEach 리프에서 서브뷰 개수가 동적으로 바뀌는 것을 피하세요. body 안에서 조건문으로 필터링하면 인덱스 기준으로 뷰가 계속 살아 있게 되므로, 대신 데이터 수준에서 필터링하세요(QueryPredicate 적용).1
  • SwiftUI는 세 가지 셰이더 진입점(colorEffect, distortionEffect, layerEffect)을 제공하며 점점 더 강력해집니다. 이웃 픽셀을 샘플링할 수 있는 것은 layerEffect뿐이며, 블러와 도메인 왜곡(domain warp)에 바로 이 기능이 필요합니다.3456
  • 셰이더는 상태가 없으므로(stateless), 애니메이션은 TimelineView의 타임스탬프를 매개변수로 넘겨 줄 때 만들어집니다. 프레임 사이에 전달되는 것은 아무것도 없습니다.37
  • AppKit과 UIKit은 @Observable로부터 자동 다시 그리기를 얻으며(더 이상 수동 needsDisplay가 필요 없습니다), SwiftUI는 NSHostingView, NSGestureRecognizerRepresentable, NSHostingMenu, NSHostingSceneRepresentation을 통해 기존 앱에 끼워집니다.8910

Lazy Stack은 추정값으로 동작합니다

Watch on Apple Developer ↗
UI Frameworks 엔지니어 Rens는 LazyVStack이 위에서 아래로 레이아웃을 진행하다가 보이는 영역이 채워지면 멈추고 나머지는 추정한다고 설명합니다.

lazy stack에 대해 가장 먼저 마음에 새겨 둘 것은, 이것이 의도적으로 정확성을 효율과 맞바꾼다는 점입니다. VStack과 달리 LazyVStack은 보이지 않는 뷰를 평가하거나 렌더링하지 않습니다. 뷰를 위에서 아래로 배치하다가 보이는 영역이 채워지면 멈추고, 스크롤로 들어오는 뷰를 추가하고 스크롤로 나가는 뷰를 제거합니다.1 이득은 분명합니다. 비용은 미묘합니다. stack이 모든 뷰를 절대 불러오지 않기 때문에, 화면 밖 뷰의 높이는 앞선 뷰들의 평균에서 추정되고, 이상적 너비는 첫 번째 서브뷰의 너비로 수렴하며, 보이는 영역 위쪽의 공간 자체도 근사값입니다.1

이 추정은 한 번 우회하고 마는 버그가 아닙니다. 다른 모든 lazy-stack 결정이 그 위에 놓이는 토대입니다. 세션 321은 방향 전환(orientation change)을 통해 그 결과를 구체적으로 보여 줍니다. iPhone을 회전하면 맨 위에 보이던 뷰는 고정된 채로 유지되지만, stack은 그 위쪽 뷰들의 정확한 새 레이아웃을 아직 측정하지 않은 상태입니다. 다시 맨 위로 스크롤하면 stack은 이를 조정해야 합니다. 보이는 영역 위쪽의 추정된 공간을 보정하고, 스크롤 뷰의 콘텐츠 오프셋을 같은 양만큼 업데이트하여 맨 위에서 콘텐츠 오프셋이 0에 도달하도록 합니다.1 lazy stack과 그것을 둘러싼 스크롤 뷰는 위치와 오프셋을 정밀하게 조율하여, 추정값이 갱신되더라도 보이는 서브뷰들의 상대 위치가 결코 튀지 않도록 합니다.1

여기서 실천 가능한 따름정리는 어떤 스크롤 API를 신뢰할지에 관한 규칙입니다. 절대 콘텐츠 오프셋은 추정값이므로, 그것을 읽으면(예를 들어 .onScrollGeometryChange로 100포인트 이후 버튼을 숨기는 식으로) 추정값이 자리를 잡으면서 기준점이 표류하는 임계값을 얻게 됩니다.2 안정적인 신호는 상대적 가시성입니다. .onScrollTargetVisibilityChange modifier는 스크롤 뷰에서 보이는 서브뷰 집합이 바뀔 때 실행되므로, “스크롤하여 보여주기” 버튼은 불안정한 픽셀 수 대신, 어떤 행이 화면에 있는지에 임계값(세션은 80%를 사용합니다)을 걸어 가시성을 정할 수 있습니다.21 같은 논리가 .scrollTransition을 문제 삼습니다. 뷰를 원래 프레임 밖으로 밀어내는 변환은 stack이 보이는 뷰를 화면 밖에 있다고 믿게 만들어 일찍 제거하게 할 수 있으므로, 어떤 스크롤 전환이든 평소라면 보이지 않을 뷰가 보이는 영역 안으로 밀려 들어오지 않게 해야 합니다.111

여러분의 뷰 구조체가 서브뷰가 아닌 이유

lazy stack이 불러오는 서브뷰는 여러분이 작성한 뷰 구조체와 일대일로 대응하지 않습니다. StepViewForEach는 단계마다 하나의 StepView로 해석되지만, 각 StepView의 body가 둘러싸는 레이아웃 없이 두 개의 최상위 뷰(다이어그램과 안내)를 반환한다면, stack은 그 각각을 따로 불러옵니다.1 stack에 중요한 숫자는 해석된 서브뷰의 개수이지, 구조체의 개수가 아닙니다.

함정은 동적인 서브뷰 개수입니다. StepView가 environment 값에 따라 서브뷰를 하나 반환하거나 0개 반환한다면, stack은 더 이상 인덱스를 신뢰할 수 없습니다. 앞선 뷰들의 개수가 바뀔 수 있기 때문입니다. 그래서 stack은 만약을 대비해 앞선 StepView 인스턴스들을 살려 둡니다. 이는 곧 무관한 environment 변화가 화면 밖으로 스크롤된 뷰들의 body 평가를 유발할 수 있고, stack이 그 상태를 해제하지 않는다는 뜻입니다.1 해법은 필터를 뷰에서 떼어 내 데이터로 옮기는 것입니다. SwiftData를 쓴다면, 조건을 QueryPredicate에 넣어 뷰를 전혀 구성하지 않고도 서브뷰 개수를 알 수 있게 하세요.1 body 안에서 옵셔널을 언래핑하는 것도 같은 효과(뷰를 더 오래 살려 두는)를 냅니다. 더 깔끔한 방법은 lazy stack이 부분적으로만 해석된 행을 붙들고 있게 두는 대신, 상위 단계에서 ContentUnavailableView를 보여 주는 것입니다.1

프리페치는 추정값이 빠르게 느껴지도록 만드는 메커니즘입니다. 스크롤하는 동안 스크롤 뷰는 오프셋을 업데이트하고, 뷰를 렌더링하고, 여러분의 오프셋 변경 작업을 실행할 시간이 프레임 마감(frame deadline)까지밖에 없습니다. 새 뷰를 보여주는 일이 그 예산을 초과하면 프레임이 떨어지고 끊김(hitch)이 보입니다.1 이를 막기 위해 lazy stack은 여유 시간이 있는지 확인하고, 있다면 곧 나타날 뷰의 작업 일부를 미리 수행합니다(body와 레이아웃을 평가하고, 중첩된 LazyHStack을 여러 프레임에 걸쳐 펼치기도 합니다). 그래서 뷰가 나타날 즈음에는 작업 대부분이 이미 끝나 있습니다.1 세션이 onAppear에 대해 단호한 이유가 바로 이것입니다. onAppear에서 뷰를 설정하면 프리페치된 작업을 버리게 되고, 뷰가 나타날 때 다시 하도록 강제하며, 때로는 필요 이상으로 더 많은 뷰를 끌어들여 스크롤을 저하시킵니다. 뷰가 합당한 상태로 도착하도록 이니셜라이저에서 설정하고, onAppear는 무한 스크롤에서 다음 페이지를 가져오는 것처럼 진정으로 등장에 묶인 작업에만 남겨 두세요.1 역방향 스크롤에서는 프리페치 도중 body가 실행되는데도 onAppear는 아예 한 번도 실행되지 않을 수 있습니다.1

고급 그래픽은 그저 파이프라인일 뿐입니다

Watch on Apple Developer ↗
UI Frameworks 엔지니어 Haotian은 고급 효과를 서로 연결된 표준 파이프로 규정합니다. “고급”이라는 점은 복잡성이 아니라 구성 방식에 있습니다.

세션 322는 “고급 그래픽”을 합성으로 다시 정의합니다. 각 SwiftUI modifier는 데이터를 받아들이고 변환하여 다음으로 넘기는 파이프이며, 고급스러운 결과는 어떤 단일한 복잡 API가 아니라 파이프를 연결하는 방식에 있습니다.3 세션은 평범한 단계들을 연결하여 Apple Music 스타일의 실시간 가사 뷰를 만듭니다. 커버 아트를 블러 처리해 뒤로 물러나게 하고, 그 위에 셰이더를 돌리고, 그 셰이더를 시간으로 구동하며, 가사 스크롤을 같은 시간 소스에 동기화합니다.3

진짜 선택이 일어나는 곳은 셰이더 단계입니다. SwiftUI는 능력이 점점 올라가는 세 가지 효과 진입점을 통해 Metal 셰이더 함수를 호출합니다. colorEffect는 각 픽셀의 위치와 원래 색이 주어졌을 때 그 색을 변환하며, 흑백 변환 같은 작업에는 이것으로 충분합니다.4 distortionEffect는 대신 한 위치를 다른 위치로 매핑하여(이 위치의 색을 저 위치에서 샘플링하도록 SwiftUI에 알려 줍니다) 색이 개입하지 않는 기하학적 왜곡을 처리합니다.5 layerEffect가 가장 유연합니다. 셰이더에 뷰 전체의 레이어를 넘겨 주어, 출력 픽셀이 자신의 이웃이나 영역 전체를 샘플링할 수 있게 합니다. 블러와 더 풍부한 왜곡에 정확히 이것이 필요합니다.63

세션의 도메인 왜곡 배경은 layerEffect를 사용합니다. 균일한 float2 오프셋은 모든 픽셀을 같은 양만큼 이동시키므로 이미지를 미끄러뜨리기만 합니다. 유기적인 움직임에는 픽셀별 변화가 필요하므로, 셰이더는 미리 계산된 NoiseTexture(이미지로 전달되며 Metal 쪽에서는 texture2d로 도착합니다)를 샘플링하는데, 이 텍스처의 빨강과 초록 채널이 각 UV 좌표에서 서로 다른 X와 Y 오프셋을 공급합니다.3 노이즈를 한 번 샘플링하면 이미지가 비틀리고, 두 번 샘플링하면(두 번째는 첫 샘플이 이동시킨 위치에서) 흐르는 듯한 덩어리가 만들어집니다. 이 2차(second-order) 기법이 도메인 왜곡이며, 세션은 매개변수를 실시간 미리 보기로 볼 수 있는 다운로드 가능한 샘플 앱을 안내합니다.3

두 가지 프레임워크 사실이 애니메이션을 작동하게 합니다. 셰이더는 상태가 없습니다. 이전 프레임의 기억을 전혀 갖지 않으며, 출력은 오로지 여러분이 전달한 매개변수에만 의존합니다.3 따라서 움직임은 셰이더 내부에서 나올 수 없습니다. 외부에서 공급해야 하며, 이를 공급하는 파이프가 TimelineView입니다. 애니메이션 일정(schedule)에 따라 매 프레임 타임스탬프와 함께 실행됩니다.73 그 타임스탬프를 셰이더에 전달하여 노이즈 샘플 위치에 더하면 패턴이 흐릅니다. 가사 쪽은 같은 시간 소스를 반대 방향에서 재사용합니다. 재생 타임스탬프가 현재 줄을 골라내고(굵고 선명하게, 나머지는 흐리게), onChange가 시간이 흐르는 동안 그 줄을 계속 가운데에 둡니다.123 활성 줄 위에 떠 있는 타임스탬프는 offset(두 뷰의 크기가 모두 필요한)이 아니라, 정렬 지점을 의미론적으로 재정의하는 정렬 가이드(alignment-guide) 오버라이드로 배치되어, 수동 오프셋 없이도 서브뷰의 상단 모서리가 컨테이너의 하단 모서리에 붙습니다.133

SwiftUI는 AppKit이나 UIKit 앱에 끼워집니다

Watch on Apple Developer ↗
UI Frameworks 엔지니어 David Nadoba는 Swift가 Objective-C와 함께 작동하도록 설계되었던 것처럼, SwiftUI도 처음부터 AppKit 및 UIKit과 나란히 작동하도록 설계되었다고 언급합니다.

상호 운용 세션은 도입 전체를 다시 정의하는 한 가지 지점에서 시작합니다. 대부분의 앱은 이미 암묵적으로 SwiftUI를 사용하고 있습니다. 새로운 디자인에서 NSSlider, NSSwitch, NSSegmentedControl 같은 AppKit 컨트롤은 내부적으로 SwiftUI로 렌더링되며, Liquid Glass 또한 SwiftUI를 통해 여러 프레임워크에 걸쳐 구현의 큰 부분을 공유합니다.8 따라서 “SwiftUI 도입”은 재작성이라기보다, 이음새를 어디에서 명시적으로 드러낼지에 대한 결정에 가깝습니다.

첫 단계에는 SwiftUI가 전혀 필요 없습니다. AppKit과 UIKit은 이제 @Observable 타입을 자동으로 관찰합니다. 모델 클래스에 @Observable을 표시하고, drawKnob 같은 그리기 메서드 안에서 그 속성을 읽으면, AppKit이 각 접근을 추적하고 접근된 속성이 바뀔 때 다시 그립니다. 한 슬라이더의 값이 다른 슬라이더의 모양에 영향을 줄 때마다 작성하던 수동 needsDisplay = true를 폐기하는 것입니다.814 관찰(Observation)은 draw(_:)를 넘어 updateConstraints(), layout(), updateLayer(), 그리고 NSViewController의 대응 메서드들로 확장되며, UIKit에서는 UIButton, UICollectionViewCell 등으로 한층 더 멀리 닿습니다.8 2026년 릴리스에서는 기본적으로 켜져 있으며, Info.plist를 통해 macOS 15(NSObservationTrackingEnabled)와 iOS 18(UIObservationTrackingEnabled)로 백 디플로이(back-deploy)할 수 있습니다.8

모델이 @Observable이 되고 나면, 실제 SwiftUI 이음새는 작습니다. 세션은 슬라이더 기반 색상 선택기를 Canvas로 그린 원형 SwiftUI 컨트롤로 다시 만드는데(CanvasdrawRect에 대응하는 즉시 모드(immediate-mode) API이며, 기존 Core Graphics 코드를 재사용하기 위한 withCGContext를 갖습니다), 바로 그 동일한 @Observable ColorModel을 재사용합니다.158 AppKit이 뷰를 기대하는 자리에 이를 끼워 넣으려면 NSView의 하위 클래스인 NSHostingView로 감쌉니다. 모델이 이미 업데이트를 구동하므로, 그렇게 감싸는 것만으로 필요한 일은 전부입니다.168 기존 제스처 코드는 재작성 없이 그대로 가져옵니다. ForceClickGestureRecognizerNSGestureRecognizerRepresentable을 통해 SwiftUI 뷰에 닿고(makeNSGestureRecognizerhandleNSGestureRecognizerAction을 구현합니다), 그런 다음 평범한 .gesture modifier로 붙어 SwiftUI 자체의 드래그 제스처와 공존합니다.178 같은 representable 계열에는 반대 방향으로 NSView를 끼워 넣기 위한 NSViewRepresentable도 포함됩니다.8

이 이음새는 메뉴와 씬(scene)으로까지 확장됩니다. ButtonPicker를 담은 SwiftUI ViewNSHostingMenu(NSMenu의 하위 클래스)를 통해 실제 메뉴가 되어, 메인 메뉴에 추가된 NSMenuItem의 하위 메뉴로 설정되며, keyboardShortcut이 포스 클릭을 할 수 없는 입력 장치를 위해 제스처가 아닌 경로를 그 액션에 부여합니다.188 SwiftUI 씬 전체도 붙습니다. MenuBarExtraapplicationWillFinishLaunching에서 addSceneRepresentation을 통해 추가되는 NSHostingSceneRepresentation으로 기존 앱에 닿으며, Settings 씬의 Toggle이 extra를 삽입할지 여부를 제어하고, openSettings() environment 액션이 @IBAction에서 설정을 엽니다.198 세션의 마무리 지점이 핵심을 지탱합니다. 세션이 다룬 모든 API는 2026년 릴리스 또는 그 이전에 출하되며, 혜택을 보기 위해 앱이 전부 SwiftUI일 필요는 없습니다.8 다만 이번 사이클의 다른 곳에서는 현대화 압박이 더 날카로운 모서리를 갖습니다. iOS 27은 UIKit 씬 기반 라이프 사이클을 실행 요건으로 만들기 때문에, 씬을 전혀 도입하지 않았던 앱을 최신 SDK로 다시 빌드하면 실행에 실패합니다.

SwiftUI 팀이 랩에서 덧붙인 것

WWDC26 UI Frameworks 그룹 랩에서 나온 두 가지 설명이 세션들이 묘사하는 무효화(invalidation) 모델을 더 날카롭게 다듬습니다. 둘 다 로컬에서 전사한 녹음에서 의역한 것입니다. Apple은 랩에 대한 공식 자막을 게시하지 않습니다. 이는 언어 엔지니어들이 동시성과 로드맵 질문을 받았던 Swift 팀 자체 그룹 랩을 관통했던 것과 동일한 자막 공백입니다.

body에서 계산 속성(computed property)으로 코드를 옮기는 것은 무효화 측면에서 아무런 이득이 없습니다. SwiftUI는 body를 다시 실행할 때마다 여전히 그 속성을 다시 실행하므로, 이 이동은 가독성 향상일 뿐 그 이상은 아닙니다.20 성능 경계는 한 단계 위에서 나타납니다. 별도의 뷰 타입을 추출하면 SwiftUI는 이를 독립적으로 무효화할 수 있어, 둘러싼 body 전체가 아니라 그 입력이 바뀔 때 그 뷰만 다시 실행합니다. SwiftUI가 일을 덜 하기를 원한다면, 새 속성이 아니라 새 뷰를 찾으세요.

모든 environment 변화는 그 environment 값을 읽는 모든 뷰를 무효화합니다. environment를 읽는 것은 저렴하지만, environment의 잦은 변동은 그렇지 않습니다.21 빠르게 바뀌는 값(패널의 예시는 현재 시각입니다)은 environment 밖에 두세요. 매 프레임 업데이트되는 값은 움직일 때마다 모든 읽는 쪽을 재평가로 끌어들이기 때문입니다. 변동이 심한 값은 그것을 필요로 하는 경로를 따라 아래로 전달하고, environment에는 가만히 머무는 것들을 짊어지게 하세요.

이후의 랩 세션은 세 가지 메커니즘 세부 사항을 더합니다. 모두 WWDC 2026 SwiftUI 그룹 랩을 로컬에서 전사한 녹음에서 의역한 것이며, 이에 대해 Apple은 공식 자막을 게시하지 않습니다.

부분 그래프 평가(partial graph evaluation)는 프리페치된 작업이 실제로 어디로 가는지 설명합니다. 패널은 lazy stack이 현재 프레임 렌더링 후 남은 프레임 시간에 다가오는 셀들의 body를 평가하다가, 다음 프레임이 시작되기 직전에 멈춘다고 묘사했습니다.22 그들이 짚은 함정은 이것입니다. 셀을 다시 구성하여 크기가 바뀌는 바람에 다시 레이아웃해야 하게 만드는 onAppear는 그 프리페치된 작업을 버립니다. 그들의 지침은 프리페치가 살아남도록 크기 작업을 bodyonAppear가 아니라 셀의 이니셜라이저에서 하라는 것이었습니다.22

상호 운용에 대한 멘탈 모델은 UIKit을 10년 넘게 다뤄 온 패널리스트에게서 나왔습니다. UIKit은 위에서 아래로, 윈도우에서 안쪽 리프를 향해 레이아웃하는 반면, SwiftUI는 아래에서 위로, 가장 안쪽 노드에서 바깥쪽을 향해 쌓아 올립니다.22 둘이 교차할 때, 패널은 번갈아 쌓인 층의 배치를 샌드위치 또는 케이크라고 불렀습니다. 깊은 교차가 미묘해지는 이유가 여기 있습니다. 각 프레임워크는 레이아웃 패스를 서로 반대편 끝에서 구동하고 싶어 합니다.22

위의 environment 지점에는 생생한 실제 사례가 붙었습니다. 본 적 있는 가장 심한 environment 오용을 묻자, 패널은 스크롤 위치를 environment에 넣는 것을 꼽았습니다. 이것은 스크롤하는 동안 매 프레임 업데이트되므로 매 프레임 모든 읽는 쪽을 무효화합니다.22

두 번째 SwiftUI 그룹 랩 세션은 네 가지 메커니즘 세부 사항을 더했습니다. 모두 WWDC 2026 SwiftUI 그룹 랩(세션 2)을 로컬에서 전사한 녹음에서 의역한 것이며, Apple은 랩에 대한 공식 자막을 게시하지 않습니다.

onGeometryChange(for:of:action:) modifier는 들리는 것보다 더 좋게 읽히는데, 두 클로저가 작업을 나누는 방식 덕분입니다. transform 클로저는 매 프레임 실시간 지오메트리로 실행되지만, 액션이 실행될지를 좌우하는 것은 그것이 반환하는 값뿐입니다. 결과 타입이 Equatable이고 그 값이 바뀔 때만 액션이 실행되기 때문입니다.23 따라서 거친(coarse) 값(원시 크기가 아니라 크기 구간이나 레이아웃 분기점)을 반환하면, 프레임 속도(frame-rate) 신호가 연속이 아니라 임계값에서 두 번만 실행되는 신호로 바뀝니다. 패널은 이를 GeometryReader가 감싸는 서브뷰에 비싸므로 메인 레이아웃을 구동하지 않고 측정하도록 배경(background)에 가둬야 한다는 경고와 함께 짝지었습니다.23

잘 만든 동적 속성(dynamic property)은 대부분의 onChange 작업을 곧장 대체할 수 있습니다. DynamicPropertyupdate() 메서드는 뷰의 body 직전에 실행되므로, 커스텀 속성 래퍼는 그 시점에 이미 캐시된 값(패널의 예시는 이미지였습니다)을 동기적으로 제공하고, onAppear 왕복과 그것이 유발하는 다시 렌더링을 건너뛸 수 있습니다.24 패널의 표현은, onChange의 대부분 용도는 잘 만들어진 동적 속성으로 대체할 수 있다는 것이었습니다.24

ScrollView 안에서 보고된 레이아웃 경계 밖에 그리는 뷰는 컬링(culling)될 수 있습니다. 시스템이 실제로 칠하는 곳이 아니라 통보받은 경계를 기준으로 화면 밖이라고 판단하기 때문입니다.25 패널은 이를 호스트 뷰를 넘쳐 흐른 뒤 스크롤 도중 사라지는 커스텀 드롭다운과 오버레이 뒤에 있는 구체적 실패 동작으로 지목했고, 같은 위험이 자신의 앵커를 넘어 확장되는 overlay 콘텐츠에도 적용된다고 언급했습니다.25

시트(sheet)를 포함한 모든 것 위에 전체 화면 오버레이를 띄우는 것에는 깔끔한 순수 SwiftUI 해법이 없습니다. 패널의 지침은 UIKit 씬 라이프 사이클을 통해 새 UIWindow로 내려가라는 것이었으며, 더 깊은 원칙은 마지막 윈도우가 이기고, 맨 위에 무엇이 놓이는지에 대해 단일한 진실 공급원이 있어야 한다는 것이었습니다.26 그들은 더 나은 해법이 종종 모든 것 위에 방해가 되는 덮개를 던지는 것이 아니라, 사용자가 기대하는 내비게이션 스택을 복원하는 것이라고 덧붙였습니다.26

무엇을 먼저 도입할 것인가

메커니즘으로 설명된 릴리스는 새로움이 아니라 지렛대(leverage)로 순서를 정할 때 보답합니다.

  1. AppKit/UIKit 코드의 모델 클래스를 @Observable로 전환하세요. 수동 needsDisplay 호출을 삭제하고, 관찰하는 모든 그리기와 레이아웃 메서드에 자동 다시 그리기를 부여하며, 이후의 NSHostingView 끼워 넣기를 사소하게 만드는 전제 조건입니다.814 세 세션을 통틀어 가장 위험이 낮고 즉각적 보답이 가장 큰 수입니다.
  2. lazy stack에 동적 서브뷰 개수가 있는지 감사하세요. 조건부로 0개 또는 1개의 서브뷰를 반환하거나 body 안에서 옵셔널을 언래핑하는 ForEach 리프는 인덱스 기준으로 뷰를 살려 두고 있습니다. 필터를 QueryPredicate로(또는 계층 상위로) 옮기면 메모리와 항목으로 스크롤하는 성능이 모두 개선됩니다.1
  3. 뷰 설정을 onAppear에서 이니셜라이저로 옮기세요. 프리페치는 프리페치된 작업이 살아남을 때만 도움이 됩니다. onAppear에서 크기나 콘텐츠를 변형하는 설정은 그 작업을 버립니다.1 이는 조용하고 폭넓은 스크롤 매끄러움의 수입니다.
  4. 절대 스크롤 오프셋 읽기를 상대적 가시성 API로 대체하세요. 콘텐츠 오프셋에 묶인 것은 무엇이든 표류합니다. .onScrollTargetVisibilityChange는 실제로 어떤 행이 보이는지에 묶입니다.21
  5. 셰이더는 작은 효과가 제값을 하는 곳에서만 찾으세요. colorEffectdistortionEffect에서 시작하고, 효과가 이웃을 샘플링해야 할 때만 layerEffect로 끌어올리며, 셰이더 내부의 상태를 기대하기보다 TimelineView 타임스탬프로 어떤 움직임이든 구동하세요.4567

세 세션을 관통하는 흐름은 이것입니다. 프레임워크를 밀어붙이기 전에 그것을 예측하세요. lazy stack은 추정하고, 셰이더는 잊으며, 상호 운용은 재작성이 아니라 이음새입니다. 이 세 가지 사실을 염두에 두고 만들면 나머지는 따라옵니다.

FAQ

제 SwiftUI lazy-stack 스크롤 위치가 튀거나 표류하는 이유는 무엇인가요?

LazyVStack은 화면 밖 뷰를 불러오지 않기 때문에, 그 높이와 보이는 영역 위쪽 공간을 추정합니다. 그래서 절대 콘텐츠 오프셋은 프레임워크가 실제 레이아웃을 알아 가면서 보정하는 추정값입니다(예를 들어 방향 전환 후, 다시 맨 위로 스크롤하면 그 추정값을 조정합니다).1 UI를 절대 오프셋에 묶으면 추정값이 자리를 잡으면서 임계값이 표류합니다. 대신, 어떤 서브뷰가 실제로 보이는지를 기준으로 실행되는 .onScrollTargetVisibilityChange를 사용하세요.2

iOS 27의 SwiftUI lazy stack에서 스크롤을 매끄럽게 유지하려면 어떻게 해야 하나요?

프리페치가 제 일을 하게 두세요. lazy stack이 뷰가 나타나기 전에 수행하는 작업이 버려지지 않도록 뷰를 이니셜라이저에서 설정하고, onAppear에서 뷰의 크기나 콘텐츠를 변형하지 마세요.1 또한 ForEach 리프에서 서브뷰 개수가 동적으로 바뀌는 것을 피하고, 뷰가 나타난 후의 레이아웃 변경(onGeometryChange로 구동되는 높이 같은)을 피하세요. 둘 다 stack이 작업을 다시 하거나 스크롤 도중 위치를 다시 계산하도록 강제하기 때문입니다.1

colorEffect, distortionEffect, layerEffect는 언제 사용해야 하나요?

각 픽셀의 색을 그 위치와 원래 색에서 변환하려면(예를 들어 흑백 필터) colorEffect를 사용하세요.4 출력 위치를 샘플링할 소스 위치로 매핑하는 기하학적 효과에는 distortionEffect를 사용하세요.5 출력 픽셀이 하나 이상의 입력 픽셀에 의존할 때는 layerEffect를 사용하세요. 셰이더에 뷰 레이어 전체를 주어 이웃이나 영역 전체를 샘플링하게 하며, 블러와 도메인 왜곡에 바로 이것이 필요합니다.6

SwiftUI에서 Metal 셰이더를 어떻게 애니메이션하나요?

셰이더는 상태가 없습니다. 이전 프레임의 기억을 전혀 갖지 않고 오직 매개변수에만 의존하므로, 셰이더 내부에서 애니메이션할 수 없습니다.3 시간에 따라 바뀌는 값을 공급하세요. 애니메이션 일정의 TimelineView는 매 프레임 타임스탬프와 함께 실행됩니다. 그 타임스탬프를 매개변수로 셰이더에 전달하면(세션은 이를 노이즈 샘플 위치에 더합니다) 효과가 애니메이션됩니다.73

기존 AppKit이나 UIKit 앱을 재작성하지 않고 SwiftUI를 추가할 수 있나요?

네, 그리고 세션은 혜택을 보기 위해 어떤 앱도 전부 SwiftUI일 필요가 없다고 명시합니다.8 AppKit과 UIKit이 자동으로 다시 그리도록 모델에 @Observable을 표시한 다음, NSHostingView로 SwiftUI 뷰를 끼워 넣고, NSGestureRecognizerRepresentable로 기존 제스처 인식기를 가져오며, NSHostingMenu로 메뉴를 만들고, 앱 델리게이트에서 NSHostingSceneRepresentation으로 SwiftUI 씬을 붙이세요. 이들 모두 2026년 릴리스 또는 그 이전에 출하됩니다.816171819

전체 Apple Ecosystem 클러스터입니다. lazy stack이 뷰 구조체를 다른 서브뷰 집합으로 해석하는 이유를 설명하는 SwiftUI 기반(substrate)(result builder, 불투명 타입, 값 타입 뷰 트리), 이 성능과 상호 운용 이야기가 나란히 자리하는 iOS 27 SwiftUI 표면(재정렬, 문서, 툴바, 오류), 이제 AppKit과 UIKit에서도 자동 다시 그리기를 구동하는 @Observable 내부, 그리고 상호 운용 세션이 공유 SwiftUI에 그 프레임워크 간 구현을 돌리는 Liquid Glass 패턴. 허브는 Apple Ecosystem 시리즈입니다. AI 에이전트와 함께하는 iOS의 더 넓은 맥락은 iOS Agent Development 가이드를 참고하세요.

References


  1. Apple, WWDC26 session 321, “Dive into lazy stacks and scrolling with SwiftUI.” developer.apple.com/videos/play/wwdc2026/321. Covers lazy-stack layout and height estimation, the estimated content offset, view-struct-to-subview resolution, the dynamic-subview-count trap, prefetching across frame deadlines, and onAppear versus initializer setup. 

  2. Apple, WWDC26 session 321, “Dive into lazy stacks and scrolling with SwiftUI”. The session presents onScrollTargetVisibilityChange (a modifier whose closure runs when the set of visible scroll targets changes) as the stable, relative-visibility alternative to absolute content-offset reads. 

  3. Apple, WWDC26 session 322, “Compose advanced graphics effects with SwiftUI.” developer.apple.com/videos/play/wwdc2026/322. Frames effects as a composable pipeline; covers blur, the three shader entry points, the NoiseTexture domain-warp technique, stateless shaders driven by time, and alignment-guide attachment. 

  4. Apple Developer Documentation: colorEffect(_:isEnabled:). Returns a new view that applies a shader transforming each pixel’s color, given its position and original color. 

  5. Apple Developer Documentation: distortionEffect(_:maxSampleOffset:isEnabled:). Applies a shader that maps the position of each pixel to a source position to sample from, for geometric effects. 

  6. Apple Developer Documentation: layerEffect(_:maxSampleOffset:isEnabled:). Applies a shader as a layer effect with access to the entire view layer, allowing each output pixel to sample multiple input pixels. 

  7. Apple Developer Documentation: TimelineView. A view that updates its content according to a schedule; on an animation schedule it supplies the per-frame timestamp session 322 feeds into its shader. 

  8. Apple, WWDC26 session 272, “Use SwiftUI with AppKit and UIKit.” developer.apple.com/videos/play/wwdc2026/272. Covers automatic @Observable redraw in AppKit/UIKit, observation back-deployment via Info.plist, Canvas, NSHostingView, NSGestureRecognizerRepresentable, NSHostingMenu, and NSHostingSceneRepresentation

  9. Apple Developer Documentation: NSGestureRecognizerRepresentable. A protocol that wraps an NSGestureRecognizer for use as a SwiftUI gesture, implemented with makeNSGestureRecognizer and handleNSGestureRecognizerAction

  10. Apple Developer Documentation: MenuBarExtra. A scene that renders a menu bar item; session 272 attaches it to an existing AppKit app through NSHostingSceneRepresentation

  11. Apple Developer Documentation: scrollTransition(_:axis:transition:). Applies a transition as a view scrolls within a scroll view; session 321 warns that a transform pushing a view into the visible rect can desync a lazy stack. 

  12. Apple Developer Documentation: onChange(of:initial:_:). Runs an action when a value changes; session 322 uses it to recenter the current transcript line. 

  13. Apple Developer Documentation: alignmentGuide(_:computeValue:). Sets a view’s alignment guide so the layout system positions it semantically; session 322 overrides a bottom guide to attach a subview’s top edge to its container’s bottom edge. 

  14. Apple Developer Documentation: Observable. The macro that makes a class’s mutable properties participate in the Observation system, which AppKit and UIKit track for automatic redraw in the 2026 releases. 

  15. Apple Developer Documentation: Canvas. An immediate-mode drawing view whose closure receives a GraphicsContext; session 272 uses it to redraw the circular color picker and notes withCGContext for reusing Core Graphics code. 

  16. Apple Developer Documentation: NSHostingView. An NSView subclass that hosts a SwiftUI view hierarchy inside an AppKit view tree. 

  17. Apple Developer Documentation: NSViewRepresentable. A wrapper that lets an NSView participate in a SwiftUI view hierarchy; session 272 names it alongside NSGestureRecognizerRepresentable as part of the representable family. 

  18. Apple Developer Documentation: NSHostingMenu. An NSMenu subclass that renders a SwiftUI view as menu content, added to the main menu as the submenu of an NSMenuItem

  19. Apple Developer Documentation: keyboardShortcut(_:modifiers:). Assigns a keyboard shortcut to a control’s action; session 272 adds one to the menu button so input devices that cannot force-click still reach the feature. 

  20. Apple, WWDC 2026 UI Frameworks group lab, session 8002. Paraphrased from a locally transcribed recording; no official transcript is published. The team clarified that moving code from body into a computed property is a readability change only, and that the independent-invalidation boundary appears when you extract a separate view type. 

  21. Apple, WWDC 2026 UI Frameworks group lab, session 8003. Paraphrased from a locally transcribed recording; no official transcript is published. The team noted that every environment change invalidates all views reading that value, and advised keeping fast-changing values (the panel’s example was the current time) out of the environment. 

  22. Apple, WWDC26 session 8006, “SwiftUI Group Lab.” developer.apple.com/videos/play/wwdc2026/8006. Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftUI Group Lab; Apple publishes no official captions for the labs. Source for partial graph evaluation in leftover frame time (and the onAppear-resize warning, with sizing done in init), the UIKit-top-down-versus-SwiftUI-bottom-up interop “sandwich or cake” arrangement, and the scroll-position-in-the-environment example that invalidates every reader on every frame. 

  23. Apple, WWDC 2026 SwiftUI Group Lab (session 2). Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftUI Group Lab (session 2); Apple publishes no official captions for the labs. Source for the onGeometryChange transform-gates-action mechanism (return a coarse value to fire at thresholds) and the guidance to confine an expensive GeometryReader to a background. The two-closure shape is documented at onGeometryChange(for:of:action:): the of: transform closure derives an Equatable value from the geometry proxy and the action: closure runs only when that value changes. 

  24. Apple, WWDC 2026 SwiftUI Group Lab (session 2). Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftUI Group Lab (session 2); Apple publishes no official captions for the labs. Source for replacing most onChange work with a dynamic property that vends a cached value synchronously. Apple’s DynamicProperty protocol defines an update() method that SwiftUI calls immediately before rendering a view’s body so the property holds its most recent value. 

  25. Apple, WWDC 2026 SwiftUI Group Lab (session 2). Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftUI Group Lab (session 2); Apple publishes no official captions for the labs. Source for the culling of views that draw outside their reported layout bounds inside a ScrollView (the failure behind overflowing custom dropdowns and overlays), and the note that the same hazard applies to overlay content extending past its anchor. 

  26. Apple, WWDC 2026 SwiftUI Group Lab (session 2). Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftUI Group Lab (session 2); Apple publishes no official captions for the labs. Source for the lack of a clean pure-SwiftUI full-screen-over-sheets overlay, the drop to a new UIWindow through the UIKit scene life cycle, the “last window wins / single source of truth” principle, and the preference for restoring the navigation stack over a disruptive cover. 

관련 게시물

iOS 27의 접근성: 읽기 앱과 커스텀 컨트롤

읽기 앱과 커스텀 컨트롤을 위한 iOS 27 접근성. 텍스트 내비게이션 연결, causesPageTurn, UITextInput, adjustable 트레잇, 다이렉트 터치를 다룹니다.

8 분 소요

iOS 27의 SwiftUI 새 기능

iOS 27은 SwiftUI의 리스트, 문서, 툴바, 오류 처리를 재설계합니다. 드래그 재정렬, 읽기/쓰기 문서 모델, 툴바 오버플로, 항목 기반 알림이 추가됩니다.

18 분 소요

76점에서 100점으로: 완벽한 Lighthouse 점수 달성기

개인 포트폴리오 사이트의 모바일 Lighthouse 성능 점수를 76점(CLS 0.493)에서 모든 카테고리 완벽한 100/100/100/100으로 개선한 과정을 소개합니다.

5 분 소요