티스토리 뷰

원본 링크

 

이제 observable이 무엇인지, 어떻게 생성하는지, 어떻게 구독하는지, 그리고 작업이 끝났을 때 어떻게 구독을 취소하는지 알게 되었을 것이다. observable은 RxSwift의 가장 기초적인 부분이지만 본질적으로 읽기만 가능하다. 구독을 통해 observable이 방출하는 이벤트를 받기만 할 수 있다는 뜻이다.

일반적으로 애플리케이션을 개발할 때, 런타임 중에 observable에 어떤 값을 추가하고 구독자들에게 값을 방출해야 한다. observable과 observer 두가지 기능이 모두 필요하고 이것이 바로 subject다.

이 챕터에서는 RxSwift가 가지고 있는 여러 종류의 subject에 대해 배울 것이고, 각각이 어떻게 동작하는지, 그리고 어떤 경우에 각 subject를 선택해야 하는지 배우게 될 것이다. 그리고 subject의 wrapper인 relay에 대해서도 배우게 될 것이다.

 

 

 

Getting started

이 챕터에서 사용할 프로젝트의 RxPlayground 폴더에 있는 ./bootstrap.sh를 실행하고 RxSwiftPlayground를 선택한다. 간단한 예제부터 시작해보자. playground에 다음 코드를 작성한다.

example(of: "PublishSubject") {
	let subject = PublishSubject<String>()
}

PublishSubject는 신문 출판사(newpaper publisher)처럼 정보를 받아서 구독자들에게 전달하기 때문에 적절한 네이밍이라고 볼 수 있다. String 타입으로 만들었기 때문에 오직 문자열만 받아서 전달할 수 있다.

subject.on(.next("Is anyone listening?"))

subject에 새로운 문자열을 입력했다. 아직 구독자(subscriber)가 아무도 없기 때문에 콘솔에는 아무것도 출력되지 않는다. 구독자를 하나 만들어보자.

let subscriptionOne = subject
	.subscribe(onNext: {string in
		print(string)
	})

이전 챕터에서 osbservable에 했던것과 동일한 방법으로 subject를 subscribe한 후 next 이벤트의 element를 출력했다. 하지만 여전히 콘솔에 아무것도 출력되지 않는다. 왜 그럴까?

PublishSubject는 observable과 다르게 현재 구독자에게만 이벤트를 방출한다. 때문에 이벤트를 생성할 때 구독하고 있지 않으면 아무 이벤트도 받을 수 없다. 벌목 사업을 생각해보자. 만약 숲에 아무도 없어서 나무가 쓰러지는 소리를 아무도 들을 수 없다면 불법 벌목사업을 성공시킬 수 있을까?

예제 마지막에 다음 코드를 추가한다.

subject.on(.nect("1"))

publish subject를 만들 때 string 타입으로 만들었기 때문에 String만 추가할 수 있다는 것을 기억하자. 이번에는 subject에 구독자가 있기 때문에 값이 방출된다.

--- Example of: PublishSubject ---
1

subscribe operator가 구독자를 추가하는 것 처럼 on(.next(_:))는 subject에 값을 방출할 수 있는, 새로운 next 이벤트를 추가한다. 그리고 subscribe와 동일하게 간소화된 표현식을 제공한다.

subject.onNext("2") 

onNext(_:)는 on(.next(_:))와 가독성에서 차이가 있을 뿐 같은 동작을 한다. 이제 실행결과에 2도 출력되는 것을 볼 수 있다.

--- Example of: PublishSubject ---
1
2

이제 subject의 대해 조금 더 깊이 알아보자.

 

 

 

What are subjects?

Subject는 observable과 observer 두가지 모두의 기능을 포함한 것처럼 동작한다. 앞서 이들이 어떻게 이벤트를 방출하고 구독할 수 있는지 알아보았다. 위 예제에서 subject는 next 이벤트를 받고, 이 이벤트는 각 subscriber에게 방출했다.

RxSwift는 네가지 subject type을 제공한다.

  • PublishSubject: 비어있는 상태로 생성되며 새로운 element만 방출한다.
  • BehaviorSubject: 기본값을 가진 상태로 생성되며, 초기값 또는 최신 element를 방출한다.
  • ReplaySubject: 정해진 크기의 buffer를 가진 채로 생성되며 새 구독자에게 생성된다.
  • AsyncSubject: completed 이벤트가 방출될 때만 마지막 next 이벤트만 방출한다. 그다지 많이 사용하지 않는 subject이며 이 책에서도 사용하지 않을 것이다.

RxSwift는 Relay라는 것도 제공하는데 각 PublishSubject와 BehaviorSubject를 감싸면서(wrap) next 이벤트만 받을 수 있는 PublishRelay와 BehaviorRelay도 제공한다. relay는 error나 completed 이벤트를 받을 수 없기 때문에 종료되지 않고 계속 살아있어야 하는 sequence를 만들 때 유용하다.

Note: 이번 챕터의 예제를 작성하면서 import RxRelay를 추가해야 한다는 에러를 확인했는가? 원래 relay는 RxCocoa의 한 부분으로 작성되었지만 relay는 Cocoa framework을 사용하지 않는 부분에서도 범용적으로 쓰일 수 있기 때문에 RxRelay라는 모듈이 따로 존재한다.

이어서 subject와 relay를 어떻게 다루는지 알아볼 것이다. subject를 publish하는 것 부터 시작하자.

 

 

 

Working with publish subject

publish subject는 구독자가 구독을 한 순간부터, 구독을 취소하거나 sequence가 종료될 때 까지 계속 최신 이벤트를 받도록 만드는 데 사용할 수 있다.

marble diagram에서 제일 위에있는 줄은 publish subject이며, 두번째 세번째 줄은 구독자이다. 위로 향하는 점선은 구독(subscribe)을, 아래로 향하는 점선은 이벤트의 방출을 뜻한다.

첫번째 구독자는 1이 방출된 후에 구독을 시작했기 때문에 1은 받지 못하고 2, 3을 받았다. 두번째 구독자는 2가 방출된 후에 구독을 시작했기 때문에 3만 받았다.

playground로 돌아와서 아래 코드를 추가한다.

let subscriptionTwo = subject
	.subscribe {event in
		print("2)", event.element ?? event)
	})

event는 next일 때 방출되는 값을 element라는 optional로 가지고 있기 때문에 nil-coalescing으로 값을 얻어서 출력할 수 있다.

1, 2가 방출된 후에 subscribe 했기 때문에 subscriptionTwo는 아무런 출력을 하지 않는다. 이제 새로운 이벤트를 추가해보자.

suject.onNext("3")

이 3은 subscriptionOne, subscriptionTwo에 의해 두번 출력된다.

3
2) 3

이번에는 subscriptionOne의 구독을 취소하고 새로운 이벤트를 추가해보자.

subscriptionOne.dispose()
subject.onNext("4")

4는 subscriptionTwo에게만 방출되어 출력된다.

2) 4

publish subject는 completed나 error 이벤트, 합쳐서 stop 이벤트라고 부르는 이 이벤트들 중 하나를 받아서 종료되면 더이상 새로운 이벤트를 방출하지 않는다. 하지만 새로운 subscriber가 생기면 이 마지막 stop 이벤트를 방출한다. 아래 코드를 추가해보자.

// 1
subject.onCompleted()

// 2
subject.onNext("5")

// 3
subscriptionTwo.dispose()

let disposeBag = DisposeBag()

// 4
subject
	.subscribe {
		print("3)". $0.element ?? $0)
	}
	.disposed(by: disposeBag)

subject.onNext("?")
  1. completed 이벤트를 추가해서 subject를 종료시킨다.
  2. 또다른 next 이벤트를 subject에 추가한다. 이미 종료되었기 때문에 이 이벤트는 방출되지 않을 것이다.
  3. subscriptionTwo의 구독을 해제한다.
  4. subject를 구독하는 새로운 구독자를 추가한다. 이번에는 dispose bag을 사용해서 dispose 시킨다.

이 세번째 구독자는 어떤 이벤트를 받을까? observable에서 배운대로 라면 아무 이벤트도 받지 않을 것이라고 예상할 수 있지만 세번째 구독자는 completed 이벤트를 받는다.

2) completed
3) completed

subject는 종료된 후에도 새로운 구독자에게 자신이 종료되었음을 알린다. 현재 구독자 뿐 아니라 새로운 구독자에게도 종료 이벤트를 방출하기 때문에 sequence의 종료를 조금 더 명확하게 알릴 수 있지만 때때로 이 기능은 버그를 유발할 수 있기 때문에 조심해야 한다.

publish subject는 입찰 시스템과 같이 시간에 민감한 시스템을 구현할 때 사용할 수 있다. 10시 1분에 가입한 사용자에게 9시 59분에 경매가 1분밖에 남지않았다는 알림을 보내서 화가 난 사용자가 별점 1점을 주는 것을 방지할 수 있다.

때때로 새롭게 구독을 시작한 사용자가 가장 최근에 방출된 element를 알아야 하는 경우가 있는데, publish subject는 새로운 구독자에게 값을 전달하지 않기 때문에 "사용자가 선택한 항목", "방금 도착한 알림"과 같은 이벤트를 모델링하는 데 적합하다.

 

 

 

Working with behavior subjects

behavior subject는 publish subject와 비슷하게 동작하지만 이들은 가장 최근에 발생한 next 이벤트도 새로운 구독자에게 방출한다. marble diagram을 보자.

첫번째 줄은 subject를 뜻한다. 두번째 줄로 표현된 첫번째 구독자는 1이 방출된 후에 구독을 시작했고 가장 최근에 방출된 element인 1부터 받을 수 있다. 같은 방법으로 두번재 구독자는 2가 방출된 후에 구독을 시작했지만 구독을 요청했을 때 2도 받고 순서대로 3을 받을 수있다.

// 1
enum MyError: Error {
	case anError
}

// 2
func print<T: CustomStringConvertible>(label: String, event: Event<T>) {
	print(label, (event.element ?? event.error) ?? event)
}

// 3
example(of: "BehaviorSubject") {
	// 4
	let subject = BehaviorSubject(value: "initial value")
	let disposeBag = DisposeBag()
}
  1. error를 정의한다.
  2. event 정보를 출력할 수 있도록 print 함수를 확장한다.
  3. 새로운 예제 함수를 작성한다.
  4. BehaviorSubject를 만들면서 initial value로 초기화 한다.
Note: BehaviorSubject는 항상 최근 element를 방출하기 때문에 initial value를 필요로 한다. 초기값을 정할 수 없는 경우에는 BehaviorSubject 대신 PublishSubject를 사용하거나 optional로 초기화 할 수 있다.
subject
	.subscribe {
		print(label: "1)", event: $0)
	}
	.disposed(by: disposeBag)

subject를 만들고 곧바로 구독했기 때문에 초기값이 방출된다.

--- Example of: BehaviorSubject ---
1) Initial value

이제 behavior subject를 구독하기 전에 이벤트를 추가하는 코드를 작성해보자.

subject.onNext("X")

이제 구독하기 전 가장 최근 값이 X가 되었기 때문에 X가 출력된다.

--- Example of: BehaviorSubject ---
1) X 

아래 코드를 추가하고 실행하기 전에 어떤 값이 출력될지 예상해보자.

// 1
subject.onError(MyError.anError)

// 2
subject
	.subscribe {
		print(label: "2)", event: $0)
	}
	.disposed(by: disposeBag)
  1. subject에 error 이벤트를 추가했다.
  2. 새로운 구독을 추가했다.
1) anError
2) anError

예상이 맞았는가? error는 각 구독자에게 모두 방출되어 출력된다.

behavior subject는 최신 데이터로 화면을 그려야 하는 경우에 유용하게 사용할 수 있다. 예를 들어 유저 프로필 화면을 behavior subject로 연결해서 프로필 정보가 바뀔 때 마다 새로운 정보로 패치하도록 만들 수 있다.

behavior subject는 가장 최신 정보를 전송(replay)하기 때문에 "요청 처리중" 또는 "현재 시간은 9시 41분"과 같이 상태를 모델링 하는 데 사용할 수 있다.

가장 최신정보 보다 더 많은 정보를 얻고싶을 때는 어떻게 해야 할까? 예를 들어 검색화면은 가장 최근에 검색했던 항목 뿐 아니라 이전에 검색했던 내용들을 보여줄 수 있어야 한다. 이제 replay subject가 등장할 차례이다.

 

 

 

Working with replay subjects

Replay subject는 초기에 설정한 크기의 버퍼에 일시적으로 자신이 최근에 방출했던 element를 저장한다. 그리고 버퍼에 있는 element들을 새로운 구독자에게 전송한다.

버퍼의 크기가 2인 marble diabram을 보자.

첫번째 구독자(두번째 줄)는 이미 replay subject(첫번째 줄)을 구독하고 있기 때문에 방출되는 모든 element를 받는다. 두번째 구독자(세번째 줄)은 2가 방출된 뒤에 구독했지만 이전에 방출되었던 1, 2,를 모두 받는다.

replay subject를 사용할 때 버퍼는 항상 메모리를 사용한다는 점을 기억하자. replay subject에 image처럼 크기가 큰 element를 사용한다면 그만큼 큰 크기의 메모리 버퍼가 필요할 것이다.

또 주의해야 할 점은 replay subject를 배열로 생성하는 것이다. 각 배열에 대해 버퍼를 생성해야 하기 때문에 메모리 사용이 그만큼 많아지게 된다.

example(of: "ReplaySubject") {
	// 1
	let subject = ReplaySubject<String>.create(bufferSize: 2)
	let disposeBag = DisposeBag()

	// 2
	subject.onNext("1")
	subject.onNext("2")
	subject.onNext("3")

	// 3
	subject
		.subscribe {
			print(label: "1)", event: $0)
		}
		.disposed(by: disposeBag)

	subject
		.subscribe {
			print(label: "2)", event: $0)
		}
		.disposed(by: disposeBag)
}
  1. 버퍼 크기가 2인 replay subject를 생성한다. replay subject는 create(bufferSize:) 함수를 통해 만들 수 있다.
  2. 세개의 element를 추가한다.
  3. 두개의 구독자를 추가한다.

버퍼 크기가 2이기 때문에 가장 처음 입력된 1은 방출되지 않는다.

--- Example of: ReplaySubject ---
1) 2
1) 3
2) 2
2) 3

다음, 아래 코드를 추가하자.

subject.onNext("4")

subject
	.subscribe {
		print(label: "3)", event: $0)
	}
	.disposed(by: disposedBag)

위 코드에서 네번재 element를 추가했다. 그리고 새로운 구독자를 만들어서 구독하도록 했다. 처음 두 구독자는 이미 구독을 하고있는 상태이기 때문에 정상적으로 4를 받을 수 있지만 새로운 구독자의 경우 버퍼에 있는 최근 element를 모두 받게 된다.

1) 4
2) 4
3) 3
3) 4

아직까지 잘 작동하는 것 처럼 보인다. 하지만 error를 발생시켜보면 어떻게 될까? 세번째 구독을 추가하는 코드 바로 위에 이 코드를 추가해보자.

subject.onError(MyError.anError)
1) 4
2) 4
1) anError
2) anError
3) 3
3) 4
3) anError

error 이벤트는 sequence를 종료하고 더이상 값을 방출하지 않게 한다고 배웠다. 하지만 버퍼가 가지고 있는 3, 4가 그대로 세번재 구독자에게 방출된다.

다음 코드를 error 다음에 추가해보자.

subject.dispose()

명시적으로 dispopse()를 호출해서 subject를 강제로 종료시켰다. 새로운 구독자는 subject가 이미 dispose되었다는 에러만 받게 될 것이다.

3) Object 'RxSwift...ReplayMany<Swift.String>' was already disposed.

이처럼 명시적으로 dispose()를 호출하는 것은 일반적인 방법은 아니다. dispose bag에 추가하면 dispose bag이 owner(view model이나 view controller)에 의해 해제될 때 자동으로 dispose된다.

이러한 경계에 대해 항상 조심하는 것이 좋다.

Note: RelayMany가 뭔지 궁금할 수 있다. RelayMany는 replay subject를 생성하는 내부적인 타입이다.

publish, behavior, replay subject를 통해 대부분의 모델을 구현할 수 있다. 하지만 올드스쿨로 돌아가서 observable에게 현재 가지고 있는 값을 물어보고 싶을 때도 있다. 이럴 때를 위해 Relay가 있다.

 

 

 

Working with relays

앞서 relay는 element를 전달하는 기능을 유지한 채 subject를 감싸는(wrap) 것이라고 배웠다. 일반적인 observable을 포함한 다른 subject와 다르게, relay에 값을 추가하려면 accept(:) 함수를 사용해야 한다. 다시말해 onNext(:)를 사용하지 않는다. 그 이유는 relay는 error나 completed 이벤트를 방출할 수 없고 단지 값을 받기만 할 수 있기 때문이다.

PublishRelay는 PublishSubject를 감싸고, BehaviorRelay는 BehaviorSubject를 감싼다. 차이점은 relay는 절대 종료되지 않는다는 것을 보장한다는 점이다.

example(of: "PublishRelay") {
	let relay = PublishRelay<String>()
	
	let disposeBag = DisposeBag()
}

이름만 빼면 PublishSubject를 생성하는 것과 다른점이 없다. 하지만 relay에 새로운 값을 추가하기 위해서 accept(_:)를 사용해야 한다.

relay.accept("Knock knock, anyone home?")

아직 구독자를 만들지 않았기 때문에 아무것도 방출되지 않는다. 구독자를 만들고 또다른 값을 추가해보자.

relay
	.subscribe {onNext: {
		print($0)
	})
	.disposed(by: disposeBag)

relay.accept("1")

결과는 subject를 사용했을 때와 동일하다.

--- Example of: PublishRelay ---
1

하지만 relay는 error나 completed 이벤트를 방출할 수 없기 때문에 subject에서 사용했던 다음 코드들은 컴파일 에러를 발생시킨다.

relay.accept(MyError.anError)
relay.onCompleted()

publish relay는 publish subject를 감싸고, accept와 절대 끝나지 않는다는 점을 빼면 똑같이 작동한다는 것을 기억하자.

Behavior relay도 마찬가지로 completed나 error가 없어서 종료되지 않는다. behavior subject를 감싸기 때문에 생성할 때 초기값이 있어야 하며 구독자에게 초기값 또는 가장 최신 값을 전달한다. behavior relay의 특별한 점은 언제든지 현재 최신 값을 물어볼 수 있다는 점이다. 이 기능은 기존 프로그래밍과 reactive 프로그래밍을 결합한다.

example(of: "BehaviorRelay") {
	// 1
	let relay = BehaviorRelay(value: "Initial value")
	let disposeBag = DisposeBag()

	// 2
	relay.accept("New initial value")

	// 3
	relay
		.subscribe {
			print(label: "1)", event: $0)
		}
		.disposed(by: disposeBag)
}
  1. behavior relay를 생성한다. 초기값을 통해 String으로 추론되지만 명시적으로 타입을 지정할 수도 있다.
  2. relay에 새로운 값을 추가한다.
  3. relay를 구독한다.
--- Example of: BehaviorRelay ---
1) New initial value

그리고 다음 코드를 추가한다.

// 1
relay.accept("1")

// 2
relay
	.subscribe {
		print(label: "2)", event: $0)
	}
	.disposed(by: disposeBag)

// 3
relay.accept("2")
  1. relay에 새로운 값을 추가한다.
  2. relay에 새로운 구독을 추가한다.
  3. 또다른 값을 relay에 추가한다.

기존에 구독을 하고있던 1)은 새로운 값 "1"을 받고, 이 값이 가장 최신 값이기 때문에 새로운 구독자도 구독을 할 때 같은 값을 받는다. 그리고 두 구독자 모두 두번째 값 "2"를 받는다.

1) 1
2) 1
1) 2
2) 2

마지막으로 현재 relay가 가지고 있는 값을 출력한다.

print(relay.value)

behavior relay는 현재 가지고 있는 값에 직접 접근할 수 있다. 이 경우에 가장 최신값인 "2"가 된다.

2

이 기능은 reactive 프로그래밍과 기존 reactive가 아닌 방식을 연결하는 데 굉장히 유용하다.

behavior relay는 다양한 부분에 쓰인다. 다른 subject와 같이 새로운 값에 반응하기 위해 behavior relay를 구독해도 되고, 구독까지 할 필요는 없고 단지 현재 값을 얻어올 필요가 있을 때도 value로 구독 없이 값을 얻어올 수 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함