SwiftUI Property Wrapper의 Underlying Value

윤지하·2025년 8월 14일
0

swift

목록 보기
3/9

들어가며

SwiftUI를 사용하다 보면 @State, @Binding, @ObservedObject 같은 Property Wrapper를 자주 사용합니다. 하지만 커스텀 이니셜라이저를 만들 때 "Cannot assign to property: 'self' is immutable" 같은 에러를 만나본 적이 있으신가요? 이때 필요한 것이 바로 Underlying Value입니다.

Property Wrapper의 3가지 값

SwiftUI의 Property Wrapper는 실제로 3가지 다른 값을 제공합니다:

1. Wrapped Value (일반 값)

@State private var count: Int = 0

// count로 접근 - Int 타입
Text("\(count)")  // 0
count += 1        // 직접 값 수정

2. Underlying Value (언더스코어 값)

@State private var count: Int = 0

// _count로 접근 - State<Int> 타입
let stateItself = _count  // State 구조체 자체

3. Projected Value (달러 기호 값)

@State private var count: Int = 0

// $count로 접근 - Binding<Int> 타입
TextField("Number", text: $count)  // 양방향 바인딩

Underlying Value가 필요한 경우

1. 커스텀 이니셜라이저에서 초기값 설정

가장 흔한 사용 사례입니다. 외부에서 받은 값으로 @State를 초기화해야 할 때:

struct CounterView: View {
    @State private var count: Int
    let minValue: Int
    
    init(startValue: Int, minValue: Int) {
        self.minValue = minValue
        
        // ❌ 컴파일 에러!
        // self.count = startValue
        
        // ✅ 올바른 방법
        self._count = State(initialValue: startValue)
    }
    
    var body: some View {
        Text("Count: \(count)")
    }
}

2. 조건부 초기값 설정

저장된 값이 있으면 사용하고, 없으면 기본값을 사용하는 패턴:

struct PersistentCounter: View {
    @State private var count: Int
    let key: String
    
    init(key: String, defaultValue: Int = 0) {
        self.key = key
        
        // UserDefaults에서 값 확인
        if let savedValue = UserDefaults.standard.object(forKey: key) as? Int {
            self._count = State(initialValue: savedValue)
        } else {
            self._count = State(initialValue: defaultValue)
        }
    }
    
    var body: some View {
        // ...
    }
}

3. Core Data와 함께 사용

Core Data 엔티티의 값으로 State를 초기화:

struct TaskView: View {
    @State private var isCompleted: Bool
    @ObservedObject var task: Task  // Core Data Entity
    
    init(task: Task) {
        self.task = task
        // Core Data의 현재 값으로 State 초기화
        self._isCompleted = State(initialValue: task.isCompleted)
    }
    
    var body: some View {
        Toggle("Completed", isOn: $isCompleted)
            .onChange(of: isCompleted) { newValue in
                task.isCompleted = newValue
                // Core Data 저장
            }
    }
}

4. 계산된 초기값 사용

복잡한 로직으로 초기값을 결정해야 할 때:

struct SmartCounter: View {
    @State private var count: Int
    @State private var step: Int
    
    init(range: ClosedRange<Int>) {
        let midPoint = (range.lowerBound + range.upperBound) / 2
        let calculatedStep = max(1, (range.upperBound - range.lowerBound) / 10)
        
        self._count = State(initialValue: midPoint)
        self._step = State(initialValue: calculatedStep)
    }
    
    var body: some View {
        Stepper("Value: \(count)", value: $count, step: step)
    }
}

각 Property Wrapper별 Underlying Value 사용법

@State

struct MyView: View {
    @State private var text: String
    
    init(initialText: String) {
        _text = State(initialValue: initialText)
    }
}

@StateObject

class ViewModel: ObservableObject {
    @Published var data: String
    
    init(data: String) {
        self.data = data
    }
}

struct MyView: View {
    @StateObject private var viewModel: ViewModel
    
    init(initialData: String) {
        _viewModel = StateObject(wrappedValue: ViewModel(data: initialData))
    }
}

@Binding

struct ChildView: View {
    @Binding var value: Int
    
    init(value: Binding<Int>) {
        self._value = value
    }
    
    // 또는 더 간단하게
    init(value: Binding<Int>) {
        self._value = value
    }
}

@AppStorage

struct SettingsView: View {
    @AppStorage private var username: String
    
    init(userKey: String, defaultName: String = "Guest") {
        _username = AppStorage(wrappedValue: defaultName, userKey)
    }
}

주의사항

1. init에서만 사용

Underlying value는 주로 이니셜라이저에서만 사용합니다. View의 body나 다른 메서드에서는 일반적으로 wrapped value를 사용하세요.

2. 타입 확인

  • count: Int 타입
  • _count: State<Int> 타입
  • $count: Binding<Int> 타입

각각 다른 타입이므로 용도에 맞게 사용해야 합니다.

3. @StateObject의 특별한 경우

@StateObjectwrappedValue 파라미터를 사용합니다:

// @State와 다른 형식
_viewModel = StateObject(wrappedValue: ViewModel())

정리

Underlying Value는 SwiftUI에서 Property Wrapper의 초기값을 동적으로 설정할 때 필수적인 개념입니다. 특히 다음과 같은 경우에 유용합니다:

  1. 외부에서 받은 값으로 State 초기화
  2. 조건에 따른 초기값 설정
  3. Core Data나 UserDefaults와 연동
  4. 계산된 값으로 초기화

_variableName 형식으로 접근하며, 주로 이니셜라이저에서 State(initialValue:)와 함께 사용됩니다.

참고 자료

profile
성장하고 싶은 개발자

0개의 댓글