Cheondi
개발 · 알고리즘·CS

리스트가 길어지자 오브젝트부터 줄여야 했다

긴 내역 목록에서 모든 아이템을 생성하는 대신 보이는 항목을 재사용하는 스크롤 구조를 배운 기록입니다.

  • #unity
  • #object-pooling
  • #scroll-view

내역이 몇 개 없을 때는 항목 수만큼 프리팹을 생성해도 문제가 없었다. 테스트 데이터가 늘자 스크롤을 여는 순간 끊기고, 화면 크기를 바꿀 때마다 레이아웃 계산이 오래 걸렸다. 처음에는 이미지 크기나 애니메이션이 무거운 줄 알았다.

Profiler를 보며 기존 코드를 따라가니 많은 GameObject 생성과 레이아웃 갱신이 한 프레임에 몰리고 있었다. 화면 밖에 있는 수백 개 항목도 모두 살아 있었다. 사용자가 실제로 보는 건 열 개 남짓인데 데이터 개수만큼 오브젝트를 만들고 있었던 셈이다.

데이터 개수와 오브젝트 개수를 분리했다

재사용 스크롤은 처음 보면 조금 이상했다. 데이터는 백 개지만 아이템 오브젝트는 화면에 보이는 수보다 조금 많이 준비한다. 스크롤 위치가 바뀌면 같은 오브젝트에 다른 데이터를 넣고 위치만 옮긴다.

전체 데이터: 0 ... 999
화면 오브젝트: 0 ... 11

스크롤 전  -> 오브젝트 0에 데이터 0 표시
스크롤 후  -> 오브젝트 0에 데이터 12 표시

처음에는 항목이 실제로 이동하는지 새로 생성되는지 구분하기 어려웠다. 아이템 인스턴스 ID와 연결된 데이터 인덱스를 함께 로그로 찍어 보니 같은 오브젝트가 반복해서 쓰이는 게 보였다.

보이는 범위를 계산해야 했다

스크롤 위치에서 첫 번째로 보일 데이터 인덱스를 구하고, 준비된 아이템 수만큼 내용을 갱신했다.

int first = Mathf.FloorToInt(scrollY / itemHeight);

for (int slot = 0; slot < pooledItems.Count; slot++)
{
    int dataIndex = first + slot;
    pooledItems[slot].Bind(rows[dataIndex]);
}

실제 구현에는 범위 검사, 가변 높이, 위아래 여유 아이템과 위치 계산이 더 필요했다. 그래도 핵심은 단순했다. 데이터 배열과 화면 오브젝트 배열이 더 이상 1:1이 아니라는 점이었다.

재사용은 초기화 문제를 만들었다

새 오브젝트를 만들 때는 기본값에서 시작한다. 재사용 아이템은 이전 데이터의 색상, 펼침 상태, 아이콘을 갖고 있었다. Bind()에서 바뀐 값만 적용하면 이전 행의 흔적이 남았다.

그래서 모든 표시 상태를 데이터에서 다시 결정하거나, 바인딩 전에 명시적으로 초기화해야 했다.

void Bind(RowData row)
{
    detailPanel.SetActive(false);
    amountText.color = normalColor;
    icon.enabled = row.hasIcon;
    amountText.text = row.amountText;
}

성능을 위해 재사용을 시작했지만 정확하게 초기화하지 않으면 전혀 다른 종류의 버그가 생겼다.

최적화도 데이터 흐름 문제였다

이 작업 전에는 최적화를 무거운 연산을 줄이는 일로만 생각했다. 재사용 스크롤은 데이터와 표현 객체의 관계를 바꾸는 설계에 가까웠다. 오브젝트 수는 줄었지만 인덱스 계산과 상태 초기화 책임이 생겼다.

이후 목록 성능 문제가 보이면 바로 풀링을 넣지는 않는다. 실제 항목 수와 생성 비용을 측정하고, 고정 높이인지, 아이템 상태를 완전히 다시 바인딩할 수 있는지 먼저 본다. 도구보다 조건을 이해해야 한다는 걸 처음 배운 최적화 작업이었다.