아요 개발 일기

[RxSwift] Observable 연산자 [disposable, create, deferred] (2) 본문

iOS/RxSwift

[RxSwift] Observable 연산자 [disposable, create, deferred] (2)

소진이 2023. 1. 3. 15:40

안녕하세요 소진입니다 :o

이번에는 저번 포스팅에 이어서 Observable 연산자에 대해 알아보도록 하겠습니다!


disposable

dispose은 구독 취소하는 개념과 동일합니다.

Observable이 모든 요소들을 방출하고서 onCompleted까지 방출이 됐다면

더이상 해당 Observable을 구독할 이유가 없겠죠?

 

그때, 아래 코드처럼

.dispose() 연산자를 이용하여 구독 취소할 수 있습니다.

Observable.just(100).subscribe(onNext: { element in
    print(element)
}).dispose()

하지만

위의 코드처럼 직접 하나하나 시퀀스를 .dispose()를 통해

구독 취소를 하는건 너무 번거롭겠죠..?

 

그럼

이 부분을 해결해 줄

DisposeBag에 대해 알아봅시다! 

 

DisposeBag

"Bag" 라는 단어에서 알 수 있듯이

Dispose를 담아주는 가방같은 역할을 합니다.

 

disposebag에 disposable을 담아 두고

마지막에 한 번에 구독 취소를 할 수 있도록 도와주는 겁니다.

 

이해를 돕기 위해서

예시 코드랑 이미지를 가지고 왔습니당

 

let subscriber1 = observable.subscribe(onNext: { _ in
		// do something
})

let subscriber2 = observable.subscribe(onNext: { _ in
		// do something
})

let subscriber3 = observable.subscribe(onNext: { _ in
		// do something
})

let subscriber4 = observable.subscribe(onNext: { _ in
		// do something
})

 

위 처럼 하나의 Observable을 1, 2, 3, 4 라는 구독자가 subscribe 하도록 코드를 짜보겠습니다!

 

그 후에

subscribe를 끊고 싶다면 .dispose 시켜주면되겠죠?

subscriber1.dispose()
subscriber2.dispose()
subscriber3.dispose()
subscriber4.dispose()

이렇게 하면 모든 구독자와 Observable의 관계는 끊어질껍니다!

 

이런 방법도 좋지만,

저희가 방금 공부했던 disposeBag를 사용해서

아래 이미지처럼 한번에 구독자를 관리해보겠습니다

 

 

위 이미지를 코드로 작성하면 아래와 같습니다 

 

// DisposeBag 선언
let disposeBag = DisposeBag()

// 한번에 구독 취소
observable.subscribe(onNext: { element in
    print(element)
}).disposed(by: disposeBag)

 

이렇게 처리하면 해당 ViewController가 해제될 때 

disposeBag도 메모리에서 해제되면서 

그 안에 있던 모든 disposable 요소들이 한번에 해제됩니다.

 

 

// 임의 해제
disposeBag = nil

 

nil을 넣어주면 임의로 해제시킬수도 있는데,

이때는 전역 변수로 선언한 disposeBag가 옵셔널 타입 이어야합니다.

 

⚠️ 메모리 누수 ⚠️

 

예를들어 뷰 컨트롤러가 있는데,

해당 뷰 컨트롤러에서 0부터 9까지 출력하는 Observable이 있다고 칩시다.

그리고 그 Observable을 구독한 구독자가 있죠

 

그때, 카운팅이 다 끝나기 전에 뷰 컨트롤러를 해제해 버린다면 어떻게 될까요?

 

 

3초 뒤에 뷰 컨트롤러가 해제되면서 deinit이 되었음에도

끝까지 카운트가 진행되는걸 볼 수 있습니다.

 

이때 사용하는게

dispose, disposeBag 입니다.

 

그렇기 때문에 무조건 필수로

dispose 혹은 disposeBag 해주어야 합니다.

 


 

create

 

자유 자재로 원하는 시점에 원하는 이벤트를 방출 시키는

Observable을 생성하고 subscribe해서 사용할 수 있습니다.

 

예를들어 create으로 API를 호출하는 Observable을 생성하여 결과를

OnNext를 통해서 구독자에게 결과 값을 전달하다던지 응용할 수 있습니다.

 

사용 방법

 

기본적으로 아래 코드와 같은 틀로 사용됩니다.

 

let customObservable = Observable<String>.create { observer in
    return Disposables.create()
}

 

 

✔️ Observable<T>에 제네릭 타입은 onNext로 방출시킬 요소의 타입

 

✔️ create 클로저의 파라미터인 Observer을 이용하여 onNext, onError, onCompleted 이벤트를 방출해줄 수 있음

 

✔️ disposable 처리는 Disposable.creat()로 함 (필수 구현 요소)

 

 

틀에대해 살펴보았으니, 

코드를 한번 짜봅시다!

 

// create로 원하는 시점에 요소 방출
let customObservable = Observable<String>.create { observer in
    observer.onNext("hi i'm sojin")
    observer.onCompleted()
    
    return Disposables.create()
}

// 구독
customObservable.subscribe (onNext: { element in
    print(element)
}, onCompleted: {
    print("onCompleted")
}).disposed(by: disposeBag)


[실행 결과]
// hi i'm sojin
// oncompleted

 

많이 어렵진 않죠??

위처럼 코드를 짤 수도 있지만

create는 변수로서 선언하는 방식보다는

함수로서 선언하는 방식이 더 자주 사용 됩니다.

함수로 변경하러 가봅시다!

 

함수로 변경하기

 

// 함수
func customObservable2() -> Observable<String> {
    return Observable<String>.create { observer in
        observer.onNext("hi i'm sojin")
        observer.onCompleted()
        
        return Disposables.create()
    }
}

// 구독
customObservable2().subscribe(onNext: { element in
    print(element)
}, onCompleted: {
    print("onCompleted")
}).disposed(by: disposeBag)

[실행 결과]
// hi i'm sojin
// onCompleted

 

이렇게 변수를 함수로서 변경해주고,

리턴 값을 Observable<T>로 바꿔주면 완전히 동일한 결과를 얻을 수 있습니다.

 

그런데..

of나 from을 사용해도 될 것 같은데,

왜 굳이 create를 사용하나 싶기도 하죠??

그럼 이번에는 create가 어떤식으로 응용되는지 살펴봅시다ㅎㅎ

 

사용 예시

create는 주로 API호출을 통해서 값을 불러올때 사용됩니다.

 

코드 예시를 가지고 왔으니

차근차근 읽어봅시당 

func getFriends() -> Observable<[Friend]> {
    return Observable.create { observer in
        Alamofire.request("http://friendservice.herokuapp.com/listFriends")
            .validate()
            .responseJSON { response in
                switch response.result {
                case .success:
                    guard let data = response.data else {
                        // if no error provided by alamofire return .notFound error instead.
                        // .notFound should never happen here?
                        observer.onError(response.error ?? GetFriendsFailureReason.notFound)
                        return
                    }
                    do {
                        let friends = try JSONDecoder().decode([Friend].self, from: data)
                        observer.onNext(friends)
                    } catch {
                        observer.onError(error)
                    }
                case .failure(let error):
                    if let statusCode = response.response?.statusCode,
                        let reason = GetFriendsFailureReason(rawValue: statusCode)
                    {
                        observer.onError(reason)
                    }
                    observer.onError(error)
                }
        }
 
        return Disposables.create()
    }
}

 

당장은 위 코드처럼 짤 수는 없지만,

create가 어떤식으로 응용되는지는 이해되었죠?

 

이런식으로 create를 이용해서 다양한 커스텀 Observable 스트림을 생성해줄 수 있습니다!

 


 

deferred

이름 뜻으로 예상할 수 있듯이 

" 무언가를 미루다 "  라는 의미로 사용되어,

observable이 생성되는 시점을 구독자에 의해서 구독되기 전까지 미뤄주는 역할을 합니다.

 

즉,

굳이 구독되기 전에 미리 계산할 필요가 없는 Observable들에 사용합니다.

(구독하기도 전에 불필요하게 소요되는 시간 낭비를 줄임)

 

사용 방법

 

아래 선언부 코드를 보면 Observable을 리턴해주고 있죠?

그래서 Observable에서 직접 호출이 가능합니다.

 

public static func deferred(_ observableFactory: @escaping () throws -> Observable<Element>)

 

아래 코드처럼 defferred 클로저 내에서 실제로 구독할 Observable을 리턴해주면 됩니다.

 

let deferredObservable = Observable<String>.deferred {
    // Observable 리턴하는 곳
}

 

간단하죠??

이제 직접 사용해봅시다!

 

let observable = Observable.just("sojin")

 

위에 선언해놓은 Observable을 구독과 동시에 연산하도록 미뤄보겠습니다.

 

let deferredOb = Observable<String>.deferred {
    return Observable.just("sojin deferred")
}

 

짠!

이렇게만 deferred로 감싸주면 끝입니다!!

 

이쯤되면

deferred를 굳이 왜 사용하는지 궁금하실겁니다!

(저두 그랬어요)

 

사용 예시

 

Observable.just(doSomeMath) 라는 Observable이 있다고 가정해봅시다.

이때 doSomeMath()라는 함수는 Observable이 선언되는 시점에 미리 계산을 하게 됩니다.

 

let mathObservable = Observable.just(doSomeMath())

func doSomeMath() {
    print("1 + 1 = 2")
}

[실행 결과]
// 1 + 1 = 2

 

이렇게 mathObservable을 subscribe 하지 않았는데

doSomethint()이 호출된 것을 볼 수 있습니다.

 

.just 뿐만아니라 .from, .of 모두 선언되는 시점에 저렇게 미리 계산이 됩니다.

 

함수가 간단한 계산만 한다면 구독하기전에 미리 계산하는게 문제가 되지 않지만,

함수가 엄청 복잡해서 10초 이상 걸린다면 

Observable을 구독하기도 전에 10초 이상 소요 시키는 것이 큰 낭비가되고, 

주요 스레드를 방해하는 불상사가 생깁니다.

 

실제 앱에서는 10초간 정지되는 것 처럼 보이겠죠?

 

let deferredSequence = Observable<Any>.deferred {
		return Observable.just(doSomething())
}

 

이런 구독되기 전에 미리 계산 할 필요가 없는 Observable들을 deferred로 처리하면됩니다.

 

아직도 확 와닿지는 않죠??

더 유용한 예시를 봅시다!

 

아래와 같이 위치 권한이 획득 상태에 대해 정보를 가져오는 Observable이 있다고 가정해봅시다.

기본 위치 권한 값은 false 상태라고 설정합니다.

 

func permissionObservable() -> Observable<AuthorizationStatus> {
		return .just(Auths.locationAuthorizationStatus())
}

 

이렇게 선언 되어있는 상태에서

Observable을 구독하기 전에 사용자가 위치 권한을 false에서 true로 변경 했다고 합시다.

그 이후 위치 권한에 대한 저 Observable을 구독하면 어떤 결과가 나올까요?

 

위치 권한이 Observable이 생성 될 시점에 가져와 진 것이니

당연히 false가 나오겠죠?

하지만 실제 위치 권한 상태는 true 입니다. 

(Observable을 구독하기 전에 true로 변경했기 때문)

 

그렇다면 이때!

deferred를 사용하여

구독과 동시에 가장 최신 상태를 불러오면 되겠죠?

 

func permissionObservable() -> Observable<AuthorizationStatus> {
    return Obsrevable.deferred {
            return .just(Auths.locationAuthorizationStatus())
    }
}

 

이렇게 Observable을 구독하는 동시에 최신 값을 가져와야 하는 경우에도

deferred 연산자가 유용하게 사용됩니다.

 

드디어 Observables 끝!!

정말 고생하셨어요🥹

다음 포스팅에서는

Traits을 알아보도록 하겠습니다!!

 


 

마치며..

 

dispose, disposeBag

Observable subscribe 할 때 무조건 .dispose() 혹은 dispose(by: )에 disposeBag을 넣어서 관리

(dispose보단 disposeBag 사용 권장)

 

create

커스텀한 Observable 스트림을 만들고 싶을때 사용

 

deferred

무거운 작업의 Observable을 만들어 사용할 때 사용

   --> 구독하는 시점과 동시에 작업을 시작할 수 있도록하여 쓸데없는 낭비를 막기

구독과 동시에 최신값이 필요한 경우 Obserbable을 deferred로 감싸서 사용

 

 

참고 블로그

http://adamborek.com/creating-observable-create-just-deferred/