이미지 처리 — 로컬, 네트워크, 캐싱 전략
이미지는 앱 성능에 가장 큰 영향을 미치는 요소 중 하나입니다. 제대로 된 로딩과 캐싱 전략이 필수입니다.
프로필 사진, 상품 이미지, 배너 등 모바일 앱에서 이미지는 어디에나 있습니다. 하지만 최적화 없이 사용하면 메모리 폭발, 버벅거림, 데이터 낭비로 이어집니다.
이미지 소스 종류
로컬 이미지
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
// 디바이스 해상도에 맞는 이미지를 자동으로 선택
네트워크 이미지
// 반드시 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 이미지
// 작은 아이콘이나 인라인 이미지에 적합
<Image
source={{ uri: 'data:image/png;base64,iVBOR...' }}
style={{ width: 50, height: 50 }}
/>
로딩 상태 처리
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',
},
});
이미지 프리페칭
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보다 훨씬 나은 성능을 얻을 수 있습니다.
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-image | FastImage |
|---|---|---|
| 캐싱 | 자동 (memory + disk) | 수동 설정 |
| Blurhash | 지원 | 미지원 |
| 애니메이션 GIF | 지원 | 지원 |
| SVG | 지원 | 미지원 |
| 유지보수 | 활발 | 중단됨 |
반응형 이미지
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"
/>
);
}
이미지 리스트 최적화
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%',
},
});
메모리 관리 팁
- **적절한 크기의 이미지 사용 **: 4000x3000 이미지를 100x100으로 표시하면 메모리 낭비
- resizeMethod: Android에서 큰 이미지를 작게 표시할 때
resize사용 - ** 캐시 정책 **: 불필요한 캐시는 정기적으로 정리
- ** 리스트 이미지 **: FlatList의 가상화를 활용하여 보이지 않는 이미지 언마운트
// Android에서 큰 이미지 최적화
<Image
source={{ uri: largeImageUrl }}
style={{ width: 100, height: 100 }}
resizeMethod="resize" // 메모리에서 리사이즈 후 렌더링
resizeMode="cover"
/>
정리
- 네트워크 이미지는 ** 반드시 크기를 지정 **해야 합니다
- 로딩/에러 상태를 처리하여 ** 사용자 경험을 개선 **하세요
- Expo 프로젝트에서는
expo-image사용을 권장합니다 aspectRatio스타일을 활용하면 반응형 이미지를 쉽게 구현할 수 있습니다- 큰 이미지는 ** 서버에서 적절한 크기로 리사이즈 **한 후 전달하는 것이 가장 효과적입니다
댓글 로딩 중...