드래그 & 드롭 시스템 - 입력은 UI에서 처리하고, 정리 규칙은 데이터로 위임하는 구조

목차

1. 시스템 요구 사항

인벤토리 정리는 플레이어가 가장 자주 수행하는 상호작용 중 하나다.

아이템을 다른 슬롯으로 옮기거나, 같은 아이템을 한 곳으로 모으거나, 일부만 나눠 배치하는 행위는 인벤토리 사용의 핵심 흐름을 구성한다.

이 과정이 버튼 클릭이나 별도의 메뉴를 통해서만 이루어진다면, 관리 동작 자체가 플레이 흐름을 끊는 요소로 작용하게 된다.

특히 스택형 인벤토리에서는 단순한 이동과 정리가 동일한 입력으로 처리되기 어렵다.

같은 아이템을 옮길 경우에는 병합이 일어나야 하고, 다른 아이템일 경우에는 자리를 교환해야 하며, 경우에 따라서는 일부만 분할해 옮기는 기능도 필요하다.

이 모든 동작을 개별 버튼이나 UI 메뉴로 분리할 경우, 기능은 명확해질 수 있지만 조작 부담이 급격히 증가한다.

또 하나의 중요한 문제는 정리 규칙의 위치였다.

드래그 & 드롭은 UI 이벤트이기 때문에, 자칫하면 병합, 스왑, 분할 같은 규칙이 UI 코드 안으로 흘러들어가기 쉽다.

이렇게 되면 입력 방식이 늘어날수록 동일한 규칙을 여러 곳에서 중복 구현하게 되고, 인벤토리 정리 로직의 일관성이 깨질 위험이 커진다.

따라서 이 시스템에서는 드래그 & 드롭을 입력 방식에 불과한 트리거로 취급하고, 실제 정리 규칙은 스택 정리 시스템에서 정리한 데이터 레이어의 병합·스왑 정책을 그대로 재사용하는 구조가 필요했다.

따라서 UI 계층에서는 어떤 상태 변화도 직접 만들지 않으며, 오직 정리 요청만을 전달하도록 역할을 제한했다.

2. 설계  목표

- 드래그 & 드롭을 통해 슬롯 이동과 정리를 직관적으로 수행할 수 있을 것

- 같은 아이템일 경우 병합을 우선하고, 불가능할 때만 스왑이 발생할 것

- 드래그 입력 자체는 UI 계층에서 처리하고, 정리 규칙은 데이터 계층에 위임할 것

- UI 입력 방식이 늘어나더라도 인벤토리 정리 규칙은 한 곳에서 유지될 것

- 드래그 중 현재 이동 중인 아이템을 시각적으로 명확히 인지할 수 있을 것

3. 흐름도

[슬롯 드래그 시작]

       ↓

[드래그 아이콘 생성 및 마우스 추적]

       ↓

[다른 슬롯 위에 드롭]

       ↓

[같은 슬롯인지 여부 확인]

       ↓

[데이터 레이어에 정리 요청]

       ↓

[병합 시도 → 실패 시 스왑]

       ↓

[정리 결과에 따른 UI 갱신]

이 흐름의 핵심은, 드래그 & 드롭 자체는 아무런 정리 규칙을 알지 못한 채, 단 두 슬롯을 정리해 달라는 요청만 전달한다는 점이다.

병합이 일어날지, 스왑이 일어날지는 오직 데이터 레이어의 정책에 의해 결정된다.

4. 구현

4.1. 드래그 시작과 시각적 피드백
public void OnBeginDrag(PointerEventData eventData)
{
    if (eventData.button != PointerEventData.InputButton.Left &&
        eventData.button != PointerEventData.InputButton.Right)
        return;

    if (slotData == null || slotData.IsEmpty) return;

    CloseTooltip();

    if (dragIconObj == null)
    {
        dragIconObj = new GameObject(
            "DragIcon",
            typeof(RectTransform),
            typeof(CanvasRenderer),
            typeof(Image)
        );

        dragIconRT = dragIconObj.GetComponent<RectTransform>();
        dragIconImage = dragIconObj.GetComponent<Image>();
        dragIconImage.raycastTarget = false;

        dragIconRT.SetParent(rootCanvas.transform, false);
        dragIconRT.sizeDelta = icon.rectTransform.sizeDelta;
    }

    dragIconObj.SetActive(true);
    dragIconImage.sprite = icon.sprite;
    dragIconRT.position = eventData.position;
}

드래그는 UI 이벤트 시스템인 EventSystem과 IPointer 계열 인터페이스(IBeginDragHandler, IDragHandler, IDropHandler 등)를 기반으로 구현되었다.

슬롯 UI는 EventSystem이 전달하는 드래그 관련 인터페이스를 구현해 입력 이벤트에 반응하도록 구성했다.

EventSystem은 씬 내의 UI 입력 이벤트를 중앙에서 관리하는 시스템으로, 현재 포인터가 가리키는 UI 오브젝트를 Raycast로 판별한 뒤 해당 오브젝트에 적절한 이벤트를 전달하는 역할을 한다.

이를 통해 슬롯은 매 프레임 입력을 직접 감지할 필요 없이, 드래그 시작, 드래그 중, 드롭 같은 명확한 이벤트 단계에서만 반응할 수 있다.

이 방식의 가장 큰 장점은 입력 처리 흐름이 UI 이벤트로 정규화된다는 점이다.

마우스, 터치, 패드 입력 모두 동일한 이벤트 구조로 전달되기 때문에 입력 장치가 늘어나더라도 구조를 변경할 필요가 없다.

반면 Raycast 대상 설정이나 UI 계층 구조에 따라 이벤트가 누락될 수 있다는 단점이 있지만, 인벤토리 정리는 전형적인 UI 상호작용이기 때문에 이러한 제약은 감수할 만하다고 판단했다.

드래그가 시작되면 슬롯이 비어 있는지 먼저 확인해, 비어 있는 슬롯에서의 드래그를 구조적으로 차단한다.

이후 실제 슬롯 오브젝트를 이동시키는 대신, 별도의 드래그 아이콘을 생성해 마우스를 따라다니게 하는 방식을 사용했다.

이 방식은 슬롯 레이아웃을 직접 건드리지 않기 때문에, 드래그 도중 UI 구조가 흔들리거나 드롭 실패 시 상태가 어긋나는 문제를 방지할 수 있다.

드래그 아이콘은 매 드래그마다 새로 생성하지 않고 재사용하도록 설계했다.

인벤토리 UI에서는 드래그가 빈번하게 발생하므로, 불필요한 오브젝트 생성과 GC 부담을 줄이기 위한 선택이었다.

4.2. 드래그 중 위치 갱신과 종료 처리
public void OnDrag(PointerEventData eventData)
{
    if (dragIconObj != null && dragIconObj.activeSelf)
    {
        dragIconRT.position = eventData.position;
    }
}

public void OnEndDrag(PointerEventData eventData)
{
    if (dragIconObj != null)
    {
        dragIconObj.SetActive(false);
    }
}

드래그 중에는 포인터 이동 이벤트가 발생할 때마다 드래그 아이콘의 위치를 갱신한다.

이 단계는 오직 시각적 피드백만을 담당하며, 인벤토리 데이터에는 전혀 접근하지 않는다.

입력 처리와 데이터 변경을 명확히 분리함으로써, 드래그 중 발생할 수 있는 예외 상황이 인벤토리 상태에 영향을 주지 않도록 했다.

드래그 종료 시에는 성공 여부와 무관하게 항상 동일한 종료 처리를 수행한다.

아이콘을 비활성화하는 단순한 처리로 끝내도록 구성해, 드래그 도중 예외가 발생하더라도 UI 상태가 꼬이지 않도록 했다.

4.3. 드롭 처리와 데이터 레이어 위임
public void OnDrop(PointerEventData eventData)
{
    var fromSlot = eventData.pointerDrag?.GetComponent<InventorySlot>();
    if (fromSlot == null) return;
    if (fromSlot == this) return;

    if (fromSlot.slotData == null || fromSlot.slotData.IsEmpty) return;

    SlotManager.instance.SwapOrMerge(
        fromSlot.SlotIndex,
        this.SlotIndex
    );
}

드롭 이벤트의 핵심 역할은 어디서 어디로 옮기려 했는지를 식별하는 것이다.

실제 병합이나 스왑 판단은 이 단계에서 전혀 수행하지 않는다.

드롭된 대상이 자기 자신인 경우를 즉시 차단해 불필요한 연산을 제거하고, 이후 SlotManager.SwapOrMerge를 호출해 정리 요청을 데이터 레이어로 위임한다.

SlotManager는 스택 정리 시스템에서 정의한 정책 계층으로, 병합을 먼저 시도하고 실패할 경우 스왑으로 폴백하는 규칙을 단일하게 유지한다.

UI는 이 정책을 알 필요가 없으며, 어떤 결과가 나왔든 마지막에 한 번만 UI 갱신을 수행한다.

이 구조 덕분에 드래그 & 드롭은 단순한 입력 방식으로 유지되고, 인벤토리 정리 규칙은 데이터 레이어에 안전하게 고정된다.

5. 개발 의도

이 드래그 & 드롭 시스템에서 가장 중요하게 고려한 점은, 입력 방식이 늘어나더라도 인벤토리 정리 규칙이 절대 분산되지 않도록 만드는 것이었다.

드래그 & 드롭은 직관적인 입력 방식이지만, 구현을 잘못하면 병합 · 스왑 · 분할 같은 핵심 규칙이 UI 코드 안으로 쉽게 침투한다.

이 시스템에서는 드래그 & 드롭을 오직 정리 요청을 발생시키는 트리거로만 사용하고, 실제 결과는 항상 데이터 레이어의 정책에 의해 결정되도록 구조를 고정했다.

그 결과 키보드 단축키, 자동 정렬 버튼, 패드 입력 같은 다른 입력 방식이 추가되더라도, 인벤토리 정리 규칙은 수정 없이 그대로 재사용할 수 있다.

결과적으로 이 드래그 & 드롭 시스템은 단순한 편의 기능이 아니라, 인벤토리 정리 규칙을 UI로부터 보호하고 데이터 무결성을 유지하기 위한 중요한 구조적 장치로 설계되었다.