SwiftUI 에서 "@", 프로퍼티 래퍼(PropertyWrapper)

Uno·2023년 8월 25일
0

SwiftUI

목록 보기
30/30
post-thumbnail

개요

클린코드 관련 글들을 보면, 꼭 나오는 문장이 있습니다.

“중복된 코드를 제거해라!”

중복된 코드를 제거하기 위해서 가장 쉽게 생각할 수 있는 방법은 메서드로 공통된 로직을 묶어서, 호출만 서로 다른 시점에 하는 방법 입니다.

// 메서드를 정의
func someMethod() { ... }

// 호출되는 곳 1
struct ContentView: View {
...

	someMethod()

... 

}

// 호출되는 곳 2
struct LoginView: View {
...

	someMethod()

... 

}

메서드로 하게되면, 어느정도 문제를 해결할 수 있습니다. 그러다가 자연스럽게 이렇게 질문할 수 있습니다.

“메서드 단위로 중복을 해결하는 방법은 클래스 혹은 구조체 내에서 접근을 해야하는데… 프로퍼티 단위로 중복이 발생하면, 더 작은 단위로 로직을 공통화할 수 없을까?

그것에 대한 대답이 Swift 에서는 Property Wrapper 라고 생각합니다. :)

반복되는 코드를 간단하게 적용하자: Property Wrapper

Swift 5.1 에서 추가된 문법입니다. 아래 링크를 보면, 이 문법을 제안하는 제안서가 있습니다.

PropertyWrapper(이하 프로퍼티 래퍼) 를 먼저 알아보고 다시 View 로 돌아오겠습니다.

Swift 언어 자체에서 lazy 생성자를 언어 차원에서 제공해줍니다. 만약에 언어차원에서 지원하지 않고, 사용하고 싶은 경우, 다음과 같은 코드를 작성해줘야 합니다.

struct Foo {
	// lazy var foo = 1738
	private var _foo: Int?
	var foo: Int {
		mutating get {
			// 값이 있으면, 바로 리턴
			if let value = self._foo { return value }
			// 값이 없으면, 초기값을 리턴
			let initialValue = 1738
			self._foo = initialValue
			return initialValue
		} 
		set {
			self._foo = newValue
		}
	}
}

// 호출부
var temp: Foo = Foo()
print(temp.foo) // 초기값
temp.foo = 123
print(temp.foo) // 123
  • 이런식으로 코드를 작성하게 되면, 몇 가지 단점이 있습니다.
  1. 일단 가독성이 떨어집니다. 이 코드를 처음부터 끝까지 읽어야 Lazy 생성자로 동작함을 알 수 있습니다.
  2. Lazy Initialize 를 할 때마다, 위 코드가 반복되는 불편함이 있습니다.

중복되는 코드의 다른 예시를 보여드리겠습니다.

ImageInfo 라는 구조체가 있다고 해봅시다.

struct ImageInfo {
    
    private var _image: String = ""
    
    var image: String {
        get { self._image + ".png" }
        set { self._image  = newValue }
    }
    
    init(image: String?) {
        guard let image = image  else {
            self._image = "Default Value is here..."
            return
        }
        self._image = image
    }
}

일반적인 이미지 정보를 담고 있는 구조체 입니다.

그런데 게시글에 올리는 구조체 코드를 작성하는데 위 코드에 있던 image 와 같은 코드가 있습니다.

struct Posting {
    
    private var uuid: UUID = UUID()
    private var contents: String = ""
    private var author: String = ""
    private var _image: String = ""
    
    
    var image: String {
        /* 중복되는 코드 */
        get { self._image + ".png" }
        set { self._image  = newValue }
        /* --------- */
    }
    
    init(
        image: String?,
        contents: String,
        author: String
    ) {
        guard let image = image  else {
            self._image = "Default Value is here..."
            return
        }
        self._image = image
        self.contents = contents
        self.author = author
    }
}

이렇게 되면, 같은 로직임에도 중복해서 작성해야 겠죠. 그리고 코드를 내부까지 봐야하는 불편함도 함께 공존합니다. 더 나아가서, 이미지 명명에 대한 정책이 변경되면(확장자 변경) 이 일어나면, 복사된 로직을 모두 찾아서 변경해야 할 겁니다.

이런 중복을 제거할 수 있는 문법이 프로퍼티 래퍼입니다. 아래 코드가 직접 정의한 예시입니다.

@propertyWrapper
struct ImageString {
    private var image: String = ""
    
    var wrappedValue: String {
        get { self.image + ".png" } // 확장자를 추가하는 부분
        set { self.image = newValue }
    }
    
    init(wrappedValue initialValue: String) {
        self.image = initialValue
    }
}

struct UserProfile {
    @ImageString var profileImage: String = "profile"
}

let userProfile = UserProfile()
print(userProfile.profileImage) // profile.png

프로퍼티 래퍼라는 이름답게, 프로퍼티에 행동을 추가한다고 생각한다고 저는 이해했습니다.

클래스에 행동을 추가하던 것을 프로퍼티 단위로 옮겨온 느낌이죠.

GitHub 제안서에 있는 내용

Github 에서 제안서로 작성된 에시는 지연 초기화(Lazy) 를 예시로 코드를 작성했습니다.

@propertyWrapper // 프로퍼티 래퍼의 정의임을 나타낸다.
enum Lazy<Value> { 
// 재내릭을 통해, 적용될 타입을 추상화한다. 
// enum 또한 ValueType 이므로 프로퍼티 래퍼를 사용할 수 있다.

	case uninitialzed(() -> Value) // 초기화 되지 않은 상태
	case initialized(Value) // 초기화된 상태
	
	// cf) [@autoclosure] 매개변수를 클로저로 감싸줌
	init(wrappedValue: @autoclosure @escaping () -> Value) {
		// 초기화 되지 않은 상태에서 할 행동을 정의한다.
		self = .uninitialized(wrappedValue)
	}

	// [wrappedValue] 는 @propertyWrapper 정의 시, 필수요소이다.
	var wrappedValue: Value {
		mutating get {
			switch self {
			// 정의되지 않은 상태에서 접근받고 + 동작에 대한 input 을 받았다면,
			// input 은 초기화시킨 값으로 저장하고, 입력받은 값을 바로 리턴한다.
			case .uninitialized(let initializer): // 특정 값을 입력받음
				let value = initializer()
				self = .initialized(value)
				return value
			// 이미 초기화된 상태라면, 받은 값을 다시 리턴한다.
			case .initialized(let value):
				return value
			}
		} // 값을 입력한다는 것은 초기화한다는 의미이므로, 초기화 시킨다.
		set { self = .initialized(newValue) }
	}
}

// 호출부
@Lazy var foo = 1738

// 위 코드는 아래 코드와 동일하다.
private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
  get { return _foo.wrappedValue }
  set { _foo.wrappedValue = newValue }
}

맨 마지막 “호출부” 부터만 봐도 효과가 눈에 띕니다. 아래와 같은 코드를 매번 작성할 필요 없이, 어노테이션 문법 한번으로 모든 프로퍼티에 적용하고 수정하는 것이 가능합니다.

개발하면서 볼 수 있는 예시

아마 SwiftUI 를 개발하다보면, 다음과 같은 어노테이션 문법을 볼 수 있습니다.

@State / @Binding / @ObservedObject / @EnvironmentObject / @FetchRequest

그 중에서 @State 부분 코드 공개된 부분만 보면 아래와 같습니다.
(주석제거)

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {

    public init(wrappedValue value: Value)

    public init(initialValue value: Value)

    public var wrappedValue: Value { get nonmutating set }

    public var projectedValue: Binding<Value> { get }
}
  • State 는 프로퍼티래퍼 말고도 다른 값들도 함께 적용되어 있지만, 분명히 프로퍼티 래퍼도 있습니다.

참고자료

profile
iOS & Flutter

0개의 댓글