반응형

Swift 6 렌더링 최적화: ObservableObject에서 @Observable 마이그레이션 가이드

iOS 17부터 도입되어 Swift 6 동시성(Concurrency) 모델의 표준으로 자리 잡은 @Observable 매크로는 기존 Combine 기반 ObservableObject의 보일러플레이트 코드를 대폭 줄이고 뷰 렌더링 성능을 최적화합니다. 기존 ObservableObject 패턴에서 @Observable 기반 아키텍처로 안전하게 마이그레이션하는 단계별 가이드와 코드 변경 사항을 정리합니다.


 

1. ObservableObject와 @Observable의 차이점 및 작동 원리

기존 Combine 기반 ObservableObject와 Observation 프레임워크 기반 @Observable의 핵심 차이는 데이터 변경 추적의 세밀도(Granularity)에 있습니다.

  • ObservableObject (Combine 방식): @Published 속성이 붙은 프로퍼티 중 하나만 변경되어도 objectWillChange 신호가 발송됩니다. 해당 객체를 구독하는 SwiftUI 뷰는 사용하지 않는 프로퍼티가 변경되어도 뷰 전체가 다시 그려지는 렌더링 비효율이 발생합니다.
  • @Observable (Observation 프레임워크): Swift 5.9 및 iOS 17부터 도입된 Observation 프레임워크는 프로퍼티 단위 트래킹(Property-level tracking)을 지원합니다. SwiftUI 뷰의 body 내에서 실제 접근(Access)하여 읽어오는 프로퍼티의 변경만 감지하여 해당 뷰만 선택적으로 리렌더링합니다.

 

2. 주요 마이그레이션 단계별 코드 비교

 

(1) Model 및 ViewModel 클래스 선언 변경

ObservableObject 프로토콜과 @Published 프로퍼티 래퍼를 제거하고 클래스 상단에 @Observable 매크로를 부여합니다.

[기존 코드: ObservableObject]

import SwiftUI
import Combine

final class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var age: Int = 0
    var isAdult: Bool {
        return age >= 19
    }
}

[마이그레이션 코드: @Observable]

import SwiftUI

@Observable 
final class UserViewModel {
    var name: String = ""
    var age: Int = 0
    var isAdult: Bool {
        return age >= 19
    }
}
  • @Published 키워드가 불필요합니다. 클래스 내의 모든 저장 프로퍼티는 기본적으로 추적 대상으로 지정됩니다.
  • 추적에서 제외하고 싶은 프로퍼티가 있다면 @ObservationIgnored 매크로를 붙여 제외할 수 있습니다.

 

(2) SwiftUI 뷰 래퍼 변경 (@StateObject / @ObservedObject)

뷰 내부에서 소유하거나 전달받는 데이터 래퍼 선언 방식이 단순화됩니다.

[기존 코드: @StateObject 및 @ObservedObject]

struct UserProfileView: View {
    // 뷰가 객체 수명을 소유할 때
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        UserDetailView(viewModel: viewModel)
    }
}

struct UserDetailView: View {
    // 상위 뷰로부터 객체를 전달받을 때
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        Text(viewModel.name)
    }
}

[마이그레이션 코드: @State 및 일반 변수]

struct UserProfileView: View {
    // @StateObject -> @State로 대체
    @State private var viewModel = UserViewModel()
    
    var body: some View {
        UserDetailView(viewModel: viewModel)
    }
}

struct UserDetailView: View {
    // @ObservedObject -> 일반 let/var 프로퍼티로 대체
    var viewModel: UserViewModel
    
    var body: some View {
        Text(viewModel.name)
    }
}

 

(3) Environment 수동 전달 방식 변경 (@EnvironmentObject)

.environmentObject(_:) 및 @EnvironmentObject 조합은 타입을 키로 사용하는 .environment(_:) 및 @Environment로 일관되게 변경됩니다.

[기존 코드: @EnvironmentObject]

// 상위 뷰
ContentView()
    .environmentObject(userViewModel)

// 하위 뷰
struct SettingsView: View {
    @EnvironmentObject var viewModel: UserViewModel
    
    var body: some View {
        Text(viewModel.name)
    }
}

[마이그레이션 코드: @Environment]

// 상위 뷰
ContentView()
    .environment(userViewModel)

// 하위 뷰
struct SettingsView: View {
    @Environment(UserViewModel.self) private var viewModel
    
    var body: some View {
        Text(viewModel.name)
    }
}

 

(4) 양방향 데이터 바인딩 ($) 처리 (@Bindable)

TextField나 Toggle처럼 뷰 내부 프로퍼티에 양방향 바인딩(Binding)을 형성해야 하는 경우 @Bindable 래퍼를 활용합니다.

struct EditProfileView: View {
    @Bindable var viewModel: UserViewModel
    
    var body: some View {
        Form {
            TextField("이름", text: $viewModel.name)
            Stepper("나이: \(viewModel.age)", value: $viewModel.age)
        }
    }
}

 

3. Swift 6 동시성(Strict Concurrency) 대응

Swift 6 컴파일 환경에서는 스레드 간 데이터 레이스(Data Race) 방지를 위한 엄격한 동시성 검사(Complete Strict Concurrency Check)가 기본 적용됩니다. UI 렌더링에 연결되는 ViewModel 클래스는 메인 스레드 격리를 명시해야 합니다.

import SwiftUI

@MainActor
@Observable 
final class UserViewModel {
    var name: String = ""
    var age: Int = 0
    
    func fetchUserData() async {
        // 비동기 작업 처리 후 안전한 메인 액터 상태 업데이트
        let fetchedName = await NetworkService.getUserName()
        self.name = fetchedName
    }
}
  • @MainActor를 클래스 레벨에 지정하여 모든 프로퍼티 접근과 메서드 실행이 메인 스레드에서만 일어남을 보장합니다.

 

4. 변경 사항 종합 비교표

구분 기존 ObservableObject @Observable 마이그레이션
클래스 프로토콜 class Model: ObservableObject @Observable class Model
프로퍼티 래퍼 @Published var data: String var data: String (기본 추적)
뷰 소유권 래퍼 @StateObject private var model @State private var model
전달 객체 래퍼 @ObservedObject var model var model: Model
환경 객체 주입 .environmentObject(model) .environment(model)
환경 객체 수신 @EnvironmentObject var model @Environment(Model.self) var model
양방향 바인딩 $model.data @Bindable var model 사용 후 $model.data
추적 제외 설정 일반 var 선언 @ObservationIgnored var temp

 

5. 결론

@Observable 도입은 불필요한 뷰 리렌더링을 차단하여 성능을 높이고, @Published, @StateObject, @EnvironmentObject 등 불필요한 프로퍼티 래퍼 오버헤드를 제거하여 코드 유지보수성을 극대화하는 마이그레이션 방안입니다.

반응형
Posted by 까칠코더
,