SwiftUI의 View 렌더링은 언제 이루어지는가?

Coden·2022년 8월 30일
12

SwiftUI

목록 보기
1/1

해외 블로그를 많이 참고하였으며, 공부하면서 정리한 내용이라 잘못된 부분이 있을 수 있습니다. 댓글 피드백 환영합니다!


SwiftUI의 View 렌더링은 언제 이루어질까?

어느날 문득 다음과 같은 내용들이 궁금해졌다.

  1. @Published 프로퍼티를 View에서 쓰고 있지 않을 때에도 해당 프로퍼티 변화가 뷰 렌더링을 일으킬까?
  2. @State 프로퍼티는 어떨까? View의 body에서 안쓰고 있을 때 값 변화를 일으키면 뷰 렌더링이 다시 될까?
  3. SwiftUI는 뷰 렌더링을 언제 하는걸까? '화면에 보여줄 값이 바뀌지 않은 상황에서도 뷰는 다시 렌더링될까?'

Test

테스트 1 - View에서 ViewModel의 @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를 재호출 시킬까?


테스트 gif

놀랍게도 body가 재호출 되었다!!

그렇다면 body에 해당하는 뷰가 다시 렌더링(re-rendering) 된 것일까? 이것은 조금 있다가 알아보도록 하자.


테스트 2 - View가 State 프로퍼티를 가지고는 있으나 body에서 안쓰고 있음 > 해당 프로퍼티 변화가 View의 body를 재호출시키는가?

//
//  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를 재호출 시킬까?


테스트 gif

놀랍게도 body는 재호출 되지 않았다!!

그렇다면 이번에는 'body에 해당하는 뷰가 다시 렌더링(re-rendering) 되지 않았다'고 볼 수 있는 것일까?

참고: body > Text에서 count 프로퍼티를 사용했을 때에는 글자 탭 > count 변동 > body 프로퍼티 재호출이 발생했다.


body가 호출되는 것이 곧 렌더링이라고 말할 수 있을까?

일단 테스트 1과 2에서 CoreAnimation Commit은 어떻게 찍히는지 Instrument를 통해 알아보았다.

테스트 1 - Published에 대한 Instrument 결과 gif 테스트 2 - State에 대한 Instrument 결과 gif

두 경우 모두 글자를 탭할 때마다 CoreAnimation Commit이 찍히는 것을 볼 수 있다.
(특이한 것은 State에 대한 테스트의 경우 body 호출이 없었음에도 CoreAnimation Commit은 찍힌다는 것이었다.)


어쨌든 CoreAnimation Pipeline에 의하면 두 경우 모두 화면을 새로 그리려는 동작이 시작된 것처럼 보인다.


정말 화면이 다시 그려지는 것이 맞을까? 뷰 렌더링이 다시 된 것일까?

이는 Symbolic Breakpoint를 걸어서 확인해보았다.

결과적으로 많은 drawRect메서드들에 breakpoint가 걸리게 되었다.
여기서 우리가 눈여겨 봐야 할 것은 빨간색으로 네모칸 친 곳의 CGDrawingView이다. SwiftUI는 label을 그릴 때 private한 CGDrawingView 를 사용하기 때문!


일단은 화면이 처음 띄워질 때에는 draw가 어떻게 동작하는지 살펴보았다.


화면이 처음 띄워질 때 draw 호출 순서 gif

화면이 띄워질 때

  1. SwiftUI@objc SwiftUI.DisplayList.ViewUpdater.Platform.CGDrawingView.draw(__C.CGRect)
  2. SwiftUISwiftUI.DisplayList.ViewUpdater.Platform.CGDrawingView.draw(__C.CGRect)
  3. SwiftUISwiftUI.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

테스트 1과 테스트2에 대한 draw 호출여부 확인

다시 테스트 1과 테스트 2로 돌아가보자.

  • Published에 대해 테스트 했던 첫번째에서는 프로퍼티를 안쓰고 있음에도 body가 호출이 됐다.
  • State에 대해 테스트 했던 두번째에서는 body가 호출이 아예 안됐다.
  • 다만 두 테스트 모두 CoreAnimation Commit은 발생하고 있었다.

그렇다면, draw도 호출되고 있었을까?


테스트 1 draw 동작여부 gif 테스트 2 draw 동작여부 gif

결과 - 두 경우 모두 일련의 draw 메서드 호출이 발생하지 않았다. 즉 실질적인 뷰 re-rendering이 이루어지지 않았다.
1. 테스트 1의 경우 body 호출은 되지만 draw 메서드들 미호출
2. 테스트 2의 경우 body 호출도 안되고 draw 메서드들도 미호출

이를 통해 하나의 결론을 얻을 수 있었다.

  • 무언가 값의 변경이 일어나면 > 뷰의 body 호출(트리거)은 될 수 있어도 re-rendering은 실질적인 내용 변화가 있는 경우에만 동작한다.

그렇다면 다음과 같은 경우에는 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번 될까?

Published 테스트 State 테스트

➡️ 한번만 렌더링 된다. 두 경우 모두 body 먼저 두번 호출된 뒤 draw 한번 호출 (첫번째 body는 렌더링되지 않음)


결론

  • 뷰 렌더링은 실질적인 내용 변화가 있는 경우에만 이루어진다.

    뷰 렌더링이 이와같이 선택적으로 일어난다고 하더라도 테스트 1번과 같은 경우에는 불필요한 body 호출이 발생하는 셈이다. (하위 뷰가 있다면 하위 뷰의 init도 다시 호출된다.) 따라서 꼭 필요한 프로퍼티에만 @Published를 쓰는 것이 바람직해 보인다.

    • body 생성은 cheap 하다고는 하지만 하위 뷰의 재 init은 컨텐츠의 불필요한 reset을 발생시킬 수 있음(side effect)
    • 특히 하위 뷰가 @StateObject가 아닌 @ObservedObject로 프로퍼티를 생성하면서 가지고 있는 경우 문제가 됨

참고 사항

  • viewModel을 ObservedObject같은 Property Wrapper로 소유하지 않는 경우에는 @Published 프로퍼티 변화가 body 재호출을 일으키지 않는다.
  • ObservableObject에 이렇게 뷰가 반응하는 이유는 objectWillChange publisher때문이다.

References


Especially Thanks To

같이 고민해주신 '비비, 린생, 하비, 수박, 토털이, 엘렌' 감사합니다!!

profile
iOS 공부중인 Coden

2개의 댓글

comment-user-thumbnail
2022년 8월 30일

정말 흥미로운 내용이네요. 덕분에 재밌었습니다 👏👏👏👏👏👏
애플 입사해서 공식문서로 써주세요

답글 달기
comment-user-thumbnail
2022년 8월 31일

호기심이 멋지네요! 정리 감사합니다~

답글 달기