React Native 의 LegendList

eeensu·2026년 3월 14일

React Native

목록 보기
21/35

개요

LegendList는 @legendapp/list 라이브러리에서 제공하는, "FlashList의 대안"으로 등장한 최신 고성능 패키지이다.

React Native 생태계에서 리스트 컴포넌트의 발전 역사를 보면 3세대에 해당한다고 볼 수 있다.

  • 1세대 : FlatList (기본 제공, 느림, 메모리 많이 먹음)
  • 2세대 : FlashList (Shopify, 빠름, 재활용 사용, 하지만 흰 공백(Blank Space) 문제 존재)
  • 3세대 : LegendList (FlashList의 단점인 흰 공백과 레이아웃 점프를 해결하려는 시도)


왜 나왔을까 또? FlashList 로 만족되지 못했나?

FlashList는 혁명적이었지만, 완벽하지 않았다. 특히 스크롤을 아주 빠르게 하면 화면에 아이템이 그려지기 전에 스크롤이 먼저 되어서 하얀 빈 공간(Blank Space)이 보이는 문제가 있다. 또한 FlashList는 리스트 아이템들의 size가 거의 동일해야 최적화가 좋아지는데, 만약 sns의 feed 등 height의 길이가 다양하다면 ? 또 최적화 되지 않을 수도 있다.

LegendList는 이 문제를 해결하고, "동적 높이(Dynamic Height)" 아이템을 더 쉽게 다루기 위해 만들어졌다.


LegendList의 핵심 기술 및 차별점

1. Zero Blank Space (빈 공간 없음)

  • FlashList : 스크롤 속도가 렌더링 속도를 앞지르면 흰 화면이 나온다.
  • LegendList : 렌더링 파이프라인을 최적화하여, 스크롤 중에 빈 공간이 보이는 것을 원천적으로 차단하는 데 집중했다. (동기식 레이아웃 처리와 예측 렌더링을 강화함)

2. 가변 높이 지원 (No Layout Jumps)

  • FlashList : estimatedItemSize가 실제와 많이 다르면 스크롤바가 튀거나(Jump), 아이템 위치가 어긋나는 현상이 생긴다. (하지만 최신 버전에선 estimatedItemSize를 쓰지 않아도 되어, 현재는 해당되지 않는다.)
  • LegendList : 아이템의 높이가 제각각이어도(예: 트위터 피드처럼 텍스트 길이가 다를 때), 레이아웃을 훨씬 안정적으로 계산한다.

3. 재활용(Recycling) + 상태 유지

FlashList처럼 View Recycling(뷰 재활용) 기술을 기본으로 사용한다. 여기에 더해, @legendapp/state (같은 팀이 만든 상태 관리 라이브러리)의 철학을 빌려와, 리렌더링을 최소화하는 최적화가 내장되어 있다.


사용법

FlashList와 매우 비슷하다. API가 거의 호환된다.

import { LegendList } from '@legendapp/list';

const MyComponent = () => {
  return (
    <LegendList
      data={data}
      renderItem={({ item }) => <MyItem item={item} />}
      
      // FlashList처럼 높이 추정치. 안 넣어줘도 알아서 예측해주긴 함
      estimatedItemSize={100} 
      
      // [차별점] 뷰 재활용을 위한 ID 설정이 더 직관적임
      keyExtractor={(item) => item.id}
    />
  );
};

핵심 props

  • recycleItems
    스크롤 밖으로 나간 아이템 컴포넌트를 재사용하는 옵션이다. 기본값은 false로, true로 설정하면 성능이 크게 향상되지만 아이템 컴포넌트 내부에 로컬 state가 있다면 예기치 않게 재사용될 수 있어 주의가 필요하다.

  • getEstimatedItemSize
    아이템의 예상 크기를 반환하는 함수로, 리스트 레이아웃을 실제 렌더링 전에 미리 계산하는 데 사용된다. 제공하지 않으면 최적 성능을 위한 권장값을 로그로 알려준다.

  • getFixedItemSize
    고정 크기인 아이템에 사용하며, 사이즈 측정 오버헤드를 없애 최적 성능을 낼 수 있다. 동적 크기 아이템은 undefined를 반환하면 된다.


그럼 각각 언제 어떤 걸 써야 best / anti 패턴 일까?

  • ScrollView

    • 추천 : 아이템 수가 적고 가벼울 때 (카테고리 탭, 태그 칩, 필터 버튼 등 수십 개 이하). 가상화 오버헤드 없이 가장 심플하다.
    • 비추천 : 아이템이 80 ~ 100개 이상이거나 이미지 등 무거운 컴포넌트가 들어가면 좋지않다.
  • FlashList

    • 추천 : 아이템이 많고 사이즈가 균일하거나 예측 가능할 때 (상품 그리드, 균일한 카드 피드, 이미지 썸네일 리스트 등).
    • 비추천 : 안정적이고 프로덕션에서 검증됨. 아이템 높이가 제각각 일 수 있는 경우 (썸내일이 있는 이미지 / 없는 이미지 등)
  • LegendList

    • 추천 : 아이템이 많으면서 사이즈가 동적일 때 (채팅 UI, 댓글, 텍스트 길이 다른 카드 피드 등). 네이티브 의존성 없이 설치 간편하고, 채팅용 inverted 없는 하단 정렬, 양방향 무한 스크롤이 필요하면 특히 강점. - 비추천 : 아이탬 사이즈가 고정적일 것이라 예측될 경우


렌더링 최적화 방법

CommonFlashList / CommonLegendList Props 최적화 Best Practice


useCallback 필요

Props이유
renderItem참조 변경 시 아이템 리렌더 유발
onEndReached스크롤 이벤트마다 호출, 참조 불안정하면 중복 트리거 가능
onRefresh리렌더마다 새 함수면 RefreshControl이 불안정하게 동작 가능

useMemo 필요

Props이유
refreshProps객체 리터럴은 매 렌더마다 새 참조 생성
contentContainerStyle스타일 객체도 동일

❌ 메모이제이션 불필요

Props이유
keyExtractor결과값만 쓰고 참조 비교 안 함
data보통 이미 외부 상태(query 등)에서 참조 안정성 보장됨
estimatedItemSize숫자 원시값, 항상 동일 참조
ItemSeparatorComponent컴포넌트 정의를 컴포넌트 외부에 선언하면 해결
ListEmptyComponent (컴포넌트인 경우)마찬가지로 외부 선언으로 해결

ItemSeparatorComponent, ListEmptyComponent의 올바른 패턴

// ✅ 컴포넌트 외부 선언 → 항상 동일 참조, 메모이제이션 불필요
const ItemSeparator = () => <View />;
const ListEmpty = () => <CommonEmpty />;

export default function MyScreen() {
  return (
    <CommonLegendList
      ItemSeparatorComponent={ItemSeparator}
      ListEmptyComponent={ListEmpty}
    />
  );
}

renderItem과 반드시 세트로

// renderItem 아무리 안정화해도 아이템 컴포넌트에 memo 없으면 의미 반감
const MyItem = React.memo(function MyItem({ item }: Props) {
  return <View />;
});

const renderItem = useCallback(({ item }: { item: Item }) => (
  <MyItem item={item} />
), []);

useCallback(renderItem) + React.memo(아이템 컴포넌트) 는 항상 세트이다-

사실 renderItem도 FlashList 내부에서 어차피 다시 호출되기 때문에, useCallback으로 감싸는 것만으로는 셀 리렌더를 완전히 막지 못한다. 진짜 효과적인 건 renderItem 안에서 반환하는 컴포넌트 자체를 memo로 감싸는 것이다.

함수 호출 → JSX element 반환 → element끼리 === 비교
=== → false (참조 비교)
→ 내부 컴포넌트를 React.memo로 감싸야

profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글