3 분 소요

R3S – Roslyn으로 R3 구독 코드를 선언적으로

현재 버전 : 1.0.0

R3S는 R3에서 반복되던 구독과 정리, 그리고 프로퍼티 노출 같은 배선 코드를 선언적 어트리뷰트로 위임합니다.
제너레이터가 Subscribe + AddTo, 공개 접근자, dispose 연결을 생성하므로,
사용자 코드는 “값이 바뀔 때 무엇을 할지”에만 집중할 수 있습니다.

R3가 무엇인지, 리액티브 모델·연산자 등은 다루지 않습니다. R3 콜백·구독을 붙이는 방식을 덜 번거롭게 만든다는 점만 다룹니다.

https://github.com/NightWish-0827/R3S.git?path=/com.nightwishlab.r3s
UPM에서 git URL로 패키지를 추가하시면 됩니다.


개선 포인트

R3S 없이는 일회성 구독마다 비슷한 패턴이 반복됩니다.
dispose 필드, Subscribe, AddTo, ReactiveProperty / Subject / ReactiveCommand 노출, 소멸 시 Dispose까지 매번 직접 작성해야 합니다.

R3S는 어트리뷰트로 이러한 배선 코드를 대신 생성합니다.
콜백 본문은 직접 작성하고, 제너레이터가 구독 한 줄, 필드용 공개 접근자, CompositeDisposable 생명 주기를 만들어 냅니다.

구독 연결

// Before
private void R3Awake()
{
    _hp.Subscribe(OnHpChanged).AddTo(_disposable);
}

// After — 대상 필드 + 핸들러만 선언하면 자동 생성
[AutoSubscribe(nameof(_hp))]
private void OnHpChanged(int value)
{
    // 로직만 작성
}

프로퍼티 노출

// Before
private readonly ReactiveProperty<int> _hp = new(100);
public ReadOnlyReactiveProperty<int> Hp => _hp;

// After
[ReactiveProperty]
private ReactiveProperty<int> _hp = new(100);
// 제너레이터: public ReadOnlyReactiveProperty<int> Hp => _hp;

ReactiveCommand 노출

// Before
private readonly ReactiveCommand<Unit> _attack = new();
public Observable<Unit> Attack => _attack;
public void ExecuteAttack() => _attack.Execute(Unit.Default);

// After
[ReactiveCommand]
private ReactiveCommand<Unit> _attack;
// 제너레이터: 초기화 + public Observable + ExecuteAttack()

R3 타입과 콜백 의도는 그대로 유지되며, 주변 보일러플레이트만 사라집니다.


핵심 기능

[AutoSubscribe] — 구독 한 줄 자동 생성

메서드에 [AutoSubscribe(nameof(필드))]를 붙이면 해당 ReactiveProperty 또는 Subject 필드에 대해 Subscribe(해당 메서드).AddTo(...)를 생성합니다.
핸들러 매개변수 타입이 스트림 항목과 맞지 않으면 생성 단계에서 오류로 감지됩니다.


AddTo 모드 — 수명 앵커 선택

모드 생성 코드 요구 사항
Disposable (기본) AddTo(_disposable) 클래스에 [AutoDispose] 필요
CancellationToken AddTo(destroyCancellationToken) MonoBehaviour 전용
MonoBehaviour AddTo(this) MonoBehaviour 전용

[ReactiveProperty] — 읽기 전용 접근자 자동 생성

ReactiveProperty<T> 필드에 붙이면 기본값 ReadOnly = trueReadOnlyReactiveProperty<T> 접근자를 생성합니다.
ReadOnly = false이면 ReactiveProperty<T>를 그대로 공개합니다.
_필드명PascalCase 프로퍼티 명명 규칙은 제너레이터가 자동으로 적용합니다.


[ReactiveCommand] — 실행 헬퍼 + Observable 노출

ReactiveCommand<T> 필드에 붙이면 타입에 따라 다음을 생성합니다.

필드 타입 생성 표면
ReactiveCommand<Unit> Observable<Unit> 게터 + Execute이름()
ReactiveCommand<T> Observable<T> 게터 + Execute이름(T value)

[Subject] — Observable만 외부에 노출

Subject<T> 필드에 붙이면 public Observable<T> 이름 => _필드; 형태를 생성합니다.
이벤트 발생은 Subject로만 하고, 외부에는 Observable만 노출하는 R3 스타일 경계를 프로퍼티 한 줄 없이 유지할 수 있습니다.


[AutoDispose]CompositeDisposable + 정리 훅

클래스에 붙이면 호스트 타입에 따라 다음을 생성합니다.

호스트 생성 요소
MonoBehaviour CompositeDisposable + R3OnDestroy()
그 외 클래스 CompositeDisposable + IDisposable + Dispose()

[AutoSubscribe(..., AddTo.Disposable)]이 올바른 dispose 대상을 공유합니다.


전체 예제

제너레이터가 같은 클래스에 멤버를 추가하려면 partial 선언이 필요합니다.

using System;
using R3;
using R3.Attributes;
using UnityEngine;

[AutoDispose]
public partial class CombatViewModel : MonoBehaviour
{
    [ReactiveProperty]
    private ReactiveProperty<int> _hp = new(100);

    [ReactiveCommand]
    private ReactiveCommand<Unit> _respawnRequested;

    [Subject]
    private Subject<int> _onDamaged;

    private void Awake()    => R3Awake();
    private void OnDestroy() => R3OnDestroy();

    [AutoSubscribe(nameof(_hp))]
    private void OnHpChanged(int hp)
    {
        if (hp <= 0)
            ExecuteRespawnRequested();
    }

    [AutoSubscribe(nameof(_onDamaged))]
    private void OnDamaged(int amount)
    {
        Debug.Log($"Damaged: {amount}");
    }
}
  • OnHpChanged_hp가 값을 내보낼 때마다 호출됩니다. _hp.Subscribe(...).AddTo(...)를 직접 작성하지 않습니다.
  • 외부 코드는 Hp, OnDamaged(Observable<int>), RespawnRequested(Observable<Unit>)로 바인딩하고, UI에서는 ExecuteRespawnRequested()만 호출합니다.
  • GameObject가 파괴되면 R3OnDestroy()CompositeDisposable을 정리하여 Disposable 모드 구독이 함께 해제됩니다.

필수 생명 주기

R3S는 Unity 메시지에 자동으로 훅을 걸지 않습니다. MonoBehaviour에서는 제너레이터가 가정하는 브릿지를 호출해야 합니다.

private void Awake()     => R3Awake();      // 미호출 시 구독이 동작하지 않음 (R3Gen008)
private void OnDestroy() => R3OnDestroy();  // 미호출 시 CompositeDisposable 미정리 (R3Gen009, R3Gen010)

진단 코드

어트리뷰트 계약 위반은 런타임 누수 대신 생성 단계의 오류로 감지됩니다.

코드 의미
R3Gen001 [AutoDispose] MonoBehaviourpartial 필수
R3Gen002 [ReactiveCommand] 필드 이름은 _camelCase 규칙
R3Gen003 [AutoSubscribe] 대상 필드 없음
R3Gen004 핸들러 매개변수 타입이 스트림과 불일치
R3Gen005 AddTo.Disposable[AutoDispose] 필요
R3Gen006 [ReactiveCommand] 필드는 ReactiveCommand<T> 여야 함
R3Gen007 Awake() 존재 필요
R3Gen008 Awake() 안에서 R3Awake() 호출 필요
R3Gen009 [AutoDispose] MonoBehaviourOnDestroy() 필요
R3Gen010 OnDestroy() 안에서 R3OnDestroy() 호출 필요

요구 사항

  • Unity 2021.3+ — Roslyn Source Generator를 공식 지원하는 버전
  • 프로젝트에 R3 런타임 패키지 — R3S는 R3 타입을 대체하지 않고, 그 타입을 사용하는 코드를 생성합니다
  • 패키지에 포함된 Roslyn 소스 제너레이터 — 패키지 임포트만으로 동작합니다

태그: C#, OpenSource, R3, Unity

카테고리:

업데이트: