Today I Learn/25.05

📅05/21 | TIL Action 이벤트 구조 설계와 구독 관리

오늘도즐겨 2025. 5. 21. 23:01

✅ 1. Action을 사용하는 이유

  • 이벤트 기반으로 여러 객체 간 느슨한 결합을 만들기 위함
  • 호출자는 구독자가 있는지 몰라도 되고, 구독자는 필요한 이벤트만 처리 가능
  • += 연산자를 통해 간단하게 구독/해제 가능

✅ 2. 기초 구조 예시

 

public class UIManager : MonoBehaviour
{
    public static UIManager Instance;
    public Action OnUpdateUI;

    private void Awake() => Instance = this;

    public void UpdateUI()
    {
        OnUpdateUI?.Invoke(); // 구독자에게 알림
    }
}

 

// 다른 스크립트에서 구독
UIManager.Instance.OnUpdateUI += UpdatePanels;

void UpdatePanels() { /* UI 갱신 코드 */ }

 

✅ 3. 이벤트를 버튼에만 쓰는 건 아님

  • Action은 버튼이 아닌 공통 로직, 상태 변경 등 어디서든 호출 가능
  • UI가 아닌 오브젝트에서도 사용할 수 있음

✅ 4. 여러 이벤트를 나눠서 정의할 수 있다

public event Action OnClickEquipBtn;
public event Action OnClickSkillTopBtn;

➡ 각각의 목적에 따라 구체적으로 역할 분리 가능
➡ += 연산자를 통해 개별적으로 구독할 수 있음

✅ 5. Action은 호출하지 않으면 아무 일도 안 일어남

  • 구독만 해놓고 Invoke() 하지 않으면 아무런 반응 없음
  • Invoke()만 해놓고 구독자가 없으면 역시 무의미

✅ 6. Action은 “구독자 패턴”이지, UI 갱신을 자동으로 해주는 게 아님

  • 직접적으로 UI를 갱신하는 게 아니라,
    "누가 UI 갱신할지"를 정하지 않은 채 신호만 보내는 구조

✅ 7. "기능"과 "알림"은 분리하는 것이 좋다

public void AddSkillPieces(TRPlayerSkill data, int amount)
{
    // 데이터만 처리
}

public void NotifySkillPieceChanged(TRPlayerSkill data)
{
    // 이벤트 발생만 처리
}

➡ SRP(단일 책임 원칙)을 지켜 헷갈림 방지 + 유지보수 편리

 

✅ 8. 합쳐서 쓰고 싶다면 중간 메서드로 래핑

public void AddSkillPiecesAndNotify(TRPlayerSkill data, int amount)
{
    AddSkillPieces(data, amount);
    NotifySkillPieceChanged(data);
}

➡ 호출하는 쪽의 선택 폭이 넓어지고 코드도 명확해짐

 

✅ 9. 실제 구독 예시

private void Start()
{
    _deckManager.OnSkillPieceChanged += UpdateSkillPieceUI;
}

private void UpdateSkillPieceUI(int skillId, int count)
{
    skillPieceText.text = count.ToString();
}

✅ 10. 사용하지 않는 Action은 과감히 제거

  • 호출도 하지 않고, 구독도 안 하면 불필요한 코드 낭비
  • 현재 구조에 꼭 필요하지 않다면 제거해서 가독성과 유지보수성 향상

🧠요약

"Action은 호출자와 구독자를 느슨하게 연결하며, 상태 변경(로직)과 알림(이벤트)을 분리하면 코드가 훨씬 깔끔해진다."

 


 

✅ 목적 요약

  • AddSkillPieces: 데이터만 처리
  • OnSkillPieceChanged: 변경 알림용 Action (필요 시 구독 가능)
  • UI는 필요한 경우 +=로 구독해서 갱신
  • 또는, DeckManager에서 명시적으로 알림을 호출

DeckManager.cs

using System;
using System.Collections.Generic;
using UnityEngine;

public class DeckManager : MonoBehaviour
{
    public static DeckManager Instance { get; private set; }

    public event Action<int, int> OnSkillPieceChanged; // skillId, count

    private Dictionary<int, int> SkillPieces = new();

    private void Awake()
    {
        Instance = this;
    }

    /// <summary>
    /// 스킬조각 데이터만 추가 (UI 알림 없음)
    /// </summary>
    public void AddSkillPieces(TRPlayerSkill data, int skillPiece)
    {
        int dataID = data.TableId;

        if (!SkillPieces.ContainsKey(dataID))
            SkillPieces[dataID] = 0;

        SkillPieces[dataID] += skillPiece;
    }

    /// <summary>
    /// 스킬조각 변경 이벤트 알림 (원할 때만 호출)
    /// </summary>
    public void NotifySkillPieceChanged(TRPlayerSkill data)
    {
        int dataID = data.TableId;

        if (SkillPieces.TryGetValue(dataID, out var value))
            OnSkillPieceChanged?.Invoke(dataID, value);
    }

    /// <summary>
    /// 합쳐서 쓰고 싶을 경우 이 메서드 사용
    /// </summary>
    public void AddSkillPiecesAndNotify(TRPlayerSkill data, int skillPiece)
    {
        AddSkillPieces(data, skillPiece);
        NotifySkillPieceChanged(data);
    }

    // 외부에서 조회용
    public int GetSkillPieceCount(int skillId)
    {
        return SkillPieces.TryGetValue(skillId, out var count) ? count : 0;
    }
}

 

🔹 UI 호출부 예시 (UISkillInfoPnl.cs 등)

private void OnClick(TRPlayerSkill data)
{
    Debug.Log($"{data.Name} 스킬조각 10개 추가");

    _deckManager.AddSkillPieces(data, 10);         // 데이터만 추가
    _deckManager.NotifySkillPieceChanged(data);    // 알림은 선택적으로
}

또는

// 만약 합쳐진 메서드로 처리하고 싶다면:
_deckManager.AddSkillPiecesAndNotify(data, 10);

🔹 구독 예시 (UI 갱신 처리자)

private void Start()
{
    DeckManager.Instance.OnSkillPieceChanged += HandleSkillPieceChanged;
}

private void HandleSkillPieceChanged(int skillId, int pieceCount)
{
    // 예: 특정 스킬 ID의 조각 수를 UI에 반영
    Debug.Log($"SkillID {skillId} → 조각 수: {pieceCount}");

    if (skillId == myTargetSkill.TableId)
    {
        mySkillPieceText.text = pieceCount.ToString();
    }
}

 

 

✅ 전체 요약표

AddSkillPieces 데이터를 변경하는 순수 기능
NotifySkillPieceChanged Action을 수동으로 트리거
OnSkillPieceChanged 필요한 UI에서만 구독
AddSkillPiecesAndNotify 두 기능을 한번에 처리 (선택 사항)
UI 스크립트 구독 또는 명시적 호출로 UI 업데이트

 

✨ 정리 

"이벤트는 발생시키는 쪽과 구독하는 쪽을 느슨하게 연결하기 위한 도구"이며,
데이터 처리와 알림 트리거는 분리하는 것이 깔끔한 구조다."


🔹 event 키워드의 의미와 차이

  • event Action은 외부에서 +=, -=만 허용하며, 직접 호출이나 대입은 막음.
  • Action만 사용할 경우 외부에서 함수 전체 덮어쓰기나 직접 호출이 가능해져 위험함.
  • 이벤트는 안전한 이벤트 호출 방식을 만들기 위해 사용되며, UI 버튼 클릭 등에서 주로 활용됨.

🔹 TryGetValue 사용법

  • 딕셔너리에서 키를 안전하게 조회할 때 사용하는 메서드.
  • 키가 없을 때도 예외가 발생하지 않고 false 반환.
if (dict.TryGetValue("key", out var value)) { ... }
  • dict[key]처럼 직접 접근할 경우 키가 없으면 예외 발생하므로, 키 존재 여부가 불확실할 때 유용.