긴 리스트의 성능 문제는 결국 "화면에 보이지 않는 아이템까지 다 그리느냐"의 싸움이다. 세 라이브러리 모두 가상화(virtualization), 즉 보이는 영역 주변만 렌더링하는 방식으로 이 문제를 푼다. 다만 그 방식과 튜닝 노브가 서로 다르다.
이 글은 실무에서 실제로 손대게 되는 성능 관련 props 위주로, 세 가상화 리스트를 구별해 정리한 것이다.
FlashList와 LegendList는 둘 다 FlatList의 drop-in 대체제를 표방한다. 그래서 API 표면 대부분이 FlatList와 호환되고, 성능과 직결되는 다음 props는 세 라이브러리에 공통으로 존재한다.
keyExtractor — 셋 다 가장 중요한 성능 prop이다. 각 아이템에 안정적인 고유 키를 부여해, data가 바뀌어도 변경된 아이템만 갱신하고 나머지 레이아웃 정보는 재사용하게 한다. 인덱스를 키로 쓰면(재정렬·prepend 시) 리스트가 크게 깨지므로 반드시 데이터 고유값을 반환해야 한다. LegendList는 중복 키를 감지하면 경고를 띄울 정도로 이 부분을 중요하게 본다.
onEndReached / onEndReachedThreshold — 페이지네이션(무한 스크롤)의 기본이다. 데이터를 한 번에 다 들고 있지 않고 끝에 가까워질 때 나눠 불러오는 것 자체가 성능 전략이다. onEndReachedThreshold는 화면 크기 배수로 계산되므로, 0.5면 "끝에서 화면 절반 거리"에서 콜백이 발동한다.
numColumns / horizontal — 레이아웃 모드를 결정하는 공통 props이며, 컬럼 구성과 측정 방식에 영향을 준다.
다만 여기서부터가 핵심인데, "깊은 성능 튜닝 노브"는 세 라이브러리가 완전히 갈라진다. FlatList는 렌더링 윈도우를 개발자가 수동으로 조율하는 방식이고, FlashList와 LegendList는 그 수치 조율을 대부분 내부로 흡수하고 아이템 재활용(recycling)으로 접근한다. 아래에서 하나씩 구별한다.
FlatList는 아이템을 재활용하지 않는다. 화면에 들어오면 마운트하고 나가면 언마운트하는 방식이라, 성능 튜닝이 "렌더링 윈도우를 얼마나 크게/촘촘하게 둘 것인가"의 수동 조율로 귀결된다. 그래서 props가 가장 많고 손이 많이 간다.
실무에서 만지는 핵심 props는 다음과 같다.
windowSize (기본값 21) — 렌더링해 둘 영역의 크기다. 단위는 아이템 개수가 아니라 화면(viewport) 높이의 배수다. 기본 21은 "보이는 화면 1 + 위로 10 + 아래로 10"을 의미한다. 줄이면 메모리는 절약되지만 빠르게 스크롤할 때 빈 칸(blank area)이 보일 수 있다.maxToRenderPerBatch (기본값 10) — 한 배치(batch)에서 렌더링하는 아이템 수다. 키우면 스크롤 중 콘텐츠 채워지는 속도가 빨라지지만 한 번에 일을 많이 해서 프레임이 끊길 수 있다.initialNumToRender (기본값 10) — 첫 렌더에서 그릴 아이템 수다. 첫 화면을 꽉 채울 최소 개수로 맞추는 게 초기 로딩 체감에 좋다.updateCellsBatchingPeriod (기본값 50ms) — 배치 렌더링 사이의 간격(ms)이다. 늘리면 렌더 빈도가 줄어 부하가 분산되지만 빈 칸 노출 시간이 길어진다.removeClippedSubviews (Android 기본값 true, iOS 기본값 false) — 화면 밖 뷰를 네이티브 뷰 계층에서 떼어내는 옵션이다. 효과는 있지만 콘텐츠 사라짐·포커스 버그 위험이 있어 공식 문서가 "본인 책임하에 사용"을 권고한다. 특히 아이템에 TextInput·비디오·복잡한 중첩 뷰가 있으면 신중해야 한다.getItemLayout — 아이템 높이가 고정일 때 가장 강력한 최적화다. 각 아이템 위치를 동적 측정 없이 계산식으로 알려줘서, 측정 비용을 없애고 scrollToIndex도 정확해진다. 가변 높이라면 쓰기 어렵다.정리하면 FlatList 튜닝은 windowSize / maxToRenderPerBatch / initialNumToRender 3종 세트를 데이터 특성에 맞게 맞추고, 고정 높이면 getItemLayout을 더하는 흐름이다.
@shopify/flash-list)FlashList는 Shopify가 만든 고성능 리스트로, 아이템을 재활용(recycling)한다. 화면 밖으로 나간 컴포넌트를 버리지 않고 새 아이템에 재사용하는 방식이라 FlatList보다 빠르다.
여기서 중요한 건 v2가 완전 재작성판이라는 점이다. 실무 관점에서 v1과의 차이를 반드시 알아야 한다.
estimatedItemSize가 사라졌다. v1의 가장 번거로운 점이 아이템 크기를 추정해 넘겨주는 것이었는데, v2는 신아키텍처의 동기 레이아웃 측정을 이용해 추정값 없이 크기를 알아서 계산한다. 즉 v1에서 쓰던 estimatedItemSize, estimatedListSize, estimatedFirstItemOffset, onBlankArea, disableAutoLayout 등은 제거되거나 더 이상 필요 없다.maintainVisibleContentPosition이 기본 활성화다. 위쪽에서 아이템이 추가·변경돼도 현재 보고 있는 콘텐츠가 튀지 않는다.masonry가 prop이 됐다. 별도 MasonryFlashList 컴포넌트는 deprecated 되고, <FlashList masonry numColumns={3} />처럼 prop으로 핀터레스트식 레이아웃을 쓴다. optimizeItemArrangement로 컬럼 높이 차이를 줄이는 옵션도 생겼다.inverted가 deprecated 됐다. 대신 maintainVisibleContentPosition로 채팅형 UI를 처리하는 방향이다.overrideItemLayout은 이제 span(컬럼 차지 수)만 지원한다. v1에서 가능했던 size 추정 지정은 빠졌다.drawDistance — 뷰포트 위아래로 미리 렌더링해 둘 버퍼(px)다. FlatList의 windowSize에 대응하는, FlashList의 미리 그리기 조절 노브다.FlashList에서 FlashListRef로 바뀌었다.실무 포인트는 명확하다. 신아키텍처만 쓴다면 FlashList v2는 추정값 입력이 사라져서 가장 손이 안 가는 선택지가 됐다. 반대로 아직 구아키텍처라면 v2는 후보에서 빠진다.
@legendapp/list)LegendList는 Legend App(Jay Meistrich)이 만든 신생 리스트로, 가장 큰 특징은 100% 자바스크립트 구현이라 네이티브 코드 의존성이 없다는 점이다. 그래서 RN Web, macOS, Windows, TV 등 어떤 RN 플랫폼에서도 동작하고, 설치 후 네이티브 빌드 이슈가 적다. FlatList와 FlashList 양쪽 모두의 drop-in 대체제를 표방한다.
성능 관점에서 알아둘 props는 다음과 같다.
recycleItems (기본값 false) — 이게 FlashList와의 결정적 차이다. LegendList는 재활용을 끄고 시작한다. 즉 기본 상태에서는 아이템마다 새 컴포넌트를 만들어서(덜 빠르지만) 내부 상태가 있는 아이템도 안전하다. 최고 성능을 원하면 명시적으로 recycleItems를 켜야 하는데, 그 경우 아이템에 로컬 state가 있으면 재사용 과정에서 엉뚱하게 섞일 수 있어 주의해야 한다. FlashList 동작과 똑같이 맞추려면 이 prop을 켜면 된다.drawDistance (기본값 250) — 뷰포트 위아래로 미리 그려둘 버퍼(px)다. 키우면 빠른 스크롤 시 빈 칸이 줄지만 메모리를 더 쓴다.estimatedItemSize / getEstimatedItemSize (선택) — 첫 프레임 레이아웃을 추정하는 힌트다. 안 줘도 동작하며(이후엔 실제 측정한 평균 크기를 사용한다), 안 주면 최적값을 로그로 제안해준다. FlashList v2처럼 "추정 없이도 됨" 방향으로 정리됐다.getFixedItemSize (v2) — 아이템 크기가 고정임을 알려주면 측정·갱신 오버헤드를 없애 최적 성능을 낸다. FlatList의 getItemLayout과 같은 발상이다.getItemType (v2) — 아이템 유형을 분류하면 같은 타입끼리 더 효율적으로 재활용된다. (FlashList에도 있는 개념이다.)maintainVisibleContentPosition (기본값 true) — 위쪽 아이템이 추가·변경·리사이즈돼도 보이는 콘텐츠가 안 튄다. 내부적으로 ScrollView의 동명 prop을 사용하므로, Android에서 쓰려면 RN 0.72 이상이 필요하다.alignItemsAtEnd / maintainScrollAtEnd — inverted 없이 채팅 UI를 만드는 props다. LegendList는 inverted를 쓰지 않고 콘텐츠를 하단 정렬해 양방향 무한 스크롤을 매끄럽게 처리하는 걸 강점으로 내세운다.initialContainerPoolRatio (기본값 2) — 미리 잡아두는 컨테이너 풀의 비율이다. 고정 크기 아이템이면 1에 가깝게, 크기 변동이 크면 더 키우는 식으로 조절한다. 풀이 부족하면 컨테이너를 추가 할당하며 재렌더가 일어나 프레임이 잠깐 끊길 수 있다.| 항목 | FlatList | FlashList v2 | LegendList v2 |
|---|---|---|---|
| 제공처 | RN 코어 | Shopify | Legend App |
| 아이템 재활용 | ❌ (마운트/언마운트) | ✅ 기본 활성 | ⚠️ 선택 (recycleItems, 기본 off) |
| 네이티브 코드 | 있음 | 있음 | ❌ 없음 (순수 JS) |
| 크기 추정 입력 | 불필요(윈도우 수동 조율) | 불필요 (v2에서 제거) | 선택 (없어도 동작) |
| 신아키텍처 | 둘 다 지원 | 필수 | 불필요(어디서나) |
| 미리 그리기 조절 | windowSize 등 3종 세트 | drawDistance | drawDistance |
| 고정 높이 최적화 | getItemLayout | 자동 측정 | getFixedItemSize |
| 콘텐츠 위치 유지 | ScrollView prop 수동 | 기본 활성 | 기본 활성 |
| 채팅 UI | inverted | maintainVisibleContentPosition | alignItemsAtEnd 등 (inverted 불필요) |
windowSize·maxToRenderPerBatch·initialNumToRender를 직접 튜닝해야 하고, 고정 높이면 getItemLayout을 더한다. 안드에서 리스트가 잘리거나 안 보이면 removeClippedSubviews(안드 기본 true)부터 의심한다.recycleItems를 직접 켜야 하고, 그 경우 아이템 내부 state 처리에 주의해야 한다.공통 원칙은 하나다. 어떤 리스트를 쓰든 keyExtractor를 제대로 주고, 아이템 컴포넌트를 React.memo 등으로 가볍게 유지하는 것이 모든 튜닝 노브보다 먼저다. 가상화 라이브러리는 "안 보이는 걸 안 그리게" 도와줄 뿐, 보이는 아이템 하나하나가 무거우면 어떤 리스트도 느려진다.