SwiftUI

[SwiftUI] 뷰의 성능 개선(3) -Identifier의 중요성 | WWDC23. Demystify SwiftUI performance

하이D:) 2024. 12. 6. 08:47

 

> 뷰의 성능 개선 시리즈

[SwiftUI] 뷰의 성능 개선(1) - Dependency 관리
[SwiftUI] 뷰의 성능 개선(2) - 뷰의 slow update 개선
[SwiftUI] 뷰의 성능 개선(3) -Identifier의 중요성
 

 

뷰의 성능에 대해 말하는 WWDC23 Demystify SwiftUI performance 세션에서 다루는 내용 중 마지막은 List와 Table의 Identifier에 관련한 내용에 대한 이야기이다. 특히나 List의 요소로 들어가는 것들이 어떻게 ID를 가지게 되고 어떤 식으로 동작하는지와 이 때문에 어떤 식으로 List와 ForEach를 사용하면 좋은지에 대한 언급을 한다. 아무래도 List의 경우에는 사용되는 화면이 많고 대량의 데이터를 로드할 경우가 있다 보니 자칫하면 성능적으로 많은 손실이 생길 수 있기 때문에 성능 개선을 위한 포인트로 강조해서 말하는 것 같다!

 

SwiftUI에서 뷰의 identifier에 대한 전반적인 관점과 처리 방법을 알고 싶다면 WWDC21 Demystify SwiftUI을 보는 것도 추천한다!

 

 

 

 

📍iOS17에서의 최적화

이 세션에서는 iOS17에서 나온 최적화에 대해 일단 자랑(?)하고 넘어가는데:D

  • 필터링 및 스크롤과 같은 경우에 대비해 다양한 최적화를 추가했고
  • 대량의 데이터를 가진 List와 Table의 업데이트가 빠르게 반응할 수 있도록 개선되었다고 한다.

 

 

 

 

 

 

📍List, Table에서 식별자(identifier)의 의미

  • List와 Table은 데이터에 어떤 변화가 생겼는지 알기 위해 식별자(identifier)를 사용한다.
  • 식별자는 바로 수집된다. (이 말은, List에서 스크롤 아래 있는 뷰의 content는 보일 때 생성(on-demand)되자만, 이와 다르게 식별자는 on-demand 하게 이루어지지 않고 바로 수집된다는 뜻! )

이 때문에 식별자를 빠르게 생성하는 게 List와 Table의 로드와 업데이트 시간이 빨라진다는 것을 의미한다

 

 

 

 

📍식별자(identifier)의 중요성 

 Identity helps SwiftUI manage view lifetime, which is crucial for incremental updates to your hierarchy.

 

  • identifier는 SwiftUI가 뷰의 lifetime을 관리하도록 돕는다.
    • ⇒ 그렇기 때문에 뷰 계층구조에 대한 업데이트(incremental update)될 때 중요한 게 identifier이기도 하다
  • 그러므로 idenfifier는 
    • ⇒ 애니메이션에 중요! : 새로운뷰(즉, 다른 식별자)에 대한 업데이트인지, 같은뷰(같은 식별자)에 대한 업데이트인지에 대한 판별을 하여 애니메이션을 적용한다.
    • ⇒ 성능에 중요 : 특히나 List와 Table에서는 식별자가 자주 수집되기 때문에 식별 성능(Identification performance)이 중요!

 

 

 

 

 

 

📍ForEach + List 동작 방법

⭐️⭐️ 여기가 별표 백만 개!! 이 걸 이해해야 identifier가 빠르게 수집될 수 있는 조건을 이해할 수 있음!! ⭐️⭐️

 

이런 식으로 List 안에 ForEach가 있는 구조는 성능 평가에 중요한 지점이다 그 이유를 천천히 보면

 

 

  • ForEach 구조체
    • ForEach 구조체의 생김새를 보면, 제네릭 형태 =>  struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable
    • 데이터 컬렉션을 결과 뷰에 맵핑하고 각 뷰에 대해 명시적인 식별자(explicit identity) 제공한다. ( explicit identity와 structural identity가 뭔지 궁금하다면 위에서도 말한 WWDC21 Demystify SwiftUI 이 세션 추천!)
  • List
    • List를 사용할 때는 내부에 몇 개의 요소(열, Row)가 들어가는지, 각 열이 어떤 식별자를 가지는지 파악해야 하는데 그러기 위해서는 ForEach는 데이터를 미리(up front) 방문해서 각 요소의 식별자를 확인해야 하는 것이다.
    • 그리고 나서 각각의 뷰를 생성하기 위해 ForEach의 content 클로저를 호출한다 

 

 

List의 row는 필요할 경우 요청으로 (on-demand) content+identity를 합성해서 row를 생성한다

 

이런 식으로 스크롤하면 더 많은 영역의 row가 생성된다

 

 

 

 

 

 

 

 

📍효율적으로 ID를 알기 위한 조건

List는 모든 행의 ID를 미리(up front) 알아야 하기 때문에 ForEach에서는 List에서 쓰이는 row의 최종 id를 확인하는 게 매우 중요!

 

근데 모든 content에 방문하지 않고 효율적으로 ID를 알려면 조건이 있다고 한다. ( content는 on-demand로 동작하기 때문에 미리 알 필요가(미리 생성할 필요가) 없기 때문)

  • List의 행의 갯수( = 데이터컬렉션의 요소 수 * 요소 별 뷰의 개수 )가 정해져 있어야 한다 → 즉, 데이터 각 요소 별로 뷰가 고정되어 있어야 한다.
  • 이와 반대로 데이터 요소 별 뷰의 개수가 정해져 있지 않으면 List에서 파악하는 row의 개수가 일정하지 않기 때문에 List의 입장에서 식별자를 알기 위해 content가 on-demand로 동작하는게 아닌,모든 뷰를 빌드해야하기때문에 성능이 안좋아진다는 것이다

 

아래에서 각 요소의 뷰의 갯수가 매번 달라지는 것(Variable views per element), 각 요소의 뷰의 개수를 알 수 없는 것(Unknown views per element), 각 요소의 뷰의 개수가 고정되어 있는 것(Constant views per element)의 예시를 설명하겠다,

 

 

 

▶️ 뷰의 갯수가 매번 달라지는 것(Variable views per element)

This is bad because it results in list needing to build all the views to retrieve the row identifiers because it doesn't know how many views each element resolves to.

 

이런 식으로 내부에 그려지는 row는 조건부로 그려짐으로써 row 개수가 Data로 파악되는 것의 개수와 달라지면 좋지 않다. 이 코드는 데이터 컬렉션의 각 요소에 대해 결정되어야 하는 뷰의 개수를 알 수 없기 때문에, row identifier를 검색하기 위해 모든 뷰를 빌드해야 하는 결과를 낳기 때문에 성능적으로 좋지 않다.

 

> 조건문의 결과에 따라 각 데이터 요소에 대한 뷰의 갯수가 달라짐

 

 

 

 

▶️ 각 요소의 뷰의 개수를 알 수 없는 것(Unknown views per element)

AnyView는 SwiftUI 입장에서 뷰에 대한 정보를 모르기 때문에 어떤 구조로 , 몇 개의 뷰가 들어가 있는지 몰라서 List가 총 row의 개수를 모르게 되고 위의 경우와 동일하게 row identifier를 검색하기 위해 모든 뷰를 빌드해야 하는 결과를 낳는다.

이렇게 때문에 ForEach의 content를 넣는 클로저에 AnyView도 넣으면 안 좋은 것!!

 

 

swiftui에서 AnyView의 사용을 지양하는 이유도 함께 보면 좋을 것 같다

https://heidi-dev.tistory.com/64

 

 

 

 

▶️ 각 요소의 뷰의 개수가 고정되어 있는 것(Constant views per element)

1번 경우를 개선하고 각 요소의 뷰의 개수가 고정되어 있도록 하기 위해서는 아래 코드처럼 data에 내부적으로 필터링을 할 수도 있다. 하지만, 비용이 많이 들 수 있으며 업데이트 속도가 느려질 수 있다. (런타임에 계속 필터링될 수 있따).

 

 

즉, 앞선 블로그 글에서 작성했던 Slow Update의 원인이 될 수 있기 때문에 외부에서 필터링한 데이터를 캐싱해 놓고 사용하자!

 

 

 

 

 

 

📍뷰의 개수를 일정하게 하기 위한 팁!

  • ForEach 안에 AnyView 사용하지 않기
    • 위에서 Unknown views per element의 예시로 보여준 AnyView는 SwiftUI 입장에서 뷰에 대한 구조/정보를 모르기 때문에 (어떤 구조로 , 몇 개의 뷰가 들어가 있는지 몰라서)
    • → List가 총 row의 개수를 모르게 된다.
    • → ID를 알리 위해 모든 뷰를 빌드해야 한다.
  • 명시적인 Stack 사용
  • ForEach 내부 구조가 nested되지 않게!

 

 

 

 


 

 

 

📍요약

종합해 보면,

  • List는 모든 행의 ID를 미리(up front) 알아야 하는데,  모든 content를 생성하지 않고도 identifier를 수집할 수 있어야 List가 빠르게 동작
  • 이게 가능하도록 하기 위해서는
    • ForEach으로부터 생성되는 행 개수를 일정하게 하는 게 중요하다. ( ForEach으로부터 생성되는 행 개수 = 데이터 컬렉션의 요소 수 * 각 요소에 대해 생성된 뷰 수 )
    • 데이터 각 요소별로 생성되는 뷰의 개수와 구조에 대해 SwiftUI가 잘 인지할 수 있도록 설계해서(AnyView사용하지 않기, 조건부로 row 그려주지 않기 등) 총 row의 개수를 파악할 수 있도록 하자