0. 서론
렛어스 고에서 숲의 스마일님이 연사한 SwiftUI 렌더링 성능 개선 컨퍼런스 영상을 보고 너무 재밌어서 여기에 했던 실험 + 내가 그동안 궁금했던 것들까지 실험해 보았다.
https://www.youtube.com/watch?v=N2Wq-MMx81c&list=PLfx4MMAj7YbHmfbcHSGIIH33yvpptIzT7&index=5
그동안은 성능과 상관없이 내가 원하는 대로 실행되면 빨리 다음 고고 였는데, 진행한 프로젝트도 쌓였고
이제는 다음 단계로 넘어가야할 차례라고 생각했다. 한번 가려웠던 부분을 긁어보자.
1. @State & @Binding
제일 간단한 State&Binding부터 살펴보자. 아래처럼 간단하게 뷰를 구성해보았다.
struct StateBinding1View: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("증가") {
count += 1
}
targetView()
}
.font(.largeTitle)
}
@ViewBuilder
private func targetView() -> some View {
let _ = Self._printChanges()
Text("렌더링 되기 싫어요")
}
}
실행을 해보면 @State로 선언된 count의 값이 바뀌면서 targetView도 같이 업데이트 되는 것을 볼 수 있다.

targetView는 업데이트 할 필요가 없는데,,,,,,, 그렇다면 어떻게 해야할까?
하위 Struct로 쪼개기
아래처럼 ChildView로 쪼개보자.
struct StateBinding2View: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("증가") {
count += 1
}
ChildView()
}
.font(.largeTitle)
}
}
private struct ChildView: View {
var body: some View {
let _ = Self._printChanges()
Text("렌더링 되기 싫어요")
}
}
실행을 해보면,, count 값이 바뀌어도 업데이트가 안되는 것을 볼 수 있다!

@Binding?
그러면 바인딩을 써보자. Binding한 값을 쓰지 않더라도 하위뷰는 업데이트 될까?
struct StateBinding3View: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("증가") {
count += 1
}
ChildView(count: $count)
}
.font(.largeTitle)
}
}
private struct ChildView: View {
@Binding var count: Int
var body: some View {
let _ = Self._printChanges()
Text("렌더링 되기 싫어요")
}
}
실행을 해보니, @Binding만 들고 있더라도 하위뷰가 업데이트 되고 있음을 확인할 수 있었다.

2. ObservableObject Protocol
이전 프로젝트에서 가장 많이 썼던 패턴인 MVVM(ObservableObject&Published)을 한번 알아보자.
제일 상위 뷰에서 @StateObject로 ViewModel을 초기화 해주고 하위뷰에서는 @ObservableObject로 받게 해주었다.
final class ViewModel1: ObservableObject {
@Published private(set) var count = 0
init() {
print("ViewModel 생성됨")
}
func increase() {
count += 1
}
deinit {
print("ViewModel 소멸됨")
}
}
struct ObservableObject1View: View {
@StateObject var viewModel = ViewModel1()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("증가") {
viewModel.increase()
}
ChildView(viewModel: viewModel)
}
.font(.largeTitle)
}
}
private struct ChildView: View {
@ObservedObject var viewModel: ViewModel1
var body: some View {
let _ = Self._printChanges()
Text("렌더링 되기 싫어요")
}
}
@Binding과 마찬가지로 ChildView에서 ViewModel을 들고 있기만 해도 업데이트 되는 것을 볼 수 있다.

그렇다면, 이런 현상을 방지하고 싶다면 어떻게 해야할까?
iOS 17부터 지원하는 @Observable 매크로를 사용하면 이를 방지할 수 있다.
3. @Observable
아래 코드는 바로 위에 코드를 Observable 매크로로 마이그레이션 한 코드이다.
@Observable
final class ViewModel3 {
private(set) var count = 0
init() {
print("ViewModel 생성됨")
}
func increase() {
count += 1
}
deinit {
print("ViewModel 소멸됨")
}
}
struct Observable1View: View {
@State var viewModel = ViewModel3()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("증가") {
viewModel.increase()
}
ChildView(viewModel: viewModel)
}
.font(.largeTitle)
}
}
private struct ChildView: View {
let viewModel: ViewModel3
var body: some View {
let _ = Self._printChanges()
Text("렌더링 되기 싫어요")
}
}
바로 결과를 보자.

ChildView에서 ViewModel을 들고 있어도 업데이트가 안되는 것을 볼 수 있다!!!!!
그렇다면... 자식 뷰 내부에서 사용되는 속성이 변경되지 않더라도, 부모의 상태(count) 변경에 따라 자식 뷰가 업데이트될까?
바로 코드를 작성해보았다.
@Observable
final class ViewModel5 {
private(set) var count = 0
private(set) var fixedNum = 0
init() {
print("ViewModel 생성됨")
}
func increase() {
count += 1
}
deinit {
print("ViewModel 소멸됨")
}
}
struct Observable3View: View {
@State var viewModel = ViewModel5()
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("증가") {
viewModel.increase()
}
ChildView(viewModel: viewModel)
}
.font(.largeTitle)
}
}
private struct ChildView: View {
let viewModel: ViewModel5
var body: some View {
VStack {
let _ = Self._printChanges()
Text("Fixed Value: \(viewModel.fixedNum)")
}
}
}
Fixed Value가 바뀌지 않으니 뷰가 업데이트 되지 않는다는 것을 볼 수 있다!

그동안에 @Observable로 구현한 프로젝트에서
뷰모델의 UI업데이트와 관련된 변수/내부 변수를 구분하기 위해서 하나의 구조체로 묶었던것이 생각나 이것도 실험 해보았다.
코드는 다음과 같다.
@Observable
final class ViewModel6 {
struct State {
var count = 0
var fixedNum = 0
}
private(set) var state: State = .init()
init() {
print("ViewModel 생성됨")
}
func increase() {
state.count += 1
}
deinit {
print("ViewModel 소멸됨")
}
}
struct Observable4View: View {
@State var viewModel = ViewModel6()
var body: some View {
VStack {
Text("Count: \(viewModel.state.count)")
Button("증가") {
viewModel.increase()
}
ChildView(viewModel: viewModel)
}
.font(.largeTitle)
}
}
private struct ChildView: View {
let viewModel: ViewModel6
var body: some View {
VStack {
let _ = Self._printChanges()
Text("Fixed Value: \(viewModel.state.fixedNum)")
}
}
}
....는 viewModel.state가 변경이 되기 때문에 ChildView가 업데이트 되었다..
그동안에 했던 프로젝트들에서 불필요한 하위뷰의 업데이트가 이루어졌다는 소리 ㅜ

4. 결론!!!!! + 내가 그동안 궁금했던 것
그래서 내린 결론은,,
타겟 버전이 iOS 17.0 이상이면 무조건 @Observable 매크로를 쓰자
사실 위 숲의 스마일님이 연사하는 컨퍼런스에서도 내린 결론이기 때문에 이미 알고 있던 사실이고,
이제는 내가 그동안 궁금했던 것을 실험 해보겠다.
@StateObject vs @ObservedObject
전에 이둘의 차이를 공부한 것이
- 객체(ViewModel)를 View가 책임져야할때는 @StateObject
- View를 객체가 책임져야할때는 @ObservableObject
따라서, 제일 상위뷰에서 ViewModel을 초기화 해줄때는 @StateObject,
이후 하위 뷰에서 해당 뷰모델을 받을때는 @ObservedObject로 결론지었다.
그래서 왜 그래야하는지? 가 궁금해서 아래 코드처럼 구현했다.
final class ViewModel2: ObservableObject {
@Published private(set) var count = 0
init() {
print("ViewModel 생성됨")
}
func increase() {
count += 1
}
deinit {
print("ViewModel 소멸됨")
}
}
struct ObservableObject2View: View {
@State private var count = 0
var body: some View {
VStack {
Text("Parent Count: \(count)")
Button("증가") {
count += 1
}
ChildView()
}.font(.largeTitle)
}
}
private struct ChildView: View {
// @StateObject로 바꾸면 초기화 되지않음
@ObservedObject var viewModel = ViewModel2()
var body: some View {
VStack {
Text("Child Count: \(viewModel.count)")
Button("증가") {
viewModel.increase()
}
}
}
}
ParentCount를 증가 시키면 ChildCount가 초기화 되는 것을 볼 수 있다.
ParentView가 body를 재계산하고 ChildView가 다시 그려지면서 @ObservedObject로 생성한 viewModel이 초기화 되는 것이였다.

따라서 @StateObject로 바꿔보면,, 원하는대로 동작하는 것을 확인할 수 있다.

내가 실험했던 풀코드는 여기서
https://github.com/Monfi98/SwiftUIRender-Practice
GitHub - Monfi98/SwiftUIRender-Practice
Contribute to Monfi98/SwiftUIRender-Practice development by creating an account on GitHub.
github.com
'iOS > Swift' 카테고리의 다른 글
| [Swift] 클로저, 클로저 캡쳐 (1) | 2025.07.08 |
|---|---|
| [Swift] ARC, weak, unowned (0) | 2025.07.01 |
| [Swift] @Published 뷰모델을 Combine Subject로 바꿔보자 (0) | 2025.06.23 |
| [Swift] SwiftData Concurrency 사용하기 (6) | 2025.06.20 |
| [Swift] 스위프트 기본문법#4 - 튜플(Tuple) (0) | 2023.05.11 |
댓글