SwiftUI에서 불필요한 렌더링 제거하기(2)

0

SwiftUI 렌더링 개선

목록 보기
2/8

서론

https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기
앞선 포스팅에서 렌더링이 다시 일어남을 확인하는 방법, 모듈화를 통해 이를 해결하는 방법을 알아보았습니다.
이번에는 이를 좀 더 깊이 알아보는 시간입니다.


Computed property 값의 변화에 View를 다시 그리는 방법

우선 두 개의 버튼을 가진 View를 만듭니다.
각각의 View는 @State 값을 변경시키지만, 해당 값이 View에 미치는 영향이 없기 때문에 View는 다시 렌더링하지 않습니다.

import SwiftUI

struct ContentView: View {
    @State private var isFirstButtonOn: Bool = false
    @State private var isSecondButtonOn: Bool = false
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button("First Button") {
                isFirstButtonOn.toggle()
            }
            Button("Second Button") {
                isSecondButtonOn.toggle()
            }
        }
    }
}

여기서 두 @State 값으로 연산하는 computed property를 만듭니다.
이렇게 되면 각각의 @State 값의 변화에 View는 자신을 다시 그리게 됩니다.

import SwiftUI

struct ContentView: View {
    private var isBothButtonOn: Bool {
        isFirstButtonOn && isSecondButtonOn
    }
    @State private var isFirstButtonOn: Bool = false
    @State private var isSecondButtonOn: Bool = false

    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button("First Button") {
                isFirstButtonOn.toggle()
            }
            Button("Second Button") {
                isSecondButtonOn.toggle()
            }
            Text("&& = \(isBothButtonOn ? "true" : "false")")
        }
    }
}


ViewModel의 @Published 값이 변경되면 모든 View가 업데이트 됨

부모 View가 ViewModel을 소유한 상황에서 ViewModel 값의 변화로 부모 View를 업데이트하는 상황입니다.
먼저 자식 View가 해당 ViewModel을 참조하지 않는 상황을 만들면, 당연히 ViewModel의 값의 변화에 자식 View가 다시 그려지지 않습니다.

import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0
    
    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button("count = \(viewModel.count)") {
                viewModel.upCount()
            }
            AnotherView()
        }
    }
}

struct AnotherView: View {
    var body: some View {
        let _ = Self._printChanges()
        Text("나는 값이 안변하는데")
    }
}

자식 View가 아무런 이유 없이 ViewModel을 참조한다고 하면, 신기하게도 @Published 값의 변화에 자식 View가 다시 그려집니다.
자식 View가 해당 값의 변화에 다시 그려질 필요가 없는데도 말이죠!

import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0
    
    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button("count = \(viewModel.count)") {
                viewModel.upCount()
            }
            AnotherView(viewModel: viewModel)
        }
    }
}

struct AnotherView: View {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        let _ = Self._printChanges()
        Text("나는 값이 안변하는데")
    }
}


손자 View는 업데이트 해야하지만, 자식 View는 업데이트 하지 않으려면

위에서 알아보았듯 ViewModel이 업데이트 되면 해당 ViewModel을 소유하고 있는 모든 View는 다시 그려져야 합니다.
자식 View와 손자 View가 있는 경우 자식 View를 업데이트 하지 않고 현재 View와 손자 View만 업데이트 할 수 는 없을까요?
우선 ViewModel을 @ObservedObject로 가지고 이를 주입하게 되면 문제가 발생합니다.

import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0
    
    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button("count = \(viewModel.count)") {
                viewModel.upCount()
            }
            AnotherView(viewModel: viewModel)
        }
    }
}

struct AnotherView: View {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Text("나는 값이 안변하는데")
            OtherView(viewModel: viewModel)
        }
    }
}

struct OtherView: View {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        let _ = Self._printChanges()
        Text("나는 변해야 해 count = \(viewModel.count)")
    }
}

해결방법은 간단합니다. 자식 View가 ViewModel을 참조하지 않으면 됩니다.
이를 위해 @EnvironmentObject를 사용합시다.

import SwiftUI

final class ViewModel: ObservableObject {
    @Published private(set) var count: Int = 0
    
    func upCount() {
        count += 1
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Button("count = \(viewModel.count)") {
                viewModel.upCount()
            }
            AnotherView()
                .environmentObject(viewModel)
        }
    }
}

struct AnotherView: View {
    var body: some View {
        VStack {
            let _ = Self._printChanges()
            Text("나는 값이 안변하는데")
            OtherView()
        }
    }
}

struct OtherView: View {
    @EnvironmentObject private var viewModel: ViewModel
    
    var body: some View {
        let _ = Self._printChanges()
        Text("나는 변해야 해 count = \(viewModel.count)")
    }
}


오늘의 결론

SwiftUI는 참 편리하지만, 완벽하게 개발하려면 정말 많이 생각하고 고려해야하는것 같습니다.
@State 값의 변화에 모듈화되지 않은 모든 View가 다시 렌더링되고,
@ObservedObject 값의 변화에는 반드시 모든 View가 다시 그려집니다.
단순히 View만 모듈화를 잘 하면 되는것이 아닌, ViewModel까지 모듈화를 잘 해야 합니다.
개발 편의를 위해 하나의 ViewModel을 여러 View에서 참조하도록 개발하는 경우가 많은데, 이 때 View가 다시 그려지는 것에 많은 주의가 필요합니다.

다음 포스트 : https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기3-다른-View의-State-값-변화로-인해-View가-초기화되는-현상

profile
https://github.com/sustainable-git

0개의 댓글

관련 채용 정보