Lazy Loading View 만들기

SteadySlower·2022년 6월 2일
1

SwiftUI

목록 보기
9/64

참고 자료 🙏

SwiftUI NavigationLink loads destination view immediately, without clicking

Lazy Loading View가 필요한 이유

예를 들어 아래와 같은 LazyVStack이 있다고 합니다. 리스트의 역할을 하고 있고요. 각각의 셀은 DetailView로 연결된 Navigation Link입니다.

struct ContentView: View {

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(0..<100) { _ in
                    NavigationLink {
                        DetailView()
                    } label: {
                        Text("To Detail")
                            .font(.system(size: 50))
                    }
                }
            }
        }
    }
}

그런데 DetailView를 보니 해당 view를 init하기 위해서는 뭔가 무거운 네트워크 작업을 해야하는군요. 사용자가 DetailView를 들어오기 전까지는 필요 없는 네트워크 작업인데도 말이죠.

struct DetailView: View {
    
	// init할 때 필요한 작업
    init() {
        heavyAPICall()
    }
    
    var body: some View {
        Text("Detail View")
    }
    
		// 무거운 네트워크 작업
    func heavyAPICall() {
        print("Too Heavy API Called 😥")
    }
    
}

위 코드를 실행해보면 아직 DetailView를 띄우지 않았는데도 미리 DetailView를 전부 init 해놓았습니다. 물론 VStack 이기 때문에 화면에 보이는 NavigationLink만 init했지만 여전히 성능 문제가 발생할 수 있습니다. 사용자가 보고자하는 Detail은 고작 몇개에 불과할텐데 전부다 무거운 네트워크 동작으로 init 해놓는다면 메모리의 낭비이자 네트워크 자원의 낭비입니다.

해당 페이지에 들어갈 때 init되도록 만들자!

LazyView의 등장!

우리의 목표는 NavigationLink의 destination이 해당 View에 진입할 때 init되도록 만들고자 합니다. 따라서 destination에 해당하는 View를 아래 View로 한단계 감싸도록 합시다.

struct LazyView<Content: View>: View {
    // 실제 View를 함수 형태로 받는다.
    let build: () -> Content
    
    // init할 때 build함수를 실행하지 않고 property에 저장만 한다.
    init(_ build: @autoclosure @escaping() -> Content) {
        self.build = build
    }
    
    //⭐️ 실제 SwiftUI가 View를 보여줄 때 접근하는 body property에서 build 함수를 실행한다.
        //👉 이렇게 하면 실제 화면에 진입하기 전에는 View가 init되지 않는다!
    var body: Content {
        build()
    }
}

코드의 원리는 다음으로 추정됩니다.

  1. SwiftUI는 필요한 View의 인스턴스를 init해서 메모리에 가지고 있다가 실제 화면에 구현해야할 때 비로소 body에 접근해서 View를 랜더링합니다. (body는 computed property이므로 접근하기 전에는 실행되지 않습니다.)
  2. 따라서 LazyView는 init될 때 내장된 View의 인스턴스를 만들지 않고 View를 만드는 함수를 build라는 이름으로 property로 가지고 있습니다. 이렇게 하면 LazyView가 init이 되더라고 내장된 View의 인스턴스는 만들어지지 않습니다.
  3. LazyView를 실제로 화면에 띄울 때 SwiftUI가 body에 접근합니다. 이 때야 비로소 build 함수를 실행하면서 내장 View의 인스턴스를 만듭니다.

참고

@autoclosure: 함수만 전달해도 자동으로 클로저를 만들어 줍니다.

//😱 @autoclosure가 없다면 아래와 같이 클로저의 형태로 전달해야 합니다.
LazyView({ DetailView() })

//😆 @autoclosure가 있다면 아래와 같이 함수의 형태로 전달해도 됩니다.

LazyView 활용하기

원하는 View를 LazyView로 한단계 감싸서 사용합니다.

이제 콘솔의 내용으로 보면 처음에는 어떤 DetailView도 init되지 않고 터치해서 화면에 띄운 DetailView만 init되는 것을 볼 수 있습니다.

struct ContentView: View {

    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack {
                    ForEach(0..<100) { i in
                        NavigationLink {
                            LazyView(DetailView(num: i))
                        } label: {
                            Text("To Detail")
                                .font(.system(size: 50))
                        }
                    }
                }
            }
        }
    }
}

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글