Apple/WWDC

SwiftUI로 지연 스택과 스크롤 자세히 살펴보기 (Dive into lazy stacks and scrolling with SwiftUI) - WWDC26

lgvv 2026. 7. 6. 23:46

SwiftUI로 지연 스택과 스크롤 자세히 살펴보기 (Dive into lazy stacks and scrolling with SwiftUI) - WWDC26

 


SwiftUI에서 lazy stacks 내부 동작 방식에 대한 이해.


하위 뷰를 지연 로드하며, 콘텐츠를 prefetch하고 부드러운 스크롤 경험에 대해서 SwiftUI가 어떻게 처리하는지와 성능 최적화 및 상태 관리, 정밀한 스크롤을 위한 팁에 대해서 설명.

 



LazyVStack은 보이지 않는 뷰를 평가하거나 렌더링하지 않음.
View가 보여지면 적절히 LazyVStack에 추가하고 View가 사라지면 LazyVStack에서 제거함.


 

LazyVStack은 모든 뷰를 로드하지 않기 때문에 화면 밖의 subviews에 대한 높이는 추정값(Estimated)를 사용.

이 추정하는 높이에 대한 값은 이전에 배치된 View 들의 평균 크기를 기반으로 하며, 이전에 배치된 뷰들과 남은 subviews의 수를 기반으로 예상함.

lazy stacks은 화면 밖의 뷰 변경사항을 인지하지 못하며, 로드되지 않기 때문에 이와 같은 방식으로 동작함.

마찬가지로 모든 뷰가 로드되지 않기 때문에 최대 너비를 찾을 수 없음.


이 예상하는 값은 스크롤 하면서 레이아웃에 대한 더 많은 정보를 알게 됨에 따라 스크롤 중에 변경될 수 있음.

예를 들어 스크롤을 끝까지 하는 경우에 마지막 뷰들이 다른 뷰들보다 약간 작을 경우 lazy stacks은 조정해야 하고, 이를 반영하여 원래의 Estimated Size를 조정함.


보이는 영역 위의 공간도 정확하지 않은데 스크롤 위치(스크롤 뷰의 content offset)은 보이는 아이템의 Estimated 위치에 따라 달라짐

보이는 영역 위의 공간이 정확하지 않은 한가지 예로는 화면 회전 후가 존재함.

 

 

회전 후에 다시 최상단으로 스크롤 할 경우 제일 Estimated Size를 수정해야 함을 의미하며, 보이는 영역 위의 공간을 수정해 나감.

줄어든 양만큼 ScrollView의 ContentOffset을 업데이트 하며, 맨위의 ContentOffset이 0이 되도록 함.

 

lazy stacks과 이를 감싸는 ScrollView는 위치와 ContentOffset을 조율함.

Estimated 값이 업데이트 될 때 보이는 subviews의 상대적 위치가 스크롤 뷰 내에서 변경되지는 않음.

 

 


LazyVStack에 LazyHStack을 중첩하면 성능 면에서도 좋을 수 있는데, 모든 사람들이 이 중첩된 뷰를 스크롤하여 보지 않기 때문.

LazyHStack의 이상적인 높이는 수직 ScrollView에서 첫번째 Subview의 높이임.

 


하지만, 가변 높이를 가지는 Label이 존재한다면 긴 Label은 잘릴 수 있으나 LazyHStack은 모든 Views 중에 가장 긴 Label이 어떤 것인지 미리 알 수가 없음.

가장 좋은 해결책은 Label의 줄 수 제한 등을 통해 View의 높이를 고정하는 것

 

 

lazy stacks이 최상의 성능을 내도록 피해야 할 패턴에 대해서 논의

.scrollTransition을 활용하면 화면 안 혹은 밖으로 스크롤 될 때 effect를 추가할 수 있음.


하지만 lazy stacks의 경우에는 화면에 있는 View만 로드하기 때문에 원래 위치를 기준으로 함.

여기서 Transform(효과가 적용된) 된 뷰들을 원래 프레임 밖으로 밀어냄.


이로 인해서 보여야 할 때 사라지게 되는데, lazy stacks이 화면 밖에 있다고 판단하기 떄문임.

 

lazy stacks의 View에 Effect를 적용한다면 원래 보이지 않을 뷰들이 보이지 않는 영역으로 밀려 들어가지 않도록 해야 함.

여기서는 다른 스케일 효과를 사용해야 함.

 

 

스크롤을 하단으로 내리는 버튼을 구현하는데, 맨 위 근처에 있을 때만 보이도록 하고 싶음.

여기에서는 .onScrollGeometryChange를 사용하여 contentOffset의 절대값을 가져오고, 100이 넘어갈 경우 버튼이 사라짐.

하지만, 이는 lazy stacks의 Content의 Estimated 값이기 때문에 버튼이 사라지는 정확한 위치가 Estimated 값이 변경되면 달라질 수 있음.

 

대신, 스크롤 뷰의 보이는 영역에서 서브뷰의 상대적 위치를 사용하는 것이 훨씬 더 좋음. .onScrollTargetVisibilityChange를 사용하여 적용할 수 있음.


해당 modifier의 클로저는 서브뷰의 visibility가 변경될 때 호출되며, ScrollView의 보이는 영역에서 호출

어떤 서브뷰가 보이는지에만 의존하며 threshold는 0.8로 되어 있음.

 

 

lazy stacks은 ScrollView의 보이는 부분으로 진입하려고 할 때 subview를 추가함.

하지만 lazy stacks이 개별적으로 로드하는 subview 들은 코드에서 정의한 View의 구조체와 항상 직접적으로 대응하지는 않음.

 

 

대부분의 경우에서 LazyVStack이 로드하는 Subview를 일컬음.

하지만, StepView에서는 StepDiagram과 StepIntructions이 View로 조재하며 이들은 VStack과 같은 어떤 레이아웃에도 포함되어 있지 않음.

ForEach가 StepView로 해석되었던 것처럼 각각의 StepView도 StepDiagram과 StepIntructions로 2개의 View로 해석됨.

뷰는 동적인 수의 뷰로도 해석될 수 있으며, 이 지점에서 주의해야 함.

 


위 예제에서는 detailLevel에 따라서 하나의 subview 혹은 0개의 subview로 해석됨.

하지만, 이 경우에는 경우에 따라서 보이기도 혹은 보이지 않기도 하기 때문에 index를 부여하여 StepView의 body는 지연 로드되지만 StepView 자체는 더 오래 살아있을 수 있음.

 

또한, writingStyle등은 불필요한 Environment 값이 body에 사용될 경우 화면 밖으로 스크롤된 뷰들도 불필요하게 업데이트 될 수 있음.


위처럼 View에서 필터링하는 방식 대신 데이터 수준에서 필터링 하는 방식을 권장.

이렇게 할 경우 lazy stacks에 Subview 수가 즉시 명확해짐.

 

뷰나 인덱스를 계산하기 위해 뷰를 구성할 필요가 없음.

 

 

 

 

View의 body에서 옵셔널 언래핑하는 것 또한 동일한 효과가 있음.

위 예제에서는 token에 대한 것들 NetworkClient에서 처리할 수 있어서 그 영역으로 이전.


lazy stacks은 데이터의 일부만 메모리에 유지하기 때문에, View의 전체에 대해서 diff를 수행할 필요가 없음.

보이는 View의 변경 사항에 대해서만 최소한의 확인만 수행함.


lazy stacks이 Subview를 한번에 모두 로드하지 않는 경우도 있는데, prefetching으로 lazy stacks이 내부적으로 사용하는 메커니즘으로 앱 스크롤 성능을 향상시킴.

 

 

스크롤하는 도중에 일정한 비율로 프레임을 그려야하기 때문에 연산을 수행하기 위한 시간이 정해져 있음.

 

이 작업에는 ScrollView가 ContentOffset을 업데이트 하는 것 또한 포함.

또한 View를 평가하고 레이아웃과 렌더링 시간도 모두 포함됨.

 

하지만, 화면에 새로운 View를 배치하는 작업은 비용이 많이 들 수 있음.

시간을 초과할 경우 프레임이 드롭됨. (버벅임 발생.)

 


프리페칭은 이러한 프레임 드롭을 방지하기 위해서 사용.

화면에 나타나려는 View의 레이아웃을 나타나기 전에 미리 처리할 수 있음.

실제 뷰가 나타날 때 대부분의 작업이 이미 완료된 상태.


일반적으로 View의 body는 한 시점에 호출되고 onAppear는 View가 화면에 배치될 때 조금 후에 호출됨.

스크롤 방향이 반전되면 View의 body가 prefetching의 일부로 호출될 수 있으며, onAppear는 전혀 호출되지 않을 수 있음.

 

 

lazy stacks에서 onAppear를 사용하는 것은 데이터 로딩을 포함한 여러 가지에 유용한데, 그 중 하나가 무한 스크롤.

하지만 각 View의 onAppear에서 모든 것을 로드하는 것은 좋은 방법은 아님.

 


위 예제에서는 onAppear는 각 View를 설정하는 데 사용.

크기와 View 내용의 많은 부분이 배치된 후에 변경됨.


즉, 프리페칭이 이전에 수행한 작업은 버려지고 뷰가 나타날 때 다시 수행해야 함.
lazy stacks은 필요 이상으로 많은 뷰를 로드할 수도 있으며, 스크롤에도 영향을 줄 수 있음.

 

대신 initializer를 활용해서 화면에 나타나기 전에 적절한 상태가 되도록 하기.

필수적이지는 않지만, View가 나타나기 전에 콘텐츠를 로드하는 것이 유용할 수 있음. 다른 케이스에서는 task를 사용하여 View가 나타날 때 인터넷에서 다이어그램을 원격으로 로드.

 


하지만 prefetching을 활용하여 조금 더 일찍 로드할 수 있음.

예를 들어서 캐시에 연결된 DiagramLoader를 Observable 객체를 사용해 생성.

 

캐시에 특정 ID의 데이터가 없다면, 초기화 될 때 데이터를 즉시 로드할 수 있음.

이니셜라이저에서 다이어그램 로딩을 즉시 시작하므로, 다이어그램이 조금 더 일찍 가져와짐.

 

화면 밖으로 스크롤된 뷰는 더 이상 렌더링되거나 업데이트 되지 않지만, 즉시 메모리에서 제거되는 것은 아님.

lazy stacks은 몇 번의 업데이트 동안 이것들을 유지하는데, 다시 화면에 스크롤될 경우를 대비하는 것임.

 

View가 최종적으로 메모리에서 삭제될 때 상태 변수도 함께 삭제됨.

 

 

화면 밖으로 스크롤된 뷰와 연결된 데이터는 삭제되므로 스크롤 후에도 살아있어야 하는 데이터에는 View 상태를 의존하지 말기.

즉, isHighlight변수는 View가 스크롤되어 밖으로 사라질 경우 상태가 사라지게 됨.

대신, 중요한 상태를 모델 객체로 이동하거나, Binding을 사용하여 외부 View로 이동.

 


일반적으로 lazy stacks은 스크롤 뷰 안에서 사용.

아래는 성능을 고려하면서 잘 작성한 하나의 예제

 

 

프로그래밍 방식의 스크롤은 lazy stacks 안에서 잘 동작하는데, 대상 View가 화면에 보이지 않아도 동작함.

 

 

화면 밖의 View로 스크롤하려면 lazy stacks이 위치를 추정해야 함.

애니메이션이 있는 스크롤에서 lazy stacks은 매 프레임마다 이 Estimated 위치를 업데이트 함.

 


StepView에서 동적인 View의 수를 가지면 성능에 영향을 줌.

ID가 있는 View로의 프로그래밍 방식 스크롤은 ForEach가 각 View가 항상 하나의 subview로 해석될 때 가장 성능이 좋음.

 

이 경우에는 lazy stacks은 스크롤할 ID를 찾기 위해서 ForEach에 쿼리를 할 수 있음(어떤 뷰도 구성하지 않은채로).

끝 근처의 subview로 스크롤 하는 것도 더 성능이 좋음.

이전과 마찬가지로 body의 조건문으로 View를 필터링하는 대신에 데이터 수준에서 필터링 하기.

 

 

너무 많은 View가 화면에 나타난 후 레이아웃을 변경한다면 프로그래밍 방식의 스크롤도 덜 부드러워짐.

이렇게 하는 일반적인 패턴은 onGeometryChagne를 사용하는 것으로 상태 값을 설정하여 다른 레이아웃 경로에서 사용하는 방식.

 

위 케이스에서는 subtitlHeight이 onGeometryChange에서 업데이트 됨. 그런 다음에 View의 body가 다시 평가되며, frame을 diagramHeight을 통해 계산하는데 사용. 이는 스크롤을 덜 부드럽게 만들기도 함.

 

lazy stacks은 View의 원래 높이를 측정하지만, View가 나타난 후에 높이가 변경되어서 다른 콘텐츠를 아래로 밀어내림.

 

 

SwiftUI의 레이아웃 기본(Primitive) 요소를 사용할 수 없다면, 대신 Layout을 직접 선언해서 사용할 것.

정리

  • 레이아웃에 대해서 이야기하고 View 구조체가 항상 단일 subview로 해석되지 않는다는 것과 이것이 lazy stacks에 미치는 영향
  • Prefetching하는 방법과 화면 밖의 뷰로 프로그래밍 방식의 스크롤을 허용하는 방법도 같이 알아봄.
  • lazy stacks에서는 absolute content size와 content offset의 사용을 피하는 것이 좋은데, 이는 Estimated 값이며 불안정하기 때문,
  • 최하단 뷰에서 데이터를 필터링하기 위해 조건부 View 콘텐츠를 사용하지 마는게 좋은데 SwiftUI View가 예상보다 오래 살아있게 할 수 있기 때문.
  • 가능하다면 onAppear가 호출되기 전에 lazy stacks의 Subview를 설정하는게 prefetching을 최상으로 동작하게 할 수 있음.
  • Subview가 나타난 후에 레이아웃을 변경하지 말것.


(참고)

https://www.youtube.com/watch?v=4FNbTRr5MtQ