스킬 UI 동적 생성 및 이벤트 바인딩 구조

목차

1. 요구 사항

2. 설계 목표

3. 흐름도

4. 구현

        4.1. UI 초기화 진입점

       4.2. 스킬 목록 동적 생성과 이벤트 바인딩

       4.3. 스킬 획득 상태와 레벨 표시 동기화

5. 개발 의도

1. 시스템 요구 사항

스킬 UI는 플레이어가 보유한 스킬 데이터를 기준으로 런타임에 목록을 생성해 화면에 출력해야 한다.

스킬은 정적으로 배치된 UI가 아니라, PlayerManager가 보유한 PlayerSkills 배열 길이에 따라 자동으로 생성되어야 하며, 각 항목은 스킬 아이콘과 이름, 그리고 레벨을 조절하기 위한 + / - 버튼을 가져야 한다.

또한 각 버튼은 자신이 속한 스킬 항목의 인덱스를 정확히 참조해야 하므로, 이벤트 바인딩 과정에서 인덱스 참조 오류(클로저 문제)가 발생하지 않아야 한다.

UI는 데이터의 표현과 입력 전달에 집중하고, 실제 스킬 레벨 변경 규칙은 별도의 로직에서 처리되도록 책임을 분리하는 구조가 필요하다.

2. 설계  목표

- PlayerSkills 배열을 기반으로 스킬 UI를 동적으로 생성한다.

- UI 항목마다 버튼 이벤트가 올바른 스킬 인덱스를 참조하도록 안전하게 바인딩한다.

- UI 생성과 초기 상태 동기화를 분리해 흐름을 명확히 한다.

- 스킬 목록을 재생성해도 중복 UI가 남지 않도록 초기화 과정을 포함한다.

3. 흐름도

Start()

 ↓

CreateSkillList()

 ↓

(기존 UI 제거 → 프리팹 생성 → 데이터 바인딩 → 버튼 이벤트 연결)

 ↓

UpdateSkillList()

 ↓

보유 스킬 포인트 UI 갱신

Start에서 UI 구성을 시작하고, CreateSkillList에서 목록을 생성한 뒤, UpdateSkillList에서 획득 여부/레벨 표시를 최종 동기화하는 흐름이다.

이 구조는 생성과 상태 반영을 분리해 UI가 언제든 재생성될 수 있도록 만든다.

4. 구현

4.1. UI 초기화 진입점
private void Start()
{
    playerManager = PlayerManager.instance;
    errorTxt = errorPanel.GetComponentInChildren<TextMeshProUGUI>();
    CreateSkillList();
}

Start는 Unity의 생명주기 함수로, 해당 GameObject가 활성화된 뒤 첫 프레임에 호출된다.

Awake보다 늦게 실행되기 때문에, 다른 매니저나 싱글톤 인스턴스가 준비된 뒤 참조를 잡는 데 유리하다.

여기서는 PlayerManager.instance를 통해 플레이어의 스킬 데이터와 스킬 포인트를 읽어오기 위한 참조를 확보한다.

싱글톤 구조의 장점은 플레이어 상태가 전역적으로 하나라는 게임 설계와 잘 맞고, UI가 어디서 열리든 동일한 데이터 원본을 바라보게 만든다는 점이다.

반면 단점은 결합도가 높아질 수 있다는 점인데, 이 스킬 UI는 PlayerManager의 스킬 데이터가 곧 원천 데이터이므로 구조적으로 자연스러운 선택이다.

또한 errorPanel 내부의 TextMeshProUGUI를 GetComponentInChildren으로 찾는다.

GetComponentInChildren은 오브젝트 하위 계층에서 해당 컴포넌트를 탐색하는 Unity API로, UI 프리팹 구조가 에러 패널 아래 텍스트가 포함된 형태일 때 빠르게 참조를 얻을 수 있다.

장점은 인스펙터에 일일이 텍스트 레퍼런스를 연결하지 않아도 된다는 점이고, 단점은 런타임 탐색 비용과 계층 구조 변경에 대한 취약성이다.

이 코드는 시작 시점에 한 번만 수행되므로 비용 문제는 제한적이며, 에러 패널의 구조가 안정적이라는 전제에서 충분히 합리적이다.

마지막으로 CreateSkillList를 호출해 실제 스킬 항목 UI를 생성한다.

Start에서 바로 호출하는 이유는 스킬 UI가 열자마자 즉시 목록이 보여야 하는 UI이기 때문이다.

Update에서 매 프레임 갱신하는 방식은 UI가 바뀌지 않는 프레임에도 비용이 발생하므로, 초기화는 Start에서 한 번 확정적으로 만드는 것이 더 적절하다.

4.2. 스킬 목록 동적 생성과 이벤트 바인딩
void CreateSkillList()
{
    foreach (Transform child in SkillList) Destroy(child.gameObject);

    for (int i = 0; i < playerManager.PlayerSkills.Length; i++)
    {
        int index = i;
        GameObject skillWDInstance = Instantiate(skillWDPrefab, SkillList);

        skillWDInstance.transform.Find("SkillIcon").GetComponent<Image>().sprite
            = playerManager.PlayerSkills[i].Icon;

        skillWDInstance.transform.Find("SkillName").GetComponent<TextMeshProUGUI>().text
            = playerManager.PlayerSkills[i].SkillName;

        skillWDInstance.transform.Find("PlusBtn").GetComponent<Button>()
            .onClick.AddListener(() => UpdateSkillLevel(skillWDInstance, index, true));

        skillWDInstance.transform.Find("MinusBtn").GetComponent<Button>()
            .onClick.AddListener(() => UpdateSkillLevel(skillWDInstance, index, false));
    }

    UpdateSkillList();
    haveSkillPoint.text = "스킬 포인트 : " + playerManager.CurrentSkillPoint.ToString();
}

CreateSkillList는 UI 생성을 전담하는 함수다.

먼저 SkillList 패널의 모든 자식 오브젝트를 Destroy로 제거한다.

이 과정은 스킬 UI가 여러 번 열리거나, 스킬 목록을 다시 갱신해야 할 때 중복 생성되는 상황을 차단한다.

Destroy는 Unity에서 오브젝트를 제거하는 기본 함수이며, 호출 즉시 사라지는 것이 아니라 프레임 종료 시점에 정리되는 특성이 있다.

이 함수는 UI를 새로 구성하는 시점에만 호출되므로, 잦은 반복 호출이 아니라는 전제에서 안정적인 선택이다.

다만 대규모 UI나 빈번한 리빌드가 발생하는 경우에는 오브젝트 풀링 방식이 성능상 유리할 수 있는데, 현재 구조는 스킬 수가 제한적이고 UI 갱신 빈도가 높지 않다는 상황에서 단순성과 명확성을 우선한 형태다.

그 다음 for문으로 PlayerSkills 배열 길이만큼 반복하며 스킬 프리팹을 Instantiate 한다.

Instantiate는 프리팹을 런타임에 복제해 GameObject를 생성하는 Unity API이며, 두 번째 인자로 부모 Transform을 넣으면 생성과 동시에 올바른 UI 계층에 배치된다.

이 구조의 핵심은 스킬 개수가 늘어나도 UI 코드를 바꿀 필요가 없다는 점이다.

즉, 데이터가 UI를 결정하는 구조이기 때문에 확장성이 자연스럽게 확보된다.

각 항목에서 int index = i 를 따로 저장하는 부분은 이벤트 바인딩 안정성 측면에서 매우 중요하다.

C#에서 람다식을 AddListener로 등록할 때, 반복 변수 i를 직접 캡처하면 클로저로 인해 의도치 않게 모든 이벤트가 동일한 i 값을 참조하는 문제가 발생할 수 있다.

index로 복사해두면 각 반복마다 고유한 값이 람다에 캡처되어 버튼이 눌렸을 때 정확한 스킬 인덱스를 참조한다.

이 코드는 버튼이 어떤 스킬을 조작하는지를 보장하기 위한 필수 안전장치에 해당한다.

아이콘과 이름 세팅은 transform.Find로 특정 자식 오브젝트를 찾아 Image와 TextMeshProUGUI 컴포넌트를 얻고, sprite와 text를 대입하는 방식이다.

이는 Unity UI에서 가장 직관적인 바인딩 방식으로, 데이터 변경이 즉시 화면에 반영된다.

단점은 Find가 문자열 기반 탐색이라 UI 구조가 바뀌면 런타임에서 찾지 못해 오류로 이어질 수 있고, 반복 호출 시 비용이 누적될 수 있다는 점이다.

하지만 이 호출은 UI를 만드는 순간에만 수행되고, 스킬 개수도 무한히 많지 않으므로 현재 단계에서는 구현 명료성이 더 큰 가치다.

이후 최적화가 필요해지면, 프리팹에 'SkillItemView' 같은 컴포넌트를 붙이고 Awake에서 참조를 캐싱하는 방식으로 개선할 수 있다.

버튼 이벤트 연결은 Button.onClick.AddListener를 사용한다.

이는 UnityEvent 기반 이벤트 시스템으로, UI 버튼이 눌렸을 때 등록된 콜백이 호출된다.

Update에서 입력을 감지하는 방식과 달리, 버튼 클릭이라는 이벤트가 발생했을 때만 실행되므로 불필요한 프레임 단위 연산을 하지 않는다.

이 구조는 UI가 입력 이벤트 중심으로 동작하도록 만든다는 점에서 유지보수성과 성능 모두에 유리하다.

여기서 UpdateSkillLevel(skillWDInstance, index, true/false)를 호출하도록 한 이유는, 증가와 감소 로직이 결국 같은 규칙을 공유하는 레벨 변경이기 때문이다.

이전처럼 Plus/Minus 두 함수를 분리하면 조건문과 UI 갱신 코드가 중복되기 쉬운데, 단일 함수에 플래그로 통합하면 로직 변경 지점이 하나로 모인다.

이 게시글에서는 상세 규칙 자체는 다음 글에서 설명하되, 이벤트 바인딩을 단일 엔트리 포인트로 수렴시키는 설계가 코드 구조를 안정적으로 만든다는 점이 핵심이다.

마지막으로 UpdateSkillList를 호출해 획득 여부와 레벨 표시 같은 상태 반영을 수행하고, haveSkillPoint 텍스트를 현재 포인트로 갱신한다.

CreateSkillList가 생성만 담당하고 표시 상태는 UpdateSkillList로 마무리하는 구조는, UI 항목 생성과 상태 동기화를 분리해 향후 상태만 갱신이 필요할 때 재생성 없이 UpdateSkillList만 호출하는 확장 여지를 남긴다.

4.3. 스킬 획득 상태와 레벨 표시 동기화
public void UpdateSkillList()
{
    for (int i = 0; i < playerManager.PlayerSkills.Length; i++)
    {
        GameObject skillWDInstance = SkillList.GetChild(i).gameObject;

        bool isAcquired = playerManager.PlayerSkills[i].IsAcquisition;
        skillWDInstance.transform.Find("NotHave").GetComponent<Image>().enabled = !isAcquired;

        if (isAcquired)
            skillWDInstance.transform.Find("SkillLevel").GetComponent<TextMeshProUGUI>().text
                = playerManager.PlayerSkills[i].CurrentLevel.ToString();
        else
            skillWDInstance.transform.Find("SkillLevel").GetComponent<TextMeshProUGUI>().text = "0";
    }
}

UpdateSkillList는 생성된 UI 항목들의 표시 상태를 실제 데이터와 일치시키는 동기화 함수다.

스킬 UI에서 중요한 점은 획득하지 않은 스킬은 잠금 상태처럼 보여야 한다는 요구인데, 이 코드는 IsAcquisition 값을 기준으로 NotHave 이미지를 켜고 끄는 방식으로 그 상태를 표현한다.

여기서 Image.enabled를 조작한 이유는 GameObject 자체를 껐다 켜는 것보다 구조가 단순하고, 레이아웃 변형 없이 오버레이 이미지만 표시하거나 숨길 수 있기 때문이다.

SetActive로 오브젝트를 꺼버리면 하위 컴포넌트 전체가 비활성화되어 다른 UI 구성과 충돌할 수 있는데, enabled는 특정 렌더링 요소만 제어하는 방식이라 UI 레이아웃을 더 안정적으로 유지한다는 장점이 있다.

또한 스킬이 획득된 상태라면 CurrentLevel을 UI에 출력하고, 획득되지 않았다면 '0' 을 출력한다.

이 선택은 스킬이 잠겨 있으면 레벨이 의미가 없다는 UI 규칙을 명확히 보여준다.

데이터적으로 CurrentLevel이 어떤 값을 가지든, UI는 획득 여부가 false일 때 무조건 0으로 표현하므로 사용자 관점에서 혼동이 줄어든다.

이 함수는 Update에서 매 프레임 호출되지 않는다.

이유는 스킬 획득 여부나 레벨이 매 프레임 바뀌는 값이 아니라, 이벤트(획득, 버튼 클릭)로만 변경되는 값이기 때문이다.

따라서 변화가 발생한 시점에만 호출되는 구조가 성능적으로 낭비가 없고, 상태 변경 지점이 명확해 디버깅에도 유리하다.

5. 개발 의도

이 게시글의 스킬 UI는 데이터 기반 동적 생성을 중심으로 설계했다.

스킬 개수와 구성은 PlayerManager의 데이터 배열이 결정하며, UI는 그 데이터를 읽어 프리팹을 생성하고 아이콘과 이름을 바인딩한다.

버튼 이벤트는 각 항목이 정확한 인덱스를 참조하도록 캡처 변수를 사용해 클로저 문제를 예방했고, 입력은 Update 기반 폴링이 아니라 Button.onClick 이벤트 기반으로 처리하여 불필요한 프레임 연산을 줄였다.

또한 생성(CreateSkillList)과 상태 동기화(UpdateSkillList)를 분리해, UI가 재생성 없이도 상태만 갱신할 수 있는 구조를 만들었다.

이 분리는 이후 게시글에서 다룰 레벨 변경 규칙, 스킬 포인트 트랜잭션, 오류 처리를 UI 생성 구조와 섞지 않게 해주며, 기능 확장 시에도 각 책임이 명확하게 유지되도록 돕는다.