이미지가 안 떠요 (캐싱, 메모리, 그리고 에러 처리)
1. "이미지 로딩이 왜 이렇게 느리죠?"
Image.network를 썼는데, 스크롤을 내렸다가 다시 올리면 이미지가 또 깜빡거리며 로딩됩니다.
사용자는 "앱이 버벅거린다"고 느낍니다.
게다가 가끔 이미지가 깨져서 나오거나, 아예 엑박(X)이 뜹니다.
Flutter의 기본 Image.network는 디스크 캐싱(Disk Caching)을 지원하지 않기 때문입니다.
메모리에만 잠깐 들고 있다가, 스크롤 범위 밖으로 나가면(Dispose) 날려버립니다.
2. 해결책 1 - CachedNetworkImage (필수)
Flutter 개발자라면 숨 쉬듯이 써야 하는 패키지, cached_network_image입니다.
이 친구는 이미지를 다운로드해서 로컬 파일 시스템에 저장해둡니다. 다음 실행 때는 인터넷이 끊겨도 이미지가 나옵니다.
dependencies:
cached_network_image: ^3.3.0
CachedNetworkImage(
imageUrl: "https://example.com/image.jpg",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
이것만 써도 성능 문제의 80%는 해결됩니다. 하지만 나머지 20%는 디테일에 있습니다.
3. 해결책 2 - 메모리 폭발 방지 (Resize)
요즘 폰 카메라는 1200만 화소(4000x3000)가 넘습니다. 이 고화질 사진을 작은 썸네일(50x50)에 띄우려고 원본 그대로 로딩하면? 메모리(RAM)가 터집니다. (OOM Crash)
반드시 memCacheWidth나 memCacheHeight를 사용해서 메모리에 올릴 때 리사이징을 해야 합니다.
CachedNetworkImage(
imageUrl: "https://huge-image.com/4k.jpg",
memCacheWidth: 200, // 👈 중요: 실제 렌더링 크기에 맞춰 줄여서 로드
memCacheHeight: 200,
)
디스크에는 원본을 저장하더라도, 메모리에는 줄여서 올려야 앱이 안 죽습니다.
4. 해결책 3 - SSL 인증서 오류 (HandshakeException)
"이미지 URL이 https인데 안 떠요."
로그를 보면 HandshakeException: CERTIFICATE_VERIFY_FAILED가 뜹니다.
개발 서버나 오래된 서버의 SSL 인증서가 유효하지 않아서 그렇습니다.
해결법 (HttpOverrides 사용):
(주의: 배포용 앱에서는 보안상 사용하지 않는 게 좋습니다. 개발 단계에서만 쓰세요.)
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (X509Certificate cert, String host, int port) => true;
}
}
void main() {
HttpOverrides.global = MyHttpOverrides();
runApp(MyApp());
}
5. 제대로 이해하기 1: Shimmer Effect & BlurHash
사용자는 뺑글이(CircularProgressIndicator)를 싫어합니다. 인스타그램이나 유튜브처럼 스켈레톤 UI (Shimmer)를 보여주는 게 UX 국룰입니다.
CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(color: Colors.white),
),
)
더 고급스러운 경험을 원한다면 BlurHash를 쓰세요. 이미지의 평균 색상을 암호화된 문자열로 미리 받아서, 로딩 전부터 "대충 이런 색의 이미지다"라고 흐릿하게 보여주는 기술입니다.
6. HTTP 헤더와 CDN 제대로 파보기
이미지가 갱신되었는데 앱에서는 계속 옛날 이미지가 뜬다면?
캐시 키(Cache Key)가 URL 기준이라서 그렇습니다.
CDN(AWS CloudFront 등)을 쓴다면 보통 URL 뒤에 v=2 같은 쿼리 파라미터를 붙여서 강제로 캐시를 갱신합니다.
만약 Authorization 토큰이 필요한 이미지라면?
CachedNetworkImage(
imageUrl: "https://private.com/profile.jpg",
httpHeaders: {
"Authorization": "Bearer $token", // 👈 헤더 추가 가능
},
)
Memory Cache vs Disk Cache 자세히 살펴보기
캐시는 두 계층으로 나뉩니다.
- Memory Cache (L1):
ImageCache(Flutter 기본). 속도가 매우 빠르지만 앱 끄면 사라짐. RAM을 먹음. - Disk Cache (L2):
sqflite나 파일시스템. 속도는 느리지만(IO 발생) 영구 저장됨. 스토리지 용량을 먹음.
cached_network_image는 1번과 2번을 모두 사용합니다.
이미지를 처음 요청하면: Network -> Disk -> Memory -> UI 순서로 이동합니다.
두 번째 요청하면: Memory -> UI (초고속).
앱 껐다 켜면: Disk -> Memory -> UI.
이미지 캐시 제한 늘리기:
기본 ImageCache는 100MB, 1000개 이미지가 한계입니다. 인스타그램 같은 앱이라면 부족할 수 있습니다.
void main() {
// 캐시 용량 증설
PaintingBinding.instance.imageCache.maximumSizeBytes = 1024 * 1024 * 500; // 500MB
PaintingBinding.instance.imageCache.maximumSize = 5000; // 5000개
runApp(MyApp());
}
Troubleshooting 403 & 404 Errors 더 알아보기
이미지가 안 나올 때 Icon(Icons.error)만 띄우고 넘어가나요?
로그를 보면 403 Forbidden이나 404 Not Found인 경우가 많습니다.
1. AWS S3 403 Forbidden:
URL에 Signed Parameters (Expires, Signature)가 포함되어 있는데 만료된 경우입니다.
앱에서 이미지를 요청할 때마다 서버에서 새로운 Signed URL을 받아와야 합니다.
CachedNetworkImage는 URL이 바뀌면 새 이미지로 인식하므로 자동으로 갱신됩니다.
2. 봇 차단 (User-Agent Blocking):
일부 서버는 모바일 앱(HttpClient)의 접근을 봇으로 오인해 차단합니다.
이럴 때는 user-agent 헤더를 브라우저처럼 위장해야 합니다.
httpHeaders: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
},
3. Hotlinking Protection:
이미지를 다른 사이트에서 못 쓰게 막는 Referer 체크가 걸려있을 수 있습니다.
Referer 헤더에 해당 도메인을 넣어주면 해결됩니다.
9. 요약
CachedNetworkImage는 선택이 아니라 필수다.memCacheWidth로 메모리 폭발을 막아라.- HTTPS 인증서 에러는
HttpOverrides로 우회 가능하다. - UX를 위해 Shimmer나 BlurHash를 적용하라.
- 캐시 용량이 부족하면
PaintingBinding에서 늘려라. - 403/404 에러 시 헤더(User-Agent, Referer)를 확인하라.
이미지만 잘 처리해도 앱의 완성도가 200% 올라갑니다.