SwiftUI) View의 re-init

SteadySlower·2023년 6월 27일
0

SwiftUI

목록 보기
50/64

이번 포스팅에서는 SwiftUI의 ObservedObject와 StateObject의 차이에 대해서 알아보려고 했습니다만, 그 주제에 대해서 소개하기 위해서 View의 re-init에 대해서 작성을 하다가 보니까 글이 길어져서 별도의 포스팅으로 만들어 보았습니다.

이번 포스팅에서는 SwiftUI에서 View가 re-init되는 타이밍과 re-init될 때 @State 변수의 상태에 대해서 알아보도록 하겠습니다.

View가 re-init 되는 타이밍

ObservedObject와 StateObject의 차이를 알기 위해서는 View가 init되는 타이밍, 즉 View의 initializer가 실행되는 타이밍을 아는 것이 중요합니다. 특히 re-init, 즉 이미 한번 init이 되어서 화면에 존재하는 View가 다시 init되는 타이밍에 주목할 필요가 있습니다. re-init되는 타이밍은 다양하게 있는데 이번 포스팅에서는 가장 중요한 2가지만 살펴보도록 하겠습니다.

View의 State가 변경될 때

@State로 선언된 변수가 변경될 때 View는 re-init됩니다. View 안에서는 @State로 선언된 변수가 아니고 일반 var로 선언된 변수는 변경할 수 없습니다. 오직 @State 변수만을 변경할 수 있습니다. 아래 예시 코드를 한번 봐주세요.

struct ParentView: View {
    
    @State var bool: Bool
    
    init() {
        self.bool = true
        print("Parent View inited!")
    }
    
    var body: some View {
        VStack {
            Toggle("토글", isOn: $bool)
            Text(bool ? "토글 온" : "토글 오프")
        }
    }
}

제가 설명한 대로라면 위 View는 토글을 이용해서 @State 변수인 bool의 값을 변경할 때마다 re-init되어서 콘솔에 print문이 출력이 되어야 하는데요. 실제로 해보면 그렇지 않습니다. 그럼 이번에는 아래처럼 위 코드를 변경해보겠습니다.

struct ParentView: View {
    
    @State var bool: Bool
    
    init() {
        self.bool = true
        print("Parent View inited!")
    }
    
    var body: some View {
        VStack {
            Toggle("토글", isOn: $bool)
            ChildView(bool: bool)
        }
    }
}

struct ChildView : View {
    
    let bool: Bool
    
    init(bool: Bool) {
        self.bool = bool
        print("Child View inited!")
    }
    
    var body: some View {
        Text(bool ? "토글 온" : "토글 오프")
    }
    
}

위의 Text를 ChildView로 감싸서 별도의 initializer를 가진 다른 View로 만들었습니다. 이 경우에는 ParentView에서 @State를 변경할 때 ChildView의 initializer가 실행되는 것을 볼 수 있습니다. re-init되는 것이죠. 사실 이전 코드의 Text도 계속 re-init되고 있었습니다. 단지 initializer에 print문이 없어서 우리가 알지 못했던 것이죠.

Layout이 변경 될 때

부모 View의 크기가 변경되어 새로운 Layout이 필요할 때도 reinit됩니다. 아래 코드를 보겠습니다.

struct ParentView: View {
    
    @State var bool: Bool
    
    init() {
        self.bool = true
        print("Parent View inited!")
    }
    
    var body: some View {
        VStack {
            Toggle("토글", isOn: $bool)
            ChildView2()
        }
        .frame(height: bool ? 200 : 300)
    }
}

struct ChildView2: View {
    
    init() {
        print("Child View 2 inited!")
    }
    
    var body: some View {
        Image(systemName: "pencil")
            .resizable()
    }
    
}

.resizable이 적용된 이미지는 부모 View의 크기에 영향을 받아 자신의 크기를 정합니다. 따라서 부모 View의 크기가 변하면 자신의 크기도 변하게 됩니다. 따라서 bool 값이 변하면 부모 View의 크기가 변하고 자식 View가 init되는 것을 볼 수 있습니다.

그렇다면 아래 코드를 볼까요? 이번에는 하위 이미지의 크기가 정해져있는 상황입니다. 부모 View의 크기가 바뀌어도 하위 View의 크기가 변할 필요가 없습니다. 하지만 토글을 눌러보면 자식 View가 re-init되는 것을 볼 수 있습니다.

SwiftUI의 입장에서는 자식 View의 크기는 직접 계산해봐야 알 수 있습니다. 따라서 View의 크기가 같더라도 re-init할 수 밖에 없는 것이죠.

struct ChildView2: View {
    
    init() {
        print("Child View 2 inited!")
    }
    
    var body: some View {
        Image(systemName: "pencil")
            .resizable()
            .frame(width: 50, height: 50)
    }
    
}

re-init될 때 @State 변수의 상태

자 다시 위에 Text가 있었던 자식 뷰를 보도록 하겠습니다. 이번에는 자식뷰의 내부에 다른 토글을 넣어보도록 하겠습니다. 자식 View는 이제 자신만의 토글과 더불어 자신만의 @State 변수인 bool2를 가지고 있습니다. 그리고 자식 View의 initializer에서는 해당 bool2을 false로 init합니다.

struct ParentView: View {
    
    @State var bool: Bool
    
    init() {
        self.bool = false
        print("Parent View inited!")
    }
    
    var body: some View {
            VStack {
                Toggle("토글", isOn: $bool)
                ChildView(bool: bool)
            }
            .frame(height: bool ? 200 : 300)
    }
}

struct ChildView : View {
    
    let bool: Bool
    @State var bool2: Bool
    
    init(bool: Bool) {
        self.bool = bool
        self.bool2 = false
        print("Child View inited!")
    }
    
    var body: some View {
        VStack {
            Text(bool ? "토글 온" : "토글 오프")
            Toggle("토글2", isOn: $bool2)
        }
    }
    
}

그렇다면 ChildView가 re-init을 할 때마다 bool2 값은 false로 초기화 되어야 합니다. 하지만 아래 캡쳐 화면을 보시죠.

일단 bool2를 true로 바꾸어 놓고 자식 View를 re-init하기 위해서 부모 View의 State 변수를 토글로 바꾸어 보았는데요. 콘솔을 확인하면 자식 View는 계속 re-init되고 있지만 (즉, initializer가 실행되고 있지만) bool2 값을 false로 초기화 되지 않는 것을 볼 수 있습니다.

결론을 말씀드리면 bool2가 @State 변수로 선언된 순간 해당 변수는 SwiftUI에 등록됩니다. 더 이상 자식 View 객체가 가지고 있는 값이 아닙니다. 쉽게 말하면 별도로 관리됩니다. 따라서 initializer에 있는 “self.bool2 = false”는 더 이상 해당 변수에 영향을 미치지 않습니다.

따라서 자식 View가 re-init되어도 값의 변화가 없는 것이죠.

한 가지 실험을 더 해볼까요? 이번에는 ChildView2가 있습니다. 해당 View는 bool2 값을 받아서 init을 합니다.

struct ParentView: View {
    
    @State var bool: Bool
    
    init() {
        self.bool = false
        print("Parent View inited!")
    }
    
    var body: some View {
            VStack {
                Toggle("토글", isOn: $bool)
                ChildView(bool: bool)
            }
            .frame(height: bool ? 200 : 300)
    }
}

struct ChildView : View {
    
    let bool: Bool
    @State var bool2: Bool
    
    init(bool: Bool) {
        self.bool = bool
        self.bool2 = false
        print("Child View inited!")
    }
    
    var body: some View {
        VStack {
            Text(bool ? "토글 온" : "토글 오프")
            Toggle("토글2", isOn: $bool2)
            ChildView2(bool2: bool2)
        }
    }
    
}

struct ChildView2: View {
    
    let bool2: Bool
    
    init(bool2: Bool) {
        self.bool2 = bool2
        print("Child View 2 inited! with bool2 = \(bool2)")
    }
    
    var body: some View {
        Text(bool2 ? "토글2 온" : "토글2 오프")
    }
    
}

우리 추측으로는 ChildView2는 bool과는 관련이 없는 값이므로 bool 값을 변경할 때는 re-init이 되지 않고 bool2 값을 변경할 때만 re-init된다고 생각할 수도 있는데요. 실제로 해보면 bool과 bool2이 변경될 때 모두 re-init되는 것을 볼 수 있습니다.

@State 변수의 변경은 모든 하위 View에 영향을 준다는 결론을 얻을 수 있겠네요. ChildView2도 결국 ChildView1의 하위 View이므로 ChildView1이 re-init될 때 같이 re-init되는 것입니다.

정리

@State 변수는 별도로 관리됩니다. 따라서 상위 View의 State 변화에 영향을 받지 않습니다. (bool이 바뀌어도 bool2는 불변이었던 것처럼) 유식한(?) 말로 바꾸면 View와 별도의 라이프 사이클을 가진다는 뜻입니다. 따라서 개발을 하다가 보면 상위 View에서 하위 View의 State을 바꾸고 싶을 때가 있는데요. SwiftUI에서는 원칙적으로 불가능합니다.

위 내용은 SwiftUI로 개발할 때 반드시 숙지해야 할 아주 기본적인 내용입니다. 저도 예전에 공부해두고 실무를 하고 있었는데 이번에 포스팅을 작성하면서 다시 복습을 해봤습니다.

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

0개의 댓글