FlatList는 화면에 보이는 항목만 렌더링하는 가상화된 리스트 컴포넌트입니다.

모바일 앱에서 리스트는 가장 흔한 UI 패턴입니다. 하지만 1000개의 아이템을 한 번에 렌더링하면 메모리와 성능에 심각한 문제가 생깁니다. FlatList는 이 문제를 가상화(virtualization) 로 해결합니다.


기본 사용법

TSX
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는 화면에 보이는 항목 + 약간의 버퍼 만 실제로 렌더링합니다.

PLAINTEXT
전체 데이터: [0, 1, 2, ..., 999]

실제 렌더링:
  ┌─────────────┐
  │ (버퍼) 아이템 3  │  ← 화면 위 버퍼
  │ (버퍼) 아이템 4  │
  ├─────────────┤
  │ 아이템 5        │  ← 화면에 보이는 영역
  │ 아이템 6        │
  │ 아이템 7        │
  │ 아이템 8        │
  ├─────────────┤
  │ (버퍼) 아이템 9  │  ← 화면 아래 버퍼
  │ (버퍼) 아이템 10 │
  └─────────────┘

  아이템 0~2, 11~999는 렌더링하지 않음!

면접에서 "FlatList와 ScrollView의 차이"를 물으면, 가상화를 핵심으로 설명하세요. ScrollView는 모든 자식을 한 번에 렌더링하고, FlatList는 보이는 것만 렌더링합니다.


구분선과 헤더/푸터

TSX
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 (당겨서 새로고침)

TSX
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)

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

가로 리스트

TSX
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 — 그룹화된 리스트

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

성능 기초 팁

TSX
// 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을 설정하면 스크롤 성능이 크게 개선됩니다
댓글 로딩 중...