반응형
SwiftUI @Observable 사용 시 불필요한 전체 뷰 재렌더링 원인과 최적화 방법 (iOS 17+)
iOS 17부터 도입된 @Observable 마크로는 기존 ObservableObject와 달리 프로퍼티 단위로 추적(Tracking)을 수행합니다. 그러나 뷰 내부에서 모델의 특정 프로퍼티를 직접 계산식이나 서브뷰의 인자로 참조할 때, 연관되지 않은 다른 프로퍼티의 변경까지 감지하여 뷰 전체가 불필요하게 재렌더링되는 트러블슈팅 사례가 발생합니다.
@Observable 과 ObservableObject 의 렌더링 감지 방식 비교
기존 ObservableObject는 @Published 변수가 하나만 변경되어도 objectWillChange 신호를 발생시켜 해당 모델을 소유한 뷰 전체를 다시 그렸습니다. 반면 @Observable은 뷰의 body 내에서 실제로 읽은(Read) 프로퍼티만 추적하여 렌더링 범위를 제한합니다.
| 구분 | ObservableObject | @Observable (Observation) |
| 추적 단위 | 객체 전체 (objectWillChange) | 읽기(Read)가 발생한 개별 프로퍼티 |
| 속성 래퍼 | @Published, @StateObject | 없음 (@State 또는 일반 선언) |
| 과도한 렌더링 원인 | 모델 내 임의의 @Published 변경 | body에서 비표면적 프로퍼티/메서드 참조 |
1. 문제 상황: 뷰 전체 재렌더링이 발생하는 잘못된 구현
아래 코드에서는 UserProfileView가 user.name과 user.age만 화면에 표시하지만, user.score가 변경될 때도 body 전체가 다시 계산됩니다.
import SwiftUI
@Observable
final class UserModel {
var name: String = "홍길동"
var age: Int = 30
var score: Int = 100 // 화면 표시와 직접적 연관이 없는 프로퍼티
}
struct UserProfileView: View {
var user: UserModel
var body: some View {
VStack(spacing: 12) {
Text("이름: \(user.name)")
Text("나이: \(user.age)")
// 문제 원인: user 객체 전체를 서브뷰 프로퍼티로 그대로 전달
HeaderSummaryView(user: user)
}
}
}
struct HeaderSummaryView: View {
var user: UserModel
var body: some View {
// user.score를 사용하지 않더라도 user 객체를 읽는 순간 관찰 대상에 포함됨
Text("사용자 프로필 요약")
}
}
- UserProfileView 내부에서 user 인스턴스 참조를 서브뷰로 직접 전달하면서 Observation 추적 범위가 서브뷰 영역까지 확장됩니다.
- 이로 인해 user.score 값만 업데이트되어도 UserProfileView와 HeaderSummaryView의 body가 동시에 다시 계산됩니다.
2. 해결 방안: 프로퍼티 격리 및 서브뷰 쪼개기
불필요한 재렌더링을 방지하기 위해서는 관찰 대상 프로퍼티를 필요한 뷰 단위로 엄격히 분리해야 합니다.
개선 코드 1: 서브뷰에 필요한 기본 타입(Primitive Type)만 전달
struct UserProfileView: View {
var user: UserModel
var body: some View {
let _ = Self._printChanges()
VStack(spacing: 12) {
Text("이름: \(user.name)")
Text("나이: \(user.age)")
// 모델 객체 전체 대신 필요한 데이터만 전달
HeaderSummaryView(title: "사용자 프로필 요약")
}
}
}
struct HeaderSummaryView: View {
let title: String
var body: some View {
Text(title)
}
}
개선 코드 2: @Binding 또는 래퍼 뷰를 통한 세부 관찰 분리
모델 내부의 특정 프로퍼티만 변경 및 관찰해야 할 경우, 서브뷰에 객체 전체 대신 프로퍼티의 바인딩이나 필요한 속성만 구체적으로 전달합니다.
struct UserScoreView: View {
// score 변경에만 반응하는 독립적인 서브뷰
@Bindable var user: UserModel
var body: some View {
Button("점수 증가: \(user.score)") {
user.score += 1
}
}
}
3. 실무 최적화 체크리스트
- Self._printChanges() 디버깅: body 내부 상단에 let _ = Self._printChanges()를 삽입하여 어느 프로퍼티 변경에 의해 뷰가 갱신되는지 콘솔 로그로 확인합니다.
- 계산된 프로퍼티(Computed Property) 주의: @Observable 모델 내의 계산된 프로퍼티가 내부적으로 다른 여러 프로퍼티를 읽는 경우, 해당 프로퍼티 중 하나만 바뀌어도 계산된 프로퍼티를 참조하는 뷰 전체가 갱신됩니다.
- @Bindable의 최소 범위 적용: @Bindable은 입력 컨트롤(TextField, Toggle 등)이 필요한 서브뷰 내부에서만 국소적으로 선언합니다.
핵심 요약
- @Observable은 body 내에서 실제 읽기(Read)가 발생하는 프로퍼티를 추적합니다.
- 모델 객체 전체를 서브뷰의 인자로 넘기면 서브뷰의 속성과 무관한 모델 변경에도 부모 뷰가 재렌더링될 수 있습니다.
- 서브뷰에는 모델 인스턴스 대신 필요한 기본 데이터 값만 넘기거나, 관찰 범위를 서브뷰 단위로 분리하여 성능을 최적화해야 합니다.
#SwiftUI #iOS17 #Observable #SwiftUI성능최적화 #ViewReRender #iOS개발 #SwiftUI트러블슈팅
반응형


