목록은 보이는 만큼만 만든다
한 배치에 텍스처 여덟 장까지다에서 오버드로를 줄이는 방법 중 하나로 “목록이 길면 ListView 로 가상화” 를 적어 두고 넘어갔습니다. 이번이 그 얘기입니다.
스크롤 목록은 창문 뒤에서 긴 종이를 움직이는 것이다에서 uGUI 의 스크롤을 봤습니다. 긴 종이를 만들어 놓고 창문 뒤에서 움직이는 구조였고, 종이가 길어지면 그만큼 오브젝트가 늘어났습니다. 항목 천 개짜리 목록이면 오브젝트가 천 개였고, 그래서 재활용하는 스크롤을 직접 만들거나 라이브러리를 가져다 썼습니다.
UI Toolkit 에는 그게 컨트롤로 들어와 있습니다.
한 화면을 채울 만큼만 만듭니다
문서가 ListView 가 하는 일을 이렇게 적어 뒀습니다. “instantiate the number of visual elements required to fill a page, recycle visual elements to optimize memory handling.”
한 쪽을 채울 만큼만 만들고, 만든 것을 돌려 씁니다. 스크롤해서 위로 빠진 줄의 요소를 버리지 않고 아래에 다시 끼웁니다. 새로 만드는 게 아니라 값만 갈아 넣습니다.
그래서 ScrollView 와의 갈림길이 분명합니다. 항목이 몇 개 안 되고 높이도 제각각이면 ScrollView 가 편합니다. 항목이 많거나 늘어날 수 있으면 ListView 입니다. 문서도 “a ListView is more efficient than a ScrollView” 라고 적어 두고 그 조건을 나열합니다.
만드는 일과 채우는 일이 갈려 있습니다
돌려 쓰려면 만드는 것과 값을 넣는 것이 따로여야 합니다. 이게 ListView API 의 생김새를 설명합니다.
var listView = container.Q<ListView>();
listView.makeItem = () => new Label();
listView.bindItem = (e, i) => ((Label)e).text = items[i];
listView.itemsSource = items;
listView.fixedItemHeight = 32;
| 콜백 | 언제 |
|---|---|
makeItem |
“Called as needed when the ListView needs more items to render” |
bindItem |
“Invoked to associate the element with the matching data item” |
unbindItem |
그 요소가 목록에서 떨어질 때. 걸어 둔 것을 떼는 자리 |
destroyItem |
요소를 정말 버릴 때 |
makeItem 은 처음 몇 번만 불립니다. 화면을 채울 만큼 만들고 나면 더 안 부릅니다. bindItem 은 스크롤하는 내내 불립니다.
여기서 실수하기 쉬운 자리가 하나 있습니다. makeItem 안에서 이벤트 콜백을 걸면 안 됩니다. 그 요소가 나중에 다른 항목에 쓰이면 예전 항목을 가리키는 콜백이 남아 있습니다. 거는 것은 bindItem, 떼는 것은 unbindItem 입니다.
uGUI 에서 재활용 스크롤을 직접 만들어 본 적이 있다면 이 구조가 낯익을 것입니다. 만들어 둔 칸에 인덱스를 다시 매기는 그 코드가 여기서는 bindItem 하나로 들어와 있습니다.
높이를 아느냐가 갈림길입니다
돌려 쓰려면 어느 줄이 지금 보이는지 를 알아야 하고, 그러려면 줄 높이를 알아야 합니다. virtualizationMethod 가 그 선택입니다.
| 값 | 어떻게 |
|---|---|
FixedHeight (기본) |
fixedItemHeight 로 정한 값을 모든 줄에 씁니다 |
DynamicHeight |
“the collection will wait for the actual height to be computed” |
FixedHeight 는 계산이 곱셈 한 번입니다. 스크롤 위치를 높이로 나누면 몇 번째 줄인지 바로 나옵니다.
DynamicHeight 는 실제 높이가 나올 때까지 기다려야 합니다. 줄마다 글자 수가 달라서 높이가 제각각인 채팅 목록 같은 것이 여기에 해당합니다. 편한 대신 계산이 붙습니다.
높이를 고정할 수 있으면 고정하는 편이 낫습니다. 글자가 길어질 수 있는 줄이라면 말줄임을 걸어 한 줄로 고정하는 것도 방법입니다. 짧은 글자일수록 번역에서 더 길어진다에서 본 문제가 여기서는 성능 문제로 나타납니다.
다시 그리는 방법이 셋입니다
데이터가 바뀌면 화면을 갱신해야 하는데, 방법에 따라 비용이 다릅니다.
| 방법 | 하는 일 |
|---|---|
RefreshItem(index) |
그 줄만 다시 bindItem |
RefreshItems() |
보이는 줄들을 다시 bindItem |
Rebuild() |
요소를 전부 버리고 다시 만듭니다 |
문서가 Rebuild 에 대해 “the collection view is completely rebuilt, which can be expensive” 라고 적어 뒀습니다. makeItem 부터 다시 돕니다.
값 하나가 바뀌었는데 Rebuild() 를 부르는 코드가 흔합니다. 그 줄만 바뀐 것이면 RefreshItem(index) 이면 됩니다. Rebuild 는 목록의 구조 자체가 바뀌었을 때, 예를 들어 makeItem 을 갈아 끼웠을 때 씁니다.
움직임은 클립이 아니라 두 값 사이에서 만들어진다에서 본 데이터 바인딩을 걸어 두면 이 갱신을 직접 안 불러도 됩니다. 목록 전용 바인딩 문서가 따로 있으니 목록이 데이터를 따라가야 하면 그쪽을 봅니다.
안 되는 것도 알아 둡니다
가로 목록이 안 됩니다. 문서가 “ListView doesn’t support horizontal layout” 이라고 적어 뒀고, 필요하면 ScrollView 에 flex-direction: row 를 주라고 합니다. 가로로 흐르는 아이템 띠 같은 것은 가상화를 못 쓴다는 뜻입니다.
선택이 바뀐 것을 받을 때도 주의할 자리가 있습니다. selectionChanged 대신 selectedIndicesChanged 를 쓰라고 문서가 권합니다. 값 타입이 들어 있으면 박싱이 생기기 때문입니다.
줄 안에 Toggle 을 넣으면 그 토글이 클릭을 먹어서 줄 선택이 안 됩니다. 문서가 드는 해법은 5편에서 본 그것입니다. 토글에 PointerDownEvent 를 TrickleDown 으로 걸어서 직접 SetSelection 을 부릅니다. 내려가는 길에서 먼저 잡는 것입니다.
스타일을 걸 손잡이는 .unity-list-view, .unity-list-view__item, .unity-collection-view__item--selected 같은 이름들입니다. 2편에서 본 BEM 그대로입니다.
형제가 둘 더 있습니다
같은 뿌리에서 나온 컨트롤이 둘 더 있습니다.
MultiColumnListView. 열이 여럿인 표입니다. 정렬, 드래그 앤 드롭, 열 크기 조절이 들어 있습니다TreeView. 접었다 펴는 트리입니다. 9편에서 본 것처럼 유니티 6 에서 “a new high-performance backend” 로 바뀌었습니다
셋 다 가상화와 makeItem / bindItem 구조를 공유합니다. 하나를 익히면 나머지는 속성 이름만 다릅니다.
에디터 도구를 만들 일이 있으면 이쪽이 특히 값이 큽니다. 7편에서 본 것처럼 에디터 창과 게임 화면이 같은 요소를 쓰니, 표 하나를 만들어 두면 양쪽에서 씁니다.
정리
- ListView 는 한 화면을 채울 만큼만 요소를 만들고 돌려 씁니다. 항목이 천 개여도 요소는 열몇 개입니다
- 돌려 쓰기 때문에 만드는 일(
makeItem)과 채우는 일(bindItem)이 갈려 있습니다 - 이벤트 콜백은
bindItem에서 걸고unbindItem에서 뗍니다.makeItem에서 걸면 남습니다 FixedHeight가 기본이고 계산이 쌉니다. 높이가 제각각이면DynamicHeight인데 기다림이 붙습니다- 갱신은
RefreshItem→RefreshItems→Rebuild순으로 비쌉니다.Rebuild는makeItem부터 다시 돕니다 - 가로 목록은 지원하지 않습니다. ScrollView 에
flex-direction: row를 씁니다 - 선택 변경은
selectedIndicesChanged쪽이 박싱이 없습니다 MultiColumnListView와TreeView가 같은 구조를 씁니다