SwiftUI의 성능 발휘를 위한 팁,

🌈 devleeky16498·2022년 4월 25일
1

먼저 이 아티클은 개발자 martinmitrevski라는 이의 영문을 해석하여 공지함을 알린다. 스위프트UI 또한 하나의 프레임워크로서 잘못된 프로그래밍을 하게 되는 경우 뷰가 버벅거리는 경우를 충분히 발생시킬 수 있다. 이 아티클에서는 SwiftUI를 통해서 쾌적한 성능 발휘를 위해 어떻게 해야 하는지를 다루고 있다.
메인 주소링크 : https://martinmitrevski.com/2022/04/14/swiftui-performance-tips/

뷰 애니메이션 히치(hitch)관리

iOS에서는 애플리케이션의 성능을 측정하기 위한 수치로서 애니메이션 hitch를 사용하고 있다. 이 히치라는 단위는 쉽게 언급한다면 기존에 보여져야하는 시기보다 늦게 보여지는 프레임의 차이 정도를 언급한다고 보면 된다.

원래라면 뷰는 애니메이션 없이 바로 나타내져야 하지만 우리는 애니메이션을 통해서 이 뷰의 프레임을 조절하고 사용자에게 애니메이션을 제공하고 있다.

이는 뷰가 결과적으로는 에니메이션의 히치를 처리하는 과정을 포함하기 때문에 다수의 애니메이션을 뷰에서 처리하게 되면 그만큼 메인스레드의 처리량이 늘면서 성능을 저하시킬 수 있다.

"뷰에 너무 많은 애니메이션 넣지 말기!"

메인스레드에서의 과도한 처리량

우리가 Uikit이나 SwiftUI의 사용에 상관없이, 메인 스레드에서의 과도한 데이터 처리를 사전에 방지해야 한다. 여기에서 가장 중요한 기본 이유는 앱이 느려지기 때문이다.

이러한 실수는 생각보다 발견하기 어려우며, 비동기 함수가 아닌 동기식 함수를 구현하는 과정에서 자주 나오게 된다.

SwiftUI는 뷰를 자주 다시 그려주기 때문에, 이러한 뷰 관련 처리량을 조절하는 것은 성능에 큰 영향을 미친다.

"용도에 따라 적절하게 동기와 비동기식 함수를 구현하고, 메인스레드에서의 과도한 작업을 분산시키려는 노력이 필요!"

너무 많은 계산된 프로퍼티

다음의 코드예시를 보며 이야기를 이어나가고자 한다.

struct CustomModel : Identifiable {
	let id : String
    let attachmentURL : URL?
    
    var attachment : Attachment? {
    	if let attachmentURL = attachmentURL {
        	do {
            	let attachmentData = try Data(contentsOf: attachmentURL)
                let attachment = try JSONDecoder().decode(Attachment.self, from: attachmentData)
            } catch {
            	return nil
            }
       }
       return nil
   }
   
   var hasAttachment : Bool {
      attachment != nil
   }

다음의 코드예시에는 2개의 계산된 프로퍼티가 존재한다. 하나는 attachment에 대한 로드와 관련된 프로퍼티이며, 하나는 attachment가 존재하는지에 대한 프로퍼티이다.

여기에서의 병목은, hasAttachment라는 프로퍼티가 지나치게 많이 호츨된다는 것이다. 우리는 여기에서 굳이 저 수 많은 코드들을 통해서 attachment의 존재여부를 체크할 필요가 없다.

"초기화에 너무 많은 계산을 요구하는 프로퍼티는 성능을 저하시킨다!"

데이터 베이스를 메인 스레드에서 접근할 때,

데이터베이스를 메인스레드에서 직접 접근하는 경우에는 뷰의 성능이 저하될 수 있다. 마찬가지로 데이터 베이스에 대한 접근은 시간을 필요로 한다.

이는 단순한 코드로서의 구현이 아닌, 코드를 통합 응답을 기다리고 응답이 있는 경우 이를 바탕으로 결과물을 구성하는 것이므로, 이 과정에서 뷰는 병목을 일으킬 수 있다.

"메인 스레드에서 데이터 베이스를 접근하는 행위는 성능 저하의 원인이다!"

캐싱(caching)

캐싱을 하는 과정에서 성능 저하가 일어 날 수 있다. 우리는 서버로부터 받아오는 자료들 중, 예를 들어 용량이 큰 이미지들은 캐싱을 통해서 그 대기시간을 최소화하여 유저에게 제공할 수 있다.

하지만 데이터의 숫자가 많고 실시간으로 다양한 액션이 계속해서 일어나게 되는 경우에는 캐싱에 시스템이 소요되므로 이는 성능 저하를 일으킬 수 있다.

"다수의 자료와 다이나믹한 액션에 따른 캐싱은 성능 저하를 일으킨다!"

잦은 뷰 재도시(frequent redrawing view)

SwiftUI에서 버벅임(lagging)을 가져오는 가장 큰 원인 중 하나는 뷰를 자주 다시 그려주는 작업을 수행할 때 그렇다. 특히 큰 컴포넌트나, 아니면 방대한 모디파이어를 지닌 컴포넌트에 대해서는 버벅임이 발생한다.

다음의 예시코드는 이미지의 캐싱에 대한 코드이다.

class ViewModel : ObservableObject {
	@Published var elements = [Element]()
    @Published var images = {String : UIImage]()
    
    func image(for id : String) -> UIImage {
    	if let image = image[id]
        	return image
        }
        loadImage(for : id) { [weak self] image in 
        	self?.image[id] = image
        }
        return UIImage.placeholder
    }
}

struct ListView : View {
	@StateObject var viewModel = ViewModel()
    
    var body : some View {
    	LazyVStack(spacing : 0) {
        	ForEach(viewModel.elemets) { element in
            	ListItem(element : element, image : viewModel.image(for: element.id))
            }
        }
    }
}

위의 코드는 매우 단정해 보이는 코드이다. 여기에서 우리는 우리는 이미지 캐싱을 해주고 있는데, 우리가 언제 새로운 value를 받던 지 이는 뷰를 다시 그려주게 된다. 중요한 건 데이터가 바뀐 값만 다시 그려주어야 하지만 여기에서는 그렇지 않은 모든 데이터도 전부 다시 그려진다는 것이 문제이다. 그리고 이러한 뷰의 재도시는 성능의 저하를 가져온다.

이러한 문제의 경우는 첫번째로 이미지가 로딩되는 로직을 별도의 객체로 분류하여 관리하면 좋다. 그렇게 되면 이미지의 변화만 감지하여 새롭게 그려주기 때문에 처리량이 낮아진다. 또 하나의 방법으로는 데이터의 변화를 감지하는 순간마다 뷰를 새롭게 그리는 것이 아닌, 시간 단위를 명시하여 예를 들어 1초마다 뷰를 다시 그리도록 처리하는 방법이 있다.

"뷰에서 변하는 데이터에 대한 객체를 별도로 분리하여 뷰를 구성하고 뷰의 재도시 빈도를 최대한 낮추자!"

AnyView의 사용

swiftUI에서 AnyView는 타입이 제거된 뷰이다. 따라서 some View보다 더 포괄적인 타입이다. 일종의 뷰에 대한 제네릭이라고 생각한다면 이해가 쉽다. 예시코드와 함께 살펴본다.

struct ContainerView {
	var injectedView : () -> AnyView
    
    var body : View {
    	VStack {
        	//view content
        	injectedView()
        }
    }
}
//다음과 같이 AnyView를 반환하는 함수타입의 변수를 VStack내부에 선언해주었다. 

아주 가끔, 특히 리스트 처럼 다수의 데이터를 다루는 뷰의 경우에는 AnyView를 사용할 경우 충돌이 발생한다. 이유는 swiftUI가 차이를 계산하기 위해 뷰의 타입에 의존하기 때문이다.

따라서 만약 AnyView라는 타입이 없는 뷰를 사용하게 된다면 swiftUI는 뷰는 차이를 어떻게 처리해야 할지 알지 못한다. 그에 따라 뷰를 다시 처음부터 재도시하게 된다. 그리고 이는 매우 효과적이지 못하다.

우리는 이 경우를 제네릭을 사용해서 해결할 수 있다. 만약 삽입할 뷰가 많다면 우리는 뷰팩토리(View Factory, 프로토콜)를 통해서 이를 해결해줄 수 있다.

"AnyView와 같은 타입을 식별하지 못하는 프로퍼티를 사용하지 말자"

protocol ViewFactory {
	associatedtype InjectableView : View
    func makeInjectableView() -> InjectableView
}
//프로토콜을 통해서 요구사항으로 연관된 타입과 함수를 명시했다.

extension ViewFactory {
	func makeInjectableView() -> some View {
    	Text("default view")
    }
}
//확장을 통해서 프로토콜 함수 요구사항을 구체화 해준다.

struct ExampleView<Factory : ViewFactory> : View {
	let factory : Factory
    
    var body : some View {
    	VStack {
        	Text("some content")
            factory.makeInjectableView()
        }
    }
}
//다음에서 factory라는 타입 파라미터를 가지는 뷰를 선언해주고 이를 뷰로 넣어준다.

뷰 컨테이너와 지오메트리의 사용(View containers and GeometryReader)

스크롤뷰 안에 행수가 많은 VStack을 삽입했다고 생각해보자. 당신은 곧 퍼포먼스 이슈가 발생함을 알 수 있다. 왜냐하면 VStack은 뷰를 한번에 그려낸다. 하지만 LazyVStack은 뷰를 한번에 그리지 않고 보일수 있는 부분부터 천천히 순차적으로 그려낸다. 그렇기 때문에 다수의 데이터를 핸들링하는 곳에서는 Lazy스택을 사용함이 좋다.

다음은 지오메트리리더이다. iOS커뮤니티에서는 아직도 이 지오메트리 리더가 뷰컨테이너에 지나치게 과하다는 것을 두고 논란이 많다. 하지만 개발자로서는 가능한 지오메트리 리더보다는 Spacer나 width, height등의 프로퍼티를 사용하는 것을 권장한다. 지오메트리 리더도 기본적으로 계산된 뷰를 그리기 때문이다.

"대다수의 데이터를 다룰때는 Lazy스택을 사용해주자!"
"지오메트리보다 가능한 뷰모디파이어를 통해서 뷰를 조절해주자."

무거운 미디어 파일

처음은 사전에 언급한 것과 같이, 용량이 큰 파일을 로딩하게 되는 경우 앱의 성능은 당연히 떨어지게 된다. 시간을 요구하기 때문이다. 이는 당연한 문제이다. 예를 들어 용량이 제법되는 이미지를 받아오게 되는 경우, 이미지의 용량을 백그라운드 스레드에서 조절하여 뷰로 가져오는 방법이 있다. 대표적으로는 NukeUI처럼 다운로드 후 미디어의 용량크기를 조절하는 경우가 있다.

profile
Welcome to Growing iOS developer's Blog! Enjoy!🔥

0개의 댓글