프롤로그 - "지워, 데이터 아까워"
출시 첫날, 부푼 마음으로 친구들에게 카톡을 돌렸습니다. "야, 내가 만든 앱이야. 한번만 깔아봐."
5분 뒤, 가장 친한 친구에게서 답장이 왔습니다. "야, 이거 뭐냐? 용량이 120MB네? 내 데이터 요금제 알 거지인 거 모르냐? 와이파이 잡히면 받을게."
충격을 받았습니다. 고작 로그인하고 게시판 글 몇 개 보는 기능밖에 없는 앱이었습니다. 배달의민족이나 토스 같은 슈퍼앱도 100MB 남짓인데, 제 초라한 토이 프로젝트가 120MB라뇨.
개발자로서 자존심이 상했습니다. 그때부터 저는 앱 다이어트에 목숨을 걸기 시작했습니다. 그리고 일주일 뒤, 저는 이 앱을 18MB로 줄이는 데 성공했습니다. 무려 85% 감량이었죠.
이 글은 제가 겪은 삽질의 기록이자, 플러터 앱 용량을 줄이는 가장 확실한 가이드입니다.
1. 부검 시작 - 범인을 찾아라
살을 빼려면 인바디부터 재야 하듯, 앱 용량을 줄이려면 어디가 뚱뚱한지 알아야 합니다. 무턱대고 코드를 줄이는 건 의미가 없습니다. 코드는 아무리 짜봤자 텍스트 파일이라 용량에 큰 영향을 못 줍니다. 범인은 항상 리소스(Resource)에 있습니다.
분석 도구: --analyze-size
Flutter는 아주 강력한 분석 도구를 내장하고 있습니다. 터미널에 다음 명령어를 입력해보세요.
flutter build apk --analyze-size --target-platform android-arm64
빌드가 끝나면 터미널에 트리 구조의 분석 리포트가 뜹니다. 그리고 자세한 내용은 JSON 파일로 저장되죠. 이보다 더 시각적으로 보고 싶다면 DevTools를 이용하면 됩니다.
제 앱의 부검 결과는 충격적이었습니다.
- Assets (이미지/비디오): 85MB (70%)
- Native Libraries (.so): 25MB (20%)
- Fonts: 8MB (6%)
- Code (Dart + Framework): 4MB (3%)
보시다시피 코드는 고작 4MB였습니다. 제가 밤새 짠 로직들은 용량 문제와 아무 관련이 없었던 거죠. 진짜 적은 고화질 이미지와 뚱뚱한 폰트, 그리고 불필요한 라이브러리였습니다.
2. 이미지 다이어트 - PNG를 버려라
가장 먼저 칼을 댄 곳은 이미지 폴더였습니다. 디자이너(라고 쓰고 '저'라고 읽습니다)가 작업한 4000px짜리 고해상도 이미지가 assets/images 폴더에 그대로 들어있더군요.
PNG와 JPG는 이제 그만
모바일 앱에서 PNG나 JPG는 사치입니다. 구글이 만든 WebP 포맷을 써야 합니다. WebP는 같은 화질일 때 PNG 대비 30~50% 더 작습니다. 투명도(Alpha Channel)도 지원하기 때문에 PNG의 완벽한 대체재입니다.
저는 Squoosh라는 웹사이트를 애용합니다. 여기서 PNG를 끌어다 놓고 WebP로 변환하면, 눈으로는 차이를 못 느끼는데 용량은 1MB에서 50KB로 줄어드는 마법을 볼 수 있습니다.
벡터 이미지는 SVG로
로고나 아이콘 같은 단순한 그래픽은 비트맵(픽셀 덩어리)이 아닌 벡터(수식)로 저장해야 합니다.
@2x, @3x 같은 해상도별 이미지를 따로 만들 필요도 없고, 아무리 확대해도 깨지지 않으면서 용량은 몇 KB 수준입니다.
Flutter에서 SVG를 쓰려면 flutter_svg 패키지를 사용하면 됩니다.
dependencies:
flutter_svg: ^2.0.0
SvgPicture.asset(
'assets/icons/logo.svg',
width: 100,
height: 100,
);
이 작업만으로 85MB였던 이미지 폴더를 15MB로 줄였습니다.
3. 폰트 다이어트 - "갗, 냑, 홁"을 아십니까?
이미지를 줄였더니 다음 타자는 폰트였습니다. NotoSansKR-Bold.otf 파일 하나가 무려 10MB가 넘더군요.
한글 폰트의 비밀
영어 알파벳은 대소문자 합쳐봐야 52개, 특수문자까지 해도 100개가 안 됩니다. 그래서 폰트 파일이 수십 KB밖에 안 하죠. 하지만 한글은 조합형 문자입니다. '가'부터 '힣'까지 완성형으로 만들면 11,172자가 됩니다. 폰트 파일 하나에 만 개가 넘는 글자의 모양(Glyph)을 다 넣어놨으니 용량이 클 수밖에요.
그런데 우리가 평생 살면서 "갗"이나 "냑" 같은 글자를 쓸 일이 있을까요? (물론 "자작나무 껍질을 갗이라고 한다" 같은 문장이 있을 순 있겠지만요.)
해결책 - 서브셋(Subset) 폰트
잘 쓰지 않는 글자를 과감히 덜어내고, 자주 쓰는 2,350자만 남긴 것을 서브셋 폰트라고 합니다. 이렇게 하면 10MB짜리 폰트가 3MB 정도로 줄어듭니다.
여기서 더 줄이고 싶다면? 구글 폰트(Google Fonts)를 쓰면 됩니다.
dependencies:
google_fonts: ^6.1.0
Text(
'안녕하세요',
style: GoogleFonts.notoSansKr(),
);
google_fonts 패키지는 폰트를 앱에 내장하지 않습니다. 앱이 실행될 때 필요한 폰트 파일만 인터넷에서 다운로드(HTTP) 받아서 캐싱합니다. 초기 설치 용량은 0MB가 되는 셈이죠. 물론 오프라인 환경을 고려해야 한다면 서브셋 폰트를 내장(Asset)하는 것이 좋습니다.
4. 빌드 방식의 혁명: AAB (Android App Bundle)
이미지와 폰트를 줄였지만 여전히 50MB였습니다. 왜일까요?
APK 파일을 뜯어보니(압축을 풀면 보입니다) lib 폴더 안에 똑같은 라이브러리 파일이 3개나 들어있었습니다.
armeabi-v7a(구형 안드로이드폰용)arm64-v8a(최신 안드로이드폰용)x86_64(에뮬레이터/크롬북용)
개발 편의를 위해 모든 CPU 아키텍처용 코드를 다 쑤셔 넣은 것이죠. "혹시 사용자가 5년 전 폰을 쓸지도 모르니까"라는 친절함이 용량 폭탄이 된 것입니다.
APK를 버려라
이제는 APK 시대가 끝났습니다. 구글이 미는 표준은 App Bundle (.aab)입니다.
flutter build appbundle
AAB는 완성된 설치 파일이 아닙니다. "재료 상자"에 가깝습니다. 이 파일을 구글 플레이 스토어에 업로드하면, 구글이 알아서 배포판을 만듭니다.
- 갤럭시 S23 사용자에게는
arm64-v8a코드와 고해상도 이미지만 담아서 줍니다. - 구형 폰 사용자에게는
armeabi-v7a코드와 저해상도 이미지만 담아서 줍니다.
이것을 Dynamic Delivery라고 합니다. 개발자가 신경 쓸 필요 없이, 구글 플레이가 알아서 사용자의 기기에 딱 맞는 "맞춤 정장" 같은 APK를 만들어 설치해주는 겁니다. 이것만으로 용량이 40~50% 줄어듭니다.
5. 네이티브 라이브러리와 ABI 필터링 파헤치기
만약 사내 배포나 직접 APK를 전달해야 해서 AAB를 못 쓴다면 어떻게 해야 할까요?
build.gradle을 수정해서 강제로 다이어트를 시킬 수 있습니다.
// android/app/build.gradle
android {
defaultConfig {
ndk {
// "나는 최신 폰만 지원하겠다" 선언
abiFilters 'arm64-v8a'
}
}
}
이렇게 abiFilters를 설정하면, 다른 아키텍처용 .so 파일들은 빌드 과정에서 아예 제외됩니다. 요즘 출시되는 스마트폰의 99%는 64비트(arm64-v8a)입니다. 아주 오래된 기기를 포기할 수 있다면, 이 방법으로 용량을 1/3로 줄일 수 있습니다.
하지만 주의하세요. 32비트 구형 폰을 쓰는 사용자에게는 앱이 설치되지 않거나 실행되자마자 죽을(Crash) 수 있습니다. 그래서 상용 서비스라면 AAB가 정답입니다.
6. 프로가드와 난독화 (R8 Compiler)
안드로이드 빌드 툴에는 R8이라는 강력한 녀석이 숨어있습니다. 예전엔 ProGuard라고 불렸던 놈이죠. 이 녀석이 하는 일은 두 가지입니다.
- 난독화 (Obfuscation):
UserAuthenticationManager같은 긴 클래스 이름을a.b.c같은 짧은 이름으로 바꿉니다. 해킹을 어렵게 만들 뿐 아니라, 텍스트 길이가 줄어드니 용량도 줍니다. - 트리 쉐이킹 (Tree Shaking): 이게 진짜입니다. 프로젝트에 포함된 라이브러리 중에 "실제로 쓰지 않는 코드"를 찾아서 잘라버립니다.
Flutter는 릴리즈 모드 빌드 시 기본적으로 트리 쉐이킹을 수행합니다. 하지만 더 강력하게 줄이고 싶다면 난독화 옵션을 켤 수 있습니다.
flutter build appbundle --obfuscate --split-debug-info=./debug-info
이 옵션을 쓰면 용량이 5~10% 정도 더 줄어듭니다. 단, 나중에 에러 로그(Stack Trace)를 볼 때 NullPointerException at a.b.c 처럼 나오기 때문에, debug-info 파일을 잘 보관해뒀다가 해석(Symbolicate)해야 하는 번거로움이 있습니다.
7. iOS는요? (App Thinning)
안드로이드 얘기만 해서 섭섭하셨나요? iOS는 애플이 알아서 다 해줍니다. 이름하여 앱 시닝(App Thinning)입니다.
우리가 Xcode에서 아카이브(Archive)를 해서 앱스토어 커넥트(App Store Connect)에 올리면, 애플이 알아서 기기별로 쪼갭니다(Slicing). 아이폰 14용, 아이폰 SE용 비트코드를 따로 만들어서 배포하죠.
개발할 때 주의할 점은, 로컬 빌드 파일(Runner.app)의 용량을 보고 놀라지 말라는 겁니다. 로컬 빌드에는 디버그 심볼, 모든 아키텍처 바이너리가 다 포함되어 있어서 200MB가 넘기도 합니다.
정확한 용량을 알고 싶다면, TestFlight에 업로드한 뒤 "App Store Optimized Size"를 확인해야 합니다. 그게 진짜 사용자가 다운로드할 크기입니다.
8. 최후의 수단 - Deferred Components (지연 로딩)
여기까지 했는데도 용량이 크다면? 아마 앱 안에 거대한 기능이 숨어있을 겁니다. 예를 들어 동영상 편집 기능이라거나, 머신러닝 모델 같은 것들이죠.
문제는, 이 기능을 전체 사용자의 1%만 쓴다는 겁니다. 99%의 사용자는 로그인해서 글만 읽는데, 1%를 위한 동영상 편집 엔진까지 다운로드해야 할까요?
이럴 때 쓰는 것이 지연 로딩(Deferred Loading)입니다. 안드로이드에서는 'Dynamic Feature Module'이라고 부릅니다.
- 코드를 분리합니다.
- 분리된 코드는 초기 설치 시 다운로드되지 않습니다.
- 사용자가 "동영상 편집" 메뉴를 누르는 순간, 그때 구글 플레이에서 다운로드합니다.
Flutter에서도 이게 가능합니다.
import 'package:video_editor/main.dart' deferred as editor;
void openEditor() async {
// 사용자가 버튼을 눌렀을 때 비로소 다운로드 시작!
await editor.loadLibrary();
editor.runEditor();
}
deferred as 키워드를 쓰면 됩니다. 이렇게 하면 초기 설치 용량을 10MB 이하로 유지하면서도, 수백 MB짜리 기능을 가진 슈퍼앱을 만들 수 있습니다.
요약 - 다이어트 성공 비결
제가 앱 용량을 120MB에서 18MB로 줄인 비결을 정리하면 이렇습니다.
- 분석:
flutter build apk --analyze-size로 뚱뚱한 원인을 찾는다. - 이미지: PNG를 WebP로 바꾸고, 아이콘은 SVG를 쓴다. (효과 ★★★★★)
- 폰트: 서브셋 폰트를 쓰거나 구글 폰트(HTTP 다운로드)를 쓴다. (효과 ★★★)
- 배포: 무조건 AAB로 빌드해서 스토어에 올린다. (효과 ★★★★)
- 심화: 필요하다면 ABI 필터링이나 지연 로딩을 도입한다.
앱 용량은 사용자의 진입 장벽입니다. 100MB가 넘어가면 와이파이가 잡힐 때까지 설치를 미루고, 그러다 영영 잊혀집니다. 가벼운 앱이 사랑받습니다. 여러분의 앱도 지금 당장 다이어트를 시작해보세요.