_
스크롤뷰가 뭔데?
말 그대로 스크롤을 진행하여 오브젝트들을 볼 수 있는 유니티내의 컴포넌트 중 하나이다.
유니티를 제외하고도 많은 앱에서 사용하는 기능이다.
맞다. 유니티에서 이미 제공해주는 컴포넌트도 이미 완성형이고 오브젝트만 추가해서 사용하면 된다.
하지만 스크롤뷰에서 많은 오브젝트를 사용하면 말이 달라진다.
유니티에서는 스크롤뷰에서 보이지 않는 오브젝트들도 렌더링을 진행하며 많은 오브젝트들을 사용하여 메모리,CPU에 부담이 가기 시작한다.
이를 보안해서 사용하는게 리사이클뷰이다.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ReusableScrollList : MonoBehaviour
{
[SerializeField] private ScrollRect _scrollRect; // ScrollRect 컴포넌트
[SerializeField] private GameObject _slotPrefab; // 아이템 프리팹
[SerializeField] private Transform _content; // Content 트랜스폼
[SerializeField] private int _itemCount = 3; // 아이템 개수
[SerializeField] private int _bufferItems = 2; // 좌,우측에 끊김없이 보이기 위한 추가 아이템 개수
public LinkedList<GameObject> _itemList = new LinkedList<GameObject>();
private RectTransform _contentRect;
public float _itemWidth;
public int _tmpfirstVisibleIndex;//임시 첫번째 인덱스
public int _poolSize;//오브젝트의 총 개수(가로길이 계산 포함)
public int padding = 0; // 아이템 간 간격
public int left = 0; // 왼쪽 여백
public int right = 0; // 오른쪽 여백
private void Start()
{
_contentRect = _content.GetComponent<RectTransform>();
// 슬롯 너비
_itemWidth = _slotPrefab.GetComponent<RectTransform>().rect.width;
// content sizeDelta.x 크기 (컨텐츠의 총 길이는 아이템의 개수 + 좌/우 여백)
float contentWidth = (_itemWidth * _itemCount) + left + right + padding * (_itemCount - 1);
// 스크롤렉트 가로 길이, 추가 슬롯을 고려한 poolSize 계산
_poolSize = (int)(_scrollRect.GetComponent<RectTransform>().sizeDelta.x / (_itemWidth + padding)) + _bufferItems * 2;
// content 사이즈 조절 (여백 포함)
_contentRect.sizeDelta = new Vector2(contentWidth, _contentRect.sizeDelta.y);
// 슬롯 생성 및 리스트에 추가
for (int i = 0; i < _poolSize; i++)
{
GameObject item = Instantiate(_slotPrefab, _content);
_itemList.AddLast(item);
}
// 슬롯 초기 설정
SlotInit();
// ScrollRect 자식의 위치가 변경되면 실행되도록 이벤트 등록
_scrollRect.onValueChanged.AddListener(OnScroll);
}
/// <summary>슬롯의 위치를 변경하는 함수</summary>
private void OnScroll(Vector2 scrollPosition)
{
float contentX = _contentRect.anchoredPosition.x;
// 현재 contentRect의 x값에 맞춰 최소 인덱스를 구한다.
int firstVisibleIndex = Mathf.Max(0, (-1 * Mathf.FloorToInt((contentX - left) / (_itemWidth + padding))) - _bufferItems);
// 인덱스 차이가 너무 큰 경우 방어 로직 추가
if (Mathf.Abs(_tmpfirstVisibleIndex - firstVisibleIndex) > _poolSize)
{
// 큰 변화가 발생한 경우 슬롯들을 리셋
_tmpfirstVisibleIndex = firstVisibleIndex;
SlotInit();
return;
}
// 만약 이전 위치와 현재 위치가 달라졌다면?
if (_tmpfirstVisibleIndex != firstVisibleIndex)
{
// 위치 인덱스의 차이를 구한다.
int diffIndex = _tmpfirstVisibleIndex - firstVisibleIndex;
// 현재 인덱스가 더 크다면 (스크롤이 오른쪽으로 이동)
while (diffIndex < 0)
{
// 우측 이동
GameObject item = _itemList.First.Value;
_itemList.RemoveFirst();
_itemList.AddLast(item);
// 해당 아이템의 새로운 위치 계산 (left 여백을 더해준다)
int newIndex = firstVisibleIndex + _poolSize - 1;
item.transform.localPosition = new Vector3(left + newIndex * (_itemWidth + padding), 0, 0);
diffIndex++;
}
// 이전 인덱스가 더 크다면 (스크롤이 왼쪽으로 이동)
while (diffIndex > 0)
{
// 좌측 이동
GameObject item = _itemList.Last.Value;
_itemList.RemoveLast();
_itemList.AddFirst(item);
// 해당 아이템의 새로운 위치 계산 (left 여백을 더해준다)
int newIndex = firstVisibleIndex;
item.transform.localPosition = new Vector3(left + newIndex * (_itemWidth + padding), 0, 0);
diffIndex--;
}
// 위치 인덱스를 갱신한다.
_tmpfirstVisibleIndex = firstVisibleIndex;
}
}
/// <summary>슬롯 초기 설정</summary>
private void SlotInit()
{
int i = 0;
foreach (GameObject item in _itemList)
{
// 각 슬롯의 위치 설정 (left 여백 포함)
item.transform.localPosition = new Vector3(left + i * (_itemWidth + padding), 0, 0);
item.GetComponentInChildren<Text>().text = (i + 1).ToString();
i++;
}
}
}
위에 코드를 적용한 결과
스크롤을 진행할시 재사용이 잘 진행되는 것을 확인할 수 있다.
회고
기능을 완성하고나면 아 이렇게 하면 쉬웠는데 왜 이 부분을 놓쳤을까 라는 아쉬움이 있다.
또 혼자만의 힘이 아니라 GPT를 이용해서 구현을 했다는 점이 많이 아쉬운 부분이다..
어떤 부분이 문제였을까?라고 생각을 하면서 퇴근을 했다.
결론은 내 코딩의 과정중에 있었다.
어 여기 넣으면 되지 않을까?라는 생각으로 때려넣는 정확한 이유가 없는 코딩을 진행하다보니 안그래도 약한 나의 멘탈도 금방 갈리고 나도 모르게 자신감이 사라지는 것 같다.
GPT사용을 최소화 하여 진행해야겠다.