[SwiftUI] View를 redraw 하는 조건은 어떻게 될까?
들어가기 전에
개-하!
오늘은 @State
, @Binding
, @StateObject
등 뷰와 관련된 dynamic property 의 값을 바꿔보면서 이 값들의 변경이 뷰의 렌더링에 어떤 식으로 연관되어 있는지 알아보겠습니다.
거두절미하고 시작해보져 ㄱㄱ
실험 시작 👩🏻🔬
가장 먼저 firstTitle
, secondTitle
, thirdTitle
을 나타내는 세개의 Text
를 만들고, backgroundColor
를 랜덤 색상으로 설정을 하겠습니다.
그 아래 버튼을 누르면 firstTitle
값을 “hello world” 로 변경합니다.
위의 예에서 firstTitle
만을 변경했지만 secondTitle
, thirdTitle
을 나타내는 Text
뷰 모두 재렌더링 되는 것을 확인할 수 있습니다. Text
의 배경 색상이 처음과 모두 달라졌죠?
view 의 dependency (view 에 변경을 줄 수 있는 input 값) 가 변경되면 SwiftUI 는 렌더링을 위해 뷰의 body
프로퍼티를 호출하기 때문에, body
안에 있는 모든 Text
가 재렌더링 된 것 입니다 ( body
는 computable 함. 즉 완전히 호출에 의해 실행됨).
하지만 firstTitle
이 hello world
로 한번 변경되고 나면, 몇번 더 버튼을 누르더라도 할당 되는 값은 이전과 같은 “hello world” 이기 때문에 재렌더링 되지 않습니다.
이번에는 thirdTitle
을 나타내는 Text
를 ThirdView
라는 별도의 View
로 분리해보겠습니다.
버튼을 눌러서 ContentView
의 dependency 에 해당하는 firstTitle
을 변경했기 때문에, 이전과 같이 ContentView
의 body
가 호출되며 뷰를 렌더링합니다.
하지만 ThirdView
의 dependency 는 let text: String
로 따로 존재하기 때문에, 이 값이 변경되지 않는 한 ThirdView
의 body
는 호출되지 않습니다. 즉, ThirdView
는 다시 렌더링되지 않습니다 (ThirdView
만 배경색이 계속 동일한 것을 확인할 수 있습니다).
따라서 FirstView
, SecondView
, ThirdView
를 모두 분리한 뒤에 firstTitle
만을 변경하면 FirstView
에 해당하는 뷰만 재 렌더링 됩니다
정말로 FirstView
의 색상만 변경되는 것을 볼 수 있져?
위 동작은 FirstView
의 text 프로퍼티를 let
이 아닌 @Binding var
으로 바꿔도 동일합니다.
struct FirstView: View {
@Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
이번에는 기존에 있는 firstTitle
, secondTitle
을 지우고, MyViewModel
이라는 ObservableObject
를 만들어서 이 안에 firstTitle
, secondTitle
을 @Published
로 선언해보겠습니다. 사용할 때는 @StateObject
로 선언을 할거구요.
이번에도 처음 버튼을 누를때 FirstView
만 변경되는 것을 확인할 수 있습니다.
여기서 FirstView
의 text 를 다시 @Binding
으로 받아 FirstView(text: $viewModel.firstTitle)
처럼 값을 넘겨보겠습니다.
struct FirstView: View {
@Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
let text: String
으로 받았을때와 달리, firstTitle
에 "hello world" 를 할당할때마다 FirstView
가 새롭게 렌더링됩니다. (FirstView
의 배경색이 계속해서 바뀝니다.)
똑같이 @Binding
으로 FirstView
의 값을 받았음에도 불구하고, @State var firstTitle: String
의 값을 넘길때와, ObservableObject
안의 @Publised var text: String
의 값을 넘길때의 동작이 달라집니다.
SecondView
의 text
도 @Binding
으로 받도록 변경하고, 버튼 클릭을 통해 firstTitle
을 변경해보겠습니다.
struct SecondView: View {
@Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
SecondView
와 상관 없는 firstTitle
만을 변경했음에도 SecondView
또한 재렌더링 되는 것을 볼 수 있습니다.
실험 결과의 원인 🧠
실험에서 왜 이런 결과가 나타나는건지 이제부터 한번 추측을 해보겠습니다. 뇌피셜이 많이 들어가있으니 틀린 사항이 있다면 댓글 주세요 😗
우선 DynamicProperty
문서를 살펴보겠습니다.
해당 protocol 은 뷰의 외부 속성(external property)을 업데이트하는 stored property의 인터페이스입니다. Conform 하는 타입으로는 State
, Binding
, StateObject
, ObservedObject
등이 있습니다.
view 는 자신의 body
를 재계산하기 전에 이 프로퍼티들에 값을 할당하는데요, 즉 firstTitle = "hello world"
와 같이 해당 프로퍼티에 값을 할당하면 뷰의 body
가 재계산 된다고 볼 수 있습니다.
여기서 조금 더 들어가보면 DynamicProperty
protocol 은 update()
라는 함수를 기본적으로 implement 합니다.
이 함수가 뭔고 하니, SwiftUI 는 view 의 body
을 렌더링하기 전에 update()
함수를 호출해서 뷰에 최신 값이 있는지 확인하고, stored value 의 underlying value 를 업데이트한다고 합니다.
그림으로 표현해보자면 대충 이런 느낌인것 같습니다.
이를 염두해두고 위의 예시들을 렌더링 측면에서 다시 살펴보겠습니다.
버튼 클릭을 통해 ContentView
의 dependency 인 firstTitle
을 변경하면, 해당 뷰의 body
가 호출됩니다. 하지만 이때 하위 뷰(ex. FirstView
)의 dependency가 let
으로 설정되어있다면, 해당 값이 마지막으로 설정된 값과 다를 경우에만 view 의 body
가 호출됩니다 (즉 body
가 evalulation 가 된다고 해서 무조건 내부의 모든 뷰가 redraw 되는 것은 아닙니다).
만약 let
이 아닌 @Binding
으로 속성이 설정되어있다면요? 이때는 연결되어있는 값이 변경 될 때 같이 변경될 것입니다.
우선 @Binding
이 @State
와 연결되어 있는 상황을 살펴봅시다.
@State
는 이전 값과 다른 새로운 값이 할당할 때 업데이트 될 것이므로, @Binding
을 사용하고 있는 뷰도 처음 버튼을 눌러 "hello world" 라는 새 값을 할당할때만 뷰가 재렌더링 될 것입니다.
하지만 @Binding
이 ObservableObject
내의 @Published
와 연결되어있다면 어떻게 동작할까요?
FirstView(text: $viewModel.firstTitle)
우선 주의해야할 점은 하나의 @Publisehd
속성이 변경 되면 전체 ObservableObject
가 변경된 것으로 간주된다는 것입니다. 어느 @Pulished
값이 변경되든ObservableObject
의 objectWillChange
publisher 가 트리거 됩니다. (참고 — By default an ObservableObject
synthesizes an objectWillChange
publisher that emits the changed value before any of its @Published
properties changes.)
즉 @State var firstTitle
, @State var secondTitle
로 선언 되었을때는 각각이 독립적이었지만, 이 둘이 ObservableObject
의 @Published
로 들어간 순간 그냥 하나의 @StateObject var viewModel
로 묶인다는 것이죠.
어떤 @Published
에 값이 할당되더라도 ObservableObject
에 변경 일어났다고 판단하기 때문에, @StateObject var viewModel
를 사용하고 있는 모든 뷰에 영향을 줍니다 (viewModel.fisrtTitle
을 변경했을때 $viewModel.secondTitle
을 사용하는 SecondView
도 다시 렌더링 되었던게 기억 나시나요?)
또한 @State
에는 값을 할당해도 이전 값과 동일하면 업데이트 하지 않는데 반해, @Published
는 값이 할당 되기만하면 이전 값과 상관없이 objectWillChange
publisher 를 트리거하는 듯 합니다. 그렇기 때문에 버튼을 클릭할 때마다 "hello world" 라는 동일한 값이 할당 됨에도 새롭게 뷰가 렌더링 된거겠죠? 🤔
여기까지 보고, 다시 위 섹션의 코드와 gif 들을 보면 이해가 되실까요? 어찌저찌 잘 끼워맞춰봤는데 말이에요 🫠
결론
그래서 오늘의 결론은?
- view 의
body
가 evalulation 가 된다고 해서 무조건 내부의 모든 뷰가 redraw 되는 것은 아님. 따라서body
가 evaluate 될때 재렌더링 되는 범위가 줄어들도록View
를 작게 쪼개고 ObservableObject
도@Published
가 많아지지 않게 작게 관리하자.
입니둥.
그럼 끗!
참고
[Apple Docmument] DynamicProperty