Skip to content →

Property Wrapper, 무엇이 무엇을 감싸는가?

Swift 5.1에 소개된 Property Wrapper는 제게 굉장히 생소한 개념이었습니다. 번역도 어떻게 해야 할지 모르겠네요. “속성 싸개” 라고 해야 하나? 아무튼 속성을 감싸는 녀석이라는 뜻인데, 도대체 속성을 감싼다는 것이 무슨 의미인지조차 감을 잡기 힘들었습니다.

하지만 아마 이렇게 시작하면 많은 분들이 감을 잡기 쉬울 것 같습니다. 아래와 같은 구조의 코드는 그래도 익숙하지 않나요?

var isFlagged: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)

위 코드를 기준으로 말하면, false 라는 값을 BehaviorRelay<Bool>(value:... 라는 녀석이 감싸고 있다고 볼 수 있습니다.

이 때 falsewrappedValue 가 되고, BehaviorRelay가 false 라는 property를 wrap 하는 propertyWrapper 가 됩니다.

물론 위의 코드에서 BehaviorRelay는 propertyWrapper를 사용해 만든 클래스가 아닙니다. PropertyWrapper는 이런 BehaviorRelay와 같은 코드를 훨씬 더 깔끔하게 만들 수 있게 해 주는 녀석입니다.

결론적으로 PropertyWrapper를 사용해 BehaviorRelay를 다시 작성한다면, 아래와 같이 쓸 수 있게 됩니다.

@BehaviorRelay isFlagged = false

이 비교를 하기 위해, 먼저 propertyWrapper를 사용하지 않고 BehaviorRelay를 구현해 보겠습니다. BehaviorRelay는 RxCocoa를 사용하시는 분들은 굉장히 익숙하게 사용하시는 클래스인데, 기본적으로 “isFlagged”와 같은 값이 변화 했을 때 다른 객체들이 이를 구독하기 쉽게 만들어 줍니다. 원래는 다른 Rx연산자들을 활용해 더 섬세하게 구현되어 있지만, 지금은 개념학습이 목적이므로 NotificationCenter를 이용하여 간단하게 흉내만 내 볼게요.

class BehaviorRelay<Element> {
    var value:Element
    let notiCenter = NotificationCenter()
    init(value: Element) {
        self.value = value
        postValueChange(newVal: value)
    }

    func accept(_ newValue: Element) {        
        self.value = newValue
        postValueChange(newVal: newValue)
    }

    func bind( onNext: @escaping (Element) -> Void ) {
        notiCenter
            .addObserver(forName: NSNotification.Name("valueChange"),
                         object: nil,
                         queue: nil,
                         using: { noti in
                            let newElement = noti.userInfo?["newValue"] as! Element
                            onNext(newElement)
            })
    }

    private func postValueChange(newVal: Element) {
        notiCenter
            .post(name: NSNotification.Name("valueChange"),
                  object: self,
                  userInfo: ["newValue":newVal])
    }
}

이렇게 만들어진 BehaviorRelay는 아래와 같이 사용 될 수 있습니다.

var isFlagged:BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)

isFlagged
    .bind(onNext: { result in
        print(result) // true를 비동기로 출력
     })

isFlagged.accept(true)

PropertyWrapper를 사용하여 위의 코드를 다시 작성해 보겠습니다. 중간의 “wrappedValue”만 제외한다면 위의 구현과 크게 다르지 않습니다.

@propertyWrapper
struct RelayWrapper<Element> {
    let notiCenter = NotificationCenter()
    var wrappedValue: Element // <<- 얘를 "RelayWrapper가 감쌉니다"

    mutating func accept(_ newValue: Element) {
        postValueChange(newVal: newValue)
        self.wrappedValue = newValue
    }

    func bind(onNext: @escaping (Element) -> Void ) {
        notiCenter
            .addObserver(forName: NSNotification.Name("valueChange"),
                         object: nil,
                         queue: nil,
                         using: { noti in
                            let newElement = noti.userInfo?["newValue"] as! Element
                            onNext(newElement)

            })
    }

    private func postValueChange(newVal: Element?) {
        notiCenter
            .post(name: NSNotification.Name("valueChange"),
                  object: self,
                  userInfo: ["newValue":newVal])
    }
}

이렇게 만들어진 RelayWrapper는 아래와 같이 쓰일 수 있습니다.

@RelayWrapper var isFlagged:Bool = false 
_isFlagged.bind(onNext: { result in 
    print(result) // true를 비동기로 출력
}

_isFlagged.accept(true)

하지만 역시 언더바 prefix같은 것은 가급적이면 쓰고 싶지 않네요. RxCocoa의 BehaviorRelay를 흉내내서 위와 같이 RelayWrapper를 만든 거였다면, 이번엔 SwiftUI에 사용되는 @State 를 흉내내보도록 하겠습니다.

let viewNotiCenter = NotificationCenter()

@propertyWrapper
struct StateWrapper<Element> {
    var wrappedValue: Element {
        didSet {
            viewNotiCenter.post(name: NSNotification.Name("layoutSubview"), object: self, userInfo: nil)
        }
    }
}

class MyView:UIView {
    override func awakeFromNib() {
        super.awakeFromNib()
        viewNotiCenter.addObserver(forName: NSNotification.Name("layoutSubview"), object: nil, queue: nil, using: { _ in
            self.layoutSubviews()
        })
    }
}

class MyRect: MyView {
    @StateWrapper var text = "Hello World" // 이제 text에 변화사 생길 때마다 layoutSubview가 불리게 됩니다.
}

제 생각에 PropertyWrapper는 특정 속성이 저장되거나 불려지는 방법을 일괄적으로 강제 할 때, 또는 그 속성에 변화가 일어 날 때 발생되는 일들을 일괄적으로 강제 할 때 가장 강력하다고 생각합니다.

대표적으로 @Lazy 를 써서 속성을 lazy하게 로딩하게 한다던지, @Atomic 을 사용해서 원자적으로 속성에 writing을 할 수 있게 한다던지, @UserDefault 를 활용해서 속성에 변화가 생길 때마다 그 속성의 내용을 UserDefault에 저장하게 한다던지, @State를 써서 속성에 변화가 생길 때마다 함께 선언된 View들이 layoutSubView를 하게 한다던지 하는 용례를 생각 해 볼 수 있겠습니다.

PropertyWrapper는 굉장히 많은 잠재력을 지닌 녀석 같습니다. 프로포절을 자세히 뜯어보면 활용할 수 있는 방법도 정말 다양합니다. 특히 SwiftUI등에서는 @State, @ObjectBinding, @Environment 등등 아주 다양한 PropertyWrapper를 사용하고 있으니, SwifUI에 익숙해지기 위해선 PropertyWrapper의 용법과 이를 활용한 패턴에 익숙해져야 할 것 같습니다.

상당히 생소한 개념이고 아주 다양하게 활용 될 수 있는 녀석인 만큼, Swift 커뮤니티에서 앞으로 이를 활용해 어떤 패턴들을 정립해 나가게 될지가 기대되네요.

관련문서

Published in Basics 프로그래밍

Comments

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.

%d 블로거가 이것을 좋아합니다: