FlashList는 Shopify에서 만든 React Native용 리스트 라이브러리다.
기존의 FlatList가 가진 성능 한계(특히 저사양 안드로이드에서의 버벅임, 스크롤 시 흰 화면)를 극복하기 위해 만들어졌다. 현재 React Native 커뮤니티에서 가장 권장되는 리스트 솔루션이다.
FlatList와 사용법은 99% 비슷하지만, 작동 원리가 완전히 다르다.
기존 FlatList는 "가상화(Virtualization)" 방식을 쓴다.
FlashList는 재활용(Recycling) 방식을 쓴다. (iOS의 UICollectionView나 Android의 RecyclerView와 같은 원리다.)
설치 후 FlatList를 FlashList로 바꾸면 끝이다. 단, 필수 속성이 하나 있다.
pnpm install @shopify/flash-list
import { FlashList } from "@shopify/flash-list";
const MyList = () => {
return (
<FlashList
data={DATA}
renderItem={({ item }) => <Text>{item.title}</Text>}
// 이게 없으면 경고 뜨고 성능 안 나옴
estimatedItemSize={50}
/>
);
};
estimatedItemSize재활용 방식 때문에 개발자가 실수하기 쉬운 포인트들이 있다.
useEffect(() => {}, []) (마운트 시 실행) 코드가 새 아이템이 나타날 때 실행되지 않을 수 있다.무조건 : 데이터가 100개 이상 넘어가는 리스트
무조건 : 이미지나 복잡한 레이아웃이 포함된 리스트
권장 : 사실 그냥 FlatList 대신 기본으로 써도 무방하다. (설정만 잘하면 성능 손해 볼 게 없다.)
FlashList : Shopify가 만든 FlatList의 초고성능 대체재.
원리 : 뷰를 파괴하지 않고 재활용(Recycling)해서 내용을 갈아끼운다.
주의 : 컴포넌트가 재활용되므로 내부 state 관리에 유의해야 한다.
최적화에 방해되는, 최적화의 의미가 없어지는 경우를 조심해야한다.
const renderItem = ({ item }) => {
const filtered = bigArray.filter(x => x.id === item.id); // ❌
const computed = expensiveFunction(item); // ❌
return <Item data={computed} />;
};
renderItem은 스크롤 중 계속 호출된다. FlashList는 재사용하지만 props가 바뀌면 다시 계산된다. 결국 JS thread 점유, 프레임 드랍 등의 문제가 발생한다.
const renderItem = ({ item }) => {
return (
<Item
item={item}
onPress={() => handlePress(item.id)} // ❌
/>
);
}
// 아래와 같이 useCallback으로 참조를 맞춰줌
const onPress = useCallback((id) => {
handlePress(id);
}, []);
const renderItem = useCallback(({ item }) => {
return <Item item={item} onPress={onPress} />;
}, [onPress]);
매 렌더마다 새로운 함수 생성되고, Item props 변경되어 memo가 깨진다. 이는 FlashList recycling 효과 감소하게 되는 문제로 이어진다.
const renderItem = ({ item }) => {
if (item.type === "A") {
return <TypeA item={item} />;
}
return <TypeB item={item} />;
};
// 굳이 타입을 나눌 거면 아이탬에서 나누는게 좋다.
const Item = memo(({ item }) => {
return item.type === "A"
? <TypeA item={item} />
: <TypeB item={item} />;
});
FlashList 내부는 “같은 cell 구조” 라고 가정하고 recycling을 하려고 한다. 그러나 구조가 다르면
재사용 불가능하고 새로 mount가 발생해버린다.
// onPress에 useCallback을 씌웠더라도 넘겨주면서 함수를 호출에 새 함수로 만들면 결국엔 props엔 새 함수가 계속 전달이 됨.
const renderItem = ({ item }) => {
const onPress = (id) => () => handlePress(id); // ❌ 매번 생성
return <Item onPress={onPress(item.id)} />;
};
// 함수의 참조를 깨지않도록 해주어 메모이제이션 한 함수 그대로 넣어주여아 함
const handlePress = useCallback((item) => {
console.log(item.id);
}, []);
const renderItem = useCallback(({ item }) => {
return (
<Item
item={item}
onPress={handlePress}
/>
);
}, [handlePress]);