Property Wrapper
Property Wrapper는 Swift 5.1에 나온 개념으로,
프로퍼티가 저장되는 방식을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가합니다.
구조체, 클래스, 열거형의 local stored variable에서만 사용 가능하고,
전역 변수나 연산 프로퍼티에서는 사용 불가능해요.
정의는 아주 어렵지만... 보일러 플레이트 코드를 줄일 수 있는 유용한 기술입니다.
(*보일러 플레이트 코드 : 유사한 코드가 여러 곳에서 사용되며, 반복적으로 비슷한 형태를 띄는 코드)
SwiftUI에서 자주 보인다고 하는데요(전 아직 SwiftUI를 안 해봄 ㅠ)
@Published, @Binding, @ObservedObject, @State 등이 Property Wrapper로 구현되어 있다고 합니다.
Property Wrapper 필요성
그럼 Property Wrapper가 왜 필요한지 예시를 통해 알아봅시다.
한 자리수만 저장하는 구조체가 있다고 합시다.
struct SingleDigit {
private var _number: Int = 0
var number: Int {
get { self._number <= 9 ? self._number : 9 }
set { self._number = newValue }
}
}
이 number를 사용하는 곳이 매우 많다고 할 때,
매번 number에 값을 넣거나 사용할 때마다 10 이상인지 체크하는 것은
보일러 플레이트 코드도 많아지고 개발자도 번거롭습니다.
그럴 때 위 코드처럼 getter와 setter를 사용할 수 있겠는데요.
다음 예시도 보도록 합시다.
이 구조체는 이름과 닉네임을
앞뒤 공백이 없고 소문자로만 구성된 문자열이어야 한다고 합시다.
이를 getter와 setter를 이용해 구현하면
struct Person {
private var _name: String = ""
private var _nickname: String = ""
var name: String {
get {
return self._name
.trimmingCharacters(in: .whitespaces)
.lowercased()
}
set { self._name = newValue }
}
var nickname: String {
get {
return self._nickname
.trimmingCharacters(in: .whitespaces)
.lowercased()
}
set { self._nickname = newValue }
}
}
name과 nickname에 중복되는 코드가 생기면서 전체 길이가 길어지는 문제가 생겼습니다.
이럴 때 Property Wrapper를 사용하면 중복되는 코드를 줄이면서 매번 조건을 검사해야 하는 일을 없앨 수 있는 것이죠.
Property Wrapper 예시
Property Wrapper가 왜 필요한지 알았으니
지금부터는 어떻게 쓰는지 알아봅시다.
Swift 문서에서는 관리 코드를 한 번 작성한 다음 여러 속성에 적용하여 해당 관리 코드를 재사용하라고 나와있는데요.
말만 어렵지 실제로는 크게 어렵지 않아요!
Property Wrapper는 @propertyWrapper 키워드를 이용해 선언할 수 있습니다.
@propertyWrapper
struct LowerString {
...
}
이렇게 LowerString은 특별한 타입이라는 것을 표시하는거죠.
@propertyWrapper는 어떠한 동작을 할 타입에 붙여야 합니다.
지금은 "공백을 지워주고 소문자로 바꿔주는 동작"을 하는 타입으로 LowerString을 만들거라
LowerString 위에 @propertyWrapper를 적어주었습니다.
Property Wrapper에는 반드시 wrappedValue가 필요합니다.
변수명까지 반드시 일치해야 해요.
만약 wrappedValue를 선언하지 않는다면 컴파일 에러가 발생합니다.
이제 제대로 LowerString을 정의해볼게요.
@propertyWrapper
struct LowerString {
private var value: String = ""
var wrappedValue: String {
get { self.value }
set {
self.value = newValue
.trimmingCharacters(in: .whitespaces)
.lowercased()
}
}
init(wrappedValue initialValue: String) {
self.wrappedValue = initialValue
}
}
wrappedValue에 프로퍼티에서 반복되는 로직을 넣으면 됩니다.
init을 사용하면 구조체 인스턴스를 생성할 때 wrappedValue를 설정할 수 있고
init이 없으면 value의 초기값으로 설정됩니다.
생각보다 크게 어려운건 없죠??
이어서 LowerString을 어떻게 사용하는지도 알아봅시다.
struct Person {
@LowerString var name: String
@LowerString var nickname: String
}
중복되는 로직이 있는 프로퍼티에 @LowerString을 적으면
해당 Property Wrapper와 프로퍼티를 연결할 수 있습니다.
위 예시에서는 name과 nickname에 중복되는 로직이 있었으니
두 프로퍼티를 LowerString와 연결해주었습니다.
아래처럼 name과 nickname에 초기값을 설정할 수도 있는데요.
struct Person {
@LowerString var name: String = "Name"
@LowerString var nickname: String = "Nickname"
}
이렇게 하면 설정한 초기값이 LowerString의 init()에 들어가게 됩니다.
그래서 init()이 없다면 name과 nickname에 초기값 설정이 불가능해요.
@LowerString(wrappedValue: "Name") var name: String
@LowerString(wrappedValue: "Nickname") var nickname: String
또한 이렇게 wrappedValue를 포함한 LowerString의 프로퍼티를 초기화시킬 수도 있습니다.
이것도 init이 있어야 가능하고, 이 스타일이 가장 자주 사용된다고 합니다!
이렇게 만든 프로퍼티들을
구조체 인스턴스를 생성해서 확인해보면
var person: Person = Person(name: " YuJeongJu", nickname: "AppleDuckhoo ")
print(person.name) //yujeongju
print(person.nickname) //appleduckhoo
앞뒤 공백을 지우고 소문자로 변경한 문자열을 얻을 수 있네요.
구조체 인스턴스를 생성할 때 초기화 값을 매개변수로 전달하면
LowerString의 init()이 그 값을 받고
value에는 wrappedValue의 setter를 거쳐간 값이 저장됩니다.
마지막으로 구조체 인스턴스에서 프로퍼티를 사용하면 wrappedValue의 getter를 거친 값이 출력되는거죠.
Property Wrapper 활용
Property Wrapper는 UserDefaults를 사용할 때 아주 유용할 수 있습니다.
UserDefaults 간략화는 WWDC 19에서도 사용한 예시입니다.
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
enum GlobalSettings {
@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool
@UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
static var isBarFeatureEnabled: Bool
}
UserDefault라는 Property Wrapper가 없었다면
모든 프로퍼티에 getter/setter를 설정하여 긴 코드를 번거롭게 작성해야 했을겁니다.
중복되는 로직을 Property Wrapper로 설정하여 코드 길이를 줄이고
key와 defaultValue를 enum 안에서 초기화하니 가독성, 직관성이 올라가고 휴먼 에러도 줄일 수 있을 것입니다.
Projected Value
마지막으로 한 가지 개념이 남았습니다.
projectedValue는 Property Wrapper가 저장하기 전에 값을 조정했는지 알려줍니다.
projectedValue도 변수명을 정확히 작성해야 해요.
예시를 좀 쉽게 수정했습니다 ㅎ;
struct SomeStruct {
@SmallNumber var number: Int
}
@propertyWrapper
struct SmallNumber {
private var number: Int
private(set) var projectedValue: Bool = false
var wrappedValue: Int {
get { return number }
set {
if newValue > 10 {
number = 10
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
init() {
self.number = 0
}
}
할당된 값이 10을 초과하지 못하도록 제한하는 Property Wrapper입니다.
만약 10을 초과하면 projectedValue가 true이고 아니라면 false에요.
이 projectedValue는 projectedValue라는 이름으로 파라미터에 접근하는 것이 아니라,
$프로퍼티명으로 접근이 가능합니다!
var smallNumber: SomeStruct = SomeStruct()
smallNumber.number = 10
print(smallNumber.$number) //false
smallNumber.number = 20
print(smallNumber.$number) //true
만약 number 프로퍼티가 10이 초과된 값이 할당되었다면 $number는 false인 것입니다.
정말 띠용할 정도로 좋은 기능 아닌가요?
projectedValue가 없었다면 isBiggerThan10 같은 Bool 변수를 하나하나 만들어줬어야 했을거에요.
하지만, projectedValue가 있어서 각 프로퍼티마다 조정 유무를 확인할 수 있게 되었습니다.
Swift에서 변수/상수명은 $ 로 시작할 수 없으니 네이밍을 고민하지 않아도 되고요(제일 중요)
마무리
오늘은 Property Wrapper에 대해 알아보았습니다.
코드에서 중복을 제거하고 가독성을 높이는 아주 유용한 개념이었는데요.
잊지 말고 써먹어야겠습니다 ㅎㅎ
감사합니다!
참고
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md
https://zeddios.tistory.com/1221
https://eunjin3786.tistory.com/472?category=706829
https://wlaxhrl.tistory.com/90
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.