[UniRx 입문 강좌 1] 개념 및 기본 사용법 소개
[UniRx 입문 강좌 3] IObserver 메세지 종류와 스트림의 수명 관리
[UniRx 입문 강좌 4] Operator 활용(1) - Where & Select & SelectMany 사용법
[UniRx 입문 강좌 5] Operator 활용(2) - 다양한 오퍼레이터 소개
[UniRx 입문 강좌 6] 코루틴(Coroutine) 과 UniRx 연동
1. UniRx 의 기초, Subject
UniRx 를 공부할 때 첫번째로 볼 항목은 Subject 이다.
Subject<T> 형태로 원하는 자료형을 Subject 타입의 인스턴스로 생성하면 UniRx 의 스트림을 사용할 수 있다.
기존의 절차적 프로그래밍 방식이라면 위에서부터 순차적으로 처리를 하겠지만 UniRx 에서는 순차 수행이 아닌 메세지 발행 시 처리할 함수를 등록(Subscribe) 해 놓고 통지를 기다리는 개념이기 때문에 위와 같은 결과가 나온다.
우선 string 을 스트림 객체로서 사용하기 위해 Subject 타입으로 생성했다.
그리고 이 객체 하나에 Subscribe 를 세번 선언한 후 OnNext 로 메세지를 전달하면 그 때 비로소 Subscribe 에 지정한 람다 함수가 작동을 하여 하나의 OnNext 당 3번의 Subscribe 가 차례로 호출 된다.
여기서 알 수 있는 것은 Subject 에 OnNext 로 메세지를 전달하고 그 메세지를 수령하면 Subscribe 에서 지정한 함수를 수행한다는 것이다.
2. event 방식을 Subject 를 이용한 방식으로 바꾸기
C# 의 delegate 를 사용하다보면 event 역시 많이 사용해 봤을 것이다.
event 는 타이밍에 메시지를 통보하여 event 별로 지정해둔 처리를 불러올 수 있게 해주는 기능이다.
이 부분이 UniRx 에서 시간의 흐름과 관련된 기본 패러다임과 동일하다.
왜냐하면 UniRx 가 event 의 상위 호환 으로서 더욱 유연한 기능을 제공하는 기술이기 때문이다.
UniRx 의 Subject를 이용한 방법과 기존 event를 사용하는 방법이 어떻게 다른지 비교를 위해 우선 유니티에서 event 를 사용하는 샘플을 살펴보자.
아래 코드는 1에서 100 까지 1초 마다 timer 값을 1씩 증가시키는 샘플이다.
TimerClass 에 정의한 이벤트 핸들러 TimerEventHandler 의 delegate 함수를 TimerView 의 람다 함수로 지정해 주고 Coroutine 내에서 eventTimer 의 값을 변화시켰다.
당연히 delegate 로 등록된 TimerView 의 람다 함수가 eventTimer 값이 갱신되는 1초마다 호출되어 그 결과를 출력할 것이다.
이 예제를 UniRx 를 사용하면 아래와 같이 바꿀 수 있다.
전체적으로 비슷한 내용이지만 몇몇 주요 변경사항이 있다.
event 를 대신하여 Subject 가 사용되으며 이벤트 핸들러를 등록할 때 delegate 가 아닌 Subject의 Subscribe 가 사용되었고 이벤트 통지를 Subject의 OnNext() 라는 메소드를 통해 전달 했다.
3. Observer (관찰자) & Observable (관찰 가능한)
위에서 Subscribe 와 OnNext 를 Subject 의 메소드로 설명했는데 사실은 Subject 의 메소드가 아니라 Subject 가 구현한 IObserver 인터페이스와 IObservable 인터페이스 의 메소드 들을 구현하고 있다는 말이 더 정확하다.
OnNext 는 OnError, OnComplete 와 함께 IObserver 에 선언되어 있으며 Subscribe 는 IObservable 에 선언되어 있다.
즉, IObserver 가 OnNext 를 통해 IObservable 의 Subscribe 에 이벤트를 전달하는 개념이다.
UniRx 뿐만 아니라 리액티브 프로그래밍(Reactive Programming 이하 Rx) 를 이해하는 핵심 포인트 중 하나는 Observer 패턴을 이해하는 것이다.
Observer 는 관찰자라는 뜻이며 Observable (관찰 가능한) 이라는 개념의 객체를 사용한다.
Observable 객체를 생성한다는 것은 스트림에 흘려 보내는 값 즉, 프로그램 내에서 발생하는 각종 이벤트 들의 관찰을 시작한다는 뜻이다.
그런 의미에서 Observable을 Rx의 스트림(Stream) 과 동일한 개념으로 생각할 수도 있지만 정확하게 말하면 스트림은 이벤트 메세지가 발행되어 최종 목적지인 Subscribe 에 도달하는 순간 까지 처리되는 일련의 흐름이며 Observable 은 이벤트 메세지를 발행할 수 있는 하나의 스트림을 시작시키고 스트림 내에서 일어나는 모든 일을 관찰 할 수 있는 외부 객체라는 말이 옳을 것이다.
이 Observable 과 Stream 의 개념을 확실하게 이해하는 것이 Rx 를 이해하는 핵심 포인트이다.
3. Observable 객체 생성 방법
스트림에 이벤트를 흘려보내기 위해서 우선 첫번째로 관찰 가능한 객체 즉 Observable 을 생성해야 한다.
Observable 객체의 생성 방법은 여러가지가 있다.
- Subject 시리즈를 사용
- ReactiveProperty 시리즈를 사용
- 팩토리 메소드 시리즈를 사용
- UniRx.Triggers 시리즈를 사용
- Coroutine 을 변환하여 사용
- UGUI 이벤트를 변환하여 사용
- 그외 UniRx 가 제공하는 기능들 ...
(사실 이 중에서도 가장 많이 사용되는 케이스는 ReactiveProperty, UniRx.Triggers, UGUI 이벤트 3가지 라고 본다.)
1. Subject 시리즈를 사용
위의 예제에서도 사용했던 Subject 는 간단하게 스트림을 생성해서 자유롭게 이용할 수 있다.
Subject 에도 몇가지 종류가 있는데 간단하게 설명하고 넘어가겠다.
Subject 종류 | 설명 |
Subject<T> | 가장 단순한 형태이지만 가장 자주 사용한다. OnNext 로 이벤트를 전달 한다. |
BehaviorSubject<T> | 마지막으로 전달한 이벤트를 캐싱하고 이후에 Subscribe 될 때 그 값을 전달한다. |
ReplaySubject<T> | 과거에 전달한 모든 이벤트를 캐싱하고 이후에 Subscribe 될 때 그 값을 모두 모아 전달한다. |
AsyncSubject<T> | OnNext 사용시 즉시 전달하지 않고 내부에 캐싱한 후 OnCompleted 가 실행되면 마지막 OnNext 를 전달 한다. |
기본형인 Subject<T> 외에는 그리 사용할 일이 없을 것이다.
2. ReactiveProperty 시리즈를 사용
ReactiveProperty 는 int 나 float 같은 일반 변수 자료형에 Subject 의 기능을 붙인 것이다.
일반 변수와 같은 방식으로 아주 간편하게 사용 가능하다.
그리고 유니티 인스펙터에 출력시킬 수도 있는데 이를 위해서는 미리 각 자료형 타입 별로 구현한 Inspectable ReactiveProperty 를 사용해야 한다.
IntReactiveProperty, LongReactiveProperty, ByteReactiveProperty, FloatReactiveProperty, DoubleReactiveProperty, StringReactiveProperty, BoolReactiveProperty, Vector2ReactiveProperty, Vector3ReactiveProperty, Vector4ReactiveProperty, ColorReactiveProperty, RectReactiveProperty, AnimationCurveReactiveProperty, BoundsReactiveProperty, QuaternionReactiveProperty 가 구현되어 있으며 예를 들어 IntReactiveProperty 의 경우
[Serializeable] public class IntReactiveProperty : ReactiveProperty<int>
의 형태이므로 똑같이 사용할 수 있다.
3. 팩토리 메소드 시리즈를 사용
팩토리 메소드의 경우 Subject 기반의 방법 들에 비해 복잡한 스트림을 쉽게 만들수 있는 장점이 있다.
하지만 UniRx 자체에 간편한 생성 기능 방법이 다양하기 때문에 그다지 자주 사용하지는 않는다.
대표적으로 Observable.Create, Observable.Start, Observable.Timer/TimerFrame 등이 있으며 사용 방법을 알고 싶다면 ReactiveX Creating Observables항목 을 참고하기 바란다.
4. UniRx.Triggers 시리즈를 사용
using UniRx.Triggers; 를 추가해야 사용 할 수 있다.
유니티로 개발 하면서 Update 콜백을 대체하는 가장 편리하기 때문에 그만큼 많이 사용하는 방법이다.
Unity 의 콜백 이벤트를 UniRx 의 IObservable 로 변환하여 제공하며 GameObject 에 귀속되어 있어 Destory 시점에서 OnCompleted 를 자동으로 발행하기 때문에 수명 관리를 해줄 필요가 없다.
ObserverUpdateTrigger : UpdateAsObservable
using UniRx;
using UniRx.Triggers;
void Start()
{
this.UpdateAsObservalbe()
.Subscribe(_=>Debug.Log("Update"));
}
각 오브젝트 객체 별 Update() 대용으로 주로 사용하게 될 함수이다.
UniRx.Triggers 네임스페이스에 정의되어 있으며 위 샘플 코드와 같이 this(GameObject) 에 종속되어 사용되기 때문에 해당 오브젝트가 생존해 있는 동안만 작동하고 이 오브젝트가 Destory 되면 함께 Dispose 되므로 라이프사이클을 관리할 필요가 없다.
UpdateAsObservable 는 ObservableUpdateTrigger 클래스에 속해 있으며 UpdateAsObservable 을 호출하면 이 Object에 자동으로 ObservableUpdateTrigger 가 AddComponent 된다.
유니티의 Update 에 Update, FixedUpdate, EndOfFrame, LateUpdate 등의 다양한 업데이트 함수가 있듯이 마찬가지로 UniRx 도 여기에 대응하는 다양한 타이밍의 Update 들을 제공하는데 그 갯수가 너무 많아 소개하기 힘들기 때문에 깃허브의 위키 페이지를 참조하기 바란다. GitHub-UniRx.Triggers
5. Coroutine 을 변환하여 사용
코루틴과 UniRx 은 서로 변환하고 이용하기에 매우 편리하며 상호 보완적 관계로서 궁합이 매우 좋다.
UniRx 로 복잡한 스트림을 처리하기 위해 무조건 많은 오퍼레이터들을 중첩해서 사용하는 것보다 적절히 코루틴을 병용해 절차적으로 구현하면 훨씬 심플한 구조를 만들 수 있는 경우가 많다.
Coroutine 에서 IObservable 로 변환하려면 Observable.FromCoroutine 을 이용한다.
코루틴과 스트림의 조합에 대한 자세한 설명은 추후 별도로 포스팅 할 예정이니 이번에는 간단한 사용 예제만 소개하겠다.
6. UGUI 이벤트를 변환하여 사용
코루틴 이상으로 UniRx 와 UGUI 의 궁합도 매우 좋다.
using UniRx; 만으로 다양한 UGUI 의 컴포넌트 별로 제공되는 수많은 기능을 가져올 수 있다.
무엇보다 위의 ReactiveProperty 와 조합하면 MVP(Model-View-Presenter) 패턴을 매우 쉽게 적용할 수 있는데 이 부분은 추후 포스팅에서 다루겠다.
7. 그외 UniRx 가 제공하는 기능들...
ObservableWWW
- Unity 의 WWW를 스트림으로 취급할 수 있도록 만든 것으로 코루틴을 사용하지 않고 비동기 다운로드 처리를 할 수있어 매우 유용했던 기능이다.
하지만 유니티에서 WWW 가 Deprecate 되면서 아직 작동은 하지만 언제 기능이 사라질지 모르므로 쓰지 않는 게 좋다.
Observable.NextFrame
- 다음 프레임에 메세지를 전달하는 Observable을 생성한다.
Observable.NextFrame()
.Subscribe(_=>Debug.Log("다음 프레임에 실행된다"));
Observable.EveryUpdate
- 유니티의 Update() 콜백함수와 동일하게 매프레임 호출 되는 Observable 을 생성한다.
using UniRx;
void Start()
{
Observable
.EveryUpdate() //매 프레임 호출
.Subscribe(x => Debug.Log(x)); //구독자. x == Time.countFrame
}
Observable.EveryUpdate() 는 static 메소드 이므로 MonoBehaviour 상속 클래스가 아닌 외부에서도 호출이 가능하다.
UniRx.Triggers 의 UpdateAsObservable 과 다르게 특정 GameObject 에 종속되어 있지 않기 때문에 Dispose 시점을 수동으로 지정해줘야 하므로 Subscribe 또는 AddTo 오퍼레이터를 사용해 관리할 필요가 있다
일반적으로 특정 오브젝트에 종속되지 않는 상황, 예를 들면 FPS 출력이나 마우스 클릭 체크, 네트워크 체크 등의 경우 등에 유용하다.
또한 유니티의 Update 에 Update, FixedUpdate, EndOfFrame, LateUpdate 등의 다양한 업데이트 함수가 있듯이 마찬가지로 UniRx 도 여기에 대응해 동일한 개념의 Update 를 지원한다.
- Observable.EveryUpdate
- Observable.EveryFixedUpdate
- Observable.EveryEndOfFrame
- Observable.EveryLateUpdate
- Observable.EveryAfterUpdate
- Observable.EveryGameObjectUpdate
UniRx 입문 강좌 3) OnNext, OnError, OnCompleted, Dispose와 스트림의 수명관리
댓글