flutter

flutter_cache_manager를 사용한 Svg캐싱

bakerlee 2024. 9. 4. 18:28

앱을 개발할 때 이미지를 사용하는 일을 굉장히 빈번하다. 그리고 이미지는 앱의 퍼포먼스에 지대한 영향을 미친다.

기기의 성능이 제한적인 앱 환경은 웹 환경에 비해 더 섬세한 자원관리가 필요하다.

자원관리가 미흡할 경우 최소 퍼포먼스 저하 및 발열, 최악의 경우 os 차원에서 리소스를 많이 소모하는 앱이라는 경고창을 띄울 수 있다.

앱의 성능을 최적화하는 방법들은 이미 여러 가지가 있지만 가성비가 가장 좋은 방법 중 하나는 이미지의 캐싱이다.

아무리 이미지의 용량을 조절한다 하여도 결국 외부에서 이미지를 가져오는 행위는 리소스의 소모가 클 수밖에 없다.

언제나 새로운 이미지를 제공해야 한다면 어쩔 수 없겠지만 대부분의 요구사항에서는 가져오는 이미지의 종류가 기간별로 한정적이다.

cached_network_image?

일반적으로 flutter에서의 이미지 캐싱은 cached_network_image 통해 이루어진다.

하지만 해당 라이브러리에선 SVG 타입은 지원되지 않는다.

SVG타입은 일반적인 비트앱 구조가 아닌 vector 형식으로 그려지는 이미지이기 때문에 별도의 처리가 필요하다.

따라서 일반적으로 flutter_svg 플러그인을 사용하여 그린다. 하지만 해당 플러그인은 캐싱이 적용되지 않는다.

캐싱은 직접 구현해야 한다.

캐싱을 직접 구현하기 위해서는 flutter_cache_manager를 사용하면 쉽게 구현 가능하다.

그리고 참고로 위의 cached_network_imagecached_network_image 역시 해당 플러그인을 사용하여 캐싱을 구현한다.

class CachedNetworkImageWithSvg extends StatelessWidget {
  final String url;
​
  const CachedNetworkImageWithSvg({super.key, required this.url});
​
  @override
  Widget build(BuildContext context) {
    if (url.contains(".svg")) {
      return _CachedSvgImageBuilder(
        url: url,
      );
    } else {
      return CachedNetworkImage(imageUrl: url);
    }
  }
}
​
class _CachedSvgImageBuilder extends StatelessWidget {
  final String url;
​
  const _CachedSvgImageBuilder({super.key, required this.url});
​
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: DefaultCacheManager().getSingleFile(url),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            if (snapshot.hasData) {
              return SvgPicture.file(snapshot.data!);
            } else if (snapshot.hasError) {
              /// 명시적 error
              return const Icon(Icons.error);
            } else {
              /// url에 이미지 정보가 없는 경우
              return Container();
            }
          } else {
            /// 로딩 중, done, wait, active 상태
            return Container();
          }
        });
  }
}

우선 flutter_cache_manager의 동작에 대해서 정확하게 이해할 필요가 있다. 명시되어있지 않지만 해당 플러그인은 네트워크를 통해 이미지를 가져오는 기능이 이미 포함되어 있다.

해당 플러그인의 의존성은 아래와 같다.

clock, collection, file, flutter, http, path, path_provider, rxdart, sqflite, uuid

이 중 http는 네트워크 파싱을 위한 패키지이며, 이 플러그인은 네트워크 파싱이 이루어진다는 것을 의미한다.

우선 DefaultCacheManager().getSingleFile(url)가 어떻게 동작하는지 직접 확인해 보면 아래와 같은 형태를 볼 수 있다.

  /// Get the file from the cache and/or online, depending on availability and age.
  /// Downloaded form [url], [headers] can be used for example for authentication.
  /// When a file is cached and up to date it is return directly, when the cached
  /// file is too old the file is downloaded and returned after download.
  /// When a cached file is not available the newly downloaded file is returned.
  @override
  Future<File> getSingleFile(
    String url, {
    String? key,
    Map<String, String>? headers,
  }) async {
    key ??= url;
    final cacheFile = await getFileFromCache(key);
    if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) {
      return cacheFile.file;
    }
    return (await downloadFile(url, key: key, authHeaders: headers)).file;
  }

파일을 캐싱 혹은 온라인에서 가져온다. 캐싱 일자와 정책에 따라 유효하지 않다면 새로 다운로드한다. 인증 관련하여 해더 정보를 확인한다.

header의 max-agemax-age를 통해 설정되며 이외의 설정 또한 데이터 소스의 해더에 따라 설정된다.

기본값은 30일이다.

코드상으로 정확히 어디에 있는지는 찾지 못했지만 DefaultCacheManager().config.stalePeriod로 직접 확인한 결과
Duration(days: 30)인 것을 확인하였다.

추가로 CacheManager 내부 코드를 직접 확인해 보면 maxage 관련 사항을 일관적으로 Duration(days: 30)으로 설정하고 있는 것을 확인할 수 있다.

즉 해당 동작은

  1. 해당 URL로 캐싱된 정보가 있는지 확인한다.
  2. 유효한 캐시가 있다면 해당 정보를 반환한다.
  3. 없거나 유효하지 않다면 해당 정보를 웹에서 받아온 뒤 해당 정보를 캐싱한 뒤 반환한다.

위의 순서대로 발생한다.

inspector의 network를 통해 리소스 요청이 있는지 확인하거나, device expolorer를 통해 캐싱 정보를 직접 확인하면 파일이 캐싱된 것을 확인할 수 있다.

캐싱 키 [ key ??= url; ]

해당 코드는 상당히 중요한 부분이다. 코드의 의미는 별도로 설정된 키가 없다면, url을 키로 설정한다. 이다.

대부분의 경우 url 전문이 캐싱 키로 활용된다는 의미이며 이것이 의미하는 바는 다음과 같다.

  1. url이 동일하면 해당 경로의 이미지가 달라져도 캐싱된 정보를 가져온다.
  2. url이 다르면 해당 경로의 이미지가 같아도 해당 이미지를 다시 불러와 캐싱 동작을 수행한다.

흔하지는 않은 상황이지만 1번의 경우로 인해 이미지가 변경되지 않을 수 있다. 이런 경우에 2번의 특징을 활용하여 문제를 해결할 수 있다.

url의 구성요소 중 쿼리스트링은 수신 측에서 해당 값을 처리하지 않아도 아무런 문제가 생기지 않는다.

https://some.datasource/root/path/image.svg?last_update_date=20240315 이런 형태로 url을 반환한다면 1번의 문제를 해결할 수 있다.

 

번외. 캐싱 정책 직접 앱에서 설정하는건 어떤가요?

back과 front(app)별로 동작을 구분해야 한다. 모든 것들을 front/back 한쪽에서만 해결하려들면, 유지보수 단계에서 끔찍한 경험을 할 수 있다.

일반적이지 않은 프로세스는 더 긴 설명서와 주의가 필요하며, 가장 최악으로는 강력한 결합이 발생할 수 있다.

앱의 UI 가 변경된다고 백엔드를 수정해야 하는 상황이 발생하거나 반대의 경우가 발생한다. 뜬금없이 앱의 구조를 변경해야 하거나,
결국 뭐 하나 바꾸거나 추가하려면 영향을 끼치는 범위가 너무 넓어서 뭔가를 하지 못하는 상황을 마주하게 된다.

내가 가장 말하기 싫었던 문답이다. 말하면서도 미안하고 화가 많이 났다
Q: 다른 서비스는 다 제공하는 기능인대 우리는 왜 안돼요?, 이게 그렇게 어려운 기능이에요?
A : 우리 서비스 구조가 조금 특이해서...

이미지 캐싱 정책에 대한 관리는 리소스 관리 측면에서 볼 수 있으며 해당 사항은 imagesoure를 관리하는 부분에서 해야 하는 영역이다.

진짜 어쩔 수 없으면 직접 조절해야겠지만, 어지간하면 image source의 header를 활용해야 한다.

back level에서 조정 가능한 필수 영역은 해당 도구를 사용하도록 하고, flutter에서는 최적화영역의 문제만 고민하는게 좋다.

 

https://pub.dev/packages/flutter_cache_manager

 

flutter_cache_manager | Flutter package

Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.

pub.dev

https://pub.dev/packages/flutter_svg

 

flutter_svg | Flutter package

An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.

pub.dev