
React Native에서 리스트를 구현할 때 사용할 수 있는 모든 컴포넌트의 특징, 사용법, 성능 최적화 방법을 상세히 분석합니다.
graph TD
A[React Native List Components] --> B[ScrollView]
A --> C[FlatList]
A --> D[SectionList]
A --> E[VirtualizedList]
B --> B1[작은 리스트]
C --> C1[대용량 단순 리스트]
D --> D1[섹션별 그룹화]
E --> E1[커스텀 최적화]
가장 기본적인 스크롤 가능한 컨테이너로, 모든 자식 컴포넌트를 한 번에 렌더링합니다.
import { ScrollView, Text, View, RefreshControl } from 'react-native';
function SimpleList() {
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
// 데이터 새로고침 로직
setTimeout(() => setRefreshing(false), 2000);
}, []);
return (
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{[...Array(20)].map((_, index) => (
<View key={index} style={styles.item}>
<Text>Item {index + 1}</Text>
</View>
))}
</ScrollView>
);
}
| Prop | 타입 | 설명 |
|---|---|---|
horizontal | boolean | 수평 스크롤 활성화 |
pagingEnabled | boolean | 페이지 단위 스크롤 |
refreshControl | element | Pull-to-refresh 기능 |
stickyHeaderIndices | array | 고정 헤더 인덱스 |
onScroll | function | 스크롤 이벤트 핸들러 |
React Native에서 가장 많이 사용되는 리스트 컴포넌트로, 가상화(virtualization)를 통해 대용량 데이터를 효율적으로 처리합니다.
import { FlatList, Text, View, ActivityIndicator } from 'react-native';
function OptimizedList() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
// 아이템 렌더링 함수
const renderItem = useCallback(({ item, index }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
</View>
), []);
// 키 추출 함수
const keyExtractor = useCallback((item) => item.id.toString(), []);
// 무한 스크롤을 위한 함수
const loadMore = () => {
if (!loading) {
setLoading(true);
// API 호출 로직
fetchData(page + 1).then(newData => {
setData([...data, ...newData]);
setPage(page + 1);
setLoading(false);
});
}
};
// 아이템 사이 구분선
const ItemSeparator = () => <View style={styles.separator} />;
// 빈 리스트 컴포넌트
const EmptyComponent = () => (
<View style={styles.emptyContainer}>
<Text>데이터가 없습니다.</Text>
</View>
);
// 푸터 컴포넌트 (로딩 인디케이터)
const FooterComponent = () => {
if (!loading) return null;
return (
<View style={styles.footer}>
<ActivityIndicator size="small" />
</View>
);
};
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
// 성능 최적화 Props
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={10}
// UI Props
ItemSeparatorComponent={ItemSeparator}
ListEmptyComponent={EmptyComponent}
ListFooterComponent={FooterComponent}
// 기능 Props
onEndReached={loadMore}
onEndReachedThreshold={0.5}
refreshing={refreshing}
onRefresh={handleRefresh}
// 레이아웃 Props
numColumns={2}
columnWrapperStyle={styles.row}
/>
);
}
// 성능 최적화 예시
<FlatList
// 1. 뷰포트 밖의 아이템을 메모리에서 해제
removeClippedSubviews={true}
// 2. 한 번에 렌더링할 아이템 수
maxToRenderPerBatch={10}
// 3. 배치 렌더링 간격 (ms)
updateCellsBatchingPeriod={50}
// 4. 초기 렌더링 아이템 수
initialNumToRender={10}
// 5. 뷰포트 기준 렌더링 범위 (배수)
windowSize={10}
// 6. 아이템 레이아웃 최적화
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
// 7. 리스트 최적화 디버깅
debug={__DEV__}
/>
| Prop | 타입 | 설명 | 기본값 |
|---|---|---|---|
data | array | 렌더링할 데이터 배열 | required |
renderItem | function | 각 아이템 렌더링 함수 | required |
keyExtractor | function | 고유 키 추출 함수 | item.key |
horizontal | boolean | 수평 스크롤 | false |
numColumns | number | 열 개수 (그리드 레이아웃) | 1 |
inverted | boolean | 역순 렌더링 | false |
섹션별로 그룹화된 데이터를 표시하는데 특화된 리스트 컴포넌트입니다.
import { SectionList, Text, View } from 'react-native';
function GroupedList() {
const DATA = [
{
title: '즐겨찾기',
data: ['Pizza', 'Burger', 'Risotto'],
},
{
title: '메인 요리',
data: ['French Fries', 'Onion Rings', 'Fried Shrimps'],
},
{
title: '디저트',
data: ['Cheese Cake', 'Ice Cream'],
},
];
// 섹션 헤더 렌더링
const renderSectionHeader = ({ section: { title } }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{title}</Text>
</View>
);
// 아이템 렌더링
const renderItem = ({ item, index, section }) => (
<View style={styles.item}>
<Text style={styles.itemText}>{item}</Text>
<Text style={styles.itemMeta}>
섹션: {section.title} | 인덱스: {index}
</Text>
</View>
);
// 섹션 푸터 (선택적)
const renderSectionFooter = ({ section }) => (
<View style={styles.sectionFooter}>
<Text>총 {section.data.length}개 항목</Text>
</View>
);
return (
<SectionList
sections={DATA}
keyExtractor={(item, index) => item + index}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
// 고정 헤더
stickySectionHeadersEnabled={true}
// 섹션 간격
SectionSeparatorComponent={() => <View style={styles.sectionSeparator} />}
// FlatList와 동일한 성능 최적화 Props
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={10}
// 인덱스 표시 (iOS)
showsVerticalScrollIndicator={true}
/>
);
}
function ContactsList() {
const [contacts, setContacts] = useState([]);
const sectionListRef = useRef(null);
// 데이터를 알파벳별로 그룹화
const groupedData = useMemo(() => {
const groups = contacts.reduce((acc, contact) => {
const firstLetter = contact.name[0].toUpperCase();
if (!acc[firstLetter]) {
acc[firstLetter] = [];
}
acc[firstLetter].push(contact);
return acc;
}, {});
return Object.keys(groups)
.sort()
.map(letter => ({
title: letter,
data: groups[letter].sort((a, b) => a.name.localeCompare(b.name)),
}));
}, [contacts]);
// 섹션으로 스크롤
const scrollToSection = (sectionIndex) => {
sectionListRef.current?.scrollToLocation({
sectionIndex,
itemIndex: 0,
animated: true,
});
};
// 알파벳 인덱스 바
const AlphabetIndex = () => (
<View style={styles.alphabetIndex}>
{groupedData.map((section, index) => (
<TouchableOpacity
key={section.title}
onPress={() => scrollToSection(index)}
>
<Text style={styles.indexLetter}>{section.title}</Text>
</TouchableOpacity>
))}
</View>
);
return (
<View style={styles.container}>
<SectionList
ref={sectionListRef}
sections={groupedData}
// ... 기타 props
/>
<AlphabetIndex />
</View>
);
}
| Prop | 타입 | 설명 |
|---|---|---|
sections | array | 섹션 데이터 배열 |
renderSectionHeader | function | 섹션 헤더 렌더링 |
renderSectionFooter | function | 섹션 푸터 렌더링 |
stickySectionHeadersEnabled | boolean | 헤더 고정 여부 |
SectionSeparatorComponent | component | 섹션 구분선 |
FlatList와 SectionList의 기반이 되는 저수준 컴포넌트로, 최대한의 커스터마이징이 필요한 경우 사용합니다.
import { VirtualizedList, Text, View } from 'react-native';
function CustomVirtualizedList() {
const [data, setData] = useState([]);
// 데이터 접근 함수들
const getItem = (data, index) => data[index];
const getItemCount = (data) => data.length;
// 커스텀 렌더링 로직
const renderItem = ({ item, index }) => {
// 인덱스에 따른 다른 렌더링 로직
if (index % 5 === 0) {
return (
<View style={[styles.item, styles.highlightedItem]}>
<Text style={styles.highlightedText}>특별 아이템: {item.title}</Text>
</View>
);
}
return (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
);
};
// 커스텀 뷰포트 설정
const viewabilityConfig = {
minimumViewTime: 3000, // 3초 이상 보여야 viewed로 처리
viewAreaCoveragePercentThreshold: 50, // 50% 이상 보여야 함
itemVisiblePercentThreshold: 75, // 아이템의 75% 이상 보여야 함
};
// 보이는 아이템 변경 시 콜백
const onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
console.log('현재 보이는 아이템:', viewableItems);
console.log('변경된 아이템:', changed);
// 분석 이벤트 전송 등
viewableItems.forEach(({ item, index }) => {
trackItemView(item.id, index);
});
}, []);
return (
<VirtualizedList
data={data}
getItem={getItem}
getItemCount={getItemCount}
renderItem={renderItem}
keyExtractor={(item) => item.id}
// 고급 설정
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={onViewableItemsChanged}
// 커스텀 렌더링 범위
overscan={3} // 뷰포트 밖 추가 렌더링 아이템 수
// 성능 최적화
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
// 디버깅
debug={true}
disableVirtualization={false} // 가상화 비활성화 (디버깅용)
/>
);
}
// 양방향 무한 스크롤 구현
function BidirectionalInfiniteList() {
const [data, setData] = useState([]);
const [isLoadingTop, setIsLoadingTop] = useState(false);
const [isLoadingBottom, setIsLoadingBottom] = useState(false);
// 상단 추가 데이터 로드
const loadPrevious = async () => {
if (isLoadingTop) return;
setIsLoadingTop(true);
const newData = await fetchPreviousData();
setData([...newData, ...data]);
setIsLoadingTop(false);
};
// 하단 추가 데이터 로드
const loadNext = async () => {
if (isLoadingBottom) return;
setIsLoadingBottom(true);
const newData = await fetchNextData();
setData([...data, ...newData]);
setIsLoadingBottom(false);
};
// 스크롤 위치에 따른 데이터 로드
const onScroll = (event) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
// 상단 근처
if (contentOffset.y < 100) {
loadPrevious();
}
// 하단 근처
if (contentOffset.y + layoutMeasurement.height > contentSize.height - 100) {
loadNext();
}
};
return (
<VirtualizedList
data={data}
// 중앙에서 시작
initialScrollIndex={Math.floor(data.length / 2)}
onScroll={onScroll}
scrollEventThrottle={16}
// ... 기타 props
/>
);
}
| Prop | 타입 | 설명 |
|---|---|---|
getItem | function | 데이터에서 아이템 추출 |
getItemCount | function | 전체 아이템 수 반환 |
overscan | number | 뷰포트 밖 추가 렌더링 수 |
viewabilityConfig | object | 가시성 판단 설정 |
onViewableItemsChanged | function | 보이는 아이템 변경 콜백 |
graph TD
A[리스트가 필요한가?] -->|Yes| B{데이터 개수?}
B -->|50개 이하| C[ScrollView]
B -->|50개 이상| D{그룹화 필요?}
D -->|Yes| E[SectionList]
D -->|No| F{특수 요구사항?}
F -->|Yes| G[VirtualizedList]
F -->|No| H[FlatList]
| 컴포넌트 | 초기 로딩 | 메모리 사용 | 스크롤 성능 | 구현 난이도 |
|---|---|---|---|---|
| ScrollView | ⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| FlatList | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| SectionList | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| VirtualizedList | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ |
## FlatList 성능 최적화 체크리스트
### 필수 최적화
- [ ] keyExtractor 구현
- [ ] getItemLayout 구현 (고정 높이인 경우)
- [ ] removeClippedSubviews={true}
- [ ] windowSize 조정 (기본값: 21)
### 렌더링 최적화
- [ ] renderItem을 별도 컴포넌트로 분리
- [ ] React.memo 사용
- [ ] useCallback으로 함수 메모이제이션
### 데이터 최적화
- [ ] 불변성 유지
- [ ] 얕은 비교 가능한 데이터 구조
- [ ] 필요한 데이터만 전달
### 디버깅
- [ ] getItemLayout 정확성 검증
- [ ] 불필요한 리렌더링 확인
- [ ] 메모리 누수 체크
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
item: {
backgroundColor: 'white',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 5,
},
description: {
fontSize: 14,
color: '#666',
},
separator: {
height: 1,
backgroundColor: '#e0e0e0',
marginLeft: 16,
marginRight: 16,
},
sectionHeader: {
backgroundColor: '#f0f0f0',
paddingHorizontal: 16,
paddingVertical: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 50,
},
footer: {
paddingVertical: 20,
alignItems: 'center',
},
});
// 이미지가 포함된 리스트 아이템
const OptimizedImageItem = React.memo(({ item }) => {
return (
<View style={styles.item}>
<Image
source={{ uri: item.imageUrl }}
style={styles.image}
resizeMode="cover"
// 성능 최적화
progressiveRenderingEnabled={true}
fadeDuration={0} // Android에서 페이드 제거
/>
<Text>{item.title}</Text>
</View>
);
});
// 복잡한 데이터 처리가 필요한 경우
const ComplexItem = React.memo(({ item }) => {
// 무거운 연산은 useMemo로 캐싱
const processedData = useMemo(() => {
return expensiveCalculation(item);
}, [item.id]); // 의존성 최소화
return (
<View style={styles.item}>
<Text>{processedData.result}</Text>
</View>
);
});
// 터치 이벤트 최적화
const InteractiveItem = React.memo(({ item, onPress }) => {
// 핸들러 메모이제이션
const handlePress = useCallback(() => {
onPress(item.id);
}, [item.id, onPress]);
return (
<TouchableOpacity
style={styles.item}
onPress={handlePress}
// 터치 피드백 최적화
activeOpacity={0.7}
delayPressIn={50}
>
<Text>{item.title}</Text>
</TouchableOpacity>
);
});
// 문제: 동적 높이 아이템에서 스크롤 위치가 점프
// 해결: estimatedItemSize 제공
<FlatList
data={data}
renderItem={renderItem}
estimatedItemSize={100} // 평균 높이 추정값
// 또는 getItemLayout 구현
/>
// 문제: onEndReached가 여러 번 호출됨
// 해결: 로딩 상태 체크
const [isLoadingMore, setIsLoadingMore] = useState(false);
const handleEndReached = () => {
if (!isLoadingMore && hasMoreData) {
setIsLoadingMore(true);
loadMoreData().finally(() => setIsLoadingMore(false));
}
};
// 문제: 언마운트 후에도 비동기 작업 계속됨
// 해결: cleanup 함수 사용
useEffect(() => {
let isMounted = true;
fetchData().then(data => {
if (isMounted) {
setData(data);
}
});
return () => {
isMounted = false;
};
}, []);
React Native에서 리스트를 구현할 때는 각 컴포넌트의 특성을 이해하고 상황에 맞는 최적의 선택을 하는 것이 중요합니다.
성능 최적화는 선택사항이 아닌 필수사항임을 기억하고, 항상 실제 디바이스에서 테스트하여 최적의 사용자 경험을 제공하세요! 🚀