블로그 목록

FlashList로 React Native 리스트 성능 살리기 — FlatList가 버벅일 때

앱에 게시글 목록 화면을 만들었는데, 스크롤이 자꾸 끊겼다. 데이터가 몇백 개 쌓이니까 아래로 빠르게 내릴 때 빈 화면이 잠깐씩 보이는 현상(blank cell)이 심해졌다.

원인은 FlatList의 동작 방식이다. FlatList는 화면 밖으로 나간 셀을 언마운트했다가 다시 필요하면 새로 마운트한다. 스크롤이 빠르면 마운트가 렌더 속도를 못 따라가서 빈 칸이 생긴다. windowSize, maxToRenderPerBatch 같은 옵션을 만져봤는데 근본 해결은 아니었다.

FlashList로 교체

Shopify가 만든 @shopify/flash-list는 셀을 언마운트하지 않고 재활용(recycling) 한다. 화면 밖으로 나간 셀 컴포넌트를 버리지 않고, 새 데이터만 갈아끼워서 재사용하는 방식이다. 그래서 마운트 비용이 거의 안 든다.

좋은 점은 API가 FlatList와 거의 같다는 거다. import만 바꾸면 대부분 그대로 동작한다.

npx expo install @shopify/flash-list
import { FlashList } from '@shopify/flash-list';

function PostList({ posts }: { posts: Post[] }) {
  return (
    <FlashList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <PostCard post={item} />}
    />
  );
}

FlatListFlashList로 바꾸고, import 경로만 고치면 끝이다. 실제로 교체하고 나니 그 빈 칸 현상이 거의 사라졌다.

estimatedItemSize를 꼭 넣어야 한다

처음에 콘솔에 경고가 떴다. FlashList는 재활용을 위해 아이템의 대략적인 높이를 미리 알아야 한다. 이걸 estimatedItemSize로 알려준다.

<FlashList
  data={posts}
  renderItem={({ item }) => <PostCard post={item} />}
  estimatedItemSize={120}
/>

정확할 필요는 없고 평균값이면 된다. 카드 하나 높이를 대충 재서 넣으면 된다. 이 값이 있어야 스크롤바 위치나 초기 렌더 범위를 제대로 계산한다.

셀 재활용에서 조심할 점

재활용이 이득인 만큼 함정도 있다. 셀 컴포넌트가 재사용되기 때문에, useState로 셀 안에 로컬 상태를 두면 이전 아이템의 상태가 남아서 엉뚱하게 보일 수 있다.

예를 들어 "펼치기/접기" 상태를 셀 내부 useState에 두면, 스크롤 후 재활용된 셀에 그 상태가 그대로 붙는다. 그래서 이런 상태는 셀 밖(부모)에서 데이터로 관리하는 게 안전하다.

// 셀 내부에 상태를 두지 말고
const [expandedId, setExpandedId] = useState<string | null>(null);

<FlashList
  data={posts}
  extraData={expandedId}
  renderItem={({ item }) => (
    <PostCard
      post={item}
      expanded={item.id === expandedId}
      onToggle={() => setExpandedId(item.id)}
    />
  )}
  estimatedItemSize={120}
/>

상태가 바뀔 때 리렌더를 반영하려면 extraData에 그 값을 넘겨줘야 한다는 것도 잊지 말자.


정리하면, 긴 리스트에서 FlatList가 버벅이면 FlashList로 바꾸는 걸 먼저 시도해볼 만하다. import 교체 + estimatedItemSize 추가면 대부분 끝나고, 셀 로컬 상태만 부모로 끌어올리면 재활용 함정도 피할 수 있다. 실제 체감 차이가 꽤 컸다.