ViewModel init()이 반복 호출되는 이유: SwiftUI의 @StateObject 올바르게 사용하기

0

SwiftUI 렌더링 개선

목록 보기
5/8

ViewModel init이 지속적으로 호출되는 현상

부모 View에 카운터, 자식 View에도 카운터가 있는 View를 만들어 보았습니다.
부모 View는 @State로 number 값을 변경하여 View를 업데이트합니다.
자식 View는 @StateObject로 ViewModel을 가지고 있고, viewModel의 number 값을 변경하여 View를 업데이트합니다.

그런데, 부모 View의 number값이 변경될 때마다 자식 View의 ViewModel이 매번 init()이 됩니다.
그 이유가 무엇일까요?

import SwiftUI

class ViewModel: ObservableObject {
    @Published private(set) var number: Int = 0
    
    init() {
        print("viewModel initialized")
    }
    
    func upCount() {
        number += 1
    }
}

struct ContentView: View {
    @State private var count: Int = 0
    
    var body: some View {
        VStack {
            ChildView()
            Button("부모 뷰 카운터 = \(count)") {
                count += 1
            }
        }
        .padding()
        .border(.red)
    }
}

struct ChildView: View {
    @StateObject var viewModel: ViewModel
    
    init() {
        let viewModel = ViewModel()
        _viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        VStack {
            Button("자식 뷰 카운터 = \(viewModel.number)") {
                viewModel.upCount()
            }
        }
        .padding()
        .border(.blue)
    }
}

문제의 원인은 @StateObject를 생성 위치에 있다

struct ChildView: View {
    @StateObject var viewModel: ViewModel
    
    init() {
        let viewModel = ViewModel()
        _viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        VStack {
            Button("자식 뷰 카운터 = \(viewModel.number)") {
                viewModel.upCount()
            }
        }
        .padding()
        .border(.blue)
    }
}

위 상황에서 @StateObject 인 ViewModel을 생성할 때 자식 View의 init()에서 생성하고 있습니다.
일반적으로 @StateObject var viewModel = ViewModel() 처럼 선언하지만,
ViewModel 내부에 특정 값을 초기화해야 할 경우에는 View의 init()에서 ViewModel을 생성하게 됩니다.
하지만, 여기서 @StateObject의 초기화 과정을 명확하게 이해하지 못하면 문제가 발생할 수 있습니다.


@StateObject의 init()에는 @autoclosure가 있다.

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@frozen @propertyWrapper public struct StateObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
    @inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
    ...
}

위 코드는 SwiftUI에서 @StateObject가 구현된 부분입니다.
init() 함수에는 @autoclosure가 적용되어 있습니다.
@autoclosure는 wrappedValue에 값을 할당하며, View의 lifecycle 동안 오직 한 번만 호출됩니다.

    init() {
        let viewModel = ViewModel()
        _viewModel = StateObject(wrappedValue: viewModel)
    }

하지만, View의 init() 함수는 자주 호출될 수 있습니다.
때문에 _viewModel = StateObject(wrappedValue: viewModel) 구문은 한 번만 호출되지만,
let viewModel = ViewModel() 구문은 부모 View의 @State 값 변화에 따라 지속적으로 실행됩니다.


해결 방법

이 문제를 해결하는 방법은 두 가지가 있습니다.

  1. @StateObject(wrappedValue:)값을 _viewModel에 직접 대입하는 방법
    init() {
        _viewModel = StateObject(wrappedValue: ViewModel())
    }
  1. @StateObject(wrappedValue:) 로 ViewModel을 만든 후 _viewModel에 대입하는 방법
    init() {
        let viewModel = StateObject(wrappedValue: ViewModel())
        _viewModel = viewModel
    }

import SwiftUI

class ViewModel: ObservableObject {
    @Published private(set) var number: Int = 0
    
    init() {
        print("viewModel initialized")
    }
    
    func upCount() {
        number += 1
    }
}

struct ContentView: View {
    @State private var count: Int = 0
    
    var body: some View {
        VStack {
            ChildView()
            Button("부모 뷰 카운터 = \(count)") {
                count += 1
            }
        }
        .padding()
        .border(.red)
    }
}

struct ChildView: View {
    @StateObject var viewModel: ViewModel
    
    init() {
        _viewModel = StateObject(wrappedValue: ViewModel())
    }
    
    var body: some View {
        VStack {
            Button("자식 뷰 카운터 = \(viewModel.number)") {
                viewModel.upCount()
            }
        }
        .padding()
        .border(.blue)
    }
}
profile
https://github.com/sustainable-git

0개의 댓글

관련 채용 정보