SwiftUI - Property Wrapper

이재원·2024년 7월 21일
0

SwiftUI

목록 보기
8/9
post-thumbnail

Property Wrapper 요약

  • @propertyWrapper는 Swift에서 속성의 저장 방식을 관리하고, 코드 간에 계층을 추가하여 속성을 효과적으로 캡슐화합니다.
  • @propertyWrapper를 통해 구현된 프로퍼티는 기본적인 값 저장 로직 외에도 검증, 변형, 추가 로직을 적용할 수 있습니다.
  • 초기화 방법을 다양화하여 속성 값의 시작 조건과 제한을 사용자가 정의할 수 있게 합니다.
  • projectedValuewrappedValue의 상태나 정보에 대한 추가적인 접근을 제공하며, 이는 속성의 변경이나 추가적인 상태 정보에 유용하게 사용됩니다.
  • @propertyWrapper 사용은 코드의 재사용성과 모듈성을 증가시키며, 유지보수를 용이하게 하고, 프로젝트 전체에 일관된 로직 적용을 가능하게 합니다.

Property Wrapper

이전에 작성한 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에 접근하여 값을 가져오거나 수정할 수 있습니다.

PropertyWrapper 초기값 설정하기

위에 소개된 예제는 초기값이 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) 로 호출된 값이 저장됩니다.

Projected Value

@propertyWrapper는 추가적인 기능으로 projected Value 를 갖습니다.

projected valuewrappedValue의 값 또는 상태에 대한 메타데이터나 보조 데이터를 제공합니다.

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이 최댓값으로 제한되었는지의 여부를 확인할 수 있습니다.

Property Wrapper 사용하는 이유

그럼 여기서 굳이 @propertyWrapper를 사용하지 않아도 getter와 setter를 통해 접근 제어가 가능한데 왜 굳이 이걸 사용하는지 의문이 생길 수 있습니다.

이는 @propertyWrapper를 사용하면 기능들을 더 구조화하고, 모듈화하여 적용하는데 많은 이점을 주기 때문입니다.

아래는 주요 이점들을 정리한 내용들입니다.

1. 코드의 중복 제거 및 재사용성 증가

클래스의 경우 일반적으로 객체의 인스턴스를 생성해야 해당 객체를 사용할 수 있지만, @propertyWrapper는 속성에 직접 적용하기 때문에 인스턴스 생성 없이 여러 프로퍼티나 파일에서 사용할 수 있습니다.

또한, 이미 래퍼를 정의할 때 gettersetter를 통해 검증을 하기 때문에 추가적인 검증이 필요 없게 됩니다. 이는 코드의 중복을 제거시키는 주요 요인입니다.

2. 가독성 향상

@propertyWrapper를 사용하면 프로퍼티의 의도를 명확하게 알 수 있게 됩니다.

예를 들면, 예제로 소개된 TwelveOrLess라는 래퍼를 사용하면 해당 프로퍼티가 최대 12를 넘지 않는다는 것을 직관적으로 알 수 있습니다.

3. 표준화된 로직의 적용

@propertyWrapper를 통해 특정 프로퍼티에 일관된 로직을 적용할 수 있습니다. 이는 프로젝트 전반에 걸쳐 동일한 규칙을 적용하고자 할 때 유용합니다.

또한 프로퍼티 래퍼 내에서 로직을 변경하면, 해당 래퍼를 사용하는 모든 프로퍼티가 모두 자동으로 적용되므로 유지보수가 용이해집니다.

4. 추가적인 기능 제공

projectedValue를 통해 추가적인 정보나 기능을 추적하여 제공할 수 있습니다.

5. 프레임워크와 통합

UI 컴포넌트와 상태 관리를 통합하여 데이터 바인딩을 관리를 할 수 있게 됩니다. 이런 수준의 레벨은 함수나 클래스로 달성하기 어려운 문제들을 가능하게 합니다.

결론적으로, 함수나 클래스와 같이 전통적인 방법으로도 유효하지만, @propertyWrapper를 사용하면 좀 더 효율적이고 가독성이 좋은 코드를 작성할 수 있게 됩니다.

마무리

오늘은 propertyWrapper에 대해 알아봤습니다.

앞서 공부했던 ObservedObjectState와 같은 프로퍼티 정의에 항상 등장했던 녀석이지만 정확히 어떤 역할과 기능을 하는지는 잘 몰랐는데 이번 기회에 자세히 알 수 있어서 좋았습니다.

이 글을 읽는 분들도 애매하게 알고 넘어가기 쉬운 propertyWrapper에 대한 개념을 명확히 알게 되었으면 좋겠습니다. 🙂

출처
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Type-Properties

profile
20학번 새내기^^(였음..)

0개의 댓글