클린코드 관련 글들을 보면, 꼭 나오는 문장이 있습니다.
“중복된 코드를 제거해라!”
중복된 코드를 제거하기 위해서 가장 쉽게 생각할 수 있는 방법은 메서드로 공통된 로직을 묶어서, 호출만 서로 다른 시점에 하는 방법 입니다.
// 메서드를 정의
func someMethod() { ... }
// 호출되는 곳 1
struct ContentView: View {
...
someMethod()
...
}
// 호출되는 곳 2
struct LoginView: View {
...
someMethod()
...
}
메서드로 하게되면, 어느정도 문제를 해결할 수 있습니다. 그러다가 자연스럽게 이렇게 질문할 수 있습니다.
“메서드 단위로 중복을 해결하는 방법은 클래스 혹은 구조체 내에서 접근을 해야하는데… 프로퍼티 단위로 중복이 발생하면, 더 작은 단위로 로직을 공통화할 수 없을까?
그것에 대한 대답이 Swift 에서는 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
중복되는 코드의 다른 예시를 보여드리겠습니다.
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 에서 제안서로 작성된 에시는 지연 초기화(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 }
}