본문 바로가기

TIL

[TIL] @propertyWrapper를 활용한 UserDefaults 리팩토링

문제 상황

현재 진행중인 프로젝트에서 유저 정보 및 토큰 데이터를 모두 UserDefaults에서 관리해주었는데요.

이런 상황이 지속하다보니 UserDefaults에 대한 코드가 반복적으로 발생하고 그에 따라 해당 코드양도 늘어나게 되었습니다.

따라서 이 늘어난 코드양을 줄이고자 swift5.1부터 도입된 Property Wrapper를 활용하여 UserDefaults에 대한 코드를 리팩토링하여 줄여보고자 합니다.

extension UserDefaults {
    enum Keys: String, CaseIterable {
        case userId
        case email

        // ...
    }
}

extension UserDefaults {
    var userId: String {
        get {
            return UserDefaults.standard.string(forKey: Keys.userId.rawValue) ?? ""
        }
        set {
            UserDefaults.standard.set(
                newValue,
                forKey: Keys.userId.rawValue
            )
        }
    }
    
    var email: String {
        get {
            return UserDefaults.standard.string(forKey: Keys.email.rawValue) ?? ""
        }
        set {
            UserDefaults.standard.set(
                newValue,
                forKey: Keys.email.rawValue
            )
        }
    }

    // ...
}

 

Property Wrapper란?

Swift 공식 문서에 따르면 Property Wrapper는 아래와 같이 정의되어 있습니다.

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. 

 

위 정의에서 유추할 수 있듯이 프로퍼티를 새로운 로직으로 래핑(Wrapping)하는 타입을 의미하며, 더 간단히 풀어 말하면 프로퍼티의 getter와 setter 메서드의 동작을 구현하고 재사용하는 커스텀 타입을 정의하도록 해준다는 의미입니다.

 

Property Wrapper 구현

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

 

위 코드는 Swift 공식 문서에서 가져온 Snippet인데요. Property Wrapper를 정의하는 방법을 예시로 든 코드입니다.

 

위 코드를 보시면 @propertyWrapper란 문구를 보실 수 있는데요. 이 문구는 Declaration Attribute로 Property Wrapper 정의 시 사용되는 Attribute입니다.

 

이와 더불어 위 코드에서 Property Wrapper에 있어 가장 중요한 요소인 wrappedValue 프로퍼티를 확인해 보실 수 있습니다.

wrappedValue Property Wrapper에서 데이터에 접근할 수 있게 해주는 프로퍼티입니다. Property Wrapper에서는 오직 이 프로퍼티를 통해서만 데이터 접근이 가능합니다.

 

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

 

그리고 실질적인 Property Wrapper 사용은 위 코드와 같이 적용하고 싶은 프로퍼티 앞에 Attribute로서 선언해줌으로써 사용 가능합니다.

 

UserDefaults 리팩토링

자 그럼 이제 본래 목적이었던 Property Wrapper를 통해 기존 UserDefaults 코드를 리팩토링해 보겠습니다.

Property Wrapper 정의

우선 BaseDefaults라는 구조체를 정의해줍니다. 

그리고 UserDefaults에 저장할 타입이 동적이고, Codable 프로토콜을 준수하므로 제네릭 타입을 이용하여 타입 인자를 <T: Codable>로 정의해줍니다.

 

그 다음 초기값들을 지정해줍니다.

  • key: UserDefaults 저장 시 사용될 키
  • defaultValue: 키에 해당하는 값이 UserDefaults에 없는 경우 사용될 기본 값
  • isDate: 저장할 데이터 타입이 Date인지에 대한 플래그
  • isData: 저장할 데이터 타입이 Data인지에 대한 플래그
  • storage: UserDefaults 저장소 인스턴스

 

마지막으로 실제 저장된 데이터를 관리해주는 wrappedValue를 정의해줍니다.

  • getter: UserDefaults 저장소에서 키에 해당하는 데이터를 조회합니다. 만약 조회된 데이터가 없다면 기본값인 defaultValue가 반환됩니다.
  • setter: 데이터를 UserDefaults 저장소에 저장합니다.

UserDefaults Manager 정의

위에서 정의한 Property Wrapper를 활용해 키별로 UserDefaults 저장소 접근을 관리해주는 Manager를 정의해 줍니다.

실제 적용 예시

 

결론

로직을 캡슐화해서 결론적으로 152줄이었던 Boilerplate 코드를 116줄로 줄일 수 있었습니다. 이와 더불어 가독성을 향상시키며 앱 전체에서 일관성을 가질 수 있습니다. 

앞으로도 여러 기술들을 도입하여 Boilerplate 코드를 많이 줄어보려는 시도를 해봐야할 것 같습니다.

여기까지 읽어주셔서 감사합니다.

 

참고 사이트

https://developer.apple.com/documentation/swiftui/binding/wrappedvalue (Swift 공식 문서 - Property Wrapper)

https://velog.io/@dodo_dev/propertyWrapper%EC%99%80-UserDefaults%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%80%EC%9E%A5-%EB%B0%A9%EB%B2%95 (Property Wrapper 사용 참고)