이미지는 앱 성능에 가장 큰 영향을 미치는 요소 중 하나입니다. 제대로 된 로딩과 캐싱 전략이 필수입니다.

프로필 사진, 상품 이미지, 배너 등 모바일 앱에서 이미지는 어디에나 있습니다. 하지만 최적화 없이 사용하면 메모리 폭발, 버벅거림, 데이터 낭비로 이어집니다.


이미지 소스 종류

로컬 이미지

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

// 번들에 포함되는 정적 이미지
<Image source={require('./assets/logo.png')} style={{ width: 100, height: 100 }} />

// @2x, @3x 자동 선택
// assets/logo.png      → 1x
// assets/logo@2x.png   → 2x
// assets/logo@3x.png   → 3x
// 디바이스 해상도에 맞는 이미지를 자동으로 선택

네트워크 이미지

TSX
// 반드시 width, height 지정 필요
<Image
  source={{ uri: 'https://example.com/photo.jpg' }}
  style={{ width: 300, height: 200 }}
/>

// 헤더 추가 (인증이 필요한 이미지)
<Image
  source={{
    uri: 'https://api.example.com/photo/1',
    headers: {
      Authorization: 'Bearer token123',
    },
  }}
  style={{ width: 300, height: 200 }}
/>

Base64 이미지

TSX
// 작은 아이콘이나 인라인 이미지에 적합
<Image
  source={{ uri: 'data:image/png;base64,iVBOR...' }}
  style={{ width: 50, height: 50 }}
/>

로딩 상태 처리

TSX
import { useState } from 'react';
import { Image, View, ActivityIndicator, StyleSheet } from 'react-native';

function ImageWithLoading({ uri }: { uri: string }) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);

  return (
    <View style={styles.container}>
      {loading && (
        <View style={styles.placeholder}>
          <ActivityIndicator />
        </View>
      )}

      {error ? (
        // 에러 시 대체 이미지
        <Image
          source={require('./assets/fallback.png')}
          style={styles.image}
        />
      ) : (
        <Image
          source={{ uri }}
          style={styles.image}
          onLoadStart={() => setLoading(true)}
          onLoadEnd={() => setLoading(false)}
          onError={() => {
            setError(true);
            setLoading(false);
          }}
          // 점진적 로딩 (iOS만)
          progressiveRenderingEnabled
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    width: 300,
    height: 200,
    borderRadius: 8,
    overflow: 'hidden',
  },
  image: {
    width: '100%',
    height: '100%',
  },
  placeholder: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f0f0f0',
  },
});

이미지 프리페칭

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

// 미리 이미지를 다운로드하여 캐시에 저장
async function prefetchImages(urls: string[]) {
  const prefetchTasks = urls.map((url) => Image.prefetch(url));
  await Promise.all(prefetchTasks);
}

// 화면 진입 전에 필요한 이미지를 미리 로드
async function onScreenFocus() {
  await prefetchImages([
    'https://example.com/banner1.jpg',
    'https://example.com/banner2.jpg',
  ]);
}

expo-image (Expo 프로젝트)

Expo에서는 expo-image를 사용하면 기본 Image보다 훨씬 나은 성능을 얻을 수 있습니다.

TSX
import { Image } from 'expo-image';

// blurhash로 플레이스홀더 표시
const blurhash = 'LEHV6nWB2yk8pyoJadR*.7kCMdnj';

function OptimizedImage({ uri }: { uri: string }) {
  return (
    <Image
      source={uri}
      placeholder={{ blurhash }}
      contentFit="cover"
      transition={200}      // 페이드인 애니메이션
      style={{ width: 300, height: 200 }}
      // 자동 캐싱 지원
      cachePolicy="memory-disk"
    />
  );
}

expo-image vs react-native-fast-image

기능expo-imageFastImage
캐싱자동 (memory + disk)수동 설정
Blurhash지원미지원
애니메이션 GIF지원지원
SVG지원미지원
유지보수활발중단됨

반응형 이미지

TSX
import { useWindowDimensions, Image, StyleSheet } from 'react-native';

function ResponsiveImage({ uri, aspectRatio = 16 / 9 }: {
  uri: string;
  aspectRatio?: number;
}) {
  const { width } = useWindowDimensions();
  const imageWidth = width - 32; // 양쪽 패딩
  const imageHeight = imageWidth / aspectRatio;

  return (
    <Image
      source={{ uri }}
      style={{
        width: imageWidth,
        height: imageHeight,
        borderRadius: 8,
      }}
      resizeMode="cover"
    />
  );
}

// aspectRatio 스타일 속성 사용
function AspectRatioImage({ uri }: { uri: string }) {
  return (
    <Image
      source={{ uri }}
      style={{
        width: '100%',
        aspectRatio: 16 / 9,  // 비율 자동 계산
        borderRadius: 8,
      }}
      resizeMode="cover"
    />
  );
}

이미지 리스트 최적화

TSX
import { FlatList, Image, View, StyleSheet } from 'react-native';

function ImageGrid({ images }: { images: string[] }) {
  return (
    <FlatList
      data={images}
      numColumns={3}
      keyExtractor={(item, index) => index.toString()}
      renderItem={({ item }) => (
        <View style={styles.gridItem}>
          <Image
            source={{ uri: item }}
            style={styles.gridImage}
            resizeMode="cover"
          />
        </View>
      )}
      // 성능 최적화
      getItemLayout={(_, index) => ({
        length: ITEM_SIZE,
        offset: ITEM_SIZE * Math.floor(index / 3),
        index,
      })}
      // 한 번에 렌더링할 이미지 수 제한
      initialNumToRender={9}
      maxToRenderPerBatch={6}
      windowSize={3}
    />
  );
}

const ITEM_SIZE = 130;

const styles = StyleSheet.create({
  gridItem: {
    flex: 1,
    margin: 1,
    aspectRatio: 1,
  },
  gridImage: {
    width: '100%',
    height: '100%',
  },
});

메모리 관리 팁

  1. **적절한 크기의 이미지 사용 **: 4000x3000 이미지를 100x100으로 표시하면 메모리 낭비
  2. resizeMethod: Android에서 큰 이미지를 작게 표시할 때 resize 사용
  3. ** 캐시 정책 **: 불필요한 캐시는 정기적으로 정리
  4. ** 리스트 이미지 **: FlatList의 가상화를 활용하여 보이지 않는 이미지 언마운트
TSX
// Android에서 큰 이미지 최적화
<Image
  source={{ uri: largeImageUrl }}
  style={{ width: 100, height: 100 }}
  resizeMethod="resize"  // 메모리에서 리사이즈 후 렌더링
  resizeMode="cover"
/>

정리

  • 네트워크 이미지는 ** 반드시 크기를 지정 **해야 합니다
  • 로딩/에러 상태를 처리하여 ** 사용자 경험을 개선 **하세요
  • Expo 프로젝트에서는 expo-image 사용을 권장합니다
  • aspectRatio 스타일을 활용하면 반응형 이미지를 쉽게 구현할 수 있습니다
  • 큰 이미지는 ** 서버에서 적절한 크기로 리사이즈 **한 후 전달하는 것이 가장 효과적입니다
댓글 로딩 중...