[Swift] KVO (Key-Value Observing) 완전 정복하기 (feat. WKWebView progressBar)

2024. 10. 1. 00:11Swift, iOS Foundation

예전 WKWebView를 구현하는 글의 마지막 부분이었던 "KVO를 사용해서 웹 페이지 로딩 상태 프로그레스바로 나타내기" 코드를
이번 글에서는 리팩토링하는 내용과 함께 KVO (Key-Value Observing) 개념에 대해 완전 정복해보도록 하겠다.

WKWebView에 관한 자세한 내용이 궁금하다면 아래 링크를 타고 읽어보는 것도 ^__^

 

[WebKit] WKWebView를 사용해서 앱 사용 중, 웹으로 연결시켜보자

1️⃣ 이번 글에서 구현하고자 하는 기능은?이번 글에서는 URL 링크를 저장하고, 해당 링크를 앱 안에서 웹 페이지로 띄울 때 사용한 모든 기능들에 대해서 정리해보겠다.이번 프로젝트에서 앱

mini-min-dev.tistory.com

 

KVO (Key-Value Observing)가 뭔데?

KVO (Key-Value Observing)는 어떤 객체의 속성이 변경된 경우, 이 변경사항을 대응할 수 있도록 해주는 대표적인 코코아 디자인 패턴 (Cocoa Design Pattern)이다.

🤔 잠깐! 코코아 디자인 패턴(Cocoa Design Pattern)이 뭔데?

iOS를 비롯한 애플의 운영체제(macOS, iPadOS, watchOS, visionOS 등..)에서 사용되는 Objective-C나 Swift 프로그래밍 기반 Apple의 애플리케이션 프레임워크를 Cocoa Framework라고 부른다.
이 Cocoa Framework에서 사용하는 디자인 패턴(= 특정 문제를 해결하기 위한 설계 해결방식)을 Cocoa Design Pattern이라 부르는 것!

공식문서에서는 오늘 배울 KVO를 비롯해, Delegate, Singleton, Error Handling 등을 코코아 디자인 패턴으로 함께 소개한다.

기본적으로 "Observing"이라는 말이 들어있는 것에서 알 수 있듯이, KVO는 예전 글에서 다룬적이 있던 옵저버 패턴 (Observer Pattern)의 형태를 따른다고 생각하면 된다.

옵저버 패턴 (Observer Pattern)이 기억이 안날까봐 간단하게 그 형태를 설명하자면,
일단 특정 값을 방출하는 객체 (Publisher) = 관찰하고자 하는 대상 (Object to Observe)이 있을 것이고,
그 값을 관찰 (addObserve, subscribe)하는 객체인 관찰자 (Observer)는 관찰 대상이 새로운 값을 방출할 때마다 알림(notify)을 받게 될 것이다.

즉, 관찰자(Observer)는 관찰하고자 하는 대상(Object to Observe)이 방출하는 새로운 값에 대해 반응할 수 있게 되는 것이 옵저버 패턴(Observer Pattern)의 내용이었다.


그럼 이어서 Observing은 뭔지 알았는데... Key와 Value는 무엇을 의미하는건지도 알아볼까?

  • Key : 관찰하고자 하는 대상 (Object to Observe) 객체가 방출하는 속성에 접근하기 위해 사용하는 개념 (= KeyPath, 고유한 식별자)
  • Value : Key를 통해 접근한 속성에서 받아오는 값을 의미

아! 그러면, 이제 종합해서 KVO의 개념을 정리해보자!

💡 관찰자(Observer)는 관찰하고자 하는 객체(Object to Observe)의 속성을 Key를 통해 접근하고 / 이를 통해 Value의 변화를 지속적으로 관찰(Observing)하고 이에 따른 대응을 하는 것이 KVO (Key-Value Observing)이다.

KVO라는 단어가 담고 있는 의미를 정리하면 위와 같이 설명할 수 있다.


기본적인 WKWebView로부터 웹 페이지에 로딩 상태를 받아오는 과거 (Swift 3.2 버전 이하) KVO 코드를 보며 이 구조를 익혀보겠다.

아래 코드에서는 로딩 상태 값을 방출하는 웹 뷰 (WKWebView)가 관찰하고자 하는 객체 (Object to Observe)에 해당될 것이다.
관찰하는 관찰자에 해당하는 것은 self (해당 ViewController 인스턴스)로,
self가 webView의 특정 값을 관찰하겠다는 의미로 addObserver 메서드를 작성했다고 이해하면 된다.

그리고 바로 위에서 봤던 것처럼 웹 뷰가 방출하는 로딩 값에 접근하기 위한 Key가 #KeyPath(WKWebView.estimatedProgress)로 이루어지고 있다.

private let webView = WKWebView()

webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)

addObserver로 등록한 객체 (Object to Observe)의 특정 Key값에 해당하는 Value 값이 변하면 아래 observeValue(forKeyPath:, context:) 메서드가 자동으로 호출된다.

이 부분에서 새롭게 변해서 넘어오는 Value에 대해 우리는 처리를 해주기만 하면 되는 것이다.

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "estimatedProgress" {
        if let newProgress = change?[.newKey] as? NSNumber {
            let progress = Float(truncating: newProgress)
            progressView.progress = progress
        }
    }
}

단, 주의해야 할 점은 관찰자 (= Observer, 여기서는 self) 객체가 메모리에서 해제되었을 때,
Observer와 Publisher(= Object to Observe, 여기서는 webView가 해당) 사이에 남아있는 구독/관찰 관계를 명시적으로 끊어줘야 한다는 점이다.

만약, 관찰자 객체가 메모리에서 사라졌음에도 이와 관련된 구독 관계가 남아있다면, 이는 곧 메모리 누수나 크래시로 이어질 수 있게 된다! 

deinit {
    webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
}

 

Block Based KVO violation 해결하기

한 단계 더 나아가보자!
위와 같이 코드를 잘 사용해서 KVO 구조를 만들면 되긴 하는데.... Lint에서 아래와 같은 Warning을 발생시키고 있었다.

그냥 간단하게 말해서 지금 내가 사용한 KVO 코드는 Swift 3.2 이전에서만 사용하던 구닥다리(?) 방식이라는 것이었다.


새로운 Block Based KVO 코드를 어떻게 사용하는지 살펴보겠다.

우선 관찰에 대한 관리를 전담하는 NSKeyValueObservation 타입의 객체를 만들어줬다.
모든 관찰을 해당 객체에 추가하게 될 것이며,
이 타입을 사용함으로써 메모리가 해제될 때 명시적으로 메모리에서 해제시켜줘야 했던 번거로움을 이제는 관찰자 인스턴스가 메모리에서 해제될 때 자동으로 관찰 관계를 끊도록 도와주는 방식으로 개선되었다.

private var progressObservation: NSKeyValueObservation?

observeValue 메서드에서 새로운 값 변경 시, 처리해주던 부분도 클로저 기반의 블록 형태로 간단하게 정리되었다.

우선 객체 속성에 접근하기 위한 KeyPath는 "\."의 형태로 쉽게 접근할 수 있게 되었다.

options 파라미터는 값을 언제 전달받을지를 결정하는 요소로 아래 4가지의 속성을 사용할 수 있다.
아래 코드 같은 경우는 .new 상황에 대해서만 Value를 전달받도록 지정한 셈! (Array 구조인 만큼 동시에 여러 개를 지정하는 것도 가능하다)

  • .new : 변경 후에 새로운 값을 전달받음
  • .old : 변경 전 이전 값을 전달받음
  • .initial : 관찰을 처음 시작할 때, 즉시 현재 값을 한번 전달받음
  • .prior : 값이 변경되기 직전과 변경된 직후 두 번 값을 전달받음

마지막으로 클로저를 통해 전달받는 매개변수는 2개로,
첫 번째는 값을 방출하는 인스턴스 (이 예시의 경우 WKWebView 인스턴스가 해당), 두 번째는 변경된 속성에 대한 정보 (이 예시에서는 사용하지 않으므로 _ 처리를 해줬다)가 넘어오게 된다.

우리는 이 두 클로저 매개변수 값을 바탕으로 관찰하고자 하는 객체의 변화에 따른 대응 코드를 보다 깔끔하게 KVO로 사용할 수 있게 된 셈이라고 이해하면 된다!

progressObservation = webView.observe(
    \.estimatedProgress,
     options: [.new]) { [weak self] object, _ in
         let progress = Float(object.estimatedProgress)
         self?.progressView.progress = progress
}

오늘은 여기까지 :)

 

Using Key-Value Observing in Swift | Apple Developer Documentation

Notify objects about changes to the properties of other objects.

developer.apple.com