React Native 성능 문제의 80%는 불필요한 리렌더링에서 발생합니다.


리렌더링 추적

TSX
// 리렌더링 횟수 확인 (개발 중)
function useRenderCount(componentName: string) {
  const count = useRef(0);
  count.current += 1;
  console.log(`[${componentName}] 렌더링 횟수: ${count.current}`);
}

function ExpensiveComponent() {
  useRenderCount('ExpensiveComponent');
  // ...
}

React.memo — 불필요한 리렌더링 방지

TSX
// props가 변경되지 않으면 리렌더링 스킵
const ListItem = React.memo(function ListItem({ title, onPress }: {
  title: string;
  onPress: () => void;
}) {
  return (
    <Pressable onPress={onPress} style={styles.item}>
      <Text>{title}</Text>
    </Pressable>
  );
});

// 커스텀 비교 함수
const UserCard = React.memo(
  function UserCard({ user }: { user: User }) {
    return <Text>{user.name}</Text>;
  },
  (prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);

useCallback과 useMemo

TSX
function ParentComponent() {
  const [items, setItems] = useState<Item[]>([]);

  // useCallback — 함수 참조 안정화
  const handlePress = useCallback((id: string) => {
    setItems((prev) => prev.filter((item) => item.id !== id));
  }, []);

  // useMemo — 비싼 계산 결과 캐싱
  const totalPrice = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }, [items]);

  // useMemo — 정렬된 리스트 캐싱
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => b.createdAt - a.createdAt);
  }, [items]);

  return (
    <FlatList
      data={sortedItems}
      renderItem={({ item }) => (
        <ListItem
          title={item.title}
          onPress={() => handlePress(item.id)}
        />
      )}
    />
  );
}

useCallbackuseMemo를 무분별하게 쓰면 오히려 오버헤드가 됩니다. React.memo된 자식 컴포넌트에 전달하는 props 나 비싼 계산 에만 사용하세요.


FlatList 최적화

TSX
<FlatList
  data={data}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  // 고정 높이일 때 스크롤 성능 향상
  getItemLayout={(_, index) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  })}
  // 렌더링 배치 크기
  maxToRenderPerBatch={10}
  // 뷰포트 버퍼
  windowSize={5}
  // 초기 렌더링 수
  initialNumToRender={10}
  // 제거 지연
  removeClippedSubviews={true}
/>

이미지 최적화

TSX
// 1. 적절한 크기의 이미지 사용
// 4000x3000 원본 대신 400x300 썸네일 사용

// 2. 캐싱 라이브러리 사용
import { Image } from 'expo-image'; // 자동 캐싱

// 3. 지연 로딩
<FlatList
  data={images}
  renderItem={({ item }) => (
    <Image
      source={{ uri: item.thumbnailUrl }} // 썸네일 먼저
      style={{ width: 100, height: 100 }}
      contentFit="cover"
      transition={200}
    />
  )}
/>

InteractionManager — 무거운 작업 지연

TSX
import { InteractionManager } from 'react-native';

function DetailScreen() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 화면 전환 애니메이션이 끝난 후 데이터 로드
    const task = InteractionManager.runAfterInteractions(async () => {
      const result = await fetchHeavyData();
      setData(result);
    });

    return () => task.cancel();
  }, []);

  return data ? <DetailContent data={data} /> : <ActivityIndicator />;
}

Flipper / Perf Monitor

TSX
// 개발 메뉴 → "Show Perf Monitor"
// JS 스레드와 UI 스레드의 FPS를 실시간으로 확인

// React DevTools Profiler
// 어떤 컴포넌트가 왜 리렌더링되었는지 분석

// Flipper
// - 네트워크 요청 모니터링
// - React DevTools 통합
// - 레이아웃 인스펙터

성능 체크리스트

  1. **JS 스레드 60fps 유지 **: 무거운 계산을 메인 스레드에서 분리
  2. **UI 스레드 60fps 유지 **: useNativeDriver: true로 애니메이션 처리
  3. ** 불필요한 리렌더링 방지 **: React.memo, useCallback, useMemo
  4. **FlatList 최적화 **: getItemLayout, windowSize, removeClippedSubviews
  5. ** 이미지 최적화 **: 적절한 크기, 캐싱, 지연 로딩
  6. ** 번들 크기 최소화 **: 불필요한 라이브러리 제거, Tree Shaking

정리

  • 성능 최적화는 ** 측정 먼저, 최적화 나중 **입니다 — Perf Monitor로 병목을 찾으세요
  • React.memouseCallback은 ** 필요한 곳에만** 사용하세요
  • FlatList의 getItemLayout은 ** 스크롤 성능에 가장 큰 영향 **을 미칩니다
  • 화면 전환 후 데이터 로딩에는 InteractionManager를 활용하세요
  • 이미지는 ** 서버에서 적절한 크기로 제공 **하는 것이 가장 효과적입니다
댓글 로딩 중...