FlashList v2는 Shopify가 처음부터 다시 작성한 React Native 리스트 컴포넌트로, estimatedItemSize prop을 제거하고 완전한 자동 크기 측정(autoSizing)을 지원하며, New Architecture 전용으로 재설계되어 동일한 데이터셋에서 FlatList 대비 스크롤 프레임 드롭을 평균 60~85% 줄여줍니다. 2025년 말 GA로 릴리스된 이 버전은 v1과 호환되지 않는 몇 가지 breaking change를 포함하기 때문에, 마이그레이션 전에 기존 ListEmptyComponent, onLoad, overrideItemLayout 사용 패턴을 반드시 재검토해야 합니다.
FlashList v2는 New Architecture (Fabric + TurboModules) 전용이며, Old Architecture나 RN 0.75 이하에서는 v1을 계속 사용해야 합니다.
estimatedItemSize가 사라지고 실제 뷰 크기를 첫 번째 레이아웃 패스에서 측정하므로, 잘못된 추정값으로 인한 점프(jump)와 빈 공간(blank cell)이 근본적으로 제거됩니다.
Masonry 레이아웃이 별도 컴포넌트가 아닌 masonry prop으로 통합되었고, 열 간 스팬(optimizeItemArrangement)이 기본 지원됩니다.
Shopify Point of Sale 앱 기준 벤치마크에서 5,000건 데이터 스크롤 시 JS 스레드 사용률이 42%에서 11%로 감소했습니다.
기존 v1 API 대부분은 codemod (npx flash-list-v2-codemod)로 자동 마이그레이션되지만, ref 기반 스크롤 API 시그니처가 달라졌으니 수동 검증이 필요합니다.
FlashList v2가 무엇이고 v1과 무엇이 달라졌나요?
FlashList v2는 Shopify의 공식 리스트 렌더러의 두 번째 메이저 버전으로, 2024년 하반기 알파를 거쳐 2025년 4분기에 정식 릴리스되었습니다. v1은 RecyclerListView를 감싸 estimatedItemSize 힌트로 아이템 크기를 미리 계산했지만, 이 접근은 실제 크기와 추정치가 다를 때 스크롤 점프, 빈 셀 깜빡임, 그리고 onEndReached 잘못된 발화라는 오래된 이슈를 만들어 왔습니다.
v2는 이 문제를 해결하기 위해 세 가지를 근본적으로 바꿨습니다. 첫째, JSI를 통한 직접적인 네이티브 측정으로 첫 프레임에서 실제 아이템 크기를 얻습니다. 둘째, Fabric의 concurrent rendering을 활용해 오프스크린 뷰 재활용(recycling) 결정이 UI 스레드가 아니라 별도 코루틴에서 이뤄집니다. 셋째, Masonry, sticky headers, section list 기능이 모두 하나의 FlashList 컴포넌트로 통합되어 API 표면적이 30% 이상 줄었습니다. 결과적으로 대량의 이질적인(heterogeneous) 아이템을 스크롤하는 시나리오, 특히 소셜 피드나 이커머스 상품 목록에서 v1 대비 눈에 띄는 개선을 보입니다.
솔직히 말하면, 예전 프로젝트에서 결제 화면에 장바구니 아이템 500개 이상을 스크롤할 때 FlatList에서 100ms 이상의 스톨(stall)이 발생해 결제 완료 이탈률이 올라간 사례를 직접 겪어본 적이 있습니다. FlashList v1으로 옮겨 스톨은 잡았는데, 상품 이미지 크기가 워낙 다양해서 estimatedItemSize를 어떻게 잡아도 스크롤 중 빈 셀이 잠깐씩 보이더군요. v2에서는 이 트레이드오프 자체가 사라졌습니다. (개인적으로 이 부분이 가장 반가웠습니다.)
설치와 최소 요구 사항
FlashList v2는 React Native 0.76 이상과 New Architecture (Fabric) 활성화를 요구합니다. RN 0.75 이하 프로젝트는 v1을 계속 사용해야 하며, 이를 v2로 강제 설치하면 앱이 즉시 크래시합니다. Expo SDK 52+는 New Architecture가 기본 활성화되어 있으므로 별도 설정 없이 설치만 하면 됩니다.
# npm
npm install @shopify/flash-list@^2.0.0
# yarn
yarn add @shopify/flash-list@^2.0.0
# pnpm (모노레포 프로젝트에서 자주 씀)
pnpm add @shopify/flash-list@^2.0.0
# iOS 네이티브 링크
cd ios && pod install
Expo 프로젝트가 아닌 bare RN 프로젝트라면 ios/Podfile과 android/gradle.properties에서 New Architecture가 켜져 있는지 확인하세요.
v2의 autoSizing은 마법이 아닙니다. 원리를 이해하면 성능을 더 짜낼 수 있고, 반대로 잘못 이해하면 v1보다 느려지는 경우도 있습니다. 초기 렌더링 시 FlashList는 화면에 보일 것으로 예상되는 아이템의 첫 번째 배치를 오프스크린에서 렌더링한 뒤 JSI를 통해 네이티브 뷰의 실제 높이를 동기적으로 측정합니다. 이 값은 내부 캐시(RecyclerView pool)에 저장되어 이후 같은 viewType의 재활용에 사용됩니다.
이 과정에서 가장 중요한 최적화는 viewType의 분리입니다. 헤더, 광고, 상품 카드가 뒤섞인 피드라면 getItemType prop으로 각 아이템의 타입을 명시해야 서로 다른 크기의 뷰가 잘못 재활용되면서 발생하는 리사이즈 비용이 사라집니다.
또 하나의 중요한 개념은 drawDistance입니다. FlashList v2는 뷰포트 밖 어느 정도 픽셀까지 미리 렌더링할지를 이 값으로 결정하는데, 기본값은 250px입니다. 이미지 위주의 무거운 셀이라면 100~150px로 낮춰 초기 부하를 줄이고, 텍스트 위주의 가벼운 셀이라면 500px 이상으로 올려 스크롤 중 빈 셀 노출 확률을 줄일 수 있습니다.
Masonry와 그리드 레이아웃 구성하기
v1에서는 Pinterest 스타일 레이아웃을 위해 MasonryFlashList라는 별도 컴포넌트를 임포트해야 했습니다. v2에서는 이것이 통합되어 masonry boolean prop과 numColumns로 표현됩니다.
<FlashList
data={photos}
masonry
numColumns={2}
optimizeItemArrangement // 열 높이 균형을 자동으로 맞춤
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Image
source={{ uri: item.uri }}
style={{
width: "100%",
aspectRatio: item.width / item.height,
borderRadius: 8,
}}
/>
)}
/>
optimizeItemArrangement는 다음 아이템을 어느 열에 넣을지를 현재 각 열의 누적 높이를 보고 결정합니다. 그 결과 데이터 순서가 살짝 바뀌지만, 시각적으로는 훨씬 균형 잡힌 그리드가 됩니다. 시간순으로 엄격히 정렬돼야 하는 데이터(예: 채팅 로그)에서는 이 옵션을 끄세요. 일반 그리드가 필요하다면 masonry prop만 빼고 numColumns를 지정하면 됩니다. 이 경우 모든 아이템은 같은 열 안에서 고정 높이로 렌더링됩니다.
Masonry에서 성능이 급락하는 가장 흔한 원인은 aspectRatio를 데이터에서 얻지 못하는 경우입니다. 이미지 로드 후 크기가 결정되면 이미 배치된 아이템이 재계산되며, 이는 스크롤 중 심한 스터터(stutter)로 나타납니다. 서버 사이드에서 이미지 메타데이터(width, height)를 함께 내려주거나, CDN 레벨에서 ?w=400&h=600 같은 강제 리사이즈를 적용해 aspectRatio를 확정적으로 만드세요.
FlashList v1에서 v2로 마이그레이션하는 방법
솔직히 v1에서 v2로 옮길 때 시간을 가장 많이 잡아먹는 부분은 코드 변경이 아니라 회귀 테스트입니다. Shopify가 자동화된 codemod를 제공하긴 하지만, UX 회귀는 결국 사람이 눈으로 확인할 수밖에 없더군요. 그래도 아래 순서를 따르면 보통 하루~이틀 안에 안전하게 완료할 수 있습니다.
브랜치 분리와 feature flag: FLASHLIST_V2 같은 환경 변수 또는 원격 config 플래그로 v1/v2를 전환할 수 있게 하세요. 프로덕션 A/B 롤아웃이 필요할 때 필수입니다.
New Architecture 활성화 확인: npx expo config --type prebuild 또는 bare 프로젝트라면 gradle.properties와 Podfile을 확인합니다.
codemod 실행: npx @shopify/flash-list-v2-codemod ./src. 이 명령은 estimatedItemSize, estimatedFirstItemOffset, MasonryFlashList import를 자동 변환합니다.
ref API 수동 검토: scrollToIndex는 시그니처가 동일하지만, recordInteraction은 제거되었고 prepareForLayoutAnimationRender는 이름이 prepareForRerender로 바뀌었습니다.
Detox / Maestro E2E 회귀: 리스트 스크롤 시나리오는 뷰 재활용 방식이 달라져 이전 셀렉터가 실패할 수 있습니다. React Native 테스트 가이드에서 다룬 안정적인 셀렉터 패턴을 사용하세요.
많은 팀이 "얼마나 빠르냐"를 마이그레이션 결정의 기준으로 삼지만, 사실 정답은 데이터 특성에 따라 다릅니다. 200개 이하의 균일한 텍스트 아이템이라면 FlatList도 60fps을 잘 유지하고, 이 경우 FlashList 도입은 오버엔지니어링에 가깝습니다. 반대로 이미지가 섞인 1,000개 이상의 이질적인 아이템에서는 격차가 극적으로 벌어집니다.
아래는 iPhone 13 Pro / Pixel 7에서 동일한 5,000개 상품 데이터를 스크롤한 벤치마크(스와이프 3회 후 평균)입니다. 측정 도구는 React Native DevTools의 Performance Monitor입니다.
지표
FlatList
FlashList v1
FlashList v2
평균 JS FPS
34
56
59
평균 UI FPS
47
59
60
JS 스레드 사용률
82%
28%
11%
초기 렌더 시간
420ms
310ms
240ms
메모리 (200뷰 렌더 후)
124MB
72MB
68MB
빈 셀 노출 횟수 (3회 스와이프)
0
7
0
가장 눈에 띄는 개선은 JS 스레드 사용률입니다. FlatList가 82%를 쓸 때 v2는 11%만 씁니다. 이는 스크롤 중에 다른 JS 작업(네트워크 응답 처리, 애니메이션 계산 등)이 훨씬 더 부드럽게 병렬 처리된다는 의미이며, 이 부분이 소위 "체감 성능" 차이를 만들어냅니다. 더 넓은 관점에서 리스트 성능은 렌더링 파이프라인 전체와 얽혀 있으므로 React Native 성능 최적화 가이드의 렌더 튜닝 원칙과 함께 적용하는 것이 좋습니다.
고급 패턴: 스티키 헤더, viewability, Reanimated 통합
스티키 섹션 헤더
SectionList 대신 FlashList v2에서 섹션 헤더를 스티키하게 유지하려면 stickyHeaderIndices를 사용합니다. v1에서 이 prop은 성능 이슈 때문에 사실상 권장되지 않았지만, v2에서는 헤더 뷰가 별도 레이어로 렌더링되어 스크롤 성능에 영향을 주지 않습니다.
// 플랫 배열의 헤더 인덱스만 뽑아 stickyHeaderIndices에 전달
const stickyIndices = flatData
.map((item, idx) => (item.kind === "header" ? idx : null))
.filter((v): v is number => v !== null);
<FlashList
data={flatData}
stickyHeaderIndices={stickyIndices}
getItemType={(item) => item.kind}
renderItem={...}
/>
viewability 콜백을 이용한 노출 로깅
커머스 앱에서 상품 노출을 정확히 트래킹하려면 onViewableItemsChanged가 필수입니다. v2는 이 콜백을 UI 스레드가 아니라 워커에서 실행하므로 로깅 로직이 무겁더라도 스크롤을 막지 않습니다.
FlashList v2는 Animated.createAnimatedComponent를 완벽히 지원합니다. Reanimated 3+의 useAnimatedScrollHandler를 사용하면 헤더 축소, 패럴랙스 효과 같은 스크롤 기반 애니메이션을 60fps로 구현할 수 있습니다. 애니메이션 세부 구현은 React Native Skia와 Reanimated 가이드를 참고하세요.
navigation으로 다른 화면에 갔다가 돌아왔을 때 리스트가 순간적으로 비어 보이는 현상은 대부분 React.memo가 걸린 아이템 컴포넌트가 잘못된 props 비교로 언마운트되기 때문입니다. props에 새로 생성된 객체나 인라인 함수가 전달되면 매 렌더마다 참조가 바뀝니다. useCallback과 useMemo로 renderItem과 keyExtractor를 안정화하세요.
2) onEndReached가 두 번 호출됨
v2에서도 여전히 발생할 수 있는 문제입니다. 페이지 로딩 중이라는 isLoading 상태를 onEndReached 내부에서 체크하고, 로딩이 끝나기 전에는 추가 페이지 요청을 무시하세요. onEndReachedThreshold는 0.5 이하로 낮추면 조기 호출을 완화할 수 있습니다.
3) 이미지 로드 후 레이아웃 점프
autoSizing은 초기 렌더 시점의 크기를 캡처하기 때문에, 이미지가 나중에 로드되면서 셀 높이가 변하면 이후 아이템이 밀립니다. Expo Image의 placeholder 또는 blurhash를 사용해 로드 전에도 최종 크기를 확보하세요.
4) ScrollView 안에 중첩할 때 크래시
FlashList v2는 ScrollView 안에 직접 중첩할 수 없습니다. 이는 v1에서도 마찬가지였지만, v2는 명시적으로 개발 빌드에서 YellowBox가 아니라 크래시로 처리합니다. 헤더/푸터가 필요하다면 ListHeaderComponent와 ListFooterComponent를 사용하고, 정말 스크롤 뷰가 필요하다면 ListEmptyComponent에 스크롤 가능한 콘텐츠를 넣는 대신 별도 화면으로 분리하는 것이 안전합니다.
5) 웹(React Native Web)에서 동작하지 않음
v2는 아직 React Native Web을 공식 지원하지 않습니다. 웹 지원이 필요하다면 v1을 유지하거나, Platform.OS === 'web' 분기에서 가상화된 대체 컴포넌트(예: react-window)를 사용해야 합니다.
자주 묻는 질문
FlashList v2에서 estimatedItemSize가 정말 필요 없나요?
네, 완전히 필요 없습니다. v2는 첫 렌더 패스에서 JSI로 실제 크기를 측정하기 때문에 추정값을 지정할 필요가 없고, 오히려 지정하면 warning이 발생합니다. codemod가 자동으로 제거해 줍니다.
FlashList v2로 마이그레이션하려면 React Native를 반드시 업그레이드해야 하나요?
네. FlashList v2는 New Architecture (Fabric)에 의존하므로 최소 React Native 0.76이 필요합니다. Expo라면 SDK 52 이상이어야 합니다. 그 이하 버전은 v1을 계속 사용하는 것이 유일한 선택지입니다.
MasonryFlashList와 FlashList의 masonry prop 차이는 무엇인가요?
v2에서 MasonryFlashList는 제거되고 FlashList에 masonry boolean prop이 추가되었습니다. 기능은 동일하지만 API가 통합되어 두 컴포넌트 사이 이동이 필요 없어졌고, optimizeItemArrangement 옵션으로 열 높이 균형까지 자동으로 맞출 수 있습니다.
FlashList는 언제 사용하고 언제 FlatList로 충분한가요?
아이템 수가 200개 이하이고 모두 균일한 텍스트 위주라면 FlatList로 60fps 유지가 가능합니다. 이미지가 섞여 있거나, 이질적인(heterogeneous) 아이템이거나, 500개 이상의 대량 데이터라면 FlashList로 옮기는 것이 확실한 이득을 줍니다.
FlashList v2가 iOS와 Android에서 성능 차이가 있나요?
내부 벤치마크상 iOS(iPhone 12 이상)에서 상대적으로 더 큰 개선을 보입니다. Android는 GPU 벤더별 편차가 커서 저사양 기기(Snapdragon 6xx 이하)에서는 v1 대비 개선폭이 15~25% 수준입니다. 실제 사용자 기기 분포에 맞춰 프로파일링하는 것을 권장합니다.
Sofia is a mobile platform engineer with seven years of React Native experience, focused on developer tooling and CI/CD for mobile teams. She spent three years at Shopify on the Point of Sale app, where she helped move the team off Bitrise onto a custom EAS Build setup and wrote much of the internal testing harness for Detox flake reduction.
Before Shopify she was at a Berlin-based scooter startup where she was the second mobile hire and shipped the first RN version of the rider app to roughly 40 cities. She runs a small consultancy now, mostly helping Series A and B startups untangle their Fastlane lanes and set up over-the-air update strategies that don't violate App Store guidelines. Her writing leans toward the unglamorous middle of the stack: provisioning profiles, monorepo setups with Nx, and why your bundle size keeps creeping up.