React Native 리스트 컴포넌트

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[커스텀 최적화]

1. ScrollView

개요

가장 기본적인 스크롤 가능한 컨테이너로, 모든 자식 컴포넌트를 한 번에 렌더링합니다.

주요 특징

  • 즉시 렌더링: 모든 자식을 메모리에 로드
  • 유연성: 다양한 레이아웃 구성 가능
  • 단순성: 복잡한 설정 없이 즉시 사용 가능

사용 예시

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>
  );
}

주요 Props

Prop타입설명
horizontalboolean수평 스크롤 활성화
pagingEnabledboolean페이지 단위 스크롤
refreshControlelementPull-to-refresh 기능
stickyHeaderIndicesarray고정 헤더 인덱스
onScrollfunction스크롤 이벤트 핸들러

장점

  • ✅ 구현이 간단하고 직관적
  • ✅ 모든 React Native 컴포넌트와 호환
  • ✅ 복잡한 레이아웃 구성 가능
  • ✅ 즉각적인 스크롤 반응

단점

  • ❌ 대용량 데이터에서 성능 문제
  • ❌ 메모리 사용량이 아이템 수에 비례
  • ❌ 초기 로딩 시간이 길어질 수 있음

사용 시나리오

  • 📱 50개 이하의 작은 리스트
  • 📱 복잡한 레이아웃이 필요한 경우
  • 📱 각 아이템의 높이가 동적인 경우

2. FlatList

개요

React Native에서 가장 많이 사용되는 리스트 컴포넌트로, 가상화(virtualization)를 통해 대용량 데이터를 효율적으로 처리합니다.

주요 특징

  • 가상화: 화면에 보이는 아이템만 렌더링
  • 성능 최적화: 자동 메모리 관리
  • 풍부한 기능: 헤더, 푸터, 구분선, Pull-to-refresh 등

사용 예시

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}
    />
  );
}

성능 최적화 Props 상세 설명

// 성능 최적화 예시
<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__}
/>

주요 Props

Prop타입설명기본값
dataarray렌더링할 데이터 배열required
renderItemfunction각 아이템 렌더링 함수required
keyExtractorfunction고유 키 추출 함수item.key
horizontalboolean수평 스크롤false
numColumnsnumber열 개수 (그리드 레이아웃)1
invertedboolean역순 렌더링false

장점

  • ✅ 대용량 데이터 처리에 최적화
  • ✅ 메모리 효율적
  • ✅ 풍부한 기능 제공
  • ✅ 성능 튜닝 옵션 다양

단점

  • ❌ 동적 높이 아이템에서 스크롤 점프 발생 가능
  • ❌ 복잡한 레이아웃 구성 제한적
  • ❌ 중첩된 스크롤 뷰 처리 어려움

사용 시나리오

  • 📱 대용량 데이터 리스트 (수천~수만 개)
  • 📱 일정한 레이아웃의 아이템
  • 📱 무한 스크롤이 필요한 경우
  • 📱 그리드 레이아웃

3. SectionList

개요

섹션별로 그룹화된 데이터를 표시하는데 특화된 리스트 컴포넌트입니다.

주요 특징

  • 섹션 헤더: 각 섹션별 헤더 지원
  • 고정 헤더: 스크롤 시 헤더 고정 가능
  • FlatList 기반: FlatList의 모든 최적화 기능 포함

사용 예시

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>
  );
}

주요 Props (FlatList와 차이점)

Prop타입설명
sectionsarray섹션 데이터 배열
renderSectionHeaderfunction섹션 헤더 렌더링
renderSectionFooterfunction섹션 푸터 렌더링
stickySectionHeadersEnabledboolean헤더 고정 여부
SectionSeparatorComponentcomponent섹션 구분선

장점

  • ✅ 데이터 그룹화에 최적화
  • ✅ 고정 헤더 기능 내장
  • ✅ 섹션별 커스터마이징 가능
  • ✅ FlatList의 모든 성능 최적화 포함

단점

  • ❌ 단순 리스트에는 과도한 기능
  • ❌ 데이터 구조가 복잡
  • ❌ 섹션 간 이동이 복잡할 수 있음

사용 시나리오

  • 📱 연락처 목록
  • 📱 카테고리별 상품 목록
  • 📱 날짜별 그룹화된 데이터
  • 📱 설정 화면

4. VirtualizedList

개요

FlatList와 SectionList의 기반이 되는 저수준 컴포넌트로, 최대한의 커스터마이징이 필요한 경우 사용합니다.

주요 특징

  • 완전한 제어: 모든 가상화 로직 커스터마이징 가능
  • 유연성: 복잡한 데이터 구조 처리
  • 저수준 API: 세밀한 성능 튜닝 가능

사용 예시

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
    />
  );
}

주요 Props (FlatList와 차이점)

Prop타입설명
getItemfunction데이터에서 아이템 추출
getItemCountfunction전체 아이템 수 반환
overscannumber뷰포트 밖 추가 렌더링 수
viewabilityConfigobject가시성 판단 설정
onViewableItemsChangedfunction보이는 아이템 변경 콜백

장점

  • ✅ 완전한 커스터마이징 가능
  • ✅ 복잡한 레이아웃 구현 가능
  • ✅ 세밀한 성능 제어
  • ✅ 특수한 요구사항 충족

단점

  • ❌ 구현이 복잡함
  • ❌ 더 많은 코드 필요
  • ❌ 잘못 사용 시 성능 저하
  • ❌ 기본 기능도 직접 구현 필요

사용 시나리오

  • 📱 특수한 스크롤 동작이 필요한 경우
  • 📱 복잡한 데이터 구조
  • 📱 커스텀 가상화 로직
  • 📱 양방향 무한 스크롤

🎯 컴포넌트 선택 가이드

의사결정 플로우차트

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',
  },
});

💡 성능 최적화 팁

1. 이미지 최적화

// 이미지가 포함된 리스트 아이템
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>
  );
});

2. 무거운 연산 최적화

// 복잡한 데이터 처리가 필요한 경우
const ComplexItem = React.memo(({ item }) => {
  // 무거운 연산은 useMemo로 캐싱
  const processedData = useMemo(() => {
    return expensiveCalculation(item);
  }, [item.id]); // 의존성 최소화

  return (
    <View style={styles.item}>
      <Text>{processedData.result}</Text>
    </View>
  );
});

3. 인터랙션 최적화

// 터치 이벤트 최적화
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>
  );
});

🐛 일반적인 문제와 해결책

1. FlatList 스크롤 점프 문제

// 문제: 동적 높이 아이템에서 스크롤 위치가 점프
// 해결: estimatedItemSize 제공
<FlatList
  data={data}
  renderItem={renderItem}
  estimatedItemSize={100} // 평균 높이 추정값
  // 또는 getItemLayout 구현
/>

2. 무한 스크롤 중복 호출

// 문제: onEndReached가 여러 번 호출됨
// 해결: 로딩 상태 체크
const [isLoadingMore, setIsLoadingMore] = useState(false);

const handleEndReached = () => {
  if (!isLoadingMore && hasMoreData) {
    setIsLoadingMore(true);
    loadMoreData().finally(() => setIsLoadingMore(false));
  }
};

3. 메모리 누수

// 문제: 언마운트 후에도 비동기 작업 계속됨
// 해결: cleanup 함수 사용
useEffect(() => {
  let isMounted = true;
  
  fetchData().then(data => {
    if (isMounted) {
      setData(data);
    }
  });
  
  return () => {
    isMounted = false;
  };
}, []);

마무리

React Native에서 리스트를 구현할 때는 각 컴포넌트의 특성을 이해하고 상황에 맞는 최적의 선택을 하는 것이 중요합니다.

  • ScrollView: 작고 단순한 리스트
  • FlatList: 대부분의 일반적인 리스트
  • SectionList: 그룹화가 필요한 리스트
  • VirtualizedList: 특수한 요구사항이 있는 경우

성능 최적화는 선택사항이 아닌 필수사항임을 기억하고, 항상 실제 디바이스에서 테스트하여 최적의 사용자 경험을 제공하세요! 🚀

참고 자료

profile
takeaways

0개의 댓글