React Native FlatList: 대용량 데이터 렌더링 최적화와 가상화 전략
FlatList의 가상화 메커니즘과 ScrollView 대비 성능 이점 분석
React Native에서 ScrollView는 다양한 형태의 콘텐츠를 스크롤 가능한 영역에 표시하는 범용 컨테이너입니다. 폼, 상세 페이지, 혼합 레이아웃 등 서로 다른 구조의 콘텐츠를 조합할 때 필수적인 컴포넌트입니다.
그러나 ScrollView는 모든 자식을 초기에 렌더링하는 특성 때문에, 잘못 사용하면 심각한 성능 문제를 야기할 수 있습니다. 이 글에서는 ScrollView의 올바른 사용법과 FlatList와의 차이점, 그리고 실전 최적화 전략을 살펴봅니다.
모든 자식을 즉시 렌더링:
<ScrollView>
<Header />
<Banner />
<ProductList items={items} /> {/* 100개 항목 모두 렌더링 */}
<Footer />
</ScrollView>렌더링 동작:
화면에 보이는 항목만 렌더링:
<FlatList
data={items}
renderItem={({ item }) => <ProductCard item={item} />}
// 화면에 보이는 ~10개 항목만 렌더링
/>렌더링 동작:
| 상황 | ScrollView | FlatList |
|---|---|---|
| 항목 수 | 10개 미만 | 50개 이상 |
| 콘텐츠 구조 | 서로 다른 레이아웃 | 동일한 구조 반복 |
| 사용 사례 | 폼, 상세 페이지, 대시보드 | 목록, 피드, 검색 결과 |
| 초기 로딩 | 느림 (모두 렌더링) | 빠름 (일부만 렌더링) |
| 메모리 사용 | 항목 수에 비례 | 거의 일정 |
| 스크롤 성능 | [지원] 우수 | [지원] 우수 |
import { ScrollView, View, Text, StyleSheet } from 'react-native';
const ProfileScreen = () => {
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>사용자 프로필</Text>
</View>
<View style={styles.section}>
<Text style={styles.label}>이름</Text>
<Text style={styles.value}>홍길동</Text>
</View>
<View style={styles.section}>
<Text style={styles.label}>이메일</Text>
<Text style={styles.value}>hong@example.com</Text>
</View>
{/* 더 많은 섹션들... */}
</ScrollView>
);
};<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.carousel}
>
<Image source={{ uri: 'image1.jpg' }} style={styles.image} />
<Image source={{ uri: 'image2.jpg' }} style={styles.image} />
<Image source={{ uri: 'image3.jpg' }} style={styles.image} />
</ScrollView>활용 사례:
<ScrollView
horizontal
pagingEnabled // 페이지 단위로 스냅
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={(event) => {
const pageIndex = Math.round(
event.nativeEvent.contentOffset.x / width
);
setCurrentPage(pageIndex);
}}
>
<View style={{ width }}>
<Text>페이지 1</Text>
</View>
<View style={{ width }}>
<Text>페이지 2</Text>
</View>
<View style={{ width }}>
<Text>페이지 3</Text>
</View>
</ScrollView>사용 사례:
import { RefreshControl } from 'react-native';
const [refreshing, setRefreshing] = useState(false);
const onRefresh = async () => {
setRefreshing(true);
await fetchNewData();
setRefreshing(false);
};
<ScrollView
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#9Bd35A', '#689F38']} // Android
tintColor="#689F38" // iOS
/>
}
>
{/* 콘텐츠 */}
</ScrollView><ScrollView
maximumZoomScale={3}
minimumZoomScale={1}
bouncesZoom={true}
>
<Image
source={{ uri: 'photo.jpg' }}
style={{ width: 300, height: 400 }}
/>
</ScrollView>사용 사례:
<ScrollView stickyHeaderIndices={[0]}>
<View style={styles.stickyHeader}>
<Text>고정된 헤더</Text>
</View>
<View style={styles.content}>
{/* 스크롤 가능한 콘텐츠 */}
</View>
</ScrollView><ScrollView
keyboardShouldPersistTaps="handled" // 키보드 외부 탭 처리
keyboardDismissMode="on-drag" // 드래그 시 키보드 숨김
>
<TextInput placeholder="이름" />
<TextInput placeholder="이메일" />
<TextInput placeholder="비밀번호" />
</ScrollView>keyboardDismissMode 옵션:
"none": 자동으로 숨기지 않음"on-drag": 스크롤 시작 시 숨김"interactive" (iOS): 인터랙티브하게 숨김<ScrollView
removeClippedSubviews={true} // 화면 밖 뷰 제거
>
{/* 많은 콘텐츠 */}
</ScrollView>주의: iOS에서는 효과 없으며, 일부 레이아웃에서 깜빡임 발생 가능
// [주의] 비효율적
<ScrollView>
<View style={{ padding: 20 }}>
{/* 콘텐츠 */}
</View>
</ScrollView>
// [권장] 효율적
<ScrollView
contentContainerStyle={{ padding: 20 }}
>
{/* 콘텐츠 */}
</ScrollView>const [showHeavyContent, setShowHeavyContent] = useState(false);
useEffect(() => {
// 초기 렌더링 이후 무거운 콘텐츠 로드
setTimeout(() => setShowHeavyContent(true), 100);
}, []);
<ScrollView>
<Header />
<MainContent />
{showHeavyContent && <HeavySection />}
</ScrollView>// [주의] 중첩 스크롤 (성능 문제)
<ScrollView>
<ScrollView horizontal>
{/* 수평 스크롤 */}
</ScrollView>
</ScrollView>
// [권장] nestedScrollEnabled 사용 (Android)
<ScrollView>
<ScrollView horizontal nestedScrollEnabled>
{/* 수평 스크롤 */}
</ScrollView>
</ScrollView>const RegisterForm = () => {
return (
<ScrollView
contentContainerStyle={styles.container}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.title}>회원가입</Text>
<TextInput placeholder="이름" style={styles.input} />
<TextInput placeholder="이메일" style={styles.input} />
<TextInput
placeholder="비밀번호"
secureTextEntry
style={styles.input}
/>
<Button title="가입하기" onPress={handleSubmit} />
</ScrollView>
);
};const ProductDetail = ({ product }) => {
return (
<ScrollView>
<ScrollView horizontal pagingEnabled>
{product.images.map(img => (
<Image key={img} source={{ uri: img }} style={styles.image} />
))}
</ScrollView>
<View style={styles.info}>
<Text style={styles.name}>{product.name}</Text>
<Text style={styles.price}>{product.price}원</Text>
<Text style={styles.description}>{product.description}</Text>
</View>
<View style={styles.reviews}>
{/* 리뷰 목록 (소수) */}
</View>
</ScrollView>
);
};const Dashboard = () => {
return (
<ScrollView>
<View style={styles.stats}>
<StatCard title="오늘 방문자" value="1,234" />
<StatCard title="신규 가입" value="56" />
</View>
<View style={styles.chart}>
<LineChart data={chartData} />
</View>
<View style={styles.recentActivity}>
<Text style={styles.sectionTitle}>최근 활동</Text>
{activities.slice(0, 5).map(activity => (
<ActivityItem key={activity.id} data={activity} />
))}
</View>
</ScrollView>
);
};// [주의] 성능 문제 발생
<ScrollView>
{items.map(item => (
<ProductCard key={item.id} data={item} />
))}
</ScrollView>
// [권장] FlatList 사용
<FlatList
data={items}
renderItem={({ item }) => <ProductCard data={item} />}
keyExtractor={item => item.id}
/>// [주의] 스크롤 안 됨
<View style={{ flex: 1 }}>
<ScrollView>
{/* 콘텐츠 */}
</ScrollView>
</View>
// [권장] 정상 동작
<ScrollView style={{ flex: 1 }}>
{/* 콘텐츠 */}
</ScrollView>// [주의] 잘못된 사용
<ScrollView style={{ padding: 20 }}>
{/* padding이 스크롤 영역에 적용되지 않음 */}
</ScrollView>
// [권장] 올바른 사용
<ScrollView contentContainerStyle={{ padding: 20 }}>
{/* padding이 콘텐츠에 적용됨 */}
</ScrollView>ScrollView는 React Native에서 다양한 형태의 콘텐츠를 스크롤 가능하게 만드는 범용 컨테이너입니다. 폼, 상세 페이지, 대시보드처럼 구조가 다른 콘텐츠를 조합할 때 필수적입니다.
핵심 권장사항:
contentContainerStyle로 패딩/여백 설정keyboardShouldPersistTaps="handled" 사용참고 자료:
FlatList의 가상화 메커니즘과 ScrollView 대비 성능 이점 분석
제스처 응답 시스템의 생명 주기와 다중 컴포넌트 간 터치 이벤트 협상 전략