Ruya Games

<Unity> Scroll Rect를 사용해서 무한 스크롤 직접 구현하기 본문

Unity

<Unity> Scroll Rect를 사용해서 무한 스크롤 직접 구현하기

SadEvil 2024. 6. 12. 11:41

무한 스크롤이 정확한 용어인지는 모르겠습니다만, 스크롤창의 오브젝트들을 로드 시점에 한번에 생성하지 않고 스크롤이 바닥을 찍어갈때쯤마다 밑의 새로운 오브젝트들을 생성하는 스크롤창을 만들어보려고 합니다.

제가 알기로는 아마 비슷한 기능을 하는 에셋이 몇개 있는 것으로 알지만, 구조가 간단할것 같고 다운받아서 수정해가면서 쓰기도 귀찮아서 그냥 직접 만들어보기로 했습니다.

 


기본적인 무한 스크롤 생성 방법입니다.

스크롤이 아래 부분의 특정 지점(scrollBound)에 도달하면 LoadItem() 함수를 통해 오브젝트들을 새로 생성합니다. 이때 생성되는 아이템들의 개수 단위는 itemCount 변수에 저장된 값만큼 생성하게 됩니다.

using UnityEngine;
using UnityEngine.UI;

public class DynamicScrollRect : MonoBehaviour
{
    public ScrollRect scrollRect; //Scroll Rect 컴포넌트 할당
    public RectTransform contentBox; //Scroll Rect 자식인 content 오브젝트 할당
    public GameObject item; // 스크롤 내부에 표시될 오브젝트(prefab)
    public int itemCount = 10;
    public bool isLoading;
    
    public float scrollBound;

    public void Start()
    {
    	scrollRect.onValueChanged.AddListener(OnScroll);
    	LoadItems();
    }

    private void OnScroll(Vector2 position)
    {
      if (isLoading) return;

      if (position.y <= scrollBound)
      {
        isLoading = true;
        LoadItems();
      }
    }

    private void LoadItems()
    {
    	for(int i = 0; i < itemCount; i++)
      {
        var newItem = Instantiate(item, contentBox);
      }
      isLoading = false;
    }
}

위 스크립트를 작성 후에 에디터상에서 Scroll View를 추가해준 뒤에 오브젝트들을 할당해줍니다. 여기서 생성되는 Scroll Rect와 Content 게임 오브젝트에 컴포넌트값을 아래와같이 설정, 추가해줍니다(Vertical layout 기준).

Scroll Rect. Horizontal Scrollbar 게임오브젝트는 삭제했습니다.
Vertical LayoutGroup과 Content Size Fitter를 추가해줍니다.

플레이해보면 아래와 같이 나옵니다.


위의 기본적인 무한스크롤에서 추가로, 1.저장되어있는 데이터를 순차적으로 item에 표시해주거나 2.내부의 item 개수가 늘어남에 따라 증가하는 batches 수치를 줄여줘야 하는 경우(최적화 필요 문제)가 생길 수 있습니다.

 

아래는 해당 내용의 구현입니다. 먼저 각각의 item에 데이터를 할당해주는 과정입니다.


using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class DynamicScrollRect : MonoBehaviour
{
    public ScrollRect scrollRect; //Scroll Rect 컴포넌트 할당
    public RectTransform contentBox; //Scroll Rect 자식인 content 오브젝트 할당
    public GameObject item; // 스크롤 내부에 표시될 오브젝트(prefab)
    public int itemCount = 10;
    public bool isLoading;
    
    public float scrollBound;

    //추가된 변수들
    private int loadMultiply = 0;
    private List<int> dataList;
    
    public void Start()
    {
      dataList = new List<int>();

      //테스트를 위해 임의의 값을 리스트에 추가하는 과정입니다.
      for (int i = 0; i < 50; i++)
      {
        dataList.Add(i);
      }
      scrollRect.onValueChanged.AddListener(OnScroll);
      LoadItems();
    }

    private void OnScroll(Vector2 position)
    {
      if (isLoading) return;

      if (position.y <= scrollBound)
      {
        isLoading = true;
        LoadItems();
      }
    }

    private void LoadItems()
    {
      if (loadMultiply > dataList.Count / itemCount)
      {
        isLoading = false;
        return;
      }
      
      for(int i = 0; i < Mathf.Min(itemCount, dataList.Count - loadMultiply * itemCount); i++)
      {
          var newItem = Instantiate(item, contentBox);
          var appendedIndex = i + loadMultiply * itemCount;
          newItem.GetComponentInChildren<TextMeshProUGUI>().text = dataList[appendedIndex].ToString();
      }

      loadMultiply++;
      isLoading = false;
    }
}

loadMultiply는 인덱싱을 위해 추가된 변수이고, dataList는 사용할 데이터가 들어있는 리스트입니다.

변경되는 함수는 LoadItems()이며, Instantiate로 오브젝트를 생성할때 데이터를 넣어주는 과정을 포함시킵니다.

주의할점은 dataList에 더이상 생성할 데이터가 없는 경우에 LoadItems가 호출되는 경우와, 남은 데이터가 itemCount수 미만일때인데 이러한 경우의 예외처리를 위한 조건문이 추가되었습니다.

 

테스트를 위해 item에 TextMesh 오브젝트를 자식으로 추가했고, 테스트 결과는 아래와 같습니다.


두번째인 최적화 방법입니다.

기본적으로는 현재 화면에 표시되지 않고 있는 부분들을 그리지 않도록 하면 됩니다. 그런데 Scroll Rect의 Content 오브젝트에 Content Size Fitter가 컴포넌트로 추가되있는 경우에는 내부 Item을 비활성화해버리면 내부 스크롤 상태가 꼬이게 됩니다.

그래서 저는 Canvas Group컴포넌트를 추가해서 사용했습니다. Canvas Group은 자식 오브젝트들의 alpha값을 한번에 조정할 수 있도록 하는 컴포넌트입니다.

 

아래 실행 화면을 보면 unique한 이미지를 포함하는 아이템들이 추가될때마다 Batches값은 늘어납니다. 그리고 생성된 이후에는 스크롤을 어떻게 조작해도 이미 생성되있기 때문에 줄어들지 않습니다.

이 문제를 해결하기 위해서 각 item들의 visibility를 확인하는 함수를 추가합니다.

아래는 전체 해당 함수가 포함된 전체 코드입니다.

using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class DynamicScrollRect : MonoBehaviour
{
    public ScrollRect scrollRect; //Scroll Rect 컴포넌트 할당
    public RectTransform contentBox; //Scroll Rect 자식인 content 오브젝트 할당
    public GameObject item; // 스크롤 내부에 표시될 오브젝트(prefab)
    public int itemCount = 10;
    public bool isLoading;
    
    public float scrollBound;

    private int loadMultiply = 0;
    private List<int> dataList;

    private int displayBound;
    
    //테스트용 이미지 저장
    public List<Sprite> testImageList;
    
    public void Start()
    {
      dataList = new List<int>();

      //테스트를 위해 임의의 값을 리스트에 추가하는 과정입니다.
      for (int i = 0; i < 30; i++)
      {
        dataList.Add(i);
      }

      displayBound = (int)(scrollRect.GetComponent<RectTransform>().rect.height / item.GetComponent<RectTransform>().rect.height);
    	scrollRect.onValueChanged.AddListener(OnScroll);
    	LoadItems();
    }

    private void OnScroll(Vector2 position)
    {
      if (isLoading) return;

      if (position.y <= scrollBound)
      {
        isLoading = true;
        LoadItems();
      }
      UpdateItemVisibility();
    }

    private void LoadItems()
    {
      if (loadMultiply > dataList.Count / itemCount)
      {
        isLoading = false;
        return;
      }
      
    	for(int i = 0; i < Mathf.Min(itemCount, dataList.Count - loadMultiply * itemCount); i++)
      {
          var newItem = Instantiate(item, contentBox);
          var appendedIndex = i + loadMultiply * itemCount;
          newItem.GetComponentsInChildren<Image>()[1].sprite = testImageList[appendedIndex];
          newItem.GetComponentInChildren<TextMeshProUGUI>().text = dataList[appendedIndex].ToString();
      }

      loadMultiply++;
      isLoading = false;
      UpdateItemVisibility();
    }
    
    private void UpdateItemVisibility()
    {
      var currentScrollPosition = contentBox.anchoredPosition.y;
      
      ///currentShowingObjectIndex를 계산할때, contentBox의 LayoutGroup에 Spacing값이 0이 아니라면
      ///item.GetComponent<RectTransform>().rect.height + spacing으로 계산해야 합니다.
      var currentShowingObjectIndex = (int)(currentScrollPosition / item.GetComponent<RectTransform>().rect.height);

      var childs = contentBox.GetComponentsInChildren<CanvasGroup>();
      foreach (var child in childs)
      {
        var index = child.transform.GetSiblingIndex();
        if (index < currentShowingObjectIndex - displayBound || currentShowingObjectIndex + displayBound < index) child.alpha = 0;
        else child.alpha = 1;
      }
      
    }
}

UpdateItemVisibility라는 함수를 추가해서 스크롤 이벤트가 발생하거나 아이템이 추가될때마다 확인하도록 합니다.

이 코드를 실행하기 위해 아이템 오브젝트에 Canvas Group 컴포넌트를 추가해줘야 합니다.

아래는 실행 결과입니다.

Batches값이 특정 값 이상으로 증가하지 않는것을 확인할 수 있습니다.


무한스크롤을 구현하면서 이것저것 기능을 추가해봤습니다.

다만 이 방법들은 정답은 아니고 더 좋은 방법들이 있을것이라고 생각합니다.

도움이 되셨으면 좋겠습니다 :)