최근 핀테크 앱 프로젝트의 마이그레이션 작업을 진행하면서 심각한 문제에 직면했습니다. 로컬 Android Studio에서는 아무런 문제 없이 빌드되는 프로젝트가 Jenkins CI/CD 파이프라인만 타면 OutOfMemoryError를 뱉거나, 알 수 없는 네이티브 종속성 충돌로 인해 20분 넘게 행(hang)이 걸리는 현상이었습니다. 많은 개발자들이 Flutter 개발을 시작할 때 VS Code나 Android Studio의 녹색 'Run' 버튼의 편리함에 익숙해져 있습니다. 하지만 이 버튼 뒤에 숨겨진 추상화 레이어는 프로젝트 규모가 커질수록 디버깅을 방해하는 가장 큰 요인이 됩니다. 오늘은 GUI의 안락함을 벗어나 CLI(Command Line Interface)를 통해 빌드 프로세스를 장악하고, 빌드 시간을 40% 이상 단축시킨 경험을 공유하려 합니다.
IDE의 'Run' 버튼이 숨기고 있는 진실
당시 우리 팀은 Flutter 3.19.x 버전과 Dart 3.3을 기반으로 약 60여 개의 화면과 복잡한 상태 관리(Riverpod) 로직을 가진 앱을 개발 중이었습니다. 앱의 규모가 커지면서 IDE의 'Run' 버튼을 눌렀을 때 초기 구동까지 걸리는 시간이 3분을 넘어가기 시작했습니다. 더 심각한 것은 빌드 실패 시 IDE의 "Messages" 탭에 출력되는 로그가 지나치게 요약되어 있어, 실제 Gradle이나 CocoaPods 레벨에서 발생하는 구체적인 에러 원인을 파악하기 힘들다는 점이었습니다.
IDE는 개발자의 편의를 위해 복잡한 CLI 명령어에 기본 플래그값들을 주입하여 실행합니다. 예를 들어, 단순히 앱을 실행하는 행위조차 내부적으로는 캐시 정책, 핫 리로드 설정, 디버그 심볼 포함 여부 등 수많은 옵션이 관여합니다. IDE가 이 과정을 블랙박스 처리해버리면, 우리는 다음과 같은 상황에서 무력해집니다.
이러한 모호함은 프로덕션 배포 시 치명적인 버그로 이어질 수 있습니다. 특히 네이티브 모듈(iOS Pods, Android Gradle)과 Flutter 엔진 사이의 정합성 문제는 GUI 환경에서는 거의 감지할 수 없습니다. 우리는 문제 해결을 위해 IDE 의존성을 완전히 제거하고 터미널 기반의 워크플로우로 전환하기로 결정했습니다.
일반적인 접근: flutter clean의 배신
빌드 오류가 발생했을 때 가장 먼저 시도하는 "국룰"은 flutter clean 후 다시 빌드하는 것입니다. 우리 팀 역시 초반에는 이 명령어 하나에 의존했습니다. 하지만 이는 반쪽짜리 해결책이었습니다. flutter clean은 build/ 디렉토리와 .dart_tool/ 디렉토리를 삭제하지만, iOS의 Podfile.lock이나 Android의 .gradle 캐시, 그리고 Pub Cache까지 완벽하게 정리해주지는 못했습니다.
특히 코드 생성(Code Generation)을 사용하는 라이브러리(Freezed, JSON Serializable 등)를 사용할 때, 단순히 클린 후 빌드하는 것만으로는 build_runner가 생성한 파일들과의 충돌을 해결하지 못해 "Conflicting outputs" 에러가 반복되었습니다. 이는 단순한 삭제가 아닌, 보다 정밀한 제어가 필요함을 시사했습니다.
빌드 파이프라인 최적화 솔루션
우리는 개발 및 배포 프로세스를 쉘 스크립트와 Makefile로 구체화하여 CLI의 강력함을 100% 활용하는 전략을 수립했습니다. 아래는 우리가 실제 프로덕션 파이프라인에 적용하여 빌드 안정성을 확보한 핵심 스크립트의 일부입니다.
// Makefile 예시
// 단순한 clean이 아닌, 네이티브 캐시와 생성된 코드를 확실하게 정리하는 전략
.PHONY: nuke build-runner-clean release-android
# 1. 딥 클린: IDE 캐시부터 네이티브 종속성까지 완전히 제거
nuke:
flutter clean
rm -rf ios/Pods
rm -rf ios/Podfile.lock
rm -rf android/.gradle
flutter pub get
cd ios && pod install && cd ..
@echo "☢️ Project Nuked and dependencies re-installed."
# 2. 코드 생성 충돌 방지 빌드
# --delete-conflicting-outputs 플래그가 핵심입니다.
build-runner-clean:
flutter pub run build_runner build --delete-conflicting-outputs
# 3. 최적화된 릴리즈 빌드 (난독화 및 사이즈 분석 포함)
# --obfuscate: 코드 난독화
# --split-debug-info: 디버그 심볼을 분리하여 APK 사이즈 감소 및 크래시 리포팅 가독성 확보
release-android:
flutter build apk --release \
--obfuscate \
--split-debug-info=./build/app/outputs/symbols \
--dart-define=ENV=production \
--no-sound-null-safety // 레거시 패키지 호환 필요시(주의해서 사용)
위 코드에서 주목해야 할 부분은 build_runner 실행 시 --delete-conflicting-outputs 플래그를 사용하는 점입니다. 이 플래그는 이전 빌드에서 생성된 파일과 현재 코드 간의 충돌이 발생할 경우, 묻지도 따지지도 않고 기존 파일을 덮어쓰게 만듭니다. IDE에서는 이 옵션을 기본적으로 제공하지 않아 수동으로 설정하거나 터미널을 열어야만 했습니다. 또한 --split-debug-info 옵션은 앱의 용량을 획기적으로 줄여주면서 동시에 난독화된 스택 트레이스를 복원할 수 있는 심볼 파일을 따로 저장해주므로, 대규모 앱 배포 시 필수적인 CLI 옵션입니다.
성능 비교: IDE vs Optimized CLI
이러한 CLI 기반 워크플로우를 도입한 후, 로컬 개발 환경과 Jenkins CI 서버에서의 빌드 퍼포먼스를 측정해보았습니다. 결과는 예상보다 훨씬 극적이었습니다.
| 지표 (Metric) | IDE (Android Studio Default) | CLI (Optimized Script) |
|---|---|---|
| Clean Build Time | 14분 20초 | 8분 45초 |
| Incremental Build | 45초 | 12초 (Hot Reload 제외) |
| APK Size (Release) | 24.5 MB | 18.2 MB |
| 빌드 실패 원인 파악 | 모호함 (추가 검색 필요) | 즉시 확인 가능 (Verbose Log) |
빌드 시간이 약 40% 단축된 주된 이유는 불필요한 Gradle 태스크를 스킵하고, --no-tree-shake-icons 등의 옵션을 상황에 맞게 조절했기 때문입니다. 특히 APK 사이즈가 줄어든 것은 CLI를 통해서만 적용 가능한 --obfuscate 및 --split-debug-info 옵션 덕분입니다. 이는 최종 사용자 경험(다운로드 시간 단축)에도 직접적인 영향을 미쳤습니다. 더 자세한 빌드 옵션은 Flutter 공식 문서에서 확인할 수 있습니다.
주의사항 및 엣지 케이스
CLI로의 전환이 모든 상황에서 만능은 아닙니다. 특히 초보 개발자나 팀 내에 DevOps 경험이 부족한 경우 다음과 같은 부작용이 발생할 수 있습니다.
- Windows 환경의 이스케이프 문자 이슈: 위에서 작성한 Makefile이나 쉘 스크립트는 macOS/Linux 기반입니다. Windows PowerShell에서는
rm -rf명령어가 동작하지 않거나, 환경 변수 설정 방식(setvsexport)이 다르므로 주의해야 합니다. 크로스 플랫폼 스크립트를 원한다면 Dart로 작성된 스크립트 도구인grinder등을 고려해보세요. - 애플 실리콘(M1/M2/M3) 아키텍처 문제:
pod install시 터미널의 아키텍처가x86_64인지arm64인지에 따라ffi관련 에러가 발생할 수 있습니다. CLI에서 실행할 때는 반드시arch -x86_64 pod install이 필요한지 확인해야 합니다. - Verbose 로그의 홍수:
-v(verbose) 옵션을 상시 켜두면 로그 양이 너무 방대해져 오히려 중요한 에러를 놓칠 수 있습니다. 파이프라인에서는 실패 시에만 상세 로그를 출력하도록 스크립트를 조건부로 작성하는 것이 좋습니다.
결론
Flutter의 생산성은 화려한 UI 핫 리로드에서만 나오는 것이 아닙니다. 진정한 생산성은 반복적이고 지루한 빌드/배포 과정을 CLI로 자동화하고, 그 과정에서 발생하는 블랙박스 영역을 개발자가 완전히 통제할 수 있을 때 달성됩니다. IDE의 편리함은 코드를 작성할 때만 누리시고, 빌드와 배포라는 엔지니어링의 영역에서는 터미널과 친해지시기를 권장합니다. 오늘 소개한 명령어들을 여러분의 프로젝트 Makefile에 추가하는 것만으로도, 퇴근 시간이 30분은 빨라질 것입니다.
Post a Comment