2dowon blog logo

2dowon

CachedNetworkImage를 사용하는 Flutter 앱이 비정상 종료 될 때

March 9, 2024

flutter-app-crashed-by-cached-network-image

2023년 12월, 신규 프로젝트로 ‘브랜드 리뷰’ 기능을 오픈했었다. 그 당시에는 일정이 빠듯해 리뷰를 작성과 관련된 필수 기능만으로 해당 프로젝트를 오픈했었고, 그 이후 바로 유저들이 다른 사람들의 리뷰를 볼 수 있는 ‘브랜드 상세’ 페이지를 추가하는 등 고도화 작업을 진행해 지난 1월에 배포했다!

모든 프로젝트가 그렇듯(?) 다사다난한 작업이었는데, 그 중 가장 큰 이유가 배포 직전 발견한 이미지 튕김 현상이다. 구현하면서 개발 데이터로 테스트했을 때는 문제가 없었고, 심지어 운영계 APK로 테스트했을 때도 문제가 없었다. (나와 PM님과 시니어 프론트 개발자님에게는 말이다…ㅎ)

그렇게 해당 프로젝트를 담당했던 사람들은 무사히 계획했던 일정 내에 배포할 수 있을 것이라 생각했고, 배포하기 직전 다른 팀원들에게 문제가 없는지 APK를 공유했는데 ‘앱을 사용하다가 강제종료됩니다’라는 피드백을 받았다. ‘어..? 분명 문제 없었는데?’ 라고 생각하면서 이슈 재현을 위해 어떤 상황에서 앱이 강제 종료되는지 확인하고보니 특정 브랜드의 상세 페이지에만 접근하면 앱이 튕기는 상황이었다. 근데 나는 조금 느려지긴 해도 괜찮았는데… 라고 생각하며 해당 팀원의 개인적인? 문제인가 싶어서 다른 여러 팀원들에게 해당 상황을 재현해봤고 생각보다 많은 팀원들이 같은 현상을 경험했다. 배포 일정이 급한 상황이긴 했지만, 이런 상황에서 배포해봤자 수많은 유저들에게 불편을 줄 것이 너무나도 뻔하고.. 그러니 일단 앱이 강제 종료되는 이유를 찾아보기로 했다.

예상 원인 1 : 이미지 용량

일단 개발계에서는 다른 사람들도 앱이 강제 종료되는 현상이 없었고, 운영계에서만 앱이 강제 종료되는 현상이 발생했기 때문에 제일 먼저 의심이 가는 원인은 리뷰 이미지였다. 다른 페이지들과 다르게 이번에 신규 배포하는 ‘브랜드 상세’ 페이지에서는 다른 유저들이 남긴 리뷰들을 볼 수 있는 페이지이기 때문에 유저들이 남긴 리뷰의 이미지에 문제가 있지 않았을까 생각했다.

이를 검증하기 위해 먼저 이미지를 숨길 수 있는 토글 버튼을 만들어 테스트를 해봤고, 예상대로 리뷰의 이미지가 닫혀 있는 상황에서는 앱이 강제 종료되지 않았다. 앱이 강제 종료되는 순간은 리뷰의 이미지를 열 때 였는데, 항상 강제 종료되는 것은 아니고 몇몇 리뷰에서만 발생하는 현상이었다. 그래서 강제 종료되는 이미지를 찾아봤더니 이미지 용량이 10mb를 넘었다..
요즘 휴대폰 성능이 좋아서 고화질로 촬영한 이미지의 경우 이미지의 용량이 꽤 큰데, 이 사실을 제대로 인지하지 않은채로 이미지를 처음 저장할 때 리사이즈와 용량 압축없이 바로 원본을 저장했던게 문제였다. 그래서 일단 이미지의 용량을 줄이는 것이 가장 우선순위가 되었고, 백엔드에서 이미지를 저장하는 API를 수정했고 이미 저장된 리뷰의 이미지들은 전체적으로 용량을 줄이고 리사이즈하는 과정을 거쳤다. 그 결과 용량이 약 1/10정도로 줄었고, 대부분의 이미지가 1mb 미만으로 유지되기 시작했다.
자, 그럼 이제 더 이상 앱이 강제 종료가 안되지 않을까? 답은 예상대로 ‘NO’다. 사실 여기서 문제가 해결됐으면 굳이 이 글을 쓸 이유도 없었다.. ㅎㅎ

예상 원인 2 : CachedNetworkImage 패키지

이미지의 용량이 문제는 아니지만, 앱은 여전히 특정 이미지가 viewport에 들어올 때 강제종료되는 상황이다.
사실 이 상황에서 약간 멘붕이었다. 배포 일정은 이미 밀리고 있고, 예상했던 원인을 수정했음에도 동일한 현상이 발생하고 있기 때문이었다.

그래도 이거 고쳐야 앱을 배포할 수 있으니까 다시 처음부터 코드를 보기 시작했다. 그때서야 눈에 들어온거는 이미지를 보여줄 때 사용하는 패키지였다.
우리 앱에서는 이미지를 사용할 때 CachedNetworkImage 패키지를 사용하고 있는데, 이 패키지를 사용하면 한번 불러온 이미지를 캐시에 저장해 놓고 사용하기 때문에 네트워크 통신을 다시 안하기 때문이다.

그럼 이제 CachedNetworkImage 패키지와 앱이 강제종료되는 현상과의 관련성을 찾아야 했다. 그 과정에서 해당 이슈가 처음 발생했을 때 원인을 찾기 위해 다른 FE 동료들과 얘기했었는데, 그 때 누군가 memory 문제일수도 있지 않나? 라고 말했던게 생각이 났다.

아, CachedNetworkImage는 이미지를 캐시에 저장하는데, 너무 많은 이미지를 저장해서 memory overflow가 발생한다면 앱이 강제 종료되지 않을까?

그래서 CachedNetworkImage 문서를 다시 꼼꼼하게 읽어보기 시작했다. CachedNetworkImage에서는 flutter_cache_manager 패키지를 이용해 캐시된 파일들을 관리하는데, 해당 문서를 보면 캐시된 파일들은 maxNrOfCacheObjectsstalePeriod 라는 값을 이용해 파일이 가장 최근에 사용된 시기를 파악해서 파일이 너무 많을 때, 마지막으로 사용한 순서대로, 그리고 파일이 오래된 기간보다 오래 사용되지 않았을 때 캐시된 파일을 지속적으로 삭제한다고 나와있다. (아래 이미지 참고)

그렇다면 사실 CachedNetworkImage에서는 캐시된 파일들을 잘 관리해주고 있다는 뜻인데, 그럼에도 불구하고 일단 의심가는 이유가 있으니 추가적으로 CachedNetworkImage repo의 issue에서 우리와 비슷한 이슈가 있지는 않은지 찾아보기 시작했다. 그 결과 실제로 CachedNetworkImage repo issue에 CachedNetworkImage가 memory overflow있다는 내용의 글을 찾을 수 있었다.

2020년도에 작성된 글이니까 현재 기준으로 거의 4년 전에 open된 issue이지만, 최근까지도 관련해서 이슈가 있다는 comment가 지속적으로 있었다. 다른 사람들도 겪고 있는 이슈를 확인하니 드디어 앱이 강제종료되는 현상의 원인이 CachedNetworkImage 패키지 때문이라는 확신을 할 수 있었고, 브랜드 리뷰의 이미지를 보여줄 때 CachedNetworkImage 대신 Image.network 로 사용할 수 있도록 수정한 결과 앱이 강제 종료되는 현상을 막을 수 있었다.
그리고 추가로 Image.network 만 사용할 경우, 너무 큰 이미지를 렌더링할 때 많은 메모리가 필요하기 때문에 cacheWidth 속성을 추가로 사용해 메모리를 최적화할 수 있도록 했다. 그 때 Save Your Memory Usage By Optimizing Network Images in Flutter 이 글을 참고했었는데, 많은 도움이 되었던 글이었지만 이 글의 마지막에는 또 다시 우리의 경우 문제가 되었던 CacheNetworkImage 패키지를 추천하고 있어서 뭐지? 하면서 웃었던 것 같다.

마무리

특정 이슈에 대한 해결을 위해 적기 시작한 글이었는데, 사실 글을 작성하다보니 '디버깅'에 대해서 많이 생각하게 되었다. 단순히 에러를 해결하는 것을 넘어서 왜 이러한 이슈가 발생하고, 어떻게 해결해야 하는지에 대해서 많은 고민을 했고 그래서 이슈를 해결하기도 했지만 스스로 디버깅의 자세를 다시 한 번 생각해볼 수 있는 좋은 계기가 된 케이스여서 더더욱 기록해두고 싶었다.

아, 그래서 결국 내가 생각하는 디버깅의 본질은 아래와 같다.

  1. 에러 상황을 재현한다
  2. 에러와와 관련된 라이브러리/패키지가 있다면 관련 github issue를 찾아본다
  3. 구글링, 스택오버플로우, ChatGPT를 이용해 관련 정보를 더 찾아본다
  4. 에러의 원인을 분석하고 정리한다.

너무 당연하지만, 아래 step 순서대로 디버깅하는 것은 꽤 중요하다! 특히 1번 에러 상황 재현하기!

추후 디버깅과 관련된 생각의 글도 따로 정리해야겠다!