검색 및 초성 필터링 & 선택 상태 리셋 설계
목차
1. 시스템 요구 사항
제작 슬롯을 데이터 기반으로 동적 생성하고, 실제 운영에서 바로 부딪히는 문제는 아이템이 많아졌을 때 원하는 제작 항목을 빠르게 찾을 수 있어야 한다는 점이다.
특히 한국어 아이템 이름은 초성 입력(예: “ㅂㅈ”)이 익숙한데, 일반적인 부분 문자열 검색만으로는 초성 기반 탐색을 제공하기 어렵다.
또 하나의 핵심 요구사항은 검색 기능이 어디까지나 표시용 필터링으로만 동작해야 한다는 점이다.
검색어가 바뀌면 슬롯이 재생성되는데, 이때 이전 선택 상태가 남아 있으면 화면에 존재하지 않는 항목을 대상으로 제작 패널이 유지되거나 제작 시도가 이어질 수 있다.
따라서 검색은 제작 로직 자체를 바꾸지 않고, 슬롯 표시를 재구성하는 선에서 끝나야 하며, 검색 결과가 바뀌는 순간 선택 상태와 제작 패널은 반드시 명시적으로 초기화되어야 한다.
2. 설계 목표
- InputField 입력 변화에 따라 슬롯 목록을 즉시 재구성할 것
- 일반 검색과 초성 검색을 동시에 지원할 것
- 검색 기능은 슬롯 표시 필터 역할만 하도록 제한할 것
- 검색 결과 변경 시 선택 상태(selectedIndex)와 제작 패널을 명시적으로 리셋할 것
- 검색 로직이 제작/재화 검증 로직에 영향을 주지 않도록 분리할 것
3. 흐름도

이 흐름에서 검색은 제작 가능 여부를 판단하거나 재화를 건드리는 단계로 절대 들어가지 않는다.
검색이 하는 일은 오직 현재 craftableItems/materialItems 목록을 어떤 기준으로 다시 그릴지를 결정하는 것뿐이며, 선택과 제작은 슬롯 클릭 이후 단계에서만 발생한다.
4. 구현
4.1. 검색 이벤트 연결과 초기 진입점
private void Start()
{
itemManager = GameManager_LDW.instance.itemManager;
if (serch_InputField != null)
serch_InputField.onValueChanged.AddListener(OnSearchValueChanged);
CreateItemSlots();
}
Start 함수에서는 제작 UI가 활성화되는 시점부터 검색 기능이 자연스럽게 동작하도록 초기 설정을 수행한다.
먼저 ItemManager 참조를 가져와 이후 제작 및 재화 로직에서 사용할 기반 데이터를 준비한다.
이후 검색 입력을 담당하는 TMP_InputField의 onValueChanged 이벤트에 OnSearchValueChanged 함수를 리스너로 등록한다.
onValueChanged는 UnityEvent 기반 UI 이벤트로, 입력이 변경될 때마다 등록된 리스너가 호출된다.
이를 통해 별도의 검색 버튼 없이 입력 즉시 슬롯을 갱신하는 구조를 만들 수 있다.
이 방식은 사용자가 키를 입력하는 순간마다 결과가 바뀌는 실시간 검색 UX를 만들기에 적합하다.
단점은 입력이 잦을 경우 함수 호출 빈도가 높아질 수 있다는 점이지만, 제작 슬롯 수가 제한적이고 연산 비용이 크지 않기 때문에 이 프로젝트에서는 충분히 감당 가능한 수준이다.
이벤트 등록 전에 serch_InputField가 null인지 확인하는 조건문을 둔 이유는, 제작 시스템이 항상 검색 UI와 함께 사용된다는 전제를 깨기 위함이다.
테스트 씬이나 일부 UI가 빠진 상태에서도 제작 시스템이 동작할 수 있도록 방어 코드를 넣어 런타임 예외 가능성을 줄였다.
이는 Unity 환경에서 UI 배치가 씬마다 달라질 수 있다는 점을 고려한 설계다.
여기서 AddListener는 UnityEvent 기반 이벤트 구독 방식으로, UI 입력과 시스템 로직을 느슨하게 연결해준다.
인스펙터에서 연결하는 방식도 가능하지만, 코드에서 연결하면 검색 입력이 존재하면 제작 시스템은 자동으로 검색 기능을 지원하게 된다.
또한 serch_InputField가 null일 수 있는 상황(테스트 씬, UI 미배치 상태)을 고려해 방어차원에서 코드를 추가함으로써 런타임 에러 가능성을 줄였다.
마지막으로 CreateItemSlots 함수를 한 번 호출해, 검색어가 없는 초기 상태에서 전체 제작 슬롯이 화면에 렌더링되도록 한다.
이 호출이 없으면 검색 입력이 발생하기 전까지 제작 슬롯이 비어 있는 상태로 남게 되므로, 제작 UI 진입 시 기본 목록을 보여주기 위한 초기 렌더링 단계로 볼 수 있다.
4.2. 검색어 변경 처리와 선택 상태 리셋 정책
private void OnSearchValueChanged(string searchText)
{
searchText = searchText.Trim();
if (string.IsNullOrEmpty(searchText))
CreateItemSlots();
else
CreateItemSlots(searchText);
selectedIndex = -1;
craftPanel.SetActive(false);
}
검색 입력이 변경될 때마다 호출되는 OnSearchValueChanged 함수는 검색 기능의 중심 진입점이다.
이 함수는 검색어를 기준으로 슬롯을 재생성한다.
중요한 점은 검색 처리 이후, 반드시 선택 상태와 제작 패널을 명시적으로 초기화한다는 점이다.
이 함수는 단순히 검색 결과를 필터링하는 것뿐만 아니라, 검색이 제작 시스템의 상태에 영향을 주지 않도록 선택 상태를 명확히 초기화하는 책임까지 함께 가진다.
함수의 첫 줄에서 Trim()을 호출해 검색 문자열 양쪽의 공백을 제거한다.
이 처리는 사용자가 실수로 공백을 입력하거나 복사 · 붙여넣기 과정에서 공백이 포함되는 경우를 대비한 것이다.
Trim()을 통해 검색 문자열이 정규화되어 의도치 않은 검색 결과 0개가 되는 것을 줄일 수 있다.
이후 검색 문자열이 비어 있는지 여부에 따라 CreateItemSlots 함수또는 CreateItemSlots(searchText)를 호출한다.
검색어가 없으면 전체 슬롯을 다시 생성하고, 검색어가 있으면 필터 조건을 적용한 슬롯만 생성한다.
중요한 점은 검색 여부와 상관없이 슬롯 생성의 진입점은 항상 CreateItemSlots라는 점이다.
검색은 슬롯 생성 이전 단계에서 데이터만 걸러내는 역할을 하며, 슬롯 생성 자체의 책임은 변하지 않는다.
이 함수에서 가장 핵심적인 설계는 검색 처리 이후에 반드시 선택 상태와 제작 패널을 초기화하는 정책이다.
검색어가 변경되면 기존 슬롯 오브젝트들은 Destroy()로 제거되고, 새로운 슬롯들이 생성된다.
만약 이 시점에서 이전 선택 상태가 유지된다면, 화면에는 존재하지 않는 슬롯을 기준으로 제작 패널이 열려 있거나, 제작 버튼이 이전 아이템의 인덱스를 참조하는 위험한 상태가 될 수 있다.
이를 방지하기 위해 selectedIndex를 -1로 되돌리고 제작 패널을 비활성화한다.
이 정책을 통해 검색은 오직 표시되는 슬롯 목록만 변경하는 기능으로 제한되고, 제작 로직은 항상 명시적인 슬롯 선택 이후에만 동작하도록 보장된다.
결과적으로 검색 기능은 제작 시스템의 상태를 오염시키지 않는 보조 기능으로 작동하게 된다.
4.3. 한글 초성 검색을 위한 기반 데이터와 유니코드 처리
private static readonly char[] InitialConsonants =
{
'ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ',
'ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'
};
초성 검색을 구현하기 위해 한글 초성 테이블을 static readonly 배열로 선언했다.
한글 완성형 문자는 유니코드 상에서 초성, 중성, 종성의 조합으로 구성되며, 완성형 범위인 0xAC00부터 0xD7A3 사이에서 초성 인덱스를 계산할 수 있다.
이 범위 내에서 특정 문자의 초성 인덱스는 (문자 코드 - 0xAC00) / (21 * 28) 연산으로 계산할 수 있다.
초성 테이블을 static readonly로 선언한 이유는 이 데이터가 런타임 동안 변경될 이유가 없고, 모든 제작 시스템 인스턴스가 동일한 데이터를 공유해도 문제가 없기 때문이다.
이 선언 방식은 메모리 낭비를 막고, 코드 차원에서 상수에 가까운 데이터라는 의미를 명확히 드러낸다.
이 접근 방식은 외부 라이브러리에 의존하지 않고도 한글 초성 검색을 구현할 수 있다는 장점이 있으며, 유니코드 규칙에 기반하기 때문에 동작이 예측 가능하다.
단점은 구현 난이도가 단순 문자열 검색보다 높다는 점이지만, 한국어 사용자에게 익숙한 검색 UX를 제공하기 위해 감수할 만한 복잡도라고 판단했다.
4.4. 아이템 이름을 초성 문자열로 변환하는 함수
private string GetInitialsFromName(string source)
{
if (string.IsNullOrEmpty(source))
return string.Empty;
StringBuilder sb = new StringBuilder();
foreach (char ch in source)
{
if (ch >= 0xAC00 && ch <= 0xD7A3)
{
int unicode = ch - 0xAC00;
int initialIndex = unicode / (21 * 28);
sb.Append(InitialConsonants[initialIndex]);
}
}
return sb.ToString();
}
GetInitialsFromName 함수는 아이템 이름 문자열을 초성 문자열로 변환하는 역할을 한다.
예를 들어 “생명의 반지”라는 이름은 이 함수를 거치면 “ㅅㅁㅇㅂㅈ” 형태로 변환된다.
이 과정은 검색 대상 데이터를 초성 기준으로 정규화하기 위한 사전 처리 단계다.
함수 내부에서는 먼저 입력 문자열이 null이거나 비어 있는지를 검사해 불필요한 연산을 피한다.
이후 foreach를 통해 문자열을 한 글자씩 순회하며, 해당 문자가 한글 완성형 범위에 속하는 경우에만 초성을 계산해 추가한다. (초성 19개, 중성 21개, 종성 28개 조합이므로 21*28로 나누면 초성 인덱스가 나온다.)
공백이나 특수문자, 영어 등은 초성 검색의 대상이 아니므로 자연스럽게 제외된다.
이 함수에서 StringBuilder를 사용한 이유는 C#의 string이 불변 객체이기 때문이다.
문자열을 반복적으로 이어 붙이는 방식은 매번 새로운 문자열을 생성하게 되고, 검색 입력처럼 자주 호출되는 경로에서는 불필요한 GC 부담을 만든다.
StringBuilder는 내부 버퍼에 문자열을 누적한 뒤 한 번에 결과를 생성하기 때문에, 반복 처리에 적합하다.
이 함수는 검색 시점마다 호출되지만, 아이템 수가 제한적인 제작 시스템에서는 충분히 효율적인 선택이며, 코드 의도 또한 명확하다.
4.5. 검색어를 초성 패턴으로 변환하는 함수
private string GetInitialPatternFromFilter(string filter)
{
if (string.IsNullOrEmpty(filter))
return string.Empty;
StringBuilder sb = new StringBuilder();
foreach (char ch in filter)
{
if (ch >= 0xAC00 && ch <= 0xD7A3)
{
int unicode = ch - 0xAC00;
int initialIndex = unicode / (21 * 28);
sb.Append(InitialConsonants[initialIndex]);
}
else if (ch >= 0x3131 && ch <= 0x314E)
{
sb.Append(ch);
}
}
return sb.ToString();
}
GetInitialPatternFromFilter 함수는 플레이어가 입력한 검색어를 초성 비교가 가능한 형태로 변환한다.
검색어는 “생반”처럼 완성형일 수도 있고, “ㅅㅂ”처럼 자모만 입력될 수도 있기 때문에, 이 두 입력을 동일한 비교 기준(초성 패턴)으로 정규화한다.
이 함수는 완성형 한글의 경우 초성을 추출하고, 자모 범위(0x3131~0x314E)에 속하는 문자는 그대로 사용한다.
이 설계 덕분에 플레이어는 "ㅅ” 한 글자만 입력해도 초성 패턴 매칭(검색)이 가능하고, 완성형과 자모 입력이 동일한 필터링 로직으로 처리된다.
이 방식은 한국어 플레이어의 입력 습관을 그대로 반영한 설계로, 검색 UX를 인위적으로 제한하지 않는다.
구현 비용은 다소 증가하지만, 검색 결과가 사용자의 기대와 일치한다는 점에서 충분한 가치가 있다.
4.6. 검색 조건을 포함한 슬롯 재생성 로직
private void CreateItemSlots(string filter)
{
...
string lowerFilter = string.IsNullOrEmpty(filter) ? string.Empty : filter.ToLower();
string initialFilter = GetInitialPatternFromFilter(filter);
for (int i = 0 ; i < craftableItems.Count; i++)
{
Item item = craftableItems[i];
Item material = materialItems[i];
if (!string.IsNullOrEmpty(lowerFilter))
{
string nameLower = item.itemName.ToLower();
string nameInitials = GetInitialsFromName(item.itemName);
bool nameMatch = nameLower.Contains(lowerFilter);
bool initialMatch = !string.IsNullOrEmpty(initialFilter) && nameInitials.Contains(initialFilter);
if (!nameMatch && !initialMatch)
continue;
}
}
...
}
CreateItemSlots(string filter) 함수는 검색 조건을 포함해 실제 슬롯 생성이 이루어지는 핵심 단계다.
이 함수에서는 먼저 검색 문자열을 소문자로 정규화한 lowerFilter와 초성 검색을 위한 initialFilter를 준비한다.
문자열 검색을 위해 ToLower()와 Contains()를 사용하였다.
검색 기능은 “반지”, “체력” 같은 부분 문자열 검색이 가능해야 했기 때문에, 대소문자 이슈를 제거하기 위해 ToLower()로 정규화한 뒤 Contains로 포함 관계를 검사했다.
ToLower()를 쓰면 필터와 이름이 같은 케이스로 맞춰져 비교가 단순해지고, Contains는 완전 일치가 아니라 부분 일치 검색을 지원한다.
이 방식은 구현 비용이 낮고 직관적이며, UI 검색에 필요한 반응성을 충분히 제공한다.
한국어에는 ToLower()의 효과가 크지 않지만, 아이템 이름에 영어가 섞이는 경우까지 고려해 비교 기준을 정규화하였다.
앞서 정의한 초성 문자열 변환 로직과 StringBuilder 기반 구현을 그대로 활용해, 일반 문자열 검색과 초성 검색을 하나의 필터링 로직으로 통합했다.
이 과정에서 초성 문자열 변환은 입력 변화가 잦은 환경을 고려해, StringBuilder를 사용함으로써 불필요한 문자열 할당 비용을 최소화했다.
문자열은 불변(immutable)이기 때문에 반복 concatenation을 하면 매번 새 문자열이 만들어지는데, 검색 로직은 입력이 바뀔 때마다 실행될 수 있으므로 누적 생성 비용을 줄이는 선택이 합리적이다.
이 설계를 통해 사용자는 “생명의 반지” 같은 이름을 “ㅅㅁㅇㅂㅈ” 형태로 찾을 수 있고, “ㅅ”처럼 한 글자만 입력해도 초성 기반으로 폭넓게 필터링이 가능해진다.
현재는 제작 슬롯 검색 구조 설명에 집중하기 위해, 제작 아이템과 재료를 동일 인덱스로 매핑한 단순한 구조를 사용했다.
CreateItemSlots는 검색 조건에 따라 데이터를 걸러낸 뒤, 그 결과를 슬롯으로 렌더링하는 공통 출력 단계 역할만 수행한다.
검색 방식이 추가되더라도 슬롯 생성의 진입점은 변하지 않도록 의도적으로 설계했다.
* 결과


5. 개발 의도
이 게시글에서의 의도는 제작 시스템이 확장될 때도 안정적으로 유지되는 입력/필터 파이프라인을 만드는 것이다.
검색은 제작 로직을 건드리지 않고 슬롯 표시만 재구성해야 하며, 그 과정에서 선택 상태와 제작 패널이 남아 시스템을 오염시키면 안 된다.
그래서 검색 입력이 바뀌는 시점에 슬롯을 재생성하고, 동시에 selectedIndex를 -1로 초기화하며 제작 패널을 닫는 정책을 강제했다.
또한 초성 검색은 단순 편의 기능이 아니라, 아이템 개수가 늘어났을 때 제작 UI가 계속 사용 가능한 상태로 유지되기 위한 핵심 UX 장치다.
유니코드 기반 초성 추출과 StringBuilder를 사용한 이유는 한국어 검색 요구를 만족하면서도, 입력 변화가 잦은 환경에서 불필요한 비용을 줄이기 위한 구현 선택이었다.
