불필요한 업데이트를 유발하는 프로퍼티

GUNDY·2024년 1월 23일

SwiftUI에서 종속성은 Swift 프로퍼티와 SwiftUI의 동적 프로퍼티 등의 저장 프로퍼티로 인하여 발생한다. 종속성인 데이터가 변화하면 SwiftUI의 추적 덕분에 해당 뷰가 업데이트 됨은 쉽게 알 수 있을 것이다.

SwiftUI는 이를 종속성 그래프로 만들어 해당 데이터가 변했을 때 종속성에 의해 업데이트 할 뷰를 추적할 수 있다. 위 그래프에서는 파란색이 종속성을 나타내는 데이터이고, 녹색이 뷰이다.

이렇게 특정 종속성의 값이 변경되면 그래프에 연결된 뷰들의 body가 호출돼 업데이트하게 된다.

이 때 body 내부에 나열된 하위 뷰들의 이니셜라이저가 호출될텐데, 어떻게 업데이트를 하거나, 방지할 수 있을까?

이것은 DogView의 코드와 DogView로 시작하는 뷰 계층 구조를 표현한 트리이다. 각 화살표는 뷰의 body를 나타낸다. 더 이상 body에서 호출하게되는 하위 뷰가 없는 리프 뷰(이미지, 버튼 등)까지 도달하면 끝난다.

분명 body 내부에서 호출되는 각 뷰의 이니셜라이저로 인데 구조체의 인스턴스가 생성됨이 분명하다. 그런데 어떻게 업데이트를 진행하지 않는 걸까?

하위 뷰의 업데이트는 세 단계로 구분된다.

  1. 저장 프로퍼티를 초기화한다. 즉, 이니셜라이저가 호출된다.
  2. 동적 프로퍼티에 대한 새 값이 있는 경우 해당 값으로 업데이트 한다.
  3. 앞선 두 단계를 거친 인스턴스가 기존 뷰 인스턴스와 다르다면 body가 실행되어 업데이트한다.

그럼 여러 상황에 따른 업데이트 판정을 살펴보자. 하위 뷰에는 상위 뷰의 업데이트를 트리거한 종속성 값에 대한 종속성이 없다고 가정하겠다.

프로퍼티 기본 값이 있는 경우

처음으로 프로퍼티에 대한 기본 값이 있는 경우를 상정해보겠다.

1. Swift 프로퍼티

값 타입의 경우

struct SomeSubView: View {
    private var someProperty = SomeStruct()
    
    var body: some View {
        // someProperty를 읽은 뷰
        // 불변이기 때문에 body 내부에서 someProperty에 대해 쓸 수 없다.
    }
}

이 경우 프로퍼티를 주입받지 않고 직접 생성하게 된다. 하지만 SomeStruct에 대한 기본 이니셜라이저는 항상 같은 인스턴스를 생성하기 때문에 SomeSubView의 이니셜라이저는 호출되지만, 기존 뷰 인스턴스와 달라지지 않아 업데이트를 하지 않는다.

참조 타입의 경우

struct SomeSubView: View {
    private let someProperty = SomeClass()
    
    var body: some View {
        // someProperty를 읽은 뷰
        // 버튼 등으로 프로퍼티를 수정하더라도 스택에 저장된 포인터가 변경되는 것은 아니어서
				// 프로퍼티에 대한 수정이 가능하지만 SwiftUI에서 관리해주는 종속성이 아니기 때문에
				// 변경하더라도 뷰가 업데이트 되지 않는다.
    }
}

이 경우는 얘기가 다르다. 앞선 값 타입 프로퍼티는 매번 그 인스턴스가 같겠지만 참조 타입이라면 힙에 할당되면서 실질적으로 스택의 포인터가 가진 메모리주소가 달라질 것이다. 그러므로 기존의 뷰 인스턴스와 달라지기 때문에 body가 실행되어 업데이트 된다.

2. 동적 프로퍼티

State, StateObject

struct SomeSubView: View {
    @StateObject private var someProperty = SomeClass()
    
    var body: some View {
        // someProperty를 읽은 뷰
        // 버튼 등으로 해당 프로퍼티의 값을 수정할 수 있고 수정하게 되면
				// 종속성의 업데이트로 인해 바로 이 뷰에 대한 업데이트가 트리거된다.
    }
}

State나 StateObject를 통해 동적 프로퍼티를 만들면 뷰가 초기화될 때 뷰의 ID로 관리중인 상태 값을 가져오므로 기본 값을 새로 만드는 것을 방지한다. 그렇기 때문에 기존 뷰 인스턴스와 값이 달라지지 않으므로 뷰의 body는 호출되지 않는다.

ObservedObject

struct SomeSubView: View {
    @ObservedObject private var someProperty = SomeClass()
    
    var body: some View {
        // someProperty를 읽은 뷰
        // 버튼 등으로 해당 프로퍼티의 값을 수정할 수 있고 수정하게 되면
				// 종속성의 업데이트로 인해 바로 이 뷰에 대한 업데이트가 트리거된다.
    }
}

이 경우는 상태 변수와 달리 기본 값을 새로 만드는 것이 방지되지 않는다. 그렇기 때문에 새로 만들어진 기본 값은 결국 기존의 프로퍼티와 달라진다. 값 타입이 아니기 때문에 만들 때마다 다를 것이다. 즉, 뷰의 body가 호출된다.

프로퍼티 기본 값이 없는 경우 (의존성 주입)

프로퍼티 기본 값이 없다면 상위 뷰에서 값을 전달할 것이다. 이 때 가정했듯이 전달되는 값은 업데이트를 유발한 종속성이 아니기 때문에 대체로 뷰가 변경될 이유가 없다.

1. Swift 프로퍼티

값 타입의 경우

예상하듯이 값 타입은 전달되는 값으로 인해 새 뷰 인스턴스가 달라질 일이 없으므로 업데이트되지 않는다.

참조 타입의 경우

struct SomeSuperView: View {
    ...
    var body: some View {
        ...
		SubSubView(someProperty: SomeClass())
		...
    }
}

상위 뷰에서 참조 타입의 프로퍼티에 할당할 참조 값을 넘겨줄 때 이니셜라이저를 호출해서 전달한다면 문제가 된다. 앞서 살펴본 프로퍼티 기본 값으로 참조 타입의 이니셜라이저를 호출해주는 것과 같다. 하위 뷰의 body가 호출된다.

2. 동적 프로퍼티

동적 프로퍼티 또한 참조 타입에 대한 프로퍼티 래퍼인지, 이니셜라이저를 호출해서 전달하는지 등의 요인으로 업데이트가 결정된다.

프로퍼티로 해당 값을 가지지 않는 경우

타입 프로퍼티나 전역 변수등으로 인해 뷰 구조체가 직접 프로퍼티를 소유하지 않는다면 해당 값을 읽어서 뷰를 만들 수는 있지만 업데이트를 유발할 수 없다. SwiftUI가 종속성으로 추적해주지 않기 때문이다.

다른 프로퍼티 등으로 인해 업데이트 될 경우, 즉 body가 호출될 때만 해당 값들에 대한 업데이트가 이루어진다.

마무리

오늘은 SwiftUI에서 하위뷰의 불필요한 업데이트를 유발할 수 있는 경우를 정리해보는 시간을 가졌다. 불필요한 업데이트가 있으면 성능에 영향을 미치는 요인이 될 수 있다.

언제나 성능은 중요한 이슈이기 때문에 불필요한 업데이트를 방지해 더욱 성능적으로 우수한 앱을 만들도록 하자.

profile
개발자할건디?

0개의 댓글