Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[정주] WWDC23 ‐ Demystify SwiftUI Performance 정리

유정주 JeongJu Yu edited this page Jul 24, 2024 · 1 revision

배경

  • 앱이 복잡해질수록 성능 문제가 중요함
  • 개발 초기부터 빠른 코드를 작성하는 법을 이해하면 앱이 복잡해질 때 생기는 문제가 줄어듬
  • 피드백 루프
    • 성능 문제는 증상에서 시작함
    • 문제 해결의 시작은 측정임
    • 그다음엔 증상의 원인을 파악해야함
      • 일이 어떻게 돌아가야 하는지 직관으로 알아야 하기 때문에 어려운 단계임
      • 버그는 앱에 틀린 전제가 있을 때 발생함
      • 이 세션은 앱의 전제와 현실 사이의 불일치를 파악할 수 있도록 도와줌
    • 최적화해서 문제를 해결
    • 다시 측정, 확인해서 문제가 해결되었는지 확인
    • 해결되었다면 피드백 루프가 끝난다

종속성

  • 각 자식 뷰는 해당 뷰의 선조가 생성하는 뷰 값에 종속되지만 그 외에도 다른 형태의 종속성이 있음
  • 동적 프로퍼티에도 종속성이 흔하게 발생함
    • e.g. @Environment
  • 뷰 업데이트 순서
    • 뷰에 새로운 값을 생성 → 뷰에 저장된 모든 프로퍼티 포함
    • 뷰의 동적 프로퍼티를 모두 업데이트
    • 업데이트한 값을 이용해 body가 실행, 뷰의 자식을 생성함
  • 값이 새로 바뀌거나 종속성이 바뀐 뷰만 업데이트됨
  • 런타임의 뷰의 변화를 출력하려면 Self._printChanges 메서드를 사용하면 됨
    • lldb expression Self._printChanges()
    • 이 메서드 호출을 body에서 할 경우, 절대 App Store에 제출하면 안 됨
    • 디버깅에만 사용됨, 런타임 성능에 영향을 줌
  • 업데이트를 최적화하려면
    • View가 종속성을 약하게 가져야한다.
      • e.g. 이미지를 표시하는 View → 전체 구조체를 전달 받는 대신, 이미지만 전달 받는다.
    • 뷰를 작게 분리한다.
    • Observable 프로토콜을 사용한다. → 종속성 중 읽히는 것만 자동으로 남김
  • 업데이트가 적으면 성능이 좋아짐

인터페이스를 더 빠르게 업데이트 하는 방법

  • SwiftUI에서 View 업데이트가 늦어지는 경우
    • 동적 프로퍼티 인스턴스화가 너무 비싼 경우
      • 상태 객체의 할당, 초기화
    • body에서 작업을 하는 경우 (body 내부의 로직이 비싼 경우)
      • 문자열 편집, 데이터 필터링 등
    • 보통 이 둘은 별개가 아니라 연관되어 있다.
  • 동기적으로 진행되는 비싼 로직은 async-await과 task를 이용해 비동기로 수행
  • 반복되는 Heap 할당도 줄이면 좋음

리스트와 테이블의 아이덴티티

  • 식별자를 이용해 데이터가 어떻게 바뀌었는지 알아낸다.

  • 일관성을 위해 리스트와 테이블의 ID는 모두 즉시 수집됨

    • 식별자를 빨리 생성할 수 있다면 load 및 업데이트 시간도 빨라짐
    • 리스트와 테이블이 아니더라도 SwiftUI는 식별자를 자주 수집하므로 ID 생성이 빠를수록 좋다
  • 식별자가 바뀌었다 → 뷰가 바뀌었다 → 애니메이션과 성능에 영향을 준다

    • WWDC23 - Explore SwiftUI animations
  • ForEach의 데이터 개수는 일정해야 함 → 필터링이 필요하다면 모델로 정의

    • 안 좋은 예시 → ForEach 내부에서 조건을 줌 → 모든 cell을 확인해야 함 → ForEach 내부에 뷰가 얼마나 정의되어 있는지 알 수 없기 때문

      List {
      	ForEach(dogs) { dog in 
      		if dog.fetchToy == .ball {
      			DogCell(dog)
      		}
      	}
      }
    • 안 좋은 예시2 → dogs 개수가 많아지면 업데이트가 느려질 수 있음

      List {
      	ForEach(dogs.filter(...)) { dog in 
      			DogCell(dog)
      	}
      }
    • 좋은 예시 → 모델로 정의 → 리스트 개수가 일정, 필터가 캐싱되서 업데이트 효율적임

      List {
      	ForEach(tennisBallDogs) { dog in
      		DogCell(dog)
      	}
      }
  • ForEach 내부에는 중첩된 구조 대신 평탄화 하는게 좋음

  • 대신, 섹션화된 리스트는 중첩된 구조가 유용함

    struct DogsByToy: View {
    	var model: DogModel
    	var body: some View {
    		List {
    			ForEach(model.dogToys) { toy in
    				Section(toy.name) {
    					ForEach(model.dogs(toy: toy)) { dog in
    						DogCell(dog)
    					}
    				}
    			}
    		}
    	}
    }
  • TableRow는 항상 단일 행임

    • ForEach에서 TableRow를 사용하는 동작은 iOS 16 이하와 iOS 17 이상이 다름
      • iOS 16 이하: TableRow로 주입된 값의 ID
      • iOS 17 이상: 데이터 각각의 ID
      • dogsTableRow(dog.bestFriend) 라면, iOS 16 이하에선 bestFriend.id를, iOS 17부턴 dogs[i].id를 Table row ID로 사용함
      • 테이블 행을 식별하기 위해 ForEach를 조사할 필요가 없어져서 성능 향상이 됨
      • 버전마다 동작을 같게 하려면 명시적으로 ID를 지정해야 함

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /