iyOmSd/Title: SwiftUI

[SwiftUI] WWDC23 Demystify SwiftUI Performance

냄수 2023. 11. 30. 16:07
반응형

SwiftUI에 대한 성능적인 측면을 좀더 알아보고싶어서 찾아본 세션이에요

 

 

성능문제는 증상에서 부터 시작됩니다.

네비게이션 푸시가 느리거나 애니메이션이 끊기는현상이 보이거나 로딩 인디케이터가 뜨거나 등 현상이 일어날 때 증상을 보고 파악하게됩니다.

성능 문제가 있다는것을 파악하면 문제를 해결하는 첫단계는 측정이고

증상이 있음을 입증하고 증상의 원인을 파악합니다.

근본 원인을 파악한 후에는 최적화해서 문제를 해결합니다.

고친내용을 다시측정하고 입증하고 문제가 해결됬는지 확인합니다.

이를 반복하게 됩니다.

 

이 세션을 듣기위해서는 

SwiftUI의 identity를 이해하고있어야하고

암시적, 명시적 identity가 어떻게다른지

view lifetime과 identity의 차이를 알고있어야한다고 하네요!

자세한건

WWDC21 Demystify SwiftUI 에서 다룬다고해요!

그래서 이세션도 들어버리게되버렸습니다...!

세션이좀 길더라구요?

이 글에서 정리하기엔 너무 길어질것같아서... 다음글에서 자세히 정리해볼게요..!

 

WWDC21 내용을 간단하게만 쓰윽 보고가보자면


SwiftUI가 코드를 볼때 보는것으로 3가지가 있는데 

 

identity

SwiftUI가 앱의 여러 업데이트 과정에서 요소를 비교할때 동일하거나 다른것으로 인식하는 방식

 

lifetime

시간경과에 따른 뷰와 데이터의 존재를 추적하는 SwiftUI 방식

 

dependencies

인터페이스를 업데이트해야 하는 시기와 그이유를 SwiftUI가 이해하는 방법

 

이렇게 3개가 있습니다

 

 

🔵 identity

동일한 identity를 공유하는 뷰는 동일한 개념의 UI요소의 서로 다른 상태를 나타내고

별개의 UI요소를 나타내는 뷰는 항상 다른 identity를 갖습니다

모든뷰에서는 명시적이지 않더라도 identity가 존재함

 

Explicit identity

사용자지정 또는 데이터기반 식별자 사용

똑같은 강아지 사진이 있을때 강아지의 이름을 보고 구분할 수 있듯이 이름과 같은 식별자를 할당하는 것

강력하고 유연하지만 누군가가 어딘가에서 모든 이름을 추적해야한다는 단점이 있음

UIKit, AppKit에서는 이미 pointer id 형태로 사용하고있음

 

Structural identity

뷰 계층 구조에서 타입과 위치에 따라 뷰를 구분

 

🔵 Lifetime 

identity는 시간이 지나도 다양한 값에대해 안정적인 요소를 정의 할 수 있게 해줌

view value != view identity

뷰의 값은 일시적이므로 뷰의 수명에 의존해서는 안됨

SwiftUI에서는 비교를 수행하고 뷰가 변경되었는지 확인하기위해 값의 복사본을 보관하고 그 이후 값이 소멸

identity가 변경되거나 뷰가 제거되면 수명이 종료

 

state lifetime = view lifetime

상태의 지속성은 뷰의 수명과 관련있음

초기값으로 State에 대한 스토리지를 할당하고 뷰의 수명의 다하는 동안까지 상태값이 변경되어도 스토리지를 유지함

identity가 변경될때마다 state를 위한 스토리지를 새로 할당하게됨

 

 

🔵 Dependency

@Binding, @Environment, @State, @StateObjectemd 여러 종류의 종속성이 있음

dependency가 변경되면 뷰에서 새로운 body를 생성

뷰가 트리와 같은 구조를 형성하고있고

데이터에 종속된다던가 다른 종속된 데이터가 변경되면 해당뷰만 invalidated(무효화) 되면서 

dependency graph상에서 연결된, 새 body가 필요한 뷰만 효율적으로 업데이트함


다시 돌아와서

 

Dependencies

 

각 뷰에 자식뷰가 딸려있고 그래프는 leaf뷰에 이를 때 까지 이어집니다.

Image, Text, Color 같은 뷰 가 되겠네요

모든 뷰는 결국 leaf뷰로 끝이납니다

 

모델에서 데이터가 바뀌면 SwiftUI가 뷰를 업데이트하는데 이과정을 자세히 살펴봅시다

뷰는 동적프로퍼티인 @Environment 프로퍼티 래퍼(isPlayTime), 부모가 생성한 값(dog) 모두에 종속(dependency)돼있음

 

뷰의 동적프로퍼티를 모두 업데이트해서 값을 이 그래프에 있는 값으로 바꾸고 업데이트한 값을 이용해서 body가 실행되며 뷰의 자식을 생성합니다.

 

데이터를 변경하면 복사본이 새로 만들어지면서

VStack의 새 content를 생성하게되고

그에 따라 VStack의 자식도 업데이트됩니다.

여기서는 하나의 뷰에만 초점을 맞췄지만 dog값에 종속되는 다른뷰들도 업데이트 될 수 있습니다.

 

이 과정을 개선할수 있는 팁으로

 

업데이트 횟수를 줄여서 꼭 필요한 때만 해야합니다.

뷰가 언제 업데이트되는지 알기위해서는 Self._printChanges 메서드를 통해 뷰의 body를 호출했는지 출력할 수 있습니다.

뷰에 breakpoint를 설정하면 빌드후 lldb콘솔에서 호출할 수 있습니다.

콘솔에

expression Self.printChanges()

를 입력하면 뷰의 body를 요청한 이유를 최선을 다해서 설명해준다고하네요

이 메서드를 이용해서 뷰에 다른 종속성이 있는지 확인도 할 수 있습니다.

 

주의해야할 점으로는

이 호출은 나중에 꼭 없애야합니다.

_기호가 붙어있어서 계속 존재하리라고 보장할 수없고

런타임 성능에도 영향을 미칩니다.

앱스토어에 절대 제출하지말라고 합니다!

 

 

이제 성능개선을 위해서 위의 코드를 개선시켜봅시다.

ScalableDogImage(dog) -> ScalableDogImage(dog.image)
Text -> DogHeader

dog 종속성을 제거하고 필요한 종속성만 남겼습니다.

이렇게하면 여러 이점이 존재합니다.

코드읽기가 더 쉬워지고 DogHeader의 종속성이 사용위치에서 들어납니다.

 

작은 뷰에서는 잘 작동하지만 구조체가 커지면 조심해야합니다.

모든 종속성이 이렇게 스코핑할 가치가 있진않으니 잘 판단해야합니다.

 

View Update Tips

 

업데이트가 적다는 것은 앱에서 데이터가 바뀔 때 성능이 더 좋다는 의미입니다.

종속성을 줄이면 성능을 높일 수 있습니다.

뷰값이 실제로 종속된 데이터에만 한정되도록 줄여보는것도 좋다.

 

뷰를 추출해서 종속성을 줄이는 방법도 있다(새로운 뷰타입으로 분리)

 

Observable 프로토콜도  종속성중 읽히는것만 자동으로 남겨놓기 떄문에 종속성 스코핑에 유용하다.

 

 

Faster view updates

매번 업데이트하는 비용을 절감하는 방법

 

업데이트 속도가 느리면 생기는 부정적인 영향으로는

- 반응성 감소

- hang, hitches

 

hang이란?

뷰가처음 나타날때까지 너무오랜시간이 걸리는것처럼 사용자의 상호작용에 대한 반응이 늦어지는것

 

hitches란?

스크롤이 도중에 멈추거나 애니메이션 프레임을 건너뛰는것 처럼 사용자가 감지할수 있는 애니메이션 문제

 

 

Common Sources Of Slow Updates(느려지는 원인)

- 동적 프로퍼티 인스턴스화가 너무 비싼경우

상태객체를 할당하고 초기화하거나 상태를 초기화하는것

 

- body가 비싼경우

body내에서 작업하거나 비싼 문자열 보간이나, 데이터필터링 및 기타작업 등의 연산이 있는지 확인해보세요!

body는 가능한 싸게 만드는 것이 중요합니다.

 

- 뷰의 body에서는 identification(식별)도 종종 늦어지곤합니다.

 

 

Other Hidden Sources Of Work

모르는 사이에 작업의 여러소스가 앱에 영향을 미칠 수 있음

- 문자열 보간은 비싸기 쉬우니 자주 쓸필요가 없다면 반드시 캐싱해야합니다.

- Bundle에서 값을 찾는것도 비싼 작업입니다.

- class타입 할당을 비롯해 어떤 heap할당이라도 누적 될 수 있습니다.

 

Identify In List And Table

단순한 레이아웃 뿐만아니라 선택, swipe동작, 순서바꾸기 그외에도 많은걸 추가했습니다.

복잡하고 고급인 이런 제어기능이 앱에서 잘 작동하려면 identity를 이해하는건 필수입니다. 

 

macOS Sonoma와 iOS17에서 SwiftUI는 필터링과 스크롤등 여러가지 사항을 내부적으로 개선했다고하네요!

 

 

더 나은 성능으로 이어지는 리스트와 테이블을 구성하는 법은 아래와 같습니다.

identity를 이용해서 데이터가 어떻게 바뀌었는지 알아내고

일관성을 위해 리스트와 테이블의 identity는 모두 즉시 수집되므로

리스트와 테이블의 content의 identity를 빨리 생성할 수 있다면 로드 및 업데이트 시간도 곧바로 빨라집니다.

 

identity는 SwiftUI가 뷰 lifetime을 관리할 수 있게 도와줍니다.

이건 계층 구조를 점차 업데이트하는데 필수적입니다.

identity가 바뀌었다는것은 뷰가 바뀌었다는 뜻인데 이 점은 애니메이션과 성능적인 면에서도 중요합니다.

 

식별자가 자주 수집되기때문에 식별성능은 중요합니다.

 

 

리스트를 사용할때 리스트는 몇행을 표시해야할지, 각행의 identity가 무엇인지 알아야하므로

리스트는 데이터 컬렉션을 곧바로 찾아가서 각 element의 identity를 결정합니다.

필요할 때마다 만들어진 행은 보여지는영역과 상관관계를 맺습니다.

뷰를 스크롤하면 더 많은 뷰가 존재하게됩니다.

 

 

만약 if조건문을 이용해서 강아지를 필터한다고 가정했을 때 위와 같이 생각할 수 있지만

이것은 좋지않습니다.

리스트가 행 identity를 가져오려면 결국 모든 뷰를 빌드 할 수 밖에 없기때문입니다.

모든행을 생성해야만 하기때문에 좋지않습니다.

 

이러한 작업을 컬렉션으로 옮긴다면 어떨까요?

그래도 좋지않습니다.

규모가 커지면 연산이 더 비싸지면서 업데이트가 느려집니다.

 

 

모델로 빼서 사용하는것이 좋은 방법입니다.

필터가 캐싱되기도하고 element당 뷰의 갯수도 일정하기때문에

양쪽 모두의 이점만 누릴 수 있습니다.

 

 

Tips For Ensuring Constant Count

뷰 개수를 일정하게 유지하는 팁

주의할점으로는 이런 접근법은 리스트와 테이블안에서 ForEach를 사용할 떄만 유효합니다.

- AnyView나 조건문은 사용하지말기

- 명시적 스택은 적절한경우에만 사용하기, listRowBackground같은 몇몇 수정자는 스택 안이 아니라 스택 뒤로 가야한다는것을 기억해야합니다.

- 중첩된 ForEach구성은 가능한한 평면화해서 사용해야합니다. 단, 섹션화된 리스트인경우는 예외입니다.

 

리스트는 모든 identity를 가져와야하지만 여기서는 섹션이 사용됐기때문에 SwiftUI는 이 구성을 이해하고

리스트가 빨리 렌더링 될 수 있도록합니다.

 

element당 뷰의 개수는 반드시 상수로 유지해야합니다.

안그러면 SwiftUI가 식별자에다가 뷰까지 빌드해야 비로소 행을 식별 할 수 있기 때문입니다.

 

리스트 이야기만 했는데 테이블도 동일하게 적용됩니다.

TableRow는 항상 단일 행이므로 행의 촉개수는 컬렉션의 element개수와 동일합니다.

이 구성은 흔하기때문에 iOS17에서 새로제공하는 간소화한 이니셜라이저를 사용하면 그냥 쓸 수 있습니다.

이니셜라이저가 TableRow를 대신 만들어줍니다.

 

 

iOS16에서는 각행이 행의 값으로 식별됐는데

iOS17에서는 성능개선을 위해서 동작방식이 변경됐습니다.

TableRow를 식별하기위해 ForEach를 조사할 필요가 없기때문에 TableRow의 값대신 강아지 각각의 id가 사용됩니다.

 

 

 

 

 

 

 

반응형