[SwiftUI] View를 redraw 하는 조건은 어떻게 될까?

실험으로 알아보는 뷰의 재렌더링 조건 👩🏻‍🔬

naljin
13 min readJul 6, 2022

들어가기 전에

개-하!

오늘은 @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 함. 즉 완전히 호출에 의해 실행됨).

하지만 firstTitlehello world 로 한번 변경되고 나면, 몇번 더 버튼을 누르더라도 할당 되는 값은 이전과 같은 “hello world” 이기 때문에 재렌더링 되지 않습니다.

이번에는 thirdTitle 을 나타내는 TextThirdView 라는 별도의 View 로 분리해보겠습니다.

버튼을 눌러서 ContentView 의 dependency 에 해당하는 firstTitle 을 변경했기 때문에, 이전과 같이 ContentViewbody 가 호출되며 뷰를 렌더링합니다.

하지만 ThirdView 의 dependency 는 let text: String 로 따로 존재하기 때문에, 이 값이 변경되지 않는 한 ThirdViewbody 는 호출되지 않습니다. 즉, 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 의 값을 넘길때의 동작이 달라집니다.

SecondViewtext@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" 라는 새 값을 할당할때만 뷰가 재렌더링 될 것입니다.

하지만 @BindingObservableObject 내의 @Published 와 연결되어있다면 어떻게 동작할까요?

FirstView(text: $viewModel.firstTitle)

우선 주의해야할 점은 하나의 @Publisehd 속성이 변경 되면 전체 ObservableObject 가 변경된 것으로 간주된다는 것입니다. 어느 @Pulished 값이 변경되든ObservableObjectobjectWillChange 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 들을 보면 이해가 되실까요? 어찌저찌 잘 끼워맞춰봤는데 말이에요 🫠

결론

그래서 오늘의 결론은?

  1. view 의 body 가 evalulation 가 된다고 해서 무조건 내부의 모든 뷰가 redraw 되는 것은 아님. 따라서 body 가 evaluate 될때 재렌더링 되는 범위가 줄어들도록 View 를 작게 쪼개고
  2. ObservableObject@Published 가 많아지지 않게 작게 관리하자.

입니둥.

그럼 끗!

참고

[Apple Docmument] DynamicProperty

[Apple Docmument] update()

[Apple Docmument] ObservableObject

[Apple Docmument] objectWillChange

--

--