React Native의 데이터 목록 최적화 FlashList

eeensu·2026년 4월 18일

React Native

목록 보기
33/38

FlashList는 Shopify에서 만든 React Native용 리스트 라이브러리다.
기존의 FlatList가 가진 성능 한계(특히 저사양 안드로이드에서의 버벅임, 스크롤 시 흰 화면)를 극복하기 위해 만들어졌다. 현재 React Native 커뮤니티에서 가장 권장되는 리스트 솔루션이다.
FlatList와 사용법은 99% 비슷하지만, 작동 원리가 완전히 다르다.


1. 왜 만들었나? (FlatList의 한계)

기존 FlatList는 "가상화(Virtualization)" 방식을 쓴다.

  • 동작 : 화면에 보이는 아이템만 그리고, 스크롤해서 화면 밖으로 나가면 그 컴포넌트의 메모리를 해제(Unmount)해버린다.
  • 문제 : 스크롤을 빨리하면 새로운 아이템을 계속 생성(Mount)하고, 지나간 건 삭제(Unmount)해야 한다.
  • 결과: JS 스레드와 UI 스레드가 이 생성/삭제 작업을 처리하느라 바빠져서, 스크롤 속도를 못 따라가고 흰 공백(Blank Space)이 보인다.

2. FlashList의 핵심 기술 Cell Recycling

FlashList는 재활용(Recycling) 방식을 쓴다. (iOS의 UICollectionView나 Android의 RecyclerView와 같은 원리다.)

  • 생성 : 화면에 5개의 아이템이 필요하면 딱 5개(여유분 포함 6~7개)의 뷰(View)만 만든다.
  • 재활용 : 스크롤을 내려서 1번 아이템이 화면 위로 사라지면, 이 뷰를 파괴하지 않는다.
  • 데이터 교체 : 대신 그 뷰를 그대로 가져와서 내용(텍스트, 이미지)만 6번 아이템의 데이터로 갈아끼운 뒤 화면 맨 아래로 이동시킨다.
  • 효과 : 컴포넌트를 꼈다 뺐다 하는 무거운 작업이 사라지고, 단순히 "데이터 덮어쓰기"만 하니까 FlatList 대비 최대 5배(UI 스레드), 10배(JS 스레드) 빠르다.

3. 사용법 (FlatList와 거의 동일)

설치 후 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} 
    />
  );
};
  1. 치명적인 차이점 estimatedItemSize
    FlashList를 쓸 때 가장 헷갈리는 부분이자, 성능의 핵심이다.
  • FlatList : 아이템의 높이를 몰라도 일단 그린다음 계산한다. (그래서 레이아웃 계산 비용이 든다.)
  • FlashList : 재활용을 하려면 "대충 높이가 얼마인지" 미리 알아야 한다. 그래야 스크롤 바의 크기를 계산하고, 몇 개의 뷰를 재활용할지 결정할 수 있다.
  • 정확할 필요는 없다. 평균적인 높이값을 넣어주면 된다.
  • 안 넣으면? FlashList가 내부적으로 높이를 계산하느라 FlatList랑 다를 바 없이 느려진다. (심지어 경고 로그를 엄청 띄운다.)

5. 주의할 점 (함정)

재활용 방식 때문에 개발자가 실수하기 쉬운 포인트들이 있다.

  • 컴포넌트 내부 상태(State) 초기화 안 됨:
    • renderItem으로 그려지는 컴포넌트(Row)가 useState를 가지고 있다고 치자.
    • 뷰가 재활용될 때, 컴포넌트는 새로 마운트 되는 게 아니라 데이터만 바뀌는 것이다.
    • 따라서 조건에 따라, useEffect(() => {}, []) (마운트 시 실행) 코드가 새 아이템이 나타날 때 실행되지 않을 수 있다.
    • 해결 : key가 바뀌거나 데이터(props)가 바뀔 때 상태를 리셋하도록 코드를 짜야 한다.
  • key Prop의 중요성
    • React에서 리스트 아이템에 key를 주는 건 필수지만, FlashList에서는 더 중요하다. 엉뚱한 데이터가 덮어씌워지는 걸 막으려면 고유한 ID를 확실히 줘야 한다.
  • 중첩 리스트 주의
    • 같은 방향(수직 스크롤 안에 수직 스크롤)으로 FlashList를 중첩하면 에러가 난다. (이건 FlatList도 마찬가지지만 더 엄격하다.)
  • 최신 버전 (2.0.0 이상)
    • 아이템의 높이를 측정하여 성능 최적화에 필요한 estimatedItemSize (아이템 높이 평균값)이 필요 없어졌다.

6. 언제 써야 할까?

  • 무조건 : 데이터가 100개 이상 넘어가는 리스트

  • 무조건 : 이미지나 복잡한 레이아웃이 포함된 리스트

  • 권장 : 사실 그냥 FlatList 대신 기본으로 써도 무방하다. (설정만 잘하면 성능 손해 볼 게 없다.)

    FlashList : Shopify가 만든 FlatList의 초고성능 대체재.
    원리 : 뷰를 파괴하지 않고 재활용(Recycling)해서 내용을 갈아끼운다.

    주의 : 컴포넌트가 재활용되므로 내부 state 관리에 유의해야 한다.




renderItem 함수에서 하면 안되는 것들

최적화에 방해되는, 최적화의 의미가 없어지는 경우를 조심해야한다.

1. renderItem 안에서 무거운 연산

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 점유, 프레임 드랍 등의 문제가 발생한다.


2. inline 함수 남발 (특히 이벤트)

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 효과 감소하게 되는 문제로 이어진다.

3. 조건에 따라 다른 컴포넌트 구조 반환

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가 발생해버린다.

4. renderItem의 리턴 아이탬의 props에 커링함수 전달

// 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]);
profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글