FlatList 기초 — 대량 데이터를 효율적으로 렌더링하기
FlatList는 화면에 보이는 항목만 렌더링하는 가상화된 리스트 컴포넌트입니다.
모바일 앱에서 리스트는 가장 흔한 UI 패턴입니다. 하지만 1000개의 아이템을 한 번에 렌더링하면 메모리와 성능에 심각한 문제가 생깁니다. FlatList는 이 문제를 가상화(virtualization) 로 해결합니다.
기본 사용법
import { FlatList, View, Text, StyleSheet } from 'react-native';
interface Item {
id: string;
title: string;
description: string;
}
const DATA: Item[] = Array.from({ length: 100 }, (_, i) => ({
id: String(i),
title: `아이템 ${i + 1}`,
description: `${i + 1}번째 아이템 설명`,
}));
function BasicList() {
return (
<FlatList
data={DATA}
// keyExtractor — 각 아이템의 고유 키
keyExtractor={(item) => item.id}
// renderItem — 각 아이템의 렌더링 방법
renderItem={({ item, index }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.desc}>{item.description}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
title: {
fontSize: 16,
fontWeight: 'bold',
},
desc: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
});
가상화란?
FlatList는 화면에 보이는 항목 + 약간의 버퍼 만 실제로 렌더링합니다.
전체 데이터: [0, 1, 2, ..., 999]
실제 렌더링:
┌─────────────┐
│ (버퍼) 아이템 3 │ ← 화면 위 버퍼
│ (버퍼) 아이템 4 │
├─────────────┤
│ 아이템 5 │ ← 화면에 보이는 영역
│ 아이템 6 │
│ 아이템 7 │
│ 아이템 8 │
├─────────────┤
│ (버퍼) 아이템 9 │ ← 화면 아래 버퍼
│ (버퍼) 아이템 10 │
└─────────────┘
아이템 0~2, 11~999는 렌더링하지 않음!
면접에서 "FlatList와 ScrollView의 차이"를 물으면, 가상화를 핵심으로 설명하세요. ScrollView는 모든 자식을 한 번에 렌더링하고, FlatList는 보이는 것만 렌더링합니다.
구분선과 헤더/푸터
function ListWithExtras() {
return (
<FlatList
data={DATA}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
)}
// 아이템 사이 구분선
ItemSeparatorComponent={() => (
<View style={styles.separator} />
)}
// 리스트 상단 헤더
ListHeaderComponent={() => (
<View style={styles.header}>
<Text style={styles.headerText}>목록</Text>
</View>
)}
// 리스트 하단 푸터
ListFooterComponent={() => (
<View style={styles.footer}>
<Text>총 {DATA.length}개</Text>
</View>
)}
// 데이터가 비어있을 때
ListEmptyComponent={() => (
<View style={styles.empty}>
<Text>데이터가 없습니다</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
item: { padding: 16 },
separator: { height: 1, backgroundColor: '#eee' },
header: { padding: 16, backgroundColor: '#f5f5f5' },
headerText: { fontSize: 20, fontWeight: 'bold' },
footer: { padding: 16, alignItems: 'center' },
empty: { padding: 40, alignItems: 'center' },
});
Pull to Refresh (당겨서 새로고침)
import { useState, useCallback } from 'react';
import { FlatList, RefreshControl } from 'react-native';
function RefreshableList() {
const [data, setData] = useState(DATA);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
// API 호출 등 데이터 새로고침
const newData = await fetchData();
setData(newData);
setRefreshing(false);
}, []);
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ListItem item={item} />}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#007AFF']} // Android 스피너 색상
tintColor="#007AFF" // iOS 스피너 색상
/>
}
/>
);
}
무한 스크롤 (Infinite Scroll)
function InfiniteList() {
const [data, setData] = useState<Item[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const loadMore = useCallback(async () => {
if (loading) return; // 중복 호출 방지
setLoading(true);
const newItems = await fetchPage(page);
setData((prev) => [...prev, ...newItems]);
setPage((prev) => prev + 1);
setLoading(false);
}, [page, loading]);
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ListItem item={item} />}
// 끝에 도달했을 때 호출
onEndReached={loadMore}
// 끝에서 얼마나 떨어졌을 때 호출할지 (0~1)
onEndReachedThreshold={0.5}
// 로딩 인디케이터
ListFooterComponent={
loading ? <ActivityIndicator style={{ padding: 20 }} /> : null
}
/>
);
}
가로 리스트
function HorizontalList() {
return (
<FlatList
data={DATA}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.card}>
<Text>{item.title}</Text>
</View>
)}
horizontal // 가로 스크롤
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16 }}
ItemSeparatorComponent={() => <View style={{ width: 12 }} />}
/>
);
}
const styles = StyleSheet.create({
card: {
width: 150,
height: 200,
backgroundColor: '#f0f0f0',
borderRadius: 12,
padding: 16,
justifyContent: 'center',
alignItems: 'center',
},
});
SectionList — 그룹화된 리스트
import { SectionList, Text, View, StyleSheet } from 'react-native';
const SECTIONS = [
{ title: '과일', data: ['사과', '바나나', '딸기'] },
{ title: '채소', data: ['당근', '브로콜리', '시금치'] },
{ title: '음료', data: ['커피', '녹차', '주스'] },
];
function GroupedList() {
return (
<SectionList
sections={SECTIONS}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>{item}</Text>
</View>
)}
renderSectionHeader={({ section: { title } }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{title}</Text>
</View>
)}
stickySectionHeadersEnabled // 섹션 헤더 고정
/>
);
}
const styles = StyleSheet.create({
sectionHeader: {
backgroundColor: '#f5f5f5',
padding: 12,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
},
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
});
성능 기초 팁
// 1. renderItem 함수 외부에서 정의
const renderItem = useCallback(({ item }: { item: Item }) => (
<ListItem item={item} />
), []);
// 2. 아이템 높이가 고정이면 getItemLayout 사용
const getItemLayout = useCallback(
(_: any, index: number) => ({
length: ITEM_HEIGHT, // 아이템 높이
offset: ITEM_HEIGHT * index, // 누적 높이
index,
}),
[]
);
// 3. 적용
<FlatList
data={data}
renderItem={renderItem}
getItemLayout={getItemLayout}
// 초기 렌더링 아이템 수
initialNumToRender={10}
// 뷰포트 밖 유지할 아이템 수
maxToRenderPerBatch={10}
// 스크롤 방향 버퍼 크기
windowSize={5}
/>
정리
- FlatList는 가상화 를 통해 보이는 항목만 렌더링하여 성능을 확보합니다
keyExtractor는 반드시 고유한 값을 반환해야 합니다- Pull to Refresh, 무한 스크롤은 거의 모든 앱에서 필요한 패턴입니다
- 그룹화된 데이터에는
SectionList를 사용하세요 getItemLayout을 설정하면 스크롤 성능이 크게 개선됩니다
댓글 로딩 중...