해외 블로그를 많이 참고하였으며, 공부하면서 정리한 내용이라 잘못된 부분이 있을 수 있습니다. 댓글 피드백 환영합니다!
어느날 문득 다음과 같은 내용들이 궁금해졌다.
@Published프로퍼티를 View에서 쓰고 있지 않을 때에도 해당 프로퍼티 변화가 뷰 렌더링을 일으킬까?@State프로퍼티는 어떨까? View의 body에서 안쓰고 있을 때 값 변화를 일으키면 뷰 렌더링이 다시 될까?- SwiftUI는 뷰 렌더링을 언제 하는걸까? '화면에 보여줄 값이 바뀌지 않은 상황에서도 뷰는 다시 렌더링될까?'
@Published 프로퍼티를 안쓰고 있음 > 해당 프로퍼티 변화가 View의 body를 재호출시키는가?//
// ContentView.swift
// RenderingTest
//
// Created by JINHONG AN on 2022/08/27.
//
import SwiftUI
struct ContentView: View {
@ObservedObject private var viewModel = RenderingViewModel()
var body: some View {
Text("가나다라마바사")
.onTapGesture {
viewModel.increment()
}
}
}
final class RenderingViewModel: ObservableObject {
@Published private(set) var count = 0
func increment() {
count += 1
}
}
위와 같은 코드가 있다고 하자. ContentView에서는 viewModel을 가지고 있기는 하지만, viewModel의 @Published 프로퍼티를 어디에서도 쓰고 있지는 않다. 이 경우 count 프로퍼티의 변화는 ContentView > body를 재호출 시킬까?
놀랍게도 body가 재호출 되었다!!
그렇다면 body에 해당하는 뷰가 다시 렌더링(re-rendering) 된 것일까? 이것은 조금 있다가 알아보도록 하자.
//
// ContentView.swift
// RenderingTest
//
// Created by JINHONG AN on 2022/08/27.
//
import SwiftUI
struct ContentView: View {
@State private var count = 0
var body: some View {
Text("가나다라마바사")
.onTapGesture {
count += 1
}
}
}
이번에는 위와 같은 코드가 있다. ContentView에서는 @State 프로퍼티를 가지고는 있으나 어디에서도 쓰고 있지는 않다. 이 경우 count 프로퍼티의 변화는 ContentView > body를 재호출 시킬까?
놀랍게도 body는 재호출 되지 않았다!!
그렇다면 이번에는 'body에 해당하는 뷰가 다시 렌더링(re-rendering) 되지 않았다'고 볼 수 있는 것일까?
참고: body > Text에서 count 프로퍼티를 사용했을 때에는 글자 탭 > count 변동 > body 프로퍼티 재호출이 발생했다.
일단 테스트 1과 2에서 CoreAnimation Commit은 어떻게 찍히는지 Instrument를 통해 알아보았다.
테스트 2 - State에 대한 Instrument 결과 gif
두 경우 모두 글자를 탭할 때마다 CoreAnimation Commit이 찍히는 것을 볼 수 있다.
(특이한 것은 State에 대한 테스트의 경우 body 호출이 없었음에도 CoreAnimation Commit은 찍힌다는 것이었다.)
어쨌든 CoreAnimation Pipeline에 의하면 두 경우 모두 화면을 새로 그리려는 동작이 시작된 것처럼 보인다.
이는 Symbolic Breakpoint를 걸어서 확인해보았다.
결과적으로 많은 drawRect메서드들에 breakpoint가 걸리게 되었다.
여기서 우리가 눈여겨 봐야 할 것은 빨간색으로 네모칸 친 곳의 CGDrawingView이다. SwiftUI는 label을 그릴 때 private한 CGDrawingView 를 사용하기 때문!
draw가 어떻게 동작하는지 살펴보았다.
화면이 띄워질 때
@objc SwiftUI.DisplayList.ViewUpdater.Platform.CGDrawingView.draw(__C.CGRect) SwiftUI.DisplayList.ViewUpdater.Platform.CGDrawingView.draw(__C.CGRect) SwiftUI.ResolvedStyledText.draw(in: __C.CGRect, with: __C.CGSize, applyingMarginOffsets: Swift.Bool, stringDrawingContext: SwiftUI.StringDrawingContext) 위의 순서로 메서드 동작이 이루어진다.
이 메서드들은 비단 화면이 처음 띄워질 때 뿐만이 아니라 컨텐츠 내용이 바뀔 때에도 동일한 순서로 호출된다. (물론 body가 가장 먼저 호출된다!)
draw 동작 테스트//
// ContentView.swift
// RenderingTest
//
// Created by JINHONG AN on 2022/08/27.
//
import SwiftUI
struct ContentView: View {
@ObservedObject private var viewModel = RenderingViewModel()
var body: some View {
Text("\(viewModel.count)")
.onTapGesture {
viewModel.increment()
}
}
}
final class RenderingViewModel: ObservableObject {
@Published private(set) var count = 0
func increment() {
count += 1
}
}
body에서는 viewModel의 count를 보여주도록 구성되어있다. 처음에는 숫자 0이 떠있고 해당 숫자를 탭할 때마다 숫자는 올라간다.
테스트 gif
draw 호출여부 확인다시 테스트 1과 테스트 2로 돌아가보자.
Published에 대해 테스트 했던 첫번째에서는 프로퍼티를 안쓰고 있음에도 body가 호출이 됐다. State에 대해 테스트 했던 두번째에서는 body가 호출이 아예 안됐다. CoreAnimation Commit은 발생하고 있었다. 그렇다면, draw도 호출되고 있었을까?
테스트 2 draw 동작여부 gif
결과 - 두 경우 모두 일련의 draw 메서드 호출이 발생하지 않았다. 즉 실질적인 뷰 re-rendering이 이루어지지 않았다.
1. 테스트 1의 경우 body 호출은 되지만 draw 메서드들 미호출
2. 테스트 2의 경우 body 호출도 안되고 draw 메서드들도 미호출
이를 통해 하나의 결론을 얻을 수 있었다.
그렇다면 다음과 같은 경우에는 draw가 호출이 될까?
struct ContentView: View {
@ObservedObject private var viewModel = RenderingViewModel()
var body: some View {
Text("\(viewModel.count)")
.onTapGesture {
viewModel.increment()
}
}
}
final class RenderingViewModel: ObservableObject {
@Published private(set) var count = 0
func increment() {
count = 0 // count에 0만 대입하고 있음
}
}
결론부터 말하자면 draw 호출은 되지 않는다. body 재호출만 일어난다. (뷰에서 바뀌어야 할 내용이 없으므로!)
onAppear에서 뷰에 영향을 미치는 값을 바로 바꾼다면 렌더링은 2번 될까?
State 테스트
➡️ 한번만 렌더링 된다. 두 경우 모두 body 먼저 두번 호출된 뒤 draw 한번 호출 (첫번째 body는 렌더링되지 않음)
뷰 렌더링이 이와같이 선택적으로 일어난다고 하더라도 테스트 1번과 같은 경우에는 불필요한 body 호출이 발생하는 셈이다. (하위 뷰가 있다면 하위 뷰의 init도 다시 호출된다.) 따라서 꼭 필요한 프로퍼티에만
@Published를 쓰는 것이 바람직해 보인다.
- body 생성은 cheap 하다고는 하지만 하위 뷰의 재 init은 컨텐츠의 불필요한 reset을 발생시킬 수 있음(side effect)
- 특히 하위 뷰가
@StateObject가 아닌@ObservedObject로 프로퍼티를 생성하면서 가지고 있는 경우 문제가 됨
ObservedObject같은 Property Wrapper로 소유하지 않는 경우에는 @Published 프로퍼티 변화가 body 재호출을 일으키지 않는다.ObservableObject에 이렇게 뷰가 반응하는 이유는 objectWillChange publisher때문이다. 같이 고민해주신 '비비, 린생, 하비, 수박, 토털이, 엘렌' 감사합니다!!
정말 흥미로운 내용이네요. 덕분에 재밌었습니다 👏👏👏👏👏👏
애플 입사해서 공식문서로 써주세요