UniRx 는 씬의 오브젝트 하나로 프레임을 돌린다
Esc

    UniRx 는 씬의 오브젝트 하나로 프레임을 돌린다

    2026년 9월 28일 수정–UnityRx라이브러리
    public void OnBuyClicked()
    {
        if (Time.time - lastClick < 0.3f) return;   // 버튼마다 이 세 줄이 복사된다
        lastClick = Time.time;
        Buy();
    }

    버튼이 스무 개면 이 세 줄이 스무 벌 생기고, “0.3초 안에 두 번이면 무시” 라는 규칙은 필드와 if 문에 흩어집니다.

    UniRx 는 클릭을 값의 흐름으로 바꿔 규칙을 연산자 하나로 적게 합니다. 그리고 그 흐름의 프레임 연산은 런타임에 스스로 만드는 MainThreadDispatcher 오브젝트 하나 위에서 돕니다.

    button.OnClickAsObservable()
          .ThrottleFirst(TimeSpan.FromSeconds(0.3))
          .Subscribe(_ => Buy())
          .AddTo(this);

    클릭도 체력 변화도 값의 흐름이다

    Rx 는 시간에 걸쳐 여러 번 오는 값을 List 처럼 Where 로 거르고 Select 로 바꾸는 방식입니다. UniRx 는 그것을 유니티에 맞춰 다시 만든 것이고, 손에 잡히는 것은 넷입니다.

    무엇 하는 일
    IObservable<T> 값이 여러 번 오는 것. 여기에 연산자를 겁니다
    Subject<T> 내가 직접 값을 밀어 넣는 통로
    ReactiveProperty<T> 값과 “바뀌었다” 알림을 한 덩어리로
    AddTo · CompositeDisposable 구독을 오브젝트 수명에 묶기

    여기에 유니티 쪽이 둘 붙습니다. OnCollisionEnterAsObservable 같은 트리거 컴포넌트 39개와, Rx 원본에 없던 프레임 기반 연산자입니다.

    Observable.EveryUpdate()
              .Where(_ => Input.GetKey(KeyCode.Space))
              .Subscribe(_ => Charge())
              .AddTo(this);

    Update 를 도는 일 자체가 스트림이 됩니다.

    프레임 연산은 디스패처 오브젝트의 코루틴 위에서 돈다

    프레임 연산을 처음 쓰는 순간 UniRx 는 씬에 GameObject 를 하나 만듭니다.

    // UnityEngineBridge/MainThreadDispatcher.cs
    new GameObject("MainThreadDispatcher").AddComponent<MainThreadDispatcher>();

    이 오브젝트는 DontDestroyOnLoad 로 씬을 넘어 살아남고, 안에서 Update · FixedUpdate · EndOfFrame 용 코루틴 셋을 돌립니다. 코루틴은 매 프레임 깨어나 MicroCoroutine 이라는 배열을 처음부터 끝까지 밉니다. 구독 하나가 배열 한 칸입니다.

    MicroCoroutine 은 StartCoroutine 처럼 코루틴마다 네이티브 객체를 만들지 않고 배열에 IEnumerator 만 담습니다. EveryUpdate, TimerFrame, DelayFrame, ObserveEveryValueChanged 같은 프레임 연산자가 이 위에 올라탑니다.

    몇몇은 길이 다릅니다. EveryLateUpdate 와 ObserveOnMainThread 는 디스패처 컴포넌트의 Update 와 LateUpdate 에서, FromCoroutine 과 RepeatUntilDestroy 는 디스패처 위의 일반 유니티 코루틴으로 돕니다. 실행 기반이 따로 없는 것은 Time.frameCount 를 읽기만 하는 FrameInterval 류뿐입니다.

    UniRx 의 프레임은 결국 씬에 떠 있는 디스패처 오브젝트 하나가 돌립니다.

    시간은 스케줄러가 맡는다

    Delay(1초) 나 Interval(0.5초) 처럼 시각으로 표현되는 것은 디스패처가 아니라 IScheduler 가 맡습니다.

    public interface IScheduler
    {
        DateTimeOffset Now { get; }
        IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action);
        // 즉시 실행 오버로드 생략
    }

    Now 는 시계이고, 어느 구현체를 고르느냐는 어느 스레드에서 실행할지입니다. ImmediateScheduler 는 부른 자리에서, ThreadPoolScheduler 는 스레드풀에서, MainThread 계열은 디스패처를 거쳐 메인 스레드에서 실행합니다. 인터페이스 하나가 “언제” 와 “어디서” 를 함께 들고 있습니다.

    이 하나가 맡는 것 어떻게
    실행 위치 교체 연산자에 스케줄러를 넘겨 스레드풀에서 계산하고 메인에서 받습니다
    가상 시간 테스트 시계가 스케줄러 안에 있어서 갈아끼우면 하루치 타이머를 즉시 흘립니다
    작업 단위 취소 Schedule 이 돌려주는 IDisposable 로 예약을 거둡니다
    무한 스트림 Interval 과 Repeat 을 “다음 것을 다시 예약” 으로 표현합니다

    시간 연산자는 DateTime.Now 를 직접 읽지 않고 모두 이 인터페이스를 지나갑니다. 그래서 시간이 걸리는 로직을 기다리지 않고 검증할 수 있습니다.

    주의할 점

    1. 끝나지 않는 스트림은 반드시 수명에 묶는다

    안 지키면 생기는 문제. EveryUpdate 는 끝나지 않고, 그것을 돌리는 디스패처는 DontDestroyOnLoad 라 씬을 넘겨도 살아 있습니다. 구독한 오브젝트가 파괴되고 씬이 바뀌어도 구독은 배열 한 칸을 차지한 채 매 프레임 불립니다. 람다가 파괴된 컴포넌트를 건드리면 예외가 납니다.

    이렇게 쓴다. 끝나지 않는 스트림에는 .AddTo(this) 를 붙입니다. Take 나 TakeUntil 로 끝이 정해진 스트림만 예외입니다.

    2. 디스패처를 씬에 따로 두지 않는다

    안 지키면 생기는 문제. 디스패처는 처음 쓰일 때 스스로 생깁니다. 프리팹이나 씬에 MainThreadDispatcher 가 이미 들어 있으면 둘이 되고, 소스의 중복 방어 코드가 경고를 남기고 하나를 지웁니다. 어느 쪽이 지워지는지는 cullingMode 설정에 따라 달라집니다.

    이렇게 쓴다. 디스패처는 라이브러리가 만들게 둡니다. 하이어라키에 떠 있는 MainThreadDispatcher 를 씬 정리 코드가 지우지 않게 합니다.

    3. 예외가 난 자리에 따라 멈추는 것이 다르다

    안 지키면 생기는 문제. Select 나 Where 같은 연산자 안에서 난 예외는 OnError 로 가고 구독이 풀립니다. 버튼이라면 그 뒤로 눌러도 반응이 없습니다. EveryUpdate, Interval, Timer 는 Subscribe 람다에서 던져도 값을 만드는 코루틴이 죽어 OnError 없이 멈춥니다. 반대로 버튼, Subject, ReactiveProperty, 트리거에 건 Subscribe 람다의 예외는 값을 보낸 쪽으로 올라가고 구독은 남습니다. 이때는 에러 핸들러도 불리지 않습니다.

    예외가 난 자리 구독 에러 핸들러
    연산자 람다 (Select, Where) 끊김 불림
    EveryUpdate · Interval · Timer 의 Subscribe 람다 값이 더 안 옴 안 불림
    버튼 · Subject · ReactiveProperty · 트리거의 Subscribe 람다 남음 안 불림

    이렇게 쓴다. 입력 파싱처럼 실패가 예상되는 일은 그 코드가 있는 람다 안에서 try 로 감싸 예외가 밖으로 나가지 않게 합니다. 에러 핸들러로 받을 수 있는 것은 연산자 안의 예외뿐이라고 보고 짭니다.

    정리

    UniRx 는 입력과 상태 변화를 값의 흐름으로 다루고, 그 프레임 연산을 씬의 디스패처 오브젝트 하나로 돌립니다.

    • 손에 잡히는 것은 IObservable<T> · Subject<T> · ReactiveProperty<T> · AddTo 넷에, 트리거 컴포넌트와 프레임 연산자가 붙습니다.
    • 프레임 연산은 DontDestroyOnLoad 디스패처가 돌리는 코루틴 셋과 MicroCoroutine 배열 위에서 돕니다.
    • 시간은 IScheduler 가 맡고, 이 인터페이스 하나가 시계와 실행 위치를 함께 표현합니다.