
앱 용량이 왜 100MB죠? (Flutter 다이어트 비법)
기능도 별로 없는데 앱 용량이 100MB? 사용자는 무거운 앱을 설치하지 않습니다. 이미지 최적화, 폰트 경량화, ABI 필터링, 그리고 Android App Bundle(AAB)까지, 확실한 다이어트 비법을 공개합니다.

기능도 별로 없는데 앱 용량이 100MB? 사용자는 무거운 앱을 설치하지 않습니다. 이미지 최적화, 폰트 경량화, ABI 필터링, 그리고 Android App Bundle(AAB)까지, 확실한 다이어트 비법을 공개합니다.
매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

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