R3S
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 = true로 ReadOnlyReactiveProperty<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] MonoBehaviour는 partial 필수 |
| R3Gen002 | [ReactiveCommand] 필드 이름은 _camelCase 규칙 |
| R3Gen003 | [AutoSubscribe] 대상 필드 없음 |
| R3Gen004 | 핸들러 매개변수 타입이 스트림과 불일치 |
| R3Gen005 | AddTo.Disposable은 [AutoDispose] 필요 |
| R3Gen006 | [ReactiveCommand] 필드는 ReactiveCommand<T> 여야 함 |
| R3Gen007 | Awake() 존재 필요 |
| R3Gen008 | Awake() 안에서 R3Awake() 호출 필요 |
| R3Gen009 | [AutoDispose] MonoBehaviour는 OnDestroy() 필요 |
| R3Gen010 | OnDestroy() 안에서 R3OnDestroy() 호출 필요 |
요구 사항
- Unity 2021.3+ — Roslyn Source Generator를 공식 지원하는 버전
- 프로젝트에 R3 런타임 패키지 — R3S는 R3 타입을 대체하지 않고, 그 타입을 사용하는 코드를 생성합니다
- 패키지에 포함된 Roslyn 소스 제너레이터 — 패키지 임포트만으로 동작합니다