[리액트 네이티브] 무한렌더링으로 아이템 정렬하면서 스크롤 시 탭 상단에 고정시키기 (ScrollView의 stickyHeaderIndices, useInfiniteQuery)

이번에 새로 만들어야 하는 페이지의 조건은 당근어플처럼
- 상단탭이 있어 이동이 가능하고
- 아이템이 무한스크롤로 정렬되고
- 스크롤 시 탭이 상단에 고정되고
- 중간에 랜덤 아이템이 나오는
페이지였다.
조건이 많아 어떻게 구조를 짜야할지에 대해 고민을 많이했다.
1. FlatList vs ScrollView
1) 무한 스크롤을 쓰다보니까 처음에는 FlatList로 작업했다
- renderItem에서 분기처리해서 각 탭에 맞는 아이템을 렌더링
- 고정될 탭 아래에 스크롤은 되지만 공통적으로 들어가는 컴포넌트가 있어 리스트 가공
- 고정될 탭은 stickyHeaderIndices로 설정
- 랜덤 아이템은 리스트를 또 새롭게 가공
이렇게 되다보니 가공에 가공에 가공을.. 데이터가 티기고 티겨지는.. 너무 복잡해졌다
또한 리스트는 state로 관리되고 있었는데 탭을 옮길때 마다 데이터 전체가 갈아끼워졌고, 이 부분을 차라리 컴포넌트로 관리하는게 낫겠다 싶었다.
그래서 불필요한 데이터 가공이 없어지고, state 구조도 단순해지는 scrollView로 바꾸게 되었다.
2) scrollView
<ScrollView
stickyHeaderIndices={[1]}
onScroll={onScroll}
scrollEventThrottle={100}
>
<FirstCompo />
<StickyCompo />
<ThirdCompo />
{renderList()}
</ScrollView>
- renderList로 각 탭에 맞는 리스트 컴포넌트 분기처리
- ScrollView 내에서 공통적으로 렌더링되는 컴포넌트를 나열하고, 고정되는 컴포넌트만 stickyHeaderIndices로 설정하므로 불필요한 데이터 가공 필요 없음
- 무한렌더링은 onScroll로 작업
이렇게 ScrollView로 작업했을 때 코드가 훨씬 더 깔끔해졌다.
(작업을 다 하고 블로그 글을 쓰면서 찾아보니 각 탭별로 FlatList를 사용했어도 괜찮을 것 같았다는 생각이 든다)
2. throttle
ScrollView에서 onScroll을 사용하여 무한렌더링을 하게될 때의 문제점이 있었다. contentOffset.y를 계산해 화면 하단에 닿으면 다음페이지의 데이터를 패치하도록 했더니 스크롤이 바닥에 닿을때 같은 호출이 여러번 일어났다. 이를 해결하기위해 onScroll에 throttle을 적용했다.
function throttle(callback: Function, delay: number) {
let lastTime = 0;
return function (...args: any[]) {
const now = Date.now();
if (now - lastTime >= delay) { // 첫 요청은 실행, 요청의 최소 간격을 체크
callback.apply(this, args);
lastTime = now; // 마지막 실행시간 저장
}
};
}
const onScroll = throttle({
seconds: 200,
callback: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
const isBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 100;
if (isBottom ) {
nextPageFetch();
}
},
});
3. useQuery vs useInfiniteQuery
1) 무한스크롤을 할때 처음에는 useQuery를 사용했다.
queryKey에 파람스로 들어가는 키를 state로 넣어주고, 하단에 닿을 때 마다 setState해서 키가 변경되어 리패치되는 방식을 사용했다. 하지만 이 방식의 문제점은 파람스를 queryKey로 넣어주다보니 캐시된 데이터를 변경할 때 데이터 마다 queryKey가 달라져 찾기가 힘들다는 것이었다.
2) 이후에 useInfiniteQuery를 적용했다.
useQuery보다 무한렌더링에 적합하고, 기존에 queryKey에 파람스 state 넣었던 것을 없애서 사용했다.
const {
data,
fetchNextPage, // 다음 페이지 리패칭
hasNextPage, // 다음 페이지 존재 여부
isFetchingNextPage, //다음 페이지 불러오는중인지 판단 여부
} = useInfiniteQuery({
queryKey: ['dataList'],
initialPageParam: { lastIdx: null },
queryFn: ({ pageParam = 0 }) => fetchData(pageParam),
getNextPageParam: (lastPage) => {
return lastPage.nextCursor; // 최근에 가져온 lastPage의 데이터를 참고해 다음 페이지 요청 시 필요한 파라미터 리턴
},
});
.
작업을 하면서 확실히 FlatList가 아닌 scrollView로 많은 아이템을 렌더링하는게 최적화에는 쉽지않다는걸 느꼈다. 이미 작업을 scrollView로 해서 throttle이나 useInfiniteQuery를 적용 예시를 배우기도 했지만 다음에 이런 페이지를 작업할때는 탭별로 FlatList를 적용하도록 해야겠다는 생각이든다.