[Swift] Property Wrapper

강대훈·2025년 4월 18일
post-thumbnail

프로퍼티를 감쌀 수 있는 하나의 타입

  • 반복적으로 작성해야 하는 코드를 줄여준다.
  • 여러 프로퍼티에 재사용할 수 있다.
  • enum, class, struct에 사용 가능.

PropertyWrapper⭐️

Wrapper : 덮다, 포장하다, 감싸다

PropertyWrapper는 프로퍼티를 감싸다, 포장한다라는 의미를 가집니다. 반복되는 계산 속성이나 로직으로 인해서 코드가 불필요하게 반복되고 길어지는 것을 덜기 위해서 생겨난 타입입니다. 그럼 어떻게 사용하는지 한 번 확인해보겠습니다.

먼저 한가지 예시를 봅시다. 타입을 PropertyWrapper로 선언하는 방법은 다음과 같습니다.

@propertyWrapper
struct 이름 {

}

PropertyWrapper에서는 반드시 채택해야 하는 속성이 있습니다. wrappedValue 입니다.

@propertyWrapper
struct Upper {
	var value: String
	var wrappedValue: String {
			get {
				value.uppercased()
			}
			set {
				value = newValue
			}
      }
}

wrappedValue는 반드시 속성이여야 합니다. 저장 속성이든, 계산 속성이든 상관없지만 반드시 get 을 가지고 있어야 합니다. 그 이유에 대해서는 보다보면 점차 알 수 있습니다.

PropertyWrapper를 채택하는 방법은 다음과 같습니다.

struct Customer {
    @Upper var name: String
}

Customer타입의 name 저장 속성은 Upper타입의 PropertyWrapper로 감싸져 있습니다. 그러면 name은 실질적으로 Upper타입을 참조하게 됩니다. 이해가 안될 수 있으니 한 번 Customer타입을 초기화해보겠습니다.

초기화 하는 방법⭐️

우리는 분명히 name이라는 저장 속성을 String으로 초기화했습니다. PropertyWrapper를 채택한 Property는 실질적으로 해당 PropertyWrapper를 가르키게 됩니다. 그 중에서도 wrappedValue를 가르키게 되기 때문에, name을 String타입으로 초기화했지만, Upper타입을 가르키게 되는 것입니다.

그러면 왜 wrappedValue를 필수적으로 구현해야 되는지 알 수 있습니다. 초기화하는 과정에서 wrappedValue를 무조건 가르켜야 하기 때문입니다. 또 반드시 get 을 구현해야 하는 이유는 채택한 Property의 데이터를 PropertyWrapper에서 관리해야 하기 때문입니다.

그러면 이제 한 번 Customer타입을 초기화해보겠습니다.

var customer = Customer(name: Upper(value: "Kanghun"))

초기화가 잘 됐지만, 보통 이런식으로 초기화는 잘 하지 않습니다. 그럼 어떻게 초기화를 진행할까요?

PropertyWrapper 기본값 사용하기

@propertyWrapper
struct Upper {
    var value: String = ""

    var wrappedValue: String {
        get {
            return value.uppercased()
        }
        set {
            value = newValue
        }
    }
}

기본값을 사용하기 때문에 저장 속성이 모두 초기화되어 지정 생성자를 사용할 필요가 없어집니다.

var customer = Customer()

Customer의 name은 초기화되지 않았는데 어떻게 이게 가능한걸까요?

name은 String타입으로 선언은 되어 있지만 PropertyWrapper인 Upper타입에 감싸져있는 상태입니다. 또한 name은 실질적으로 Upper타입으로 이루어지기 때문에, Upper가 초기화된다면 name또한 초기화됐다고 판단합니다.

@Upper var name: String // 실질적으로 Upper타입입니다!

지정 생성자 사용하기

@propertyWrapper
struct Upper {
    var value: String

    var wrappedValue: String {
        get {
            value.uppercased()
        }
        set {
            value = newValue
        

    init() {
        self.value = "hello"
    }
}

struct Customer {
    @Upper var name: String
}

Customer타입의 name프로퍼티에 기본값을 줘서 초기화하는 방법입니다. 초기 값을 따로 지정하지 않은 경우에는 init() 이 호출됩니다.

let customer = Customer()
customer.value // "HELLO"

Customer타입의 name프로퍼티에 기본값을 줬다면 init(wrappedValue:) 가 호출됩니다.

@propertyWrapper
struct Upper {
    var value: String

    var wrappedValue: String {
        get {
            value.uppercased()
        }
        set {
            value = newValue
        }
    }

    init() {
        self.value = "hello"
    }
    
    init(wrappedValue: String) { // 호출!!
		    self.value = wrappedValue
    }
}
struct Customer {
    @Upper var name: String = "abcd"
}

let customer = Customer()
customer.name // "ABCD"

PropertyWrapper에도 직접 생성자 접근이 가능합니다!

@propertyWrapper
struct Upper {
    var value: String

    var wrappedValue: String {
        get {
            value.uppercased()
        }
        set {
            value = newValue
       }

    init() {
        self.value = "h"
    }

    init(wrappedValue: String) { // 호출!!
        self.value = wrappedValue
    }
}
struct Customer {
    @Upper(wrappedValue: "zzzz") var name: String
}

let customer = Customer()
customer.value // "ZZZZ"

wrappedValue⭐️

이제 초기화하는 방법까지 알아봤으니 wrappedValue가 어떻게 동작하는지 간단히 봐야할 거 같습니다.

wrappedValue는 언제 쓰이게 될까요?

→ PropertyWrapper로 감싸진 프로퍼티에 접근할 때 사용됩니다.

struct Upper {
    var value: String

    var wrappedValue: String {
        get {
            value.uppercased()
        }
        set {
            value = newValue
        }
    }
    
    init(wrappedValue: String) {
        self.value = wrappedValue
    }
}

struct Customer {
    @Upper var name: String = "zzzz"
}

var customer = Customer()
print(customer.name) // "ZZZZ"

“zzzz” 로 초기화를 했지만, 출력은 “ZZZZ”로 나옵니다. 이것은 wrappedValue의 get을 호출했기 때문입니다. PropertyWrapper로 감싸게 되면 해당 프로퍼티는 wrappedValue를 가르키게 됩니다. set을 사용할 때도 wrappedValue의 set을 호출하게 됩니다. 즉 name프로퍼티는 Upper를 가르키게 되는 것입니다.

또한 wrappedValue는 PropertyWrapper에 의해 감싸진 프로퍼티와 타입이 반드시 동일해야 합니다.

그럼으로 인해서 PropertyWrapper를 채택하는 곳에서 매우 편하게 로직에 접근할 수 있게 됩니다. 코드의 재사용성이 증가한다는 장점이 생깁니다.

그러면 마지막으로 projectedValue에 대해서도 한 번 보겠습니다.

projectedValue⭐️

wrappedValue 외에도 추가 기능을 제공하는 싶을 때 사용합니다.

projectedValue는 필수 구현자는 아닙니다. 반드시 속성 이름은 projectedValue 이어야 합니다.

추가 기능을 제공하고 싶을 때 사용하고 projectedValue에 접근할 때는 $ 를 사용합니다. 한 번 예제를 볼까요?

@propertyWrapper
struct Upper {
    var value: String

    var wrappedValue: String {
        get {
            value.uppercased()
        }
        set {
            value = newValue
        }
    }

    var projectedValue: String { // projectedValue
        get {
            value.capitalized
        }
        set {
            value = newValue
        }
    }

    init(wrappedValue: String) {
        self.value = wrappedValue
    }
}

projectedValue를 정의했고 한 번 호출해보겠습니다.

struct Customer {
    @Upper var name: String = "zzzz"
}

var customer = Customer()
print(customer.name) // "ZZZZ"
print(customer.$name) // "Zzzz"

감싸진 프로퍼티의 앞에 $ 을 붙이면 컴파일러는 자동으로 projectedValue를 호출하는 것으로 인식합니다. projectedValue에 getter에는 capitalized 가 있기 때문에 앞 문자만 대문자로 바뀐 것을 확인할 수 있습니다.

var projectedValue: Self {
    return self
}

이런식으로 활용해서 PropertyWrapper에 직접 접근하도록 허용하는 것도 가능합니다.

또한 wrappedValue와 다르게 projectedValue는 어떤 타입이든 반환할 수 있다는 장점이 있습니다. 이러한 특징으로 더욱 다양한 방식으로 사용될 수 있습니다. 대표적인 것이 SwiftUI의 State, Binding, Observable과 같은 PropertyWrapper입니다.

참고자료

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/

https://zeddios.tistory.com/1221

0개의 댓글