FlashList로 복잡한 레이아웃을 쉽게 그리기 위해 "직접" 제작한 flash-section-list에 대한 내용입니다.
목차
1. 필요성
2. demo
3. workflow
3-1. 최소 공배수
3-2. 섹션 데이터 직렬화
3-3. overrideItemLayout
3-4. getItemType
4. 한계
1. 필요성
네이버 웹툰과 같은 레이아웃을 요청받았다고 가정해 보겠습니다.
섹션간 경계 영역이 있지만, 섹션끼리 아이템 형태가 같은 경우입니다.
FlatList의 중첩 기능을 사용하면 간단하게 구현할 수 있겠지만, 리액트 네이티브가 느린 이유 포스트에서 살펴봤듯이, FlatList는 성능 문제가 있었습니다.
FlashList를 사용하여 섹션간 아이템을 재사용하고 싶은데, 단일 FlashList만 사용하여 네이버 웹툰같은 레이아웃을 그리기엔 코드가 좀 장황해 집니다.
그래서 복잡한 부분을 감추고, Section 인터페이스를 사용하여 간단하게 그릴 수 있는 FlashList 기반의 섹션 리스트를 직접 만들어 봤습니다.
data와 renderItem, numOfColumns, sticky 등의 속성을 제거하고, Section 배열을 입력으로 받습니다.
각 Section마다 data와 renderItem, numOfColumns, sticky 속성을 설정하여, 섹션별로 서로 다른 레이아웃을 구성할 수 있습니다.
2. demo
아래 영상은 위의 예제 코드를 찍은 영상이고, flash-section-list로 만들었습니다.
중간에 탭이 sticky되면서 두 탭이 서로 다른 flash-section-list를 랜더링합니다.
첫 번째 섹션은 3열로 구성되어 있고 두 번째 섹션은 2열로 구성되어 있습니다.
그리고 나머지 섹션은 캐러셀로 구성되어 있습니다.
중간에 이미지가 없는 것은 그냥 서버 오류입니다.
3. workflow
3-1. 최소 공배수
FlashList는 내부적으로 RecycleirListView의 GridLayoutProvider를 사용합니다.
- https://github.com/Shopify/flash-list/blob/v1.7.1/src/GridLayoutProviderWithProps.ts#L12
- https://github.com/Shopify/flash-list/blob/v1.7.1/src/FlashList.tsx#L228-L232
numOfColumns 속성값에 의해 그리드의 열 갯수가 결정됩니다.
여기서 이런 생각이 들었습니다.
각 섹션의 numOfColumns의 최소 공배수로 전체 리스트를 쪼개고, 각 섹션마다 서로 다른 영역을 차지하게 만들면 되지 않을까?
예를 들면, 2열 섹션과 3열 섹션으로 구성되어 있을 때, 리스트를 6열로 만들고 2열 섹션의 아이템은 3칸을 차지하고, 3열 섹션의 아이템은 2칸을 차지하면 됩니다.
그리고 FlashList는 overrideItemLayout 속성에서 각 아이템이 몇 칸을 차지할지 결정할 수 있는 기회를 제공합니다.
3-2. 섹션 데이터 직렬화
flash-section-list도 내부적으로 FlashList를 사용하고 있기 때문에, data 속성과 renderItem 속성을 입력해 주어야 합니다.
당연히 data는 각 섹션의 data를 flattening해서 만듭니다.
문제는 특정 데이터가 어떤 섹션인지, 해당 섹션은 몇 열로 구성되었는지 알 수 있어야 합니다.
이를 위해 전체 데이터에서 각 섹션이 몇 번째 인덱스에서 시작하는지 정보를 가지고 있는 sectionStartIndices를 만듭니다.
만드는 방법은 매우 간단합니다.
Section 배열을 순회하면서 지금까지 누적된 flattening된 데이터의 길이를 입력해 주면 됩니다.
let index = 0;
const sectionStartIndices: number[] = [];
const serializedData = sections.reduce<Array<any>>(
(acc, cur: DataSection<any> | ElementSection) => {
// ...
dataSections.push(section);
const {
data,
// ...
} = section;
let length = data.length;
sectionStartIndices.push(index);
// ...
acc.push(...data);
// ...
index += length;
return acc;
},
[]
);
예를 들어 [{data: [1,2,3]}, {data: [4,5,6}]이라면, serializedData는 [1,2,3,4,5,6]이 되고, sectionStartIndices는 [0, 3]이 됩니다.
만약 serializedData의 2번 인덱스가 포함된 섹션은 sectionStartIndices에서 2보다 큰 값의 이전 인덱스가 됩니다.
sectionStartIndices은 오름차순으로 정렬되어 있기 때문에, 바이너리 서치로 시간 복잡도를 줄였습니다.
3-3. overrideItemLayout
FlashList는 overrideItemLayout 속성에서 각 아이템이 몇 칸을 차지할지 결정할 수 있는 기회를 제공합니다.
overrideItemLayout에 전달된 객체를 수정해 주는 방법으로 아이템이 차지하는 영역을 결정합니다.
전체 섹션의 최소 공배수를 섹션의 numOfColumns로 나눈 크기를 사용합니다.
overrideItemLayout={(layout, _, index) => {
const sectionIndex = binarySearchClosestIndex(
sectionStartIndices,
index
);
const section = dataSections[sectionIndex];
layout.span = section.numOfColumns
? numOfColumns / section.numOfColumns
: numOfColumns;
}}
https://github.com/JoonDong2/flash-section-list/blob/main/src/FlashSectionList.tsx#L474-L476
3-4. getItemType
섹션끼리 동일한 아이템을 사용할 경우, 여전히 재사용할 수 있도록 해야 했습니다.
그리고 FlashList는 getItemType이 반환한 값으로 아이템의 재사용 여부를 결정합니다.
기본적으로 아이템 타입은 섹션의 인덱스를 사용하지만, 섹션끼리 동일한 타입을 지정하여 아이템을 재사용하도록 할 수 있습니다.
const sections: Section[] = [
{
data: Array.from({ length: 500 }).map((_, index) => getImage(index)),
renderItem: ({ item: uri }) => {
return <Image source={{ uri }} style={{ aspectRatio: 1 }} />;
},
numOfColumns: 2,
type: 'image', // 요기 !!
},
{
data: Array.from({ length: 300 }).map((_, index) => getImage(index)),
renderItem: ({ item: uri }) => {
return <Image source={{ uri }} style={{ aspectRatio: 1 }} />;
},
numOfColumns: 3,
type: 'image', // 요기 !!
},
]
4. 한계
리액트 네이티브가 느린 이유 포스트에서 언급했듯이 RecyclerListView 기반의 리스트는 근본적으로 재활용이 매무 많이 발생할 수 있는 디자인에서 사용되어야 좋은 성능이 나옵니다.
네이버 웹툰과 쿠팡 홈화면이 그렇습니다.
'리액트 네이티브' 카테고리의 다른 글
wix/react-native-navigation 코드 분석 (0) | 2024.10.29 |
---|---|
리액트 네이티브 Fabric 랜더러는 항상 빠를까? (0) | 2024.10.29 |
리액트 네이티브에서 IntersectionObserver를 대체하는 방법 (0) | 2024.10.16 |
리액트 네이티브 WebView 중첩 (0) | 2024.10.03 |
리액트 네이티브 스크롤뷰 중첩 (0) | 2024.10.03 |