🔥 속성 래퍼

1405자
17분

속성 래퍼(Property Wrapper)는 중간 층 역할을 합니다. 속성을 정의하는 코드와 속성을 저장하는 방식을 관리하는 코드를 분리해 줍니다. 예를 들어, 스레드 안전성 검사를 제공하거나 기본 데이터를 데이터베이스에 저장하는 속성이 있다면, 모든 속성에 해당 코드를 작성해야 합니다. 속성 래퍼를 사용하면 래퍼를 정의할 때 관리 코드를 한 번 작성하고, 이를 여러 속성에 적용하여 관리 코드를 재사용할 수 있습니다.

속성 래퍼를 정의하려면 wrappedValue 속성을 정의하는 구조체, 열거형 또는 클래스를 만들면 됩니다. 아래 코드에서 TwelveOrLess 구조체는 래핑하는 값이 항상 12 이하의 숫자를 포함하도록 합니다. 만약 더 큰 숫자를 저장하려고 하면 12를 저장합니다.

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}
swift

setter는 새로운 값이 12 이하인지 확인하고, getter는 저장된 값을 반환합니다.

속성에 래퍼를 적용하려면 속성으로 속성 앞에 래퍼의 이름을 작성하면 됩니다. 다음은 TwelveOrLess 속성 래퍼를 사용하여 크기가 항상 12 이하인 사각형을 저장하는 구조체입니다:

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}
 
var rectangle = SmallRectangle()
print(rectangle.height)
// 출력 결과: "0"
 
rectangle.height = 10
print(rectangle.height)
// 출력 결과: "10"
 
rectangle.height = 24
print(rectangle.height)
// 출력 결과: "12"
swift

heightwidth 속성은 TwelveOrLess의 정의에서 초기값을 가져오며, TwelveOrLess.number를 0으로 설정합니다. TwelveOrLess의 setter는 10을 유효한 값으로 처리하므로 rectangle.height에 숫자 10을 저장하는 것은 작성된 대로 진행됩니다. 그러나 24는 TwelveOrLess가 허용하는 값보다 크므로, 24를 저장하려고 하면 rectangle.height에 허용되는 최대값인 12가 대신 설정됩니다.

속성에 래퍼를 적용하면 컴파일러는 래퍼의 인스턴스를 저장할 공간을 만들고, 래퍼를 통해 속성에 접근하는 코드를 자동으로 생성합니다. 속성 래퍼 자체는 래핑된 값을 저장하는 역할을 담당하므로, 컴파일러가 자동 생성하는 코드에는 값을 저장하는 부분이 포함되지 않습니다. 속성 래퍼의 특수한 문법을 사용하지 않고도 직접 코드를 작성하여 속성 래퍼의 동작을 구현할 수 있습니다. 예를 들어, 다음은 이전 코드 목록의 SmallRectangle 버전으로, 속성으로 @TwelveOrLess를 작성하는 대신 명시적으로 속성을 TwelveOrLess 구조체로 래핑합니다:

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 }
    }
}
swift

_height_width 속성은 속성 래퍼인 TwelveOrLess의 인스턴스를 저장합니다. heightwidth의 getter와 setter는 wrappedValue 속성에 대한 액세스를 래핑합니다.

래핑된 속성의 초기값 설정

위의 예제 코드에서는 TwelveOrLess의 정의에서 number에 초기값을 주어 래핑된 속성의 초기값을 설정합니다. TwelveOrLess로 래핑된 속성을 사용하는 코드는 TwelveOrLess - 예를 들어 SmallRectangle의 정의는 heightwidth에 초기값을 줄 수 없습니다. 초기값 설정 또는 다른 사용자 정의를 지원하려면 속성 래퍼에 이니셜라이저를 추가해야 합니다. 다음은 래핑된 값과 최대값을 설정하는 이니셜라이저를 정의하는 TwelveOrLess의 확장 버전인 SmallNumber입니다:

@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)
    }
}
swift

SmallNumber의 정의에는 세 개의 이니셜라이저인 init(), init(wrappedValue:), init(wrappedValue:maximum:)가 포함되어 있으며, 아래 예제에서 래핑된 값과 최대값을 설정하는 데 사용됩니다. 초기화 및 이니셜라이저 구문에 대한 자세한 내용은 Initialization을 참조하세요.

속성에 래퍼를 적용하고 초기값을 지정하지 않으면 Swift는 init() 이니셜라이저를 사용하여 래퍼를 설정합니다. 예를 들면 다음과 같습니다:

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}
 
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// 출력 결과: "0 0"
swift

heightwidth를 래핑하는 SmallNumber 인스턴스는 SmallNumber()를 호출하여 생성됩니다. 해당 이니셜라이저 내부의 코드는 기본값인 0과 12를 사용하여 초기 래핑된 값과 초기 최대값을 설정합니다. 속성 래퍼는 여전히 SmallRectangle에서 TwelveOrLess를 사용한 이전 예제와 같이 모든 초기값을 제공합니다. 해당 예제와 달리 SmallNumber는 속성 선언의 일부로 해당 초기값을 작성하는 것도 지원합니다.

속성에 대한 초기값을 지정하면 Swift는 init(wrappedValue:) 이니셜라이저를 사용하여 래퍼를 설정합니다. 예를 들면 다음과 같습니다:

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}
 
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// 출력 결과: "1 1"
 
swift

래퍼가 있는 속성에 = 1을 작성하면 init(wrappedValue:) 이니셜라이저를 호출하는 것으로 변환됩니다. heightwidth를 래핑하는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 1)을 호출하여 생성됩니다. 이니셜라이저는 여기에 지정된 래핑된 값을 사용하고 기본 최대값인 12를 사용합니다.

사용자 정의 속성 뒤에 괄호로 인자를 작성하면 Swift는 해당 인자를 받아들이는 이니셜라이저를 사용하여 래퍼를 설정합니다. 예를 들어, 초기값과 최대값을 제공하면 Swift는 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)
// 출력 결과: "2 3"
 
narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// 출력 결과: "5 4"
swift

height를 래핑하는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 5)를 호출하여 생성되고, width를 래핑하는 인스턴스는 SmallNumber(wrappedValue: 3, maximum: 4)를 호출하여 생성됩니다.

속성 래퍼 인자를 포함하면 래퍼에서 초기 상태를 설정하거나 래퍼가 생성될 때 래퍼에 다른 옵션을 전달할 수 있습니다. 이 구문은 속성 래퍼를 사용하는 가장 일반적인 방법입니다. 속성에 필요한 모든 인자를 제공할 수 있으며, 이 인자들은 이니셜라이저에 전달됩니다.

속성 래퍼 인자를 포함할 때 할당을 사용하여 초기값을 지정할 수도 있습니다. Swift는 할당을 wrappedValue 인자처럼 취급하고 포함된 인자를 받아들이는 이니셜라이저를 사용합니다. 예를 들면 다음과 같습니다:

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}
 
var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// 출력 결과: "1"
 
mixedRectangle.height = 20
print(mixedRectangle.height)
// 출력 결과: "12"
swift

height를 래핑하는 SmallNumber 인스턴스는 SmallNumber(wrappedValue: 1)을 호출하여 생성되며, 기본 최대값인 12를 사용합니다. width를 래핑하는 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 호출하여 생성됩니다.

속성 래퍼에서 값 투영

래핑된 값 외에도 속성 래퍼는 projected value를 정의하여 추가 기능을 노출할 수 있습니다. 예를 들어, 데이터베이스에 대한 액세스를 관리하는 속성 래퍼는 투영된 값에 flushDatabaseConnection() 메서드를 노출할 수 있습니다. 투영된 값의 이름은 $로 시작한다는 점을 제외하면 래핑된 값과 동일합니다. 코드에서 $로 시작하는 속성을 정의할 수 없기 때문에 투영된 값은 정의한 속성과 절대 충돌하지 않습니다.

위의 SmallNumber 예제에서 속성을 너무 큰 숫자로 설정하려고 하면 속성 래퍼는 저장하기 전에 숫자를 조정합니다. 아래 코드는 SmallNumber 구조체에 projectedValue 속성을 추가하여 속성 래퍼가 저장하기 전에 속성의 새 값을 조정했는지 여부를 추적합니다.

@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)
// 출력 결과: "false"
 
someStructure.someNumber = 55
print(someStructure.$someNumber)
// 출력 결과: "true"
swift

someStructure.$someNumber를 작성하면 래퍼의 투영된 값에 접근합니다. 4와 같은 작은 숫자를 저장한 후에는 someStructure.$someNumber의 값이 false입니다. 그러나 55와 같이 너무 큰 숫자를 저장하려고 하면 투영된 값은 true가 됩니다.

속성 래퍼는 투영된 값으로 모든 유형의 값을 반환할 수 있습니다. 이 예제에서 속성 래퍼는 숫자가 조정되었는지 여부라는 한 가지 정보만 노출하므로 해당 Bool 값을 투영된 값으로 노출합니다. 더 많은 정보를 노출해야 하는 래퍼는 다른 유형의 인스턴스를 반환하거나 래퍼의 인스턴스를 투영된 값으로 노출하기 위해 self를 반환할 수 있습니다.

속성 getter나 인스턴스 메서드와 같이 해당 유형의 일부인 코드에서 투영된 값에 접근할 때는 다른 속성에 접근할 때처럼 속성 이름 앞에 self.를 생략할 수 있습니다. 다음 예제의 코드는 heightwidth 주위의 래퍼의 투영된 값을 $height$width로 참조합니다:

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
    }
}
swift

속성 래퍼 구문은 getter와 setter가 있는 속성에 대한 문법적 설탕일 뿐이므로 heightwidth에 접근하는 것은 다른 속성에 접근하는 것과 동일하게 동작합니다. 예를 들어 resize(to:)의 코드는 속성 래퍼를 사용하여 heightwidth에 접근합니다. resize(to: .large)를 호출하면 .large에 대한 switch 케이스가 사각형의 높이와 너비를 100으로 설정합니다. 래퍼는 해당 속성의 값이 12보다 커지는 것을 방지하고 값을 조정했다는 사실을 기록하기 위해 투영된 값을 true로 설정합니다. resize(to:) 끝에서 return 문은 속성 래퍼가 height 또는 width를 조정했는지 여부를 결정하기 위해 $height$width를 확인합니다.

이상으로 속성 래퍼에 대해 자세히 알아보았습니다. 속성 래퍼를 사용하면 속성 저장 방식을 관리하는 코드를 재사용할 수 있어 코드 중복을 줄이고 관심사를 분리할 수 있습니다. 래퍼를 속성에 적용하는 것은 간단하며, 초기값 설정, 투영된 값 노출 등 다양한 사용자 정의 옵션을 제공합니다.

속성 래퍼는 Swift 5.1부터 도입되었으며, 프레임워크와 라이브러리에서 널리 사용되고 있습니다. 예를 들어, SwiftUI에서는 @State, @Binding, @ObservedObject 등의 속성 래퍼를 사용하여 뷰의 상태와 데이터 흐름을 관리합니다. 또한 속성 래퍼는 코어 데이터와의 통합, 사용자 기본 설정 저장, 스레드 안전성 보장 등 다양한 용도로 활용될 수 있습니다.

속성 래퍼를 직접 만들어 사용해 보는 것도 좋은 연습이 될 것입니다. 간단한 예제부터 시작해서 점차 복잡한 기능을 구현해 보세요. 속성 래퍼는 강력하고 유연한 도구이므로, 창의력을 발휘하여 다양한 문제를 해결하는 데 활용해 보시기 바랍니다!