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]처럼 직접 접근할 경우 키가 없으면 예외 발생하므로, 키 존재 여부가 불확실할 때 유용.