데이터 기반 제작 슬롯 동적 생성 구조

목차

1. 시스템 요구 사항

제작 시스템의 첫 번째 핵심은 제작 가능한 아이템이 늘어나거나 바뀌더라도, UI 슬롯이 코드 수정 없이 즉시 반영되는 구조를 만드는 것이었다.

제작 UI를 고정된 슬롯 집합으로 두면, 아이템이 하나 추가될 때마다 프리팹을 복제하거나 레이아웃을 다시 맞추는 작업이 반복된다.

이런 방식은 콘텐츠가 늘어날수록 비용이 기하급수적으로 증가하고, 데이터와 UI가 분리되지 않기 때문에 버그가 발생했을 때 원인을 추적하기도 어려워진다.

따라서 제작 슬롯은 인스펙터에 등록된 데이터 리스트를 기준으로 자동 생성되어야 했고, 슬롯을 생성하는 코드가 아이템 목록의 변화에 직접 종속되지 않도록 설계되어야 했다.

또한 제작 시스템은 이후 UI 필터링이나 검색 기능을 위해, 슬롯 생성 구조 자체가 외부 조건에 의해 유연하게 재사용될 수 있도록 설계되어야 했다.

2. 설계  목표

- 제작 슬롯 UI를 데이터 기반으로 동적 생성할 것

- 검색 필터 유무와 상관없이 단일 생성 경로를 유지할 것

- 슬롯 생성과 슬롯 내부 정보 바인딩 책임을 분리할 것

- 아이템 추가/삭제가 리스트 수정만으로 반영되도록 할 것

3. 흐름도

제작 슬롯은 항상 데이터 리스트를 기준으로 생성되며, 검색 기능은 슬롯 생성 이전 단계에서 데이터 리스트를 필터링하는 보조 역할로만 작동한다.

4. 구현

4.1. 제작 슬롯 생성 진입 구조

제작 슬롯 생성은 검색 여부와 관계없이 항상 동일한 경로를 거치도록 설계하였다.

이를 위해 필터가 없는 기본 생성 함수와, 필터 문자열을 받는 실제 구현 함수를 분리하였다.

public void CreateItemSlots()
{
    CreateItemSlots(string.Empty);
}

CreateItemSlots()는 제작 UI가 처음 열릴 때나 검색어가 비어 있을 때 호출되는 기본 진입점이다.

실제 슬롯 생성 로직은 문자열 필터를 인자로 받는 함수로 위임되며, 이 구조를 통해 검색 여부와 관계없이 단일 슬롯 생성 경로를 유지할 수 있었다.

이 방식은 오버로드를 활용하여 설계하였다.

오버로드를 사용하면 전체 생성과 필터에 따른 생성이 서로 다른 함수처럼 보이지만 실제 구현은 하나로 수렴한다.

이 방식의 장점은 호출부가 단순해진다는 점이다.

예를 들어 Start()에서 최초 목록을 띄울 때는 CreateItemSlots()만 호출하면 되고, 검색 이벤트에서는 CreateItemSlots(searchText)로만 호출하면 된다.

결과적으로 슬롯 생성 로직이 어디서 분기되는지가 아니라 필터 문자열만 다르다는 형태로 구조가 정리되며, 유지보수 시 수정 지점이 하나로 고정된다.

4.2. 데이터 리스트 기반 슬롯 동적 생성
private void CreateItemSlots(string filter)
{
    foreach (Transform child in contentPanel)
        Destroy(child.gameObject);

    ...

    for (int i = 0; i < craftableItems.Count; i++)
    {
        Item item = craftableItems[i];
        Item material = materialItems[i];

        ...

        GameObject slot = Instantiate(itemSlotPrefab, contentPanel);
        SetSlotInfo(slot, item, material, i);
    }
}

제작 슬롯을 동적으로 재생성할 때, 기존 슬롯이 남아 있으면 UI가 중복되거나 클릭 이벤트가 엉키는 문제가 발생한다.

그래서 contentPanel의 자식들을 순회하며 기존 슬롯을 제거하고, 이후 새로 생성하는 방식으로 일관되게 처리했다.

기존 슬롯 삭제는 Transform 계층 순회와 Destroy()를 이용했다.

기존 슬롯은 contentPanel의 자식 오브젝트를 순회하며 제거하였다.

이 방식은 화면에 표시된 슬롯 상태와 코드의 상태가 어긋나지 않도록 보장하며, 별도의 슬롯 리스트를 관리하지 않아도 되는 장점이 있다.

이 방식의 장점은 현재 화면에 존재하는 슬롯 상태와 코드가 알고 있는 슬롯 상태가 어긋나지 않는다는 점이다.

슬롯 리스트를 별도로 들고 관리하면 생성/삭제 누락으로 참조가 남는 문제가 생길 수 있는데, 계층 구조 기반으로 삭제하면 그런 불일치 가능성이 낮아진다.

다만 이 방식은 검색어 입력마다 전체 재생성되기 때문에, 슬롯 수가 매우 많아지는 경우 Object Pool로 전환하는 확장 여지는 남겨둘 수 있다.

이 프로젝트에서는 제작 슬롯 수가 제한적이고, 검색 빈도 또한 높지 않기 때문에 Object Pool보다 코드 단순성과 가독성을 우선하였다.

Instantiate는 Unity의 런타임 오브젝트 생성 API다.

프리팹을 함께 사용하면 UI 구조를 코드에서 하드코딩하지 않고, 에디터에서 정의한 형태를 그대로 재사용할 수 있다.

이 방식의 장점은 UI 레이아웃이 바뀌어도 코드 수정이 거의 필요 없다는 점이다.

예를 들어 슬롯에 재료 아이콘을 하나 더 넣거나 텍스트 배치를 바꾸더라도, 코드가 어떤 프리팹을 생성하는가만 유지하면 된다.

제작 시스템의 목표가 데이터 기반 유지보수 최소화였기 때문에, 프리팹-Instantiate 방식은 설계 의도와 일치한다.

현재는 craftableItems와 materialItems를 동일 인덱스로 매핑하는 구조를 사용했다.

이는 제작 시스템의 핵심을 UI 동적 생성과 검색 구조에 두고, 레시피 구조는 단순화하기 위한 의도적인 선택이다.

추후 제작 재료가 복수로 확장되거나 제작 조건이 복잡해질 경우, CraftRecipe 구조체나 ScriptableObject 기반 레시피로 분리하는 방식으로 자연스럽게 확장 가능하다.

4.3. 슬롯 정보 바인딩 분리

슬롯을 생성하는 로직과 슬롯 내부에 데이터를 채우는 로직을 분리하기 위해 SetSlotInfo 함수를 두었다.

private void SetSlotInfo(GameObject slot, Item item, Item material, int index)
{
    slot.transform.Find("ItemNameTxt").GetComponent<TextMeshProUGUI>().text = item.itemName;
    slot.transform.Find("ItemIcon").GetComponent<Image>().sprite = item.itemImage;
    slot.transform.Find("MaterialIcon").GetComponent<Image>().sprite = material.itemImage;

    TextMeshProUGUI matAmountTxt = slot.transform.Find("MaterialAmountTxt").GetComponent<TextMeshProUGUI>();
    TextMeshProUGUI coinAmountTxt = slot.transform.Find("CoinAmountTxt").GetComponent<TextMeshProUGUI>();

    if (item.itemCode == "200_01_01")
    {
        matAmountTxt.text = "x 3";
        coinAmountTxt.text = "x 100";
    }
    else
    {
        matAmountTxt.text = "x 1";
        coinAmountTxt.text = "x 50";
    }

    slot.GetComponent<Button>().onClick
        .AddListener(() => OnItemClicked(item, index));
}

이 분리는 단순한 정리가 아니라 유지보수 전략이다.

슬롯 생성 로직은 무엇을 보여줄지(필터/목록)에 집중하고, SetSlotInfo는 어떻게 보여줄지(UI 요소 바인딩)에 집중한다.

이런 책임 분리가 되어 있으면 이후 제작 비용 표시 방식이 바뀌거나, 특정 아이템에만 별도 태그를 표시하는 요구사항이 생겨도 SlotInfo 쪽만 수정하면 된다.

제작 비용은 현재 아이템 코드 기준으로 분기 처리하였다.

이는 제작 시스템 UI 구조 설명에 집중하기 위해 비용 로직을 단순화한 것이다.

이후에는 아이템 데이터나 제작 레시피 테이블로 분리 가능하다.

Button.onClick과 람다 캡처로 슬롯 클릭 연결하였다.

슬롯 클릭 시 제작 패널로 연결하기 위해 Button.onClick에 리스너를 등록했다.

이 방식의 장점은 슬롯 프리팹마다 별도의 스크립트를 붙여서 인덱스를 저장하지 않아도, 생성 시점에 이 슬롯이 어떤 아이템과 연결되는지를 즉시 결정할 수 있다는 점이다.

특히 index를 함께 넘기기 때문에 이후 로직(재료/코인/제작 검증)이 동일한 인덱스를 기준으로 동기화되며, 슬롯과 데이터 간 매핑이 명확해진다.

람다 캡처에서 반복 변수 i를 직접 참조하면 의도치 않은 동작이 발생할 수 있다.

이 코드는 SetSlotInfo로 index 값을 전달받아 캡처하기 때문에, 슬롯과 데이터 간 매핑이 안정적으로 유지된다.

5. 개발 의도

이 제작 시스템의 핵심은 UI를 예쁘게 만드는 것이 아니라, 데이터가 바뀌면 UI가 자동으로 따라오게 만드는 구조를 확보하는 것이었다.

제작 가능한 아이템은 콘텐츠 확장과 밸런싱 과정에서 계속 변하는데, 그 변화가 코드 수정으로 이어지면 프로젝트가 커질수록 유지보수 비용이 감당되지 않는다.

그래서 제작 슬롯은 인스펙터에 등록된 리스트를 단일 데이터 소스로 삼고, 슬롯 UI는 그 결과를 동적으로 렌더링하는 구조로 설계했다.

또한 슬롯 생성 구조는 이후 검색이나 필터링 기능과 자연스럽게 결합될 수 있도록 설계하여, 아이템 개수가 늘어나더라도 제작 UI의 확장성이 유지되도록 했다.