해외 블로그를 많이 참고하였으며, 공부하면서 정리한 내용이라 잘못된 부분이 있을 수 있습니다. 댓글 피드백 환영합니다!
어느날 문득 다음과 같은 내용들이 궁금해졌다.
@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를 통해 알아보았다.
두 경우 모두 글자를 탭할 때마다 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이 떠있고 해당 숫자를 탭할 때마다 숫자는 올라간다.
테스트 gifdraw
호출여부 확인다시 테스트 1과 테스트 2로 돌아가보자.
Published
에 대해 테스트 했던 첫번째에서는 프로퍼티를 안쓰고 있음에도 body가 호출이 됐다. State
에 대해 테스트 했던 두번째에서는 body가 호출이 아예 안됐다. CoreAnimation Commit
은 발생하고 있었다. 그렇다면, draw
도 호출되고 있었을까?
결과 - 두 경우 모두 일련의 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번 될까?➡️ 한번만 렌더링 된다. 두 경우 모두 body 먼저 두번 호출된 뒤 draw 한번 호출 (첫번째 body는 렌더링되지 않음)
뷰 렌더링이 이와같이 선택적으로 일어난다고 하더라도 테스트 1번과 같은 경우에는 불필요한 body 호출이 발생하는 셈이다. (하위 뷰가 있다면 하위 뷰의 init도 다시 호출된다.) 따라서 꼭 필요한 프로퍼티에만
@Published
를 쓰는 것이 바람직해 보인다.
- body 생성은 cheap 하다고는 하지만 하위 뷰의 재 init은 컨텐츠의 불필요한 reset을 발생시킬 수 있음(side effect)
- 특히 하위 뷰가
@StateObject
가 아닌@ObservedObject
로 프로퍼티를 생성하면서 가지고 있는 경우 문제가 됨
ObservedObject
같은 Property Wrapper로 소유하지 않는 경우에는 @Published
프로퍼티 변화가 body 재호출을 일으키지 않는다.ObservableObject
에 이렇게 뷰가 반응하는 이유는 objectWillChange
publisher때문이다. 같이 고민해주신 '비비, 린생, 하비, 수박, 토털이, 엘렌' 감사합니다!!
정말 흥미로운 내용이네요. 덕분에 재밌었습니다 👏👏👏👏👏👏
애플 입사해서 공식문서로 써주세요