[플러터] 이미지 Pre-Cache를 통해 성능을 올려보자
앱의 성능을 올려볼 방법이 없을까?
최근 작업을 하면서 성능을 올릴만한게 뭐 없을까? 라는 고민을 하며 회사에서 작업을 하던중 앱의 화면에 asset이 많이 들어가면 화면이 버벅이는거 같은 느낌을 받았습니다.
이 느낌은 아이폰에서 특히 더 많이 느껴지는거같았고 어떻게 해결할 수 있는 방법이 없을지 찾아보던 중 이미지를 precache 할 수 있다는것을 알아냈습니다.
이미지 Precache는 Flutter에서 제공하는 기능 중 하나로, 화면에 표시되기 전에 이미지를 미리 로드하여 캐시에 저장하는 과정입니다. 이를 통해 사용자가 이미지를 요청할 때마다 실시간으로 다운로드하는 대신, 이미지가 사전에 로드되어 빠르게 표시될 수 있습니다.
그래서 어떻게 쓰는건데?
기본적인 사용법은 아래와 같습니다.
이미지를 사용하기 전 precacheImage 메서드를 통해 ImageProvider의 구현체인 NetworkImage 혹은 AssetImage 객체를 생성해 ImageCache 내에 등록하는겁니다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// precacheImage(NetworkImage('https://example.com/image.jpg'), context); // or
precacheImage(AssetImage('assets/image.jpg'), context);
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Image Precache Example'),
),
body: Center(
child: Image.network('https://example.com/image.jpg'),
),
);
}
}
이런식으로 새로운 개념을 가지고 놀고있었는데 문득 든 생각이 '이거 플러그인으로 뺴놓으면 편하겠는데?' 라는 생각이 들었습니다.
그때부터 바로 뚝딱뚝딱 작업에 돌입!
플러그인으로 만들어보자!
메인 페이지에서 캐시된 이미지와 캐시되지 않은 이미지를 구분해서 보기위해 각 페이지를 나누고 assets 폴더에 들어있는 모든 이미지를 캐시하게끔 작업했습니다.
여기서 들었던 생각이 '그러면 에셋의 모든 경로를 하드코딩으로 입력해줘야하나? 그럼 너무 비효율적인데?' 라고 생각하고 로컬 파일주소에 접근할 수 있는 방법을 찾다가 아래와 같은 방법을 발견했습니다.
final manifestJson = await DefaultAssetBundle.of(context).loadString('AssetManifest.json');
final Map<String, dynamic> manifestMap = json.decode(manifestJson);
final assetList = manifestMap.keys.toList();
AssetManifest.json 파일은 Flutter 앱에서 사용되는 에셋들의 정보를 포함하는 파일입니다. 이 파일은 앱이 빌드될 때 생성되며, 앱이 어떤 에셋을 포함하고 있는지에 대한 메타데이터를 담고 있습니다.
AssetManifest.json 파일을 까보면 이런식으로 생겼고 각 경로의 key 값을 모아 내 로컬의 에셋의 경로를 찾아올 수 있었습니다.
{
"assets/test_cached.jpg": [
"assets/test_cached.jpg"
],
"packages/cupertino_icons/assets/CupertinoIcons.ttf": [
"packages/cupertino_icons/assets/CupertinoIcons.ttf"
]
}
또한 이미지가 아닌 에셋들을 걸러내 주기 위해 아래와 같이 필터링을 실행해줍니다.
List<String> extensionList = ['svg', 'png', 'jpg', 'jpeg'];
imageList.removeWhere((e) => !extensionList.contains(e.split('.').last));
위에서 얻은 경로를 바탕으로 svg 파일과 기타 이미지 파일을 나눠서 precache를 진행하면 거의 완성입니다.
/// 이미지 전달받아서 캐싱함
void _imageCache(String path, BuildContext context) {
bool isSvg = path.contains('.svg');
if(isSvg){
final loader = SvgAssetLoader(path);
svg.cache.putIfAbsent(loader.cacheKey(null), () => loader.loadBytes(null));
}else{
precacheImage(AssetImage(path), context);
}
}
마지막으로 위 작업들을 진행하기 위해서는 BuildContext 를 전달 받아야하는 유저 입장에서의 불편함이 있는데 이를 해결하기위해
void startImageCache(){
final binding = WidgetsFlutterBinding.ensureInitialized();
binding.addPostFrameCallback((_) async {
BuildContext? context = binding.rootElement;
if(context != null) _startImageCache(context);
});
}
이런식으로 플러그인 내부에서 위젯이 그려지고 context가 생기면 받아서 호출해주게끔 작업을 진행했습니다.
따라서 아래와 같이 main 함수 내에서도 별다른 제약없이 호출할 수 있게되었습니다!
void main() {
GhAssetPreCache().startImageCache();
runApp(const MyApp());
}
그래서 결과물은?
약 2mb 정도 되는 이미지를 카피해서 하나는 캐시를 진행하고 하나는 캐시를 시키지 않고 테스트 한 영상입니다.
'image cache' 버튼에서는 별다른 로딩없이 페이지가 열릴때부터 이미지가 잘 보이는 반면
'image non cache' 버튼에서는 흰바탕이었다가 화면이 어느정도 노출된 이후 이미지가 보이게 되는걸 볼 수 있습니다.
마무리
코드 라인이 많지도 않고 보고나면 어렵지도 않지만 앞으로 상당히 유용하게 쓰일것으로 보입니다.
위 플러그인은 아래 링크에 배포되어있고 추후에 좋은 아이디어가 떠오를때마다 업데이트를 할 예정입니다.