Roen의 iOS 개발로그

NSUbiquitousKeyValueStore 2탄: UIKit 샘플코드 분석

by Steady On
아무리 뒤져도 한글 자료가 없어서 목마른 놈이 우물을 판다는 심정으로 공부하면서 정리하는 시리즈입니다.
자료가 없다보니 애플 공식문서(https://developer.apple.com/)를 토씨하나 빼먹지 않고 파헤쳐보겠습니다.
영어를 아주 잘하지는 못하므로 번역기의 도움도 받는지라 틀린 표현이 있을 수 있는 점 감안하고 보시면 되겠습니다.

 

기본세팅은 생략합니다.

NSUbiquitousKeyValueStore를 쓰기위해 프로젝트를 Xcode에서 어떻게 셋팅하면 되는지는 굳이 설명하지 않겠다. 왜냐면.... 일단 캡처가 너무 귀찮다. 대충 Apple Developer 멤버쉽에 등록한 계정이 있어야하고, Xcode에 프로젝트 설정부분에 들어가서 Signing & Capability에서 iCloud를 사용한다고 하고 거기서 key-value Store를 체크하면 된다. 그럼 .entitlements이라는 파일이 생기는데 거기에 iCloud Key-Value Store가 생겼는지 확인하면 된다.

그럼, 나는 뭘 알아볼거냐면, 1탄에서 계속 나왔던 NotificationCenter에 didChangeExternallyNotification을 등록해서 iCloud에 등록된 정보와 로컬의 정보중 무엇이 더 최신인지 확인 하고 알아서 동기화 되게 하는 방법을 볼거다.

일단, Documentation Archive의 내용부터 살펴보자.

 

Prepare Your App to Use the iCloud Key-Value Store

유저의 iCloud계정으로 로그인 된 어떤 기기에서든 앱을 실행하면, key-value 변화를 해당 계정에 업로드 할 수 있습니다. 이러한 변화에 추적을 계속하기 위해서는 앱이 실행되는 동안 NSUbiquitousKeyValueStoreDidChangeExternallyNotification을 notification에 등록하십시오. 그런 다음, 앱이 가장 최신의 데이터로 시작되도록 하기 위해, iCloud에서 key와 value들을 synchronize 메서드를 통해 얻습니다.(앱이 값이 변경되자마자 가능한 빠르게 iCloud에 업로드 되어야 하는게 아니라면, 앱의 생명주기 동안 synchronize 메서드는 다시 호출될 필요가 없습니다.)

다음 코드스니펫은 iCloud key-value store를 사용하기 위한 준비 방법을 나타냅니다. 이 코드를 당신의 앱에 적용하세요: didFinishLaunchingWithOptions: method (iOS) or applicationDidFinishLaunching: method (OS X).
/** Listen for key-value store changes from iCloud.
    This notification is posted when the value of one or more keys in the local
    key-value store changed due to incoming data pushed from iCloud.
    
    iCloud에서 key-value store의 변화를 감지합니다.
    이 알림은 로컬의 key-value store에 있는 하나 이상의 key에 대한 값이 변경되었을 때
    iCloud에서 푸쉬된 수신 데이터가 있을때 게시됩니다.
*/
NotificationCenter.default.addObserver(self,
    selector: #selector(ubiquitousKeyValueStoreDidChange(_:)),
    name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
    object: NSUbiquitousKeyValueStore.default)
    
/** Note: By passing the default key-value store object as "object" it tells iCloud that
    this is the object whose notifications you want to receive.
    
    참고: 기본 key-value store object를 "object"로 전달하면 iCloud에 알림을 받을 객체임을 알립니다.
*/

(위 코드는 원래 Obj-C코드로 작성되어 있었으나 필자가 공식문서에 있는 swift코드로 변경하여 작성한 것이다.

NSUbiquitousKeyValueStoreDidChangeExternallyNotification 알림 처리 메서드에서, 사용자의 정보 dictionary를 검토하고 앱의 UserDefaults에 변경을 작성할 것인지 결정합니다. 다음에 설명된 바와 같이, iCloud에서 발생한 변경 사항을 기반으로 앱의 설정을 변경할지 여부를 신중하게 결정하는 것이 중요합니다.

일단 다음으로 이어지는 내용은 1탄에서 말한 "Resolving Key-Value Conflicts"이다. 요약하면, 데이터 변경사항을 쓰기전에 iCloud의 데이터와 비교하여 어떤 것이 더 최신인지 확인하고, 최신의 것을 적용한다는 것이다.

그리고 위의 코드는 일단 swift는 아니고 Obj-C코드인 것 같다. 근데 내가 알고 싶은 것은 swift의 코드이기에, 이번에는 공식문서의 샘플 코드 쪽을 좀 뜯어보았다. 근데 이게 또 UIKit 코드라 SwiftUI 프로젝트에 적용하려면 약간의 이해가 필요할 것 같다.

 

UIKit code 분석하기

개인적으로 UIKit은 그냥 아주 살짝 맛만 보고 손을 뗏지만, 우리에겐 구글이 있으니까 많은 분들의 지식을 모아서 코드를 좀 파헤쳐 보았다. 일단 공식문서 자체의 내용을 좀 먼저 보면, 앞에는 샘플코드 프로젝트를 실행하기 위한 설정에 관련된 부분이 나오는데 이건 따로 번역은 하지 않겠다.(그리고 딱히 entitlements파일을 바꾸지 않아도 실행이 안되는건 아니다.)

 

Key-Value Store 변경 사항에 대응하기 위한 등록

샘플은 NotificationCenter를 사용하여 알림 didChangeExternallyNotification에 옵서버로 등록합니다. 다음 예제에서는 key-value store 변경에 대응하기 위해 앱을 관찰자로 설치합니다:
NotificationCenter.default.addObserver(self,
    selector: #selector(ubiquitousKeyValueStoreDidChange(_:)),
    name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
    object: NSUbiquitousKeyValueStore.default)
addObserver를 호출한 직후, synchronize()를 호출하는 것으로 만들어진 지난 업데이트에서 key-value store 변경 사항을 얻습니다. 운영 체제는 키와 값이 iCloud에 업로드되는 시기를 제어합니다. synchronize()를 호출하여 로컬 캐시를 저장하고 업로드 대기 중인 새 데이터를 iCloud에 알립니다.

이게 무슨 말이냐면, 위에 있는 코드는 

이렇게 ViewController의 extension에 작성되어 있는 메서드에서 사용된다. 이 메서드는 viewDidLoad에서 호출되어 진다.

viewDidLoad 메서드는 이름에서 알 수 있다시피 View의 로딩이 완료 되었을 때 시스템에 의해 자동으로 호출되는 녀석이고, 보통 초기 화면을 구성하는 용도로 사용한다고 합니다. SwiftUI로 따지면 onAppear나 init 같은 녀석인것 같다. NotificationCenter는 앱 내부에서 일어나는 이벤트를 받는 객체다. addObserver 메서드를 통해 감시할 대상을 등록해두면 얘가 변경과 같은 어떤 이벤트가 일어나는지 잘 보고 있다가 이벤트가 일어나면 약속된 객체를 보내준다. 자세한 내용은 SwiftUI로 바꿔서 써볼 때 조금 더 자세히 써보겠다. 일단 지금은 NotificationCenter라는 애가 key-value store를 감시하고 있다가 어떤 키나 밸류에 변경이 일어나면 그 변경을 감지한다라고만 이해하자. 결론적으로 여기까지의 코드는 "앱이 실행되고 View가 로딩될 때 key-value store를 감시하라고 Notification에 등록을 해줬다."가 되겠다.

 

Key-Value Store 변경에 대한 반응

이 샘플은 NSNotificationCenter를 사용하여 iCloud의 값 변화를 감지하기 위해 didChangeExternallyNotification의 알림을 감시자로써 등록합니다.
이 샘플은 key-value notification의 변화를 감시하고 있는 ubiquitousKeyValueStoreDidChange의 기능을 알려줍니다.
이샘플은 notification의 userInfo로부터 NSUbiquitousKeyValueStoreChangeReasonKey를 사용하여 변화의 원인을 조사합니다.
guard let userInfo = notification.userInfo else { return }
      
// Get the reason for the notification (initial download, external change or quota violation change).
guard let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else { return }
key-value notification이 변경되는 이유는 다음 중 하나입니다:
- NSUbiquitousKeyValueStoreServerChange: 값이 다른 사용자/사용자로부터 외부적으로 변경되었습니다.
- NSUbiquitousKeyValueStoreInitialSyncChange: 초기 다운로드는 장치가 처음으로 iCloud 계정에 연결될 때와 사용자가 기본 iCloud 계정을 전환할 때 발생합니다.
- NSUbiquitousKeyValueStoreQuotaViolationChange: 앱의 키 값 저장소가 iCloud 서버의 공간 할당량을 초과했습니다.
- NSUbiquitousKeyValueStoreAccountChange: 사용자가 기본 iCloud 계정을 변경했습니다.
변경된 key-values를 가져오려면 notification의 userInfo로부터 NSUbiquitousKeyValueStoreChangedKeysKey의 키를 사용하십시오.
guard let keys =
          userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else { return }
let possibleColorIndexFromiCloud =
          NSUbiquitousKeyValueStore.default.longLong(forKey: gBackgroundColorKey)

공식문서는 이렇게 하고 샘플코드에 대한 설명을 끝내버린다. 이코드들이 어디에 있는거냐면, 저~~ 위에 있는 NotificationCenter에 해당 옵저버를 등록하는 부분을 보자.

NotificationCenter.default.addObserver(self,
    selector: #selector(ubiquitousKeyValueStoreDidChange(_:)),
    name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
    object: NSUbiquitousKeyValueStore.default)

`addObserver` 메서드에 대해 먼저 좀 보자.

`addObserver`의 정의를 보면, "알림 센터에 항목을 추가하여 제공된 selector를 알림과 함께 호출합니다."라고 되어있다. 그럼 selector는 또 뭐냐!

 
_ observer
관찰자로 등록할 개체입니다.

aSelector
수신자가 관찰자에게 보내는 메시지를 지정하여 notification 센터에 알립니다. 선택기가 지정하는 메서드에는 인수가 하나만 있어야 합니다(NSNotification 인스턴스).

aName
관찰자에게 전달하기 위해 등록할 notification의 이름입니다. 이 notification 이름을 가진 항목만 배달하려면 notification 이름을 지정하십시오.
nil일 때, 발신인은 notification 이름을 배달 기준으로 사용하지 않습니다.

anObject
관찰자에게 알림을 보내는 객체입니다. 이 sender의 알림만 배달하려면 notification 보낸 sender를 지정하십시오.
nil일 때, notification 센터는 sender 이름을 배달 기준으로 사용하지 않습니다.

selector는 NotificationCenter 객체한테서 변화가 있는 userInfo 값을 받아서 유효성을 확인하고 그 데이터를 가져와서 반영하게 하는 녀석이라고 생각하면 될것 같다. 아래의 코드 예시와 주석을 같이보면 좀 이해가 될 것이다! 그리고 이 샘플 코드에서는 이 selector가 `ubiquitousKeyValueStoreDidChange`로 들어가 있다. 이게 뭔지 정의된 부분을 좀 살펴보면

@objc
    func ubiquitousKeyValueStoreDidChange(_ notification: Notification) {
        
        /** 다음을 통해 notification으로부터 더 많은 정보를 얻을 수 있습니다.
            notification의 userInfo에 있는 NSUbiquitousKeyValueStoreChangeReasonKey 또는 NSUbiquitousKeyValueStoreChangedKeysKey 상수
         */
		guard let userInfo = notification.userInfo else { return }
        
        // 알림의 이유(초기 다운로드, 외부 변경 또는 할당량 위반 변경)를 가져옵니다.

		guard let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else { return }
		
        /** 이유는 다음과 같습니다:
            NSUbiquitousKeyValueStoreServerChange:
            값이 다른 사용자/장치에서 외부적으로 변경되었습니다.
            변경 내용을 가져오고 해당 키를 로컬로 업데이트합니다.
         
            NSUbiquitousKeyValueStoreInitialSyncChange:
            초기 다운로드는 장치가 처음으로 iCloud 계정에 연결될 때 발생합니다,
            사용자가 기본 iCloud 계정을 전환하는 경우.
            변경 내용을 가져오고 해당 키를 로컬로 업데이트합니다.

            로컬 user defaults과 병합합니다.
            그러나 이 샘플의 경우 값이 하나뿐이므로 여기서 병합할 필요가 없습니다.

            참고: 그 이유로 "NSUbiquitousKeyValueStoreInitialSyncChange"을 받으신다면,
            로컬 값을 서버 값으로 "수정"할 수 있습니다.

            NSUbiquitousKeyValueStoreQuotaViolationChange:
            앱의 key-value store이 iCloud 서버의 공간 할당량인 1MB를 초과했습니다.

            NSUbiquitousKeyValueStoreAccountChange:
            사용자가 기본 iCloud 계정을 변경했습니다.
            로컬 키 값 저장소의 키 및 값이 새 계정의 키 및 값으로 대체되었습니다,
            상대 타임스탬프에 관계없이 사용할 수 있습니다.
         */
        
        // 중요한 키가 업데이트되었는지 확인하고 업데이트된 경우 해당 키 아래에 저장된 새 값을 사용합니다.
		guard let keys =
            userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else { return }
		
		guard keys.contains(gBackgroundColorKey) else { return }

        if reasonForChange == NSUbiquitousKeyValueStoreAccountChange {
            // 사용자가 계정을 변경했으므로 UserDefaults(마지막으로 저장된 색상)을 사용하려면 뒤로 이동하십시오.
            chosenColorValue = UserDefaults.standard.integer(forKey: gBackgroundColorKey)
            return
        }
        
        /** 클라우드의 값으로 "선택된색상"을 변경합니다. 그러나 우리가 값의 해석방법을 알고 있는 경우에만 가능합니다.
            앱의 다른 버전에서 생성되었을수도 있기 때문에, 클라우드를 통해 들어오는 모든 value의 유효성을 확인하는 것은 중요합니다. 
         */
		let possibleColorIndexFromiCloud =
            NSUbiquitousKeyValueStore.default.longLong(forKey: gBackgroundColorKey)
		
        if let validColorIndex = ColorIndex(rawValue: Int(possibleColorIndexFromiCloud)) {
            chosenColorValue = validColorIndex.rawValue
            return
        }
        
        /** 위에서 우리가 해석할 수 없는 값이 들어왔을 때,
             예기치 않는 값을 처리하는 가장 좋은 방법은 값이 나타내는 것, 그리고 앱이 수행하는 것에 따라 달라집니다.
             가장 좋은 것은 해석할 수 없는 값은 무시하고 업데이트를 적용하지 않는 것 입니다.
         */
        Swift.debugPrint("WARNING: Invalid \(gBackgroundColorKey) value,")
        Swift.debugPrint("of \(possibleColorIndexFromiCloud) received from iCloud. This value will be ignored.")
    }

달려있는 주석을 최대한 해석해서 달아보았다.

 

맨 위에서부터 정리하자면,

1. 앱이 실행됨과 동시에 NotificationCenter에 UbiquitousKeyValueStore의 변화를 감지할 수 있는 observer를 만들어야한다는 것

2. Observer를 만들기 위해서는 Selector 메서드를 만들어야 한다.

3. selector는 NotificationCenter가 받아온 변화를 가져다가 기댓값과 다른 경우 에러를 처리하고, 정상적인 값인 경우 앱에 반영하는 역할을 한다.

 

이걸 SwiftUI 코드로 바꾸는건 다음 포스팅에서!!!

 

 

그리고 아래 주의사항은 덤!

주의사항

key-value store는 자주 변경되지 않는 데이터를 저장하기 위한 것입니다. 장치를 테스트할 때 장치의 앱이 key-value store를 자주 변경하는 경우, 시스템은 서버에 대한 왕복 횟수를 최소화하기 위해 일부 변경 사항의 동기화를 연기할 수 있습니다. 앱이 자주 변경할수록 변경이 지연되고 다른 장치에 즉시 표시되지 않습니다.
공식문서의 주의사항. 예시를 생각해보면, 기본 메모앱이나 미리알림앱에서 변경한 사항이 다른 기기에서 실시간으로 동기화되지 않는 것이 있겠다.

 

참고링크

https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForKey-ValueDataIniCloud.html#//apple_ref/doc/uid/TP40012094-CH7-SW6

 

Designing for Key-Value Data in iCloud

Designing for Key-Value Data in iCloud To store discrete values in iCloud for app preferences, app configuration, or app state, use iCloud key-value storage. Key-value storage is similar to the local user defaults database; but values that you place in key

developer.apple.com

https://developer.apple.com/documentation/foundation/icloud/synchronizing_app_preferences_with_icloud

 

Synchronizing App Preferences with iCloud | Apple Developer Documentation

Store app preferences in iCloud and share them among instances of your app running on a user’s connected devices.

developer.apple.com

https://zeddios.tistory.com/m/43

 

iOS ) View Controller의 생명주기(Life-Cycle)

안녕하세요! 오늘은 View Controller생명 주기에 대해 알아보겠습니다.iOS를 시작하려고 하거나, 배우고 있는 분들이라면 반드시 알아야 해요.하나하나 제대로 알아봅시다 ㅎㅎ View Controller의 생명주

zeddios.tistory.com

블로그의 정보

Roen의 iOS 개발로그

Steady On

활동하기