데이터 기반 제작 슬롯 동적 생성 구조
목차
1. 시스템 요구 사항
제작 시스템의 첫 번째 핵심은 제작 가능한 아이템이 늘어나거나 변경되더라도, UI 슬롯이 코드 수정 없이 즉시 반영되는 구조를 만드는 것이었다.
제작 UI를 고정된 슬롯 집합으로 구성할 경우, 아이템이 추가될 때마다 프리팹을 복제하거나 레이아웃을 다시 맞추는 작업이 반복된다.
이런 방식은 콘텐츠가 늘어날수록 비용이 기하급수적으로 증가하고, 데이터와 UI가 분리되지 않기 때문에 버그가 발생했을 때 원인을 추적하기도 어려워진다.
따라서 제작 슬롯은 인스펙터에 등록된 데이터 리스트를 단일 기준으로 삼아 자동 생성되어야 했고, 슬롯을 생성하는 로직은 아이템 목록의 변화에 직접 종속되지 않도록 설계되어야 했다.
또한 제작 시스템은 이후 UI 필터링이나 검색 기능을 위해, 슬롯 생성 구조 자체가 외부 조건에 의해 유연하게 재사용될 수 있도록 설계되어야 했다.
2. 설계 목표
- 제작 슬롯 UI를 데이터 기반으로 동적 생성할 것
- 검색 여부와 관계없이 단일 슬롯 생성 경로를 유지할 것
- 슬롯 생성과 슬롯 내부 데이터 바인딩 책임을 분리할 것
- 아이템 추가 / 삭제가 리스트 수정만으로 반영되도록 할 것
3. 흐름도

제작 슬롯은 항상 데이터 리스트를 기준으로 생성되며, 검색 기능은 슬롯 생성 이전 단계에서 데이터 리스트를 필터링하는 보조 역할로만 작동한다.
4. 구현
제작 슬롯 생성은 검색 여부와 관계없이 항상 동일한 경로를 거치도록 설계하였다.
이를 위해 필터가 없는 기본 생성 함수와, 필터 문자열을 받는 실제 구현 함수를 분리하였다.
제작 슬롯 생성 함수는 오버로드를 활용해 설계하였다.
제작 슬롯을 생성한다는 개념 자체는 하나이지만, 검색어가 있는 경우와 없는 경우에 따라 입력 값만 달라지는 구조였기 때문이다.
CreateItemSlots 함수는 검색어가 없는 기본 진입점이며, 실제 슬롯 생성 로직은 문자열 필터를 인자로 받는 CreateItemSlots(string filter)에만 존재한다.
이를 통해 전체 슬롯 생성과 검색 기반 슬롯 생성이 서로 다른 기능처럼 분리되는 것이 아니라, 슬롯 생성이라는 하나의 책임 안에서 입력만 달라지는 형태로 구조를 정리할 수 있었다.
이 방식의 장점은 호출부의 의도가 명확해진다는 점이다.
UI 초기화나 패널 오픈 시에는 CreateItemSlots 함수만 호출하면 되고, 검색 입력이 발생한 경우에는 CreateItemSlots(searchText)를 호출하면 된다.
호출 코드를 보는 것만으로도 전체 생성인지, 검색 기반 생성인지가 즉시 드러나며, 구현 내부에서 조건 분기가 늘어나는 것을 방지할 수 있다.
만약 하나의 함수 내부에서 검색 여부를 if 문으로 분기했다면, 슬롯 생성 로직과 검색 조건 처리 로직이 점점 뒤섞이게 되고, 기능 확장 시 함수의 책임이 비대해질 가능성이 컸다.
오버로드를 사용함으로써 생성 로직은 하나로 유지하고, 검색은 생성의 입력 조건 중 하나로만 다루는 구조를 명확히 할 수 있었다.
이 선택은 단순한 문법 활용이 아니라, 슬롯 생성은 하나의 기능이며, 검색은 그 기능을 제한하는 조건일 뿐이라는 설계 의도를 코드 구조로 표현한 것이다.
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);
}
}
슬롯을 새로 생성하기 전에 contentPanel의 모든 자식 오브젝트를 순회하며 제거한다.
이는 현재 화면에 표시된 슬롯 상태와 코드가 인지하고 있는 슬롯 상태를 항상 일치시키기 위한 처리다.
제작 슬롯을 동적으로 재생성할 때, 기존 슬롯이 남아 있으면 UI가 중복되거나 클릭 이벤트가 엉키는 문제가 발생한다.
슬롯 리스트를 별도로 관리하지 않고 UI 계층 구조 자체를 기준으로 삭제함으로써, 생성·삭제 누락으로 인한 참조 불일치 문제를 예방할 수 있다.
기존 슬롯 제거는 contentPanel의 Transform 계층을 순회하며 Destroy를 호출하는 방식으로 처리하였다.
Unity에서 Destroy는 오브젝트를 즉시 메모리에서 제거하는 함수가 아니라, 현재 프레임이 끝난 뒤 실제 삭제가 이루어지는 지연 파괴 방식으로 동작한다.
이 제작 시스템에서는 슬롯을 제거한 직후 동일한 프레임에서 새 슬롯을 생성하더라도, 새로 생성되는 슬롯들은 contentPanel 하위에 정상적으로 배치되며, 이전 슬롯들과의 참조 충돌이나 중복 문제는 발생하지 않는다.
슬롯 리스트를 별도로 관리하지 않고 UI 계층 구조 자체를 기준으로 순회·삭제하는 이유는, 화면에 실제로 존재하는 슬롯 상태를 단일 기준(source of truth)으로 삼기 위함이다.
이 방식은 슬롯 생성·삭제 과정에서 참조 누락이나 관리 불일치가 발생할 가능성을 낮추며, UI 상태와 코드 상태가 어긋나는 상황을 구조적으로 방지한다.
검색 입력 시마다 슬롯을 전부 제거하고 재생성하는 구조이기 때문에 슬롯 수가 매우 많아질 경우 성능 이슈가 발생할 수 있다.
다만 이 프로젝트에서는 제작 가능한 아이템 수가 제한적이고, 검색 입력 빈도 또한 높지 않기 때문에 Object Pool 기반 구조보다 코드 가독성과 설계 단순성을 우선하여 이 방식을 선택하였다.
이후 craftableItems 리스트를 기준으로 슬롯을 하나씩 생성한다.
슬롯은 프리팹을 Instantiate하여 생성되며, 이 방식은 UI 레이아웃을 코드에서 하드코딩하지 않고 에디터에서 정의한 구조를 그대로 재사용할 수 있다는 장점이 있다.
슬롯 내부에 어떤 요소가 있는지는 코드가 아니라 프리팹이 결정하므로, UI 구조 변경 시 코드 수정이 최소화된다.
현재는 craftableItems와 materialItems를 동일 인덱스로 매핑하는 구조를 사용했다.
이는 제작 시스템의 핵심을 UI 동적 생성과 검색 구조에 두고, 레시피 구조는 단순화하기 위한 의도적인 선택이다.
추후 제작 조건이 복잡해질 경우, CraftRecipe 구조체나 ScriptableObject 기반 레시피로 분리하는 방식으로 자연스럽게 확장 가능하다.
Instantiate는 Unity의 런타임 오브젝트 생성 API다.
프리팹을 함께 사용하면 UI 구조를 코드에서 하드코딩하지 않고, 에디터에서 정의한 형태를 그대로 재사용할 수 있다.
이 방식의 장점은 UI 레이아웃이 바뀌어도 코드 수정이 거의 필요 없다는 점이다.
예를 들어 슬롯에 재료 아이콘을 하나 더 넣거나 텍스트 배치를 바꾸더라도, 코드가 어떤 프리팹을 생성하는가만 유지하면 된다.
제작 시스템의 목표가 데이터 기반 유지보수 최소화였기 때문에, 프리팹-Instantiate 방식은 설계 의도와 일치한다.
4.3. 슬롯 정보 바인딩 분리
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 함수를 두었다.
CreateItemSlots는 어떤 슬롯을 만들 것인가에만 집중하고, SetSlotInfo는 이 슬롯에 어떤 데이터를 표시할 것인가만 담당한다.
이 분리는 단순한 정리가 아니라 유지보수 전략이다.
슬롯 생성 로직은 무엇을 보여줄지(필터/목록)에 집중하고, SetSlotInfo는 어떻게 보여줄지(UI 요소 바인딩)에 집중한다.
이런 책임 분리가 되어 있으면 이후 제작 비용 표시 방식이 바뀌거나, 특정 아이템에만 별도 태그를 표시하는 요구사항이 생겨도 SlotInfo 쪽만 수정하면 된다.
슬롯 클릭 이벤트는 Button.onClick에 람다를 통해 직접 연결하였다.
이 방식은 슬롯 프리팹마다 별도의 스크립트를 붙이지 않고도, 슬롯 생성 시점에 이 슬롯이 어떤 아이템과 연결되는지를 명확히 결정할 수 있다는 장점이 있다.
특히 index를 함께 캡처함으로써, 이후 제작 검증 ·재료 차감 · UI 갱신까지 모든 로직이 동일한 인덱스를 기준으로 동기화된다.
반복문 내부 변수 i를 직접 캡처하지 않고, SetSlotInfo의 매개변수로 전달된 index를 캡처하기 때문에 람다 캡처로 인한 인덱스 오류 가능성도 방지된다.
제작 비용은 현재 아이템 코드 기준으로 분기 처리하였다.
이는 제작 시스템 UI 구조 설명에 집중하기 위해 비용 로직을 단순화한 것이다.
이후에는 아이템 데이터나 제작 레시피 테이블로 분리 가능하다.
5. 개발 의도
이 제작 시스템의 핵심은 UI를 예쁘게 만드는 것이 아니라, 데이터가 바뀌면 UI가 자동으로 따라오게 만드는 구조를 확보하는 것이었다.
제작 가능한 아이템은 콘텐츠 확장과 밸런싱 과정에서 계속 변하는데, 그 변화가 코드 수정으로 이어지면 프로젝트가 커질수록 유지보수 비용이 감당되지 않는다.
그래서 제작 슬롯은 인스펙터에 등록된 리스트를 단일 데이터 소스로 삼고, 슬롯 UI는 그 결과를 동적으로 렌더링하는 구조로 설계했다.
또한 슬롯 생성 구조는 이후 검색이나 필터링 기능과 자연스럽게 결합될 수 있도록 설계하여, 아이템 개수가 늘어나더라도 제작 UI의 확장성이 유지되도록 했다.
