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 등 불필요한 프로퍼티 래퍼 오버헤드를 제거하여 코드 유지보수성을 극대화하는 마이그레이션 방안입니다.
'개발 > Swift' 카테고리의 다른 글
| Swift 6 Strict Concurrency 대응: SwiftData ModelActor 비동기 파싱 및 저장 가이드 (0) | 2026.08.05 |
|---|---|
| Xcode 27 / Swift 6.3 SPM 동적 라이브러리(Dynamic Framework) 링킹 충돌 원인과 해결 방법 (0) | 2026.08.04 |
| Swift 6 데이터 레이스 컴파일 에러 해결: MainActor와 Sendable 실무 패턴 (0) | 2026.07.31 |
| TCA Study - State · Action · Reducer · Store — TCA의 핵심 구조 완전 정복 (0) | 2025.12.16 |
| TCA Study - Swift 개발자가 TCA를 알아야 하는 이유 (0) | 2025.12.16 |
| Swift에서 정렬: sort vs sorted (0) | 2025.11.11 |
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |


