@propertyWrapper
는 Swift에서 속성의 저장 방식을 관리하고, 코드 간에 계층을 추가하여 속성을 효과적으로 캡슐화합니다.@propertyWrapper
를 통해 구현된 프로퍼티는 기본적인 값 저장 로직 외에도 검증, 변형, 추가 로직을 적용할 수 있습니다.projectedValue
는 wrappedValue
의 상태나 정보에 대한 추가적인 접근을 제공하며, 이는 속성의 변경이나 추가적인 상태 정보에 유용하게 사용됩니다.@propertyWrapper
사용은 코드의 재사용성과 모듈성을 증가시키며, 유지보수를 용이하게 하고, 프로젝트 전체에 일관된 로직 적용을 가능하게 합니다.이전에 작성한 ObservableObject, published, StateObject, State, Binding를 보면 @propertyWrapper
라는 것으로 되어 있습니다.
항상 간략하게 언급하고 지나갔었는데 이번 포스팅에서는 @propertyWrapper
에 대해 자세히 알아보고자 합니다.
공식문서를 보면 property wrapper
란 속성이 저장되는 방식을 관리하는 코드나 속성이 정의되는 코드 사이에 분리 계층을 추가하는 것이라고 나와 있습니다.
무슨 말인지 감이 안 잡히는데요. 코드를 한번 봐 보시죠.
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
위 코드는 @propertyWrapper
로 정의된 구조체입니다.
setter를 통해 12이하의 값을 항상 보장하며, getter을 통해 저장된 값을 반환해 줍니다.
이렇게 정의한 TwelveOrLess는 변수명 앞에 작성하여 사용할 수 있습니다.
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"
rectangle.height = 10
print(rectangle.height)
// Prints "10"
rectangle.height = 24
print(rectangle.height)
// Prints "12"
SmallRectangle을 인스턴스화 하면 초깃값으로 0을 갖게 되고, setter를 통해 값을 변경할 수 있습니다. 다만, 12가 넘는다면 그 값은 12로 대체되도록 설계되어 있기 때문에 24를 저장하려고 해도 12가 대신 저장되어 호출됩니다.
propertyWrappe를 사용하는 다른 방법도 있습니다.
struct SmallRectangle {
private var _height = TwelveOrLess()
private var _width = TwelveOrLess()
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
}
위 코드처럼 _height과 _width에 TwelveOrLess 객체를 저장하고, getter와 setter를 통해 wrappedValue에 접근하여 값을 가져오거나 수정할 수 있습니다.
위에 소개된 예제는 초기값이 0으로 고정되어 있습니다. 이를 initiali value를 직접 지정하여 초기화 할 수 있도록 해 보겠습니다.
@propertyWrapper
struct SmallNumber {
private var maximum: Int
private var number: Int
var wrappedValue: Int {
get { return number }
set { number = min(newValue, maximum) }
}
init() {
maximum = 12
number = 0
}
init(wrappedValue: Int) {
maximum = 12
number = min(wrappedValue, maximum)
}
init(wrappedValue: Int, maximum: Int) {
self.maximum = maximum
number = min(wrappedValue, maximum)
}
}
위 코드를 보면 총 세개의 initializer가 존재합니다.
init()
은 초기값을 지정하지 않았을 경우에 호출됩니다.
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"
init(wrappedValue:)
는 초기값을 지정해 줬을때 호출됩니다.
struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"
마지막으로는 인수를 괄호안에 작성했을 때입니다. 이 경우에는 init(wrappedValue: maximum: )
이 호출됩니다.
struct NarrowRectangle {
@SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
@SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}
var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3"
narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"
이와 같은 방식으로 초기화를 한다면 maximum
을 12가 아닌 사용자가 지정한 값으로 설정할 수 있습니다.
이는 아래와 같은 방식으로도 작성할 수 있습니다.
struct MixedRectangle {
@SmallNumber var height: Int = 1
@SmallNumber(maximum: 9) var width: Int = 2
}
var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Prints "1"
mixedRectangle.height = 20
print(mixedRectangle.height)
// Prints "12"
이 코드의 경우 width에 SmallNumber(wrappedValue: 2, maximum: 9)
로 호출된 값이 저장됩니다.
@propertyWrapper는 추가적인 기능으로 projected Value
를 갖습니다.
projected value
는 wrappedValue
의 값 또는 상태에 대한 메타데이터나 보조 데이터를 제공합니다.
Swift에서는 projectedValue를 사용하여 wrappedValue에 대한 추가적인 정보를 노출시킬 수 있는데, 이는 wrappedValue가 수정될 때 같이 변경될 수 있습니다.
projected value와 wrapped value를 서로 연결시키는 것은 @propertyWrapper의 영역이기 때문에 개발자는 projected value를 해당 프로퍼티에 포함시킬지 말지의 여부만 결정하여 작성하면 됩니다.
projected value에 접근하는 방법은 $
를 사용하는 것입니다. projected value의 이름은 wrapped value와 같기 때문입니다. 따라서 호출할 때 $
를 제외한다면 wrapped value를 호출하는 것이고, $
를 붙인다면 projected Value를 호출하는 것입니다.
@propertyWrapper
struct SmallNumber {
private var number: Int
private(set) var projectedValue: Bool
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
init() {
self.number = 0
self.projectedValue = false
}
}
struct SomeStructure {
@SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"
someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"
위 코드를 보면 12보다 작은 값이 입력된 경우에는 projectedValue에 false가 저장되고, 12보다 큰 값이 입력되면 true가 저장됩니다.
그리고 이를 $
를 통해 접근하는 것을 확인할 수 있습니다.
따라서 wrappedValue는 실제 값을 저장하고 관리하는 역할을 하고, projectedValue는 wrappedValue의 추가적인 상태나 정보를 제공하는 역할을 합니다.
prjected value는 아래와 같은 경우로 사용될 수 있습니다.
enum Size {
case small, large
}
struct SizedRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
mutating func resize(to size: Size) -> Bool {
switch size {
case .small:
height = 10
width = 20
case .large:
height = 100
width = 100
}
return $height || $width
}
}
resize(to: )의 반환값을 prjected value로 함으로써, height과 weight이 최댓값으로 제한되었는지의 여부를 확인할 수 있습니다.
그럼 여기서 굳이 @propertyWrapper를 사용하지 않아도 getter와 setter를 통해 접근 제어가 가능한데 왜 굳이 이걸 사용하는지 의문이 생길 수 있습니다.
이는 @propertyWrapper를 사용하면 기능들을 더 구조화하고, 모듈화하여 적용하는데 많은 이점을 주기 때문입니다.
아래는 주요 이점들을 정리한 내용들입니다.
클래스의 경우 일반적으로 객체의 인스턴스를 생성해야 해당 객체를 사용할 수 있지만, @propertyWrapper는 속성에 직접 적용하기 때문에 인스턴스 생성 없이 여러 프로퍼티나 파일에서 사용할 수 있습니다.
또한, 이미 래퍼를 정의할 때 getter
와 setter
를 통해 검증을 하기 때문에 추가적인 검증이 필요 없게 됩니다. 이는 코드의 중복을 제거시키는 주요 요인입니다.
@propertyWrapper를 사용하면 프로퍼티의 의도를 명확하게 알 수 있게 됩니다.
예를 들면, 예제로 소개된 TwelveOrLess라는 래퍼를 사용하면 해당 프로퍼티가 최대 12를 넘지 않는다는 것을 직관적으로 알 수 있습니다.
@propertyWrapper를 통해 특정 프로퍼티에 일관된 로직을 적용할 수 있습니다. 이는 프로젝트 전반에 걸쳐 동일한 규칙을 적용하고자 할 때 유용합니다.
또한 프로퍼티 래퍼 내에서 로직을 변경하면, 해당 래퍼를 사용하는 모든 프로퍼티가 모두 자동으로 적용되므로 유지보수가 용이해집니다.
projectedValue를 통해 추가적인 정보나 기능을 추적하여 제공할 수 있습니다.
UI 컴포넌트와 상태 관리를 통합하여 데이터 바인딩을 관리를 할 수 있게 됩니다. 이런 수준의 레벨은 함수나 클래스로 달성하기 어려운 문제들을 가능하게 합니다.
결론적으로, 함수나 클래스와 같이 전통적인 방법으로도 유효하지만, @propertyWrapper
를 사용하면 좀 더 효율적이고 가독성이 좋은 코드를 작성할 수 있게 됩니다.
오늘은 propertyWrapper
에 대해 알아봤습니다.
앞서 공부했던 ObservedObject
나 State
와 같은 프로퍼티 정의에 항상 등장했던 녀석이지만 정확히 어떤 역할과 기능을 하는지는 잘 몰랐는데 이번 기회에 자세히 알 수 있어서 좋았습니다.
이 글을 읽는 분들도 애매하게 알고 넘어가기 쉬운 propertyWrappe
r에 대한 개념을 명확히 알게 되었으면 좋겠습니다. 🙂