Property Wrapper는 코드 중복 없이 프로퍼티 접근 및 수정에 로직 계층을 추가하는 Swift 메커니즘입니다. SwiftUI에서 Property Wrappers는 뷰 상태 관리의 기초가 되었습니다: @State, @Binding, @ObservedObject, @StateObject 및 @Environment. Swift 문서(2025)에 따르면, 프로퍼티 래퍼는 프로젝트의 보일러플레이트 코드를 평균 40% 줄여줍니다. Property Wrapper를 이해하는 것은 모든 iOS 개발자가 프레임워크를 효과적으로 사용하기 위해 필수적입니다.
핵심 요점
Property Wrapper — 버전 5.1에서 도입된 Swift 언어 구조로, 프로퍼티 접근 로직을 별도 타입에 캡슐화할 수 있습니다. 각 클래스에서 반복적으로 getter와 setter를 작성하는 대신, 개발자는 래퍼를 한 번 선언하고 타입 앞에 @ 어노테이션을 통해 적용합니다. Swift는 자동으로 프로퍼티를 지정된 타입으로 래핑하며, 읽기 및 쓰기 시 wrappedValue와 projectedValue 메서드를 호출합니다. Apple(WWDC 2019)에 따르면, Property Wrappers는 SwiftUI의 핵심 추상화가 되었습니다.
프로퍼티 래퍼는 @propertyWrapper 속성을 가진 구조체 또는 클래스입니다. 내부적으로 이러한 타입은 실제 값을 반환하고 설정하는 wrappedValue 프로퍼티를 구현해야 합니다. Swift 컴파일러는 원래 프로퍼티에 대한 접근을 wrappedValue 호출로 대체하여, 호출 코드로부터 구현을 완전히 숨깁니다. 추가로, $ 기호를 통해 접근 가능한 프로젝션인 projectedValue를 정의할 수 있습니다.
Property Wrappers의 장점은 로직의 재사용성에 있습니다. 예를 들어, 이메일 검증, 값 캐싱 또는 저장소 동기화를 위한 래퍼를 만들어 프로젝트의 모든 프로퍼티에 적용할 수 있습니다. SwiftUI에서는 이 개념이 모든 곳에서 사용됩니다: 각 상태 관리 메커니즘이 별도의 Property Wrapper로 구현되어 있습니다.
@WrapperType var value: T 어노테이션으로 프로퍼티를 선언하면, Swift 컴파일러가 코드를 변환합니다. WrapperType의 인스턴스를 생성하고 wrappedValue를 통해 프로퍼티 접근을 생성합니다. 소스 코드 let x = value는 let x = _value.wrappedValue로, value = newValue는 _value.wrappedValue = newValue로 변환됩니다. 이 변환은 컴파일 타임에 발생하며 런타임 오버헤드가 없습니다.
@propertyWrapper
struct Capitalized {
private var text: String
var wrappedValue: String {
get { text }
set { text = newValue.capitalized }
}
init(initialValue: String) {
text = initialValue.capitalized
}
}
목록에는 문자열을 자동으로 대문자 형식으로 변환하는 Capitalized 래퍼가 나와 있습니다. 값을 할당하면 setter는 저장하기 전에 capitalized를 호출합니다. 이제 @Capitalized 어노테이션이 있는 모든 프로퍼티는 올바르게 포맷된 텍스트만 저장합니다. 이 접근 방식은 검증 및 포맷팅 코드의 중복을 완전히 제거합니다.
프로젝션(projectedValue) — $ 접두사를 통해 접근 가능한 추가 통신 채널입니다. SwiftUI에서는 이 기능이 모든 곳에서 사용됩니다: $state는 Binding
SwiftUI에는 상태 관리를 위한 5개의 내장 Property Wrappers가 포함되어 있습니다: @State, @Binding, @ObservedObject, @StateObject 및 @Environment. 각각은 특정 작업을 해결하며 다른 시나리오에서 사용됩니다. @State는 단순한 로컬 데이터용, @Binding은 자식 뷰에 데이터 참조 전달용, @ObservedObject와 @StateObject는 복잡한 객체용, @Environment는 계층 구조의 시스템 값용입니다.
| 래퍼 | 목적 | 소유권 |
|---|---|---|
| @State | 단일 뷰의 로컬 상태 | 현재 뷰 |
| @Binding | 부모와의 양방향 연결 | 부모 뷰 |
| @ObservedObject | 외부 객체 관찰 | 외부 소유자 |
| @StateObject | ObservableObject 생성 | 현재 뷰 |
| @Environment | 계층 구조의 시스템 값 | SwiftUI 환경 |
특정 Property Wrapper의 선택은 데이터 소스와 그 수명 주기에 따라 달라집니다. 데이터가 단일 뷰에 속하고 자식 컴포넌트에 필요하지 않다면 @State를 사용하세요. 자식 뷰가 부모의 데이터를 수정해야 한다면 @Binding을 사용하세요. 여러 뷰에서 사용되는 객체에는 @ObservedObject와 @StateObject가 적합합니다.
@State는 단일 뷰 내에 로컬 상태를 저장하기 위한 Property Wrapper입니다. SwiftUI는 @State 프로퍼티의 메모리를 자동으로 관리하며 변경 시마다 뷰를 다시 그립니다. @State는 현재 뷰에만 속하는 단순한 타입(String, Int, Bool, enum)과 구조체에 적합합니다. 값이 변경되면 SwiftUI는 body 프로퍼티를 다시 실행합니다.
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("카운트: \(count)")
Button("증가") {
count += 1
}
}
}
}
예제에서 @State 프로퍼티 count는 카운터의 현재 값을 저장합니다. SwiftUI는 이 프로퍼티를 위해 힙에 저장 영역을 만들고 CounterView의 수명 주기에 연결합니다. 버튼을 누르면 count가 1 증가하고, SwiftUI가 변경을 감지하여 body를 다시 실행해 새 값을 표시합니다. 중요: @State는 복잡한 참조 타입에 사용해서는 안 됩니다 — 이를 위해 @StateObject와 @ObservedObject가 있습니다.
@Binding은 다른 뷰에 속한 데이터 소스에 대한 참조를 생성합니다. Binding은 자체적으로 값을 저장하지 않으며, @State, @StateObject 또는 부모로부터 전달된 다른 Binding을 통해 데이터를 읽고 씁니다. 이를 통해 자식 컴포넌트는 데이터를 직접 소유하거나 콜백 없이도 상위 상태를 수정할 수 있습니다.
struct ToggleSwitch: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
목록에서 ToggleSwitch는 부모 뷰로부터 @BindingBool을 받습니다. 부모는 @State var isToggleOn = false를 생성하고 $isToggleOn을 ToggleSwitch의 초기화자에 전달합니다. 사용자가 자식 뷰 내에서 토글 스위치를 전환하면 변경 사항이 즉시 부모의 @State에 반영됩니다. Binding 메커니즘은 계층 구조에서 위쪽으로 변경 사항을 전달하기 위한 델리게이트나 클로저를 완전히 불필요하게 만듭니다.
@ObservedObject는 외부에서 전달된 ObservableObject 인스턴스를 관찰하기 위한 Property Wrapper입니다. 뷰는 이 객체를 소유하지 않으며, 부모 컴포넌트에서 생성되거나 Environment를 통해 주입됩니다. ObservableObject 내부의 @Published 프로퍼티가 변경되면, SwiftUI는 @ObservedObject를 통해 구독한 모든 뷰를 다시 그립니다.
@StateObject — 뷰에서 직접 ObservableObject를 생성하고 소유하기 위한 래퍼입니다. @ObservedObject와 달리, @StateObject는 뷰의 전체 수명 주기 동안 객체의 단일 인스턴스를 보장합니다. SwiftUI가 뷰 구조를 다시 생성해도(자주 발생함), @StateObject는 기존 객체를 유지하고 초기화자를 다시 호출하지 않습니다.
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
}
struct ProfileView: View {
@StateObject var settings = UserSettings()
var body: some View {
ChildProfileView(settings: settings)
}
}
struct ChildProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
Text("안녕하세요, \(settings.username)")
}
}
예제에서 ProfileView는 @StateObject를 통해 UserSettings를 생성하여 객체의 소유자가 됩니다. ChildProfileView는 @ObservedObject를 통해 동일한 인스턴스를 받아 관찰은 하지만 수명 주기는 관리하지 않습니다. username이 변경되면 두 뷰 모두 업데이트됩니다. ChildProfileView가 @ObservedObject 대신 @StateObject를 사용했다면, 매 렌더링마다 초기 값으로 새 인스턴스가 생성되었을 것입니다.
핵심 규칙: @StateObject는 객체를 생성하는 뷰(진실의 소스)에서 사용되고, @ObservedObject는 부모로부터 이미 생성된 객체를 받는 뷰에서 사용됩니다. 이 규칙을 위반하면 상태 손실이나 예기치 않은 데이터 재생성이 발생합니다.
Swift는 반복적인 프로퍼티 접근 로직을 위해 사용자 정의 Property Wrappers를 만들 수 있습니다. @propertyWrapper 속성을 가진 구조체나 클래스를 선언하고 wrappedValue를 구현하기만 하면 됩니다. 아래는 UserDefaults와 값을 자동으로 동기화하는 UserDefaultsWrapper 래퍼입니다.
@propertyWrapper
struct UserDefaultsWrapper<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct AppConfig {
@UserDefaultsWrapper(key: "theme", defaultValue: "light")
var theme: String
}
UserDefaultsWrapper 래퍼는 UserDefaults가 지원하는 모든 데이터 타입과 작동하도록 제네릭 T를 사용합니다. Getter는 키로 값을 읽고, setter는 씁니다. theme 프로퍼티에 @UserDefaultsWrapper(key:defaultValue:)를 적용하면 자동으로 저장소에 바인딩됩니다 — 모든 UserDefaults 로직이 래퍼 내에 숨겨집니다. 이는 Property Wrappers로 보일러플레이트 코드를 줄이는 전형적인 예입니다.
사용자 정의 래퍼를 만들 때는 성능을 고려하는 것이 중요합니다. Getter와 setter는 프로퍼티에 접근할 때마다 호출되므로, wrappedValue에 무거운 I/O 작업을 두어서는 안 됩니다. 비동기 데이터 저장을 위해서는 Property Wrappers를 ObservableObject 및 @Published와 결합하는 것이 좋습니다.
자주 묻는 질문
@State는 단순한 타입(String, Int, Bool)과 구조체를 위해 설계되었으며, @StateObject는 ObservableObject를 구현하는 참조 타입을 위한 것입니다. @State는 값을 SwiftUI 내에 직접 저장하고, @StateObject는 힙에서 클래스 인스턴스를 관리합니다.
네, @Binding은 @StateObject, @ObservedObject 또는 $ 프로젝션을 사용한 다른 Binding으로부터 생성할 수 있습니다. Binding은 ObservableObject로부터 $object.$publishedProperty를 통해 또는 InlineBinding으로부터 Binding.constant(value)를 통해 초기화할 수도 있습니다.
전역 데이터의 경우 @EnvironmentObject를 사용하거나 EnvironmentValues를 통해 ObservableObject를 주입하세요. @StateObject는 루트 뷰에 적합하며 이후 @ObservedObject를 통해 자식 컴포넌트에 전달됩니다.
@ObservedObject는 객체를 소유하지 않습니다 — 부모 뷰가 다시 생성되어 새 인스턴스를 전달하면 @ObservedObject가 해당 인스턴스로 전환됩니다. 상태 손실을 방지하려면 소유 뷰가 @StateObject를 사용해야 합니다.
네, 하지만 클래스 내에서 @Published 및 비동기 함수와 함께 ObservableObject를 조합하여 사용하는 것이 더 쉽습니다. Property Wrapper는 본질적으로 동기식이며, wrappedValue는 접근 시마다 계산되므로 장기 실행 작업에 적합하지 않습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.