개발/flutter

[Flutter] cached_network_image

iris3455 2025. 1. 17. 17:57


cache 에 이미지 저장하기

 

나는 지금까지 웹이든 앱이든 화면에 이미지를 불러올때 그냥 데이터를 불러오는 방식처럼 이미지를 불러오는줄 알았다. 근데 알고 보니  네트워크 이미지를 로드할때는 (특히 Supabase Storage와 같은 클라우드 스토리지에서 이미지를 불러올 때는) 해당 이미지를 클라이언트가 다운로드하는 방식이라고 한다. 이 다운로드 과정에서 네트워크 트래픽이나 egress가 발생하는데 이로 인해 supabase 플랜을 무료에서 pro로 변경하여 돈을 지불한 경험이 있다.. 심지어 다운로드된 이미지는 파일 시스템에 영구적으로 저장된다고 한다. (왠지 느리더라..)

따라서 이미지를 캐시 저장소에 저장하는 방법을 알아보았다.

 

cached_network_image

https://pub.dev/packages/cached_network_image

 

cached_network_image | Flutter package

Flutter library to load and cache network images. Can also be used with placeholder and error widgets.

pub.dev

나는 이미지를 캐시에 저장하기 위한 패키지 중 가장 대중적인 패키지를 선택하였다. CachedNetworkImage는 Flutter에서 네트워크 이미지를 로딩하고, 이를 로컬 캐시에 저장하여 성능을 개선하는 데 도움을 주는 패키지이다. 이를 통해 이미지가 다시 로드될 때 네트워크 요청을 줄이고, 이미 다운로드한 이미지를 빠르게 표시할 수 있다.

 

 

1. 원리

 

  • 이미지 요청
    • imageUrl을 통해 먼저 네트워크에서 이미지를 요청한다. (이때 이미지는 네트워크에서 직접 로드된다.)
    • 처음 요청 시, 이미지는 로컬 캐시에 저장되지 않으므로 네트워크 요청을 통해 다운로드한다.
  • 이미지 캐싱
    • 다운로드된 이미지는 로컬 캐시(앱의 디바이스 내 저장소)에 저장되고 Flutter는 캐시 디렉토리에 이미지를 저장하는 방식으로 이를 관리한다.
    • 이후 같은 이미지를 다시 요청하면, 네트워크 요청을 하지 않고 로컬 캐시에서 이미지를 불러온다.
  • 캐시 만료 및 삭제
    • 캐시된 이미지가 일정 시간이 지나거나, 용량이 너무 커지면 자동으로 삭제된다. 이 부분은 앱의 정책에 맞춰 설정 가능하다.
  • 로딩 상태와 오류 처리
    • 이미지가 로드되는 동안에는 placeholder 위젯을 사용하여 로딩 중 표시를 해주며, errorListener 콜백으로 에러를 감지하여 오류가 발생하면 errorWidget 위젯을 사용해 에러 상태를 보여줄 수 있다.
  • 캐시 삭제
    • flutter_cache_manager를 통해서 캐시 만료 기간을 설정할 수 있다.
    • CachedNetworkImage의 evictFromCache() 메서드를 사용하여 특정 이미지의 캐시를 수동으로 삭제가 가능하다.
    • flutter_cache_manager의 clearAll() 메서드를 사용하면, 앱 내에서 저장된 모든 캐시를 삭제할 수 있다.

 

 

 

2. 최종코드

import 'package:collecter/data/services/api_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:cached_network_image/cached_network_image.dart';

import '../../data/services/image_service.dart';

class ImageWidget extends StatelessWidget {
  final String storageFolderName;
  final String imageFilePath;
  final double borderRadius;

  const ImageWidget({
    Key? key,
    required this.storageFolderName,
    required this.imageFilePath,
    required this.borderRadius,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final String _imageUrl =
        ImageService.getFullImageUrl(storageFolderName, imageFilePath);

    return CachedNetworkImage(
        imageUrl: _imageUrl,
        imageBuilder: (context, imageProvider) => Container(
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(borderRadius),
                border: Border.all(
                  color: const Color(0xFFdee2e6),
                  width: 0.5.w,
                ),
                image: DecorationImage(
                  image: imageProvider,
                  fit: BoxFit.cover,
                ),
              ),
            ),
        errorListener: (error) {
          print("CachedNetworkImageProvider: Image failed to load!");
        },
        errorWidget: (context, url, error) {
          ApiService.trackError(
              error, StackTrace.current, 'Exception in CachedNetworkImage');
          print("CachedNetworkImageProvider: Image failed to load!");

          return Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(borderRadius),
              border: Border.all(
                color: const Color(0xFFdee2e6),
                width: 0.5.w,
              ),
            ),
            color: const Color(0xFFf1f3f5),
          );
        });
  }
}
ImageWidget(
	storageFolderName:'${collectionDetail.userId}/collections',
	imageFilePath: collectionDetail.imageFilePath!,
	borderRadius: 8.r,
)

 

 

 

끝으로

 

이전 회사에서 웹의 문제가 생기면 캐시와 쿠키를 정리하고 다시 페이지를 띄워보라고 하는데 캐시가 이런 기능이었구나. 미리 알았다면 내돈 34000원이 안나갔을테지만.. 지금이라도 알게돼서 다행이다. 점점 알아가고 배워가는게 많아져가면서 원리를 이해하게 되면서 발전한다는 생각이 들어 매우 좋았다. 개발자라는 직업에만 국한되는 경험은 아니겠지만 확실히 이슈를 해결해나가면서 많은걸 배울수 있는것 같다. 앞으로도 많이 배울수 있게 많은 이슈를 경험해가면 좋을것 같다!

Usage중 Egress 부분은 거의 없다는걸 볼 수 있다!