[Instruments 맛보기] SwiftUI 성능 최적화

🏁 학습할 내용
- SwiftUI Istruments 구성
- 1차 개선하기
- 2차 개선하기
- 실제 그래프 살펴보기
📂 SwiftUI Instruments 구성

1️⃣ Update Groups

SwiftUI가 작업을 수행하면, 표시된다.
성능을 떨어트리는 원인이 SwiftUI 관련 코드인지, 확인할 때 볼 수 있다.
아래 사진에서 Time Profiler의 CPU 사용량에서 높은 수치를 나타낼 때, Update Groups이 비어있는 것을 볼 수 있다.
해석하면, CPU 사용량의 원인은 SwiftUI 외부에 있을 가능성이 높다.

2️⃣ Long View Body Updates
SwiftUI View의 body 속성 실행시간에 대한 트랙이다.

3️⃣ Long Representable Updates
SwiftUI에서 Representable은 크게 2가지가 있다. (Gesture 제외)

뷰 및 뷰 컨트롤러에서 오래걸리는 업데이트를 식별한다.
4️⃣ Other Long Updates
SwiftUI에서 발생하는 오래걸리는 모든 업데이트를 식별한다.
1️⃣ 1차 개선하기
🤔 문제 상황
현재 View Body Updates에서 높은 수치의 CPU 사용량이 관측됨

LandmarkListItemView가 많은 시간을 차지하기 떄문에, 거기서 우클릭 후, Show Updates를 클릭

Show Updates는 View Body 모든 업데이트가 순차 목록으로 표시된다. 여기서 가장 오랜시간이 나온
2.07ms에 대해서 검사범위 확대를 누른다.

그 결과, 추적에서 선택 영역 View Body 업데이트 간격으로 설정된다.

Time Profiler를 통해 알아본 결과 다음 코드가 문제인데, 원인은 다음과 같다.
- view body text에 distance라는 computed property를 사용하는데
- view body는 메인스레드에서 실행되기 떄문에, 앱 UI 업데이트 하기전에 distance 내용이 끝날 때까지 기다려야함
- 이 계산은 밀리초 정도로 길지 않아보이지만, SwiftUI 화면 업데이트할 뷰가 많을 때, 총 소요시간에는 많은 영향을 준다.

🍎 Apple 플랫폼 render loop
메인 스레드에서 오래걸리는 작업이 왜 문제가 되는지, Apple 플랫폼 랜더 루프 동작 방식과 같이 살펴보자.
1) 매 프레임마다 앱은 터치나 키 입력과 같은 이벤트를 처리하기 위해 깨어난다.
2) UI 업데이트를 진행
3) 앱은 시스템에 작업을 넘김
4) 시스템은 다음 프레임 deadline 전에 뷰를 렌더링
5) 랜더링 출력을 화면에 표현
모든 작업은 각 프레임의 deadline 전에 마감이되야한다.

여기서 만약 무거운 작업으로 인해 프레임 deadline을 넘기게되면??
즉, 다음 업데이트에는 렌더러에게 전달할 화면이 준비되지 않음

결과적으로 이전 프레임이 다음 프레임 렌더링 완료 전까지 계속 표시되어, 프레임을 지연시키는 Frame hitch가 발생

⚠️ 주의할 점
왼쪽 (해결 전) 오른쪽 (해결 후)를 비교해보면, 대부분의 오래걸리는 이슈는 해결이됐다.
하지만 여전히 약간의 주항색 막대가 보인다.
남은 막대는 앱 실행 직후, 시스템이 앱의 초기 뷰 계층 구조를
구축하는 동안 걸리는 내용이므로, 장애가 발생한 내용은 아니다.


2️⃣ 2차 개선하기
🤔 문제 상황
- 백트레이스 파악이 힘듬
- 재 랜더링 범인 찾기
🧱 백트레이스 파악이 힘듬

UIKit
- 명령형 프레임워크
- 백트레이스가 원인과 효과를 디버깅하는데 유용
- viewDidLoad에서 isOn 값이 변경되어, 레이블이 업데이트 된다
SwiftUI

- 재귀적인 업데이트 항목이 많이 보임
- 프레임으로 구분된 무언가안에 AtrributedGraph를 갖고있다.
- 뷰를 업데이트해야하는 이유를 정확히 알 수 없음
🕵️♀️ 재 랜더링 범인 찾기
SwiftUI 업데이트 동작방식은 이 링크를 참고 https://hamp.tistory.com/281
오늘의 핵심!!
View 업데이트의 원인과 결과에 대한 정보를 확보하는 것이 SwiftUI Instruments의 핵심 기능

구성요소

- View Body
- VIew body 아이콘
- VIew body Label(이름.body)

- Arrow
- State Change Node에서, 목적지 노드로
- Label은 Updated와 Creation가 있음

- State Change Node
- 변수 이름
- 연결된 View Label

- Gesture
- 그래프 가장 왼쪽으로 계속 가면, 어떤 제스쳐(이벤트)로 State가 변경됐는 지 알 수 있음
🔍 실제 그래프 살펴보기

- 그래프는 왼쪽 -> 오른쪽으로 읽으며, 원인 -> 결과 형태다.
- 탭 이벤트를 토해 LandmarkCollection이 갱신되며, View body가 다시그려지는, Update가 30번이나 됨


- 각 항목이 즐겨찾기에 추가되었는지를 결정하는 방법ㅇ느, favoriteCollection에 각각 접근해서, 자신이 포함되었는 지 확인
- 이 때, 각 뷰는 간접적으로 즐겨찾기 배열에 접근했기 때문에 종속성이 생김


- 이 때, 즐겨찾기 배열에 변환이 생기면, 종속성이 생긴 모든 항목의 뷰는 Outdated와 Updated를 진행
- 이건 이상적인 동작이 아님, 업데이트된 3번 쨰만 다시 그려져야함



- 배열 전체를 참고하는게 아니라, 각 컴포너트들의 상태를 갖고 있는 ViewModel을 바라본다.
- 이렇게 되면, 수정된 부분만 Updated 진행


- 불필요한 업데이트가 사라진 것을 확인할 수 있음


- EnvironmentValue는 EnvironmentValues 구조체에 저장되는데, 딕셔너리 값과 비슷한 느낌
- 각 View는 EnvironmentValues 구조체에 종속성이 생김
- EnvironmentValue값이 업데이트 되면, 각 View에 알림이 가고, View의 body는 다시그려져야함


- 각 뷰는 자기가 보고 있는 EnvironmentValue의 값이 변경되면, Outdated 시킨 후, 다시 body를 다시그리고
- 보고 있는 값이 변경되지 않았다면, Outdated를 진행하지 않는다.



- EnvironmentValue는 크게 2가지 노드로 구분됨
- External Environment: colorScheme처럼 SwiftUI 외부에서 변경가능한 환경 값
- EnvironmentWriter: SwiftUI 내부에서 업데이트가 발생하는 환경 값
- 하나라도 바뀌면 알람이 오지만, 내가 추적하지 않고 있는 값이라면, Dim 처리되어 있는 것을 확인할 수 있음
- 실직적으로는 업데이트를 진행하지 않음(무시), 업데이트 된 것은 진하게 색깔이 칠해져있음