반응형

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. 실무 최적화 체크리스트

  1. Self._printChanges() 디버깅: body 내부 상단에 let _ = Self._printChanges()를 삽입하여 어느 프로퍼티 변경에 의해 뷰가 갱신되는지 콘솔 로그로 확인합니다.
  2. 계산된 프로퍼티(Computed Property) 주의: @Observable 모델 내의 계산된 프로퍼티가 내부적으로 다른 여러 프로퍼티를 읽는 경우, 해당 프로퍼티 중 하나만 바뀌어도 계산된 프로퍼티를 참조하는 뷰 전체가 갱신됩니다.
  3. @Bindable의 최소 범위 적용: @Bindable은 입력 컨트롤(TextField, Toggle 등)이 필요한 서브뷰 내부에서만 국소적으로 선언합니다.

 

핵심 요약

  1. @Observable은 body 내에서 실제 읽기(Read)가 발생하는 프로퍼티를 추적합니다.
  2. 모델 객체 전체를 서브뷰의 인자로 넘기면 서브뷰의 속성과 무관한 모델 변경에도 부모 뷰가 재렌더링될 수 있습니다.
  3. 서브뷰에는 모델 인스턴스 대신 필요한 기본 데이터 값만 넘기거나, 관찰 범위를 서브뷰 단위로 분리하여 성능을 최적화해야 합니다.

#SwiftUI #iOS17 #Observable #SwiftUI성능최적화 #ViewReRender #iOS개발 #SwiftUI트러블슈팅

반응형
Posted by 까칠코더
,