KVO 알아보기

고라니·2023년 10월 7일
0

TIL

목록 보기
38/67

이전에 KVC와 KeyPath에 대해 알아보았다. 바로 KVO를 위해서였다.
KVO가 무엇인지 KVC와 KeyPath를 왜 알아야 했는지 알아보자

KVO (Key- Value - Observing)

  • 다른 객체의 속성에 대한 변경 사항을 객체에 알리기 위한 코코아 프로그래밍 패턴

  • Model과 View처럼 논리적으로 분리된 부분 사이에서 변경사항을 전달하는데 유용 (특히 MVC 패턴에서 )

  • NSObject를 상속받은 클래스에서만 사용가능

관찰대상 준비

객체의 속성 변화를 알리기(관찰되기) 위해서는 몇가지 조건이 있다.
코드 예시를 보면서 알아보자

class Pokemon {
    var name: String = "Pikachu"
    func evolution() {
    	name = "Raichu"
    }
}

나의 피카츄가 진화하면 몬스터도감이 확인하고 알림을 주도록 만들어 보자

그러기 위해서는 NSObject를 상속받아야 하며 관찰될 프로퍼티는 @objc와 dynamic 을 추가 해주어야 한다.
KVO는 KVC의 메커니즘을 활용해 객체의 특정 속성의 변화를 감지한다.

class Pokemon {
	@objc dynamic var name: String = "Pikachu"
    func evolution() {
    	if name == "Pikachu" {
	    	name = "Raichu"
        }
    }
}

이렇게 코드를 수정해주면 관찰될 준비는 끝이다.

관찰자 정의

관찰자 클래스의 인스턴스는 하나 이상의 프로퍼티에 대한 정보를 관리한다. 관찰자를 생성할 때, 관찰하려는 속성을 참조하는 키 경로(KeyPath)와 observe(_:options:changeHandler:) 메서드를 호출하여 관찰을 시작한다.

class Pokedex: NSObject {
	@objc var myPokemon: Pokemon
    var observation: NSKeyValueObservation?

    init(pokemon: Pokemon) {
        myPokemon = pokemon
        super.init()

        observation = observe(\.myPokemon.name, options: [.old, .new], changeHandler: { object, change in
            print("포켓몬도감 알림: \(change.oldValue!)\(change.newValue!)로 진화하였습니다.")
        })
    }
}

.myPokeMon.name 경로를 통해 name을 참조하고 옵션으로 선택한 .old와 .new를 통해 변경전 값과 변경 후 값을 사용한다.

options 매개변수

observe의 options 매개변수에는 .old, .new 말고도 여러가지 옵션이 있다. 만약 필요하지 않다면 생략 가능하다. 다음은 주요 옵션들이다.

  • .initial: 관찰 시작 시점에서도 즉시 알림을 받는다. 관찰을 시작하자 마자 해당 속성의 현재 값을 확인하려 할 때 유용

  • .old: 변경 전의 속성 값을 알림에 포함 시킨다. .oldValue 키를 통해 값을 얻는다.

  • .new: 변경 후의 속성 값을 알림에 포함시킨다. .newValue 키를 통해 새로운 값을 얻을 수 있다.

  • .prior: 속성 값이 변경되기 직전과 변경된 후에 두 번 알림을 받는다. 값이 변경되기 전과 후를 모두 관찰하고자 할 때 사용된다.

.initial 과 .prior 예시 코드

import Foundation

class MyClass: NSObject {
    @objc dynamic var count = 0
}

class Observer: NSObject {
    var observation: NSKeyValueObservation?

    init(object: MyClass) {
        super.init()

        observation = object.observe(\.count, options: [.initial, .prior]) { (object, change) in
            if change.isPrior {
                print("About to change...")
            } else {
                print("Changed to \(object.count)")
            }
        }
    }
}

let myObject = MyClass()
let observer = Observer(object: myObject)

print("Update 1")
myObject.count = 1

print("Update 2")
myObject.count = 2

// 결과
// About to change...
// Changed to 0
// Update 1
// About to change...
// Changed to 1
// Update 2
// About to change...
// Changed to 2

관찰 대상, 관찰자 연결하기

관찰대상을 관찰자의 초기자에 전달한다.
몬스터 도감에 내 포켓몬을 연결해준다고 볼 수 있다.

let myPokemon = Pokemon()
let pokedex = Pokedex(pokemon: myPokemoon)

변경 감지하고 알림받기

myPokemon.evolution() // 포켓몬도감 알림: 피카츄가 라이츄로 진화하였습니다.

이제 나의 포켓몬을 진화시키면 피카츄가 라이츄로 진화한것을 포켓몬 도감이 알려준다!

장점과 단점

장점

  • 객체의 속성이 변할 때 즉시 반응할 수 있어, 데이터의 동기화나 UI업데이트 같은 작업을 즉각적으로 처리 가능

  • 직접 접근하기 어려운 외부 라이브러리 등의 변화를 감지하기 좋음 (NSObject를 상속하지 않고 있거나 @objc, dynamic 작성 안되어 있으면 소용없는디?)

  • 보통 속성 변화에 따른 동작 구현은 많은 콜백 함수나 이벤트 리스너를 작성해야 하지만 KVO를 사용하면 이런 복잡성 없이 간단하게 변화 감지 가능

  • 여러 디자인 패턴에서 모델과 뷰 사이의 통신을 자연스럽게 연결가능

단점

  • NSObject를 상속해야 하며 그렇기 때문에 class에서만 사용 가능하고 런타임에 의존하게 된다.

  • 러탄임에 프로퍼티 변경을 감지하기 위해 추카적인 작업을 수행하여 약간의 오버헤드가 발생할 수 있다. 만약 큰 데이터 세트 혹은 빈번하게 변하는 프로퍼티에 적용할 때는 고려해야 한다.

  • 순환 참조 발생 가능성이 있으며, 이는 메모리 누수로 이어질 수 있다.

언제 사용할까?

  • UI 업데이트: 특정 요소를 모델의 변화에 따라 자동으로 업데이트 할 때 모델의 변활를 감지하여 적절한 UI변화 적용하기 유용하다.

주의할 점

NSKeyValueObservation 객체가 메모리에서 완전히 해제되면 관찰이 중단된다. 하지만 다른곳에서 해당 인스턴스를 참조하고 있다면 관찰은 유지되어 예기치 못한 문제가 발생할 수 있다.

그렇기 때문에 순환참조가 발생하지 않게 weak를 적절히 사용해주는 등의 주의가 필요하며 만약 참조가 더이상 없더라도 deinit 메서드에 invalidtae()메서드를 명시적으로 호출하는 것이 명확한 코드의 표현을 위해 권장되는 방식이다.

deinit {
    observation?.invalidate()
}

마치면서

KVO는 객체의 속성이 변경되는 것에 효과적으로 반응할 수 있게 해준다. 이를 통해 모델과 뷰나 다른 로직적으로 분리된 부분들 사이의 통신을 간편하게 할 수 있다.
다만 NSObject를 상속 받아야 하는 점이 큰 단점이라고 생각한다.

주의해야할 점은 관찰이 더 이상 필요하지 않을 때 관찰을 종료해야 한다는 것이다.

profile
🍎 무럭무럭

0개의 댓글