터미널은 무서운 도구가 아니라 가장 빠른 길이다
Flutter를 처음 배울 때는 대부분 IDE의 실행 버튼을 누른다. Android Studio나 VS Code의 초록색 재생 버튼은 친절하고 편하다. 하지만 앱을 조금만 더 진지하게 만들기 시작하면 결국 터미널을 열게 된다. 패키지를 추가하고, 테스트를 돌리고, 빌드 파일을 만들고, 갑자기 꼬인 캐시를 청소해야 하기 때문이다.
중요한 사실이 하나 있다. IDE가 마법을 부리는 것이 아니다. IDE도 내부적으로는 flutter run, flutter pub get, flutter test 같은 명령어를 호출한다. Flutter 공식 문서에서도 flutter 명령줄 도구는 개발자나 IDE가 Flutter와 상호작용하는 공식 도구이며, Dart 관련 작업은 필요에 따라 dart 명령줄 도구를 사용할 수 있다고 설명한다.1
이 장은 외우기 위한 장이 아니다. 앱을 만들다가 "이럴 때 어떤 명령어였지?" 하고 막혔을 때 빠르게 펼쳐 보는 작업대다. 마치 요리할 때 냉장고 문에 붙여 둔 레시피처럼, 개발 중 자주 쓰는 명령어와 실전 패키지를 한눈에 정리해 보자.

Flutter CLI 기본 명령어
Flutter 명령어는 크게 네 가지 순간에 자주 등장한다. 프로젝트를 처음 만들 때, 앱을 실행할 때, 문제가 생겼을 때, 출시용 빌드를 만들 때다. 아래 표는 가장 자주 쓰는 명령어를 목적과 실행 시점 중심으로 정리한 것이다.
| 명령어 | 목적 | 언제 쓰나 |
|---|---|---|
flutter --help | 사용 가능한 Flutter 명령어 목록 확인 | 명령어가 기억나지 않을 때 |
flutter --version | Flutter SDK 버전 확인 | 버그 리포트, 팀 버전 확인, 문서와 버전 비교 시 |
flutter doctor | 개발 환경 진단 | 설치 직후, 에뮬레이터나 빌드가 안 될 때 |
flutter devices | 연결된 기기 목록 확인 | 실행할 기기를 고르기 전 |
flutter create my_app | 새 프로젝트 생성 | 앱 또는 패키지를 처음 만들 때 |
flutter run | 앱 실행 | 개발 중 기기나 에뮬레이터에서 앱을 띄울 때 |
flutter analyze | 정적 분석 실행 | 커밋 전, CI에서 코드 품질 확인 시 |
flutter test | 테스트 실행 | 기능 수정 후, 배포 전 |
flutter clean | 빌드 캐시와 임시 파일 삭제 | 이상한 빌드 오류, 의존성 꼬임, 플랫폼 빌드 실패 시 |
가장 먼저 익숙해져야 할 명령어는 flutter doctor다. 이름 그대로 의사처럼 현재 개발 환경을 진단한다. Android toolchain, Xcode, Chrome, 연결 기기, IDE 플러그인 상태를 검사하고 부족한 부분을 알려 준다. 설치 직후에는 무조건 한 번 실행하고, 알 수 없는 빌드 오류가 날 때도 습관처럼 실행해 보자.
flutter doctor더 자세한 진단이 필요하면 -v 옵션을 붙인다.
flutter doctor -v새 프로젝트를 만들 때는 flutter create를 사용한다.
flutter create my_notes_app
cd my_notes_app
flutter run앱의 패키지 이름, 즉 Android의 applicationId와 iOS의 bundle identifier에 영향을 주는 조직 도메인을 지정하고 싶다면 --org 옵션을 함께 쓴다.
flutter create --org com.example my_notes_app실행 가능한 기기가 여러 개라면 먼저 목록을 확인한다.
flutter devices그다음 특정 기기를 지정해 실행할 수 있다.
flutter run -d chrome
flutter run -d emulator-5554실제 개발에서는 아래 흐름을 가장 자주 반복한다.
flutter create my_app
cd my_app
flutter pub get
flutter analyze
flutter test
flutter run이 흐름은 단순하지만 강력하다. 프로젝트를 만들고, 의존성을 받고, 코드를 검사하고, 테스트하고, 실행한다. 작은 개인 프로젝트든 팀 프로젝트든 기본 리듬은 거의 같다.
Debug, Profile, Release 실행 감각 잡기
Flutter 앱은 실행 모드에 따라 성격이 달라진다. 개발 중에는 대부분 Debug 모드를 사용한다. Hot Reload가 가능하고 디버깅 정보가 풍부한 대신, 성능은 출시 앱과 다를 수 있다.
Profile 모드는 성능 측정을 위한 모드다. 디버그 기능은 줄이고 실제 실행 성능에 가까운 상태에서 프레임 드롭, 렌더링 비용, 메모리 사용량 등을 확인할 수 있다.
Release 모드는 사용자가 스토어에서 내려받는 앱에 가까운 모드다. 디버깅 편의 기능은 빠지고, 최적화가 적용된다.
| 실행 방식 | 명령어 | 용도 |
|---|---|---|
| Debug 실행 | flutter run | 일반 개발, Hot Reload |
| Profile 실행 | flutter run --profile | 성능 측정, DevTools 분석 |
| Release 실행 | flutter run --release | 출시 환경에 가까운 동작 확인 |
성능을 이야기할 때 "제 폰에서는 좀 느린데요?"라는 말만으로는 부족하다. Debug 모드에서 느린 것인지, Release 모드에서도 느린 것인지 먼저 확인해야 한다. 특히 애니메이션, 긴 리스트, 이미지가 많은 화면은 Profile 모드에서 DevTools와 함께 보는 습관을 들이면 좋다.
pub 명령어로 의존성 관리하기
Flutter 프로젝트는 혼자 모든 기능을 직접 구현하지 않는다. 라우팅은 go_router, 상태관리는 flutter_riverpod, API 통신은 http나 dio, Firebase 연동은 Firebase 패키지를 사용한다. 이런 외부 패키지를 관리하는 도구가 pub이다.
Dart 공식 문서는 pub이 Dart의 패키지 관리 도구이며, Flutter 앱이나 Flutter 전용 코드가 있는 디렉터리에서는 dart pub 대신 flutter pub을 사용하라고 안내한다.2 쉽게 말해 일반 Dart 패키지에서는 dart pub, Flutter 앱에서는 flutter pub을 기본으로 생각하면 된다.
| 명령어 | 목적 | 언제 쓰나 |
|---|---|---|
flutter pub get | pubspec.yaml에 적힌 패키지 내려받기 | 프로젝트를 처음 받았을 때, 의존성 수정 후 |
flutter pub add 패키지명 | 패키지 추가 | 새 라이브러리를 설치할 때 |
flutter pub add dev:패키지명 | 개발용 패키지 추가 | 코드 생성기, 린트, 테스트 도구 추가 시 |
flutter pub remove 패키지명 | 패키지 제거 | 더 이상 쓰지 않는 라이브러리를 정리할 때 |
flutter pub outdated | 오래된 패키지 확인 | 업데이트 가능 여부 점검 시 |
flutter pub upgrade | 가능한 범위에서 패키지 업그레이드 | 정기 업데이트, 보안 업데이트 후 |
flutter pub deps | 의존성 트리 확인 | 충돌 원인을 찾을 때 |
패키지를 추가할 때 pubspec.yaml을 직접 열어도 되지만, 보통은 명령어가 더 안전하고 빠르다.
flutter pub add go_router
flutter pub add flutter_riverpod
flutter pub add http개발할 때만 필요한 패키지는 dev_dependencies에 넣어야 한다. 예를 들어 코드 생성에 쓰는 build_runner나 테스트 목(mock) 라이브러리는 앱 실행 파일에 직접 포함될 필요가 없다.
flutter pub add dev:build_runner
flutter pub add dev:json_serializable
flutter pub add dev:flutter_lints패키지를 삭제할 때는 pubspec.yaml에서 줄만 지우지 말고 명령어를 쓰면 실수를 줄일 수 있다.
flutter pub remove http오래된 패키지를 확인하려면 다음 명령어를 사용한다.
flutter pub outdated업데이트를 적용하려면 다음 명령어를 실행한다.
flutter pub upgrade다만 upgrade는 만능 버튼이 아니다. 앱이 잘 돌아가던 상태에서 여러 패키지를 한꺼번에 올리면 예상하지 못한 깨짐이 생길 수 있다. 실전에서는 업데이트 전후로 flutter test, flutter analyze를 함께 실행하는 것이 좋다.
flutter pub outdated
flutter pub upgrade
flutter analyze
flutter testflutter와 dart 명령어 구분하기
초보자가 자주 헷갈리는 부분이 있다. 어떤 때는 flutter를 쓰고, 어떤 때는 dart를 쓴다. 기준은 생각보다 단순하다.
Flutter 앱 프로젝트 안에서 앱 실행, 빌드, 테스트, 패키지 관리를 할 때는 flutter를 쓴다. 반면 순수 Dart CLI 도구를 실행하거나 코드 생성기를 돌릴 때는 dart run을 자주 쓴다.
| 상황 | 추천 명령어 | 예시 |
|---|---|---|
| Flutter 앱 의존성 설치 | flutter pub get | flutter pub get |
| Flutter 앱 테스트 | flutter test | flutter test test/note_test.dart |
| Flutter 앱 실행 | flutter run | flutter run -d chrome |
| 코드 생성기 실행 | dart run | dart run build_runner build |
| 순수 Dart 패키지 의존성 설치 | dart pub get | dart pub get |
| 전역 CLI 도구 활성화 | dart pub global activate | dart pub global activate fvm |
예를 들어 freezed와 json_serializable을 사용하는 프로젝트에서는 코드를 생성해야 한다. 이때는 보통 다음 명령어를 사용한다.
dart run build_runner build --delete-conflicting-outputs파일이 바뀔 때마다 자동으로 다시 생성하고 싶다면 watch를 쓴다.
dart run build_runner watch --delete-conflicting-outputs이 명령어를 하루 종일 켜 두면 모델 파일을 수정할 때마다 생성 파일이 자동으로 갱신된다. 마치 주방에서 계속 돌아가는 커피머신 같다. 필요할 때마다 새 커피를 내려 준다.
분석, 포맷, 테스트 명령어
출시 가능한 앱은 "내 기기에서만 대충 돌아가는 앱"이 아니다. 코드 스타일이 일정하고, 명백한 오류가 없고, 핵심 로직이 테스트로 보호되어야 한다. 이때 자주 쓰는 명령어가 분석, 포맷, 테스트 명령어다.
flutter analyzeflutter analyze는 Dart 분석기를 실행해 잠재적인 오류, 타입 문제, 사용하지 않는 import, 권장하지 않는 코드 패턴을 찾아 준다. 팀 프로젝트라면 Pull Request를 올리기 전에 반드시 실행하는 습관을 들이자.
코드 포맷은 Dart 도구를 사용한다.
dart format lib test현재 디렉터리 전체를 포맷하고 싶다면 다음처럼 실행한다.
dart format .테스트는 범위에 따라 다르게 실행할 수 있다.
flutter test
flutter test test/unit/note_repository_test.dart
flutter test test/widget/note_list_screen_test.dart커버리지 파일을 만들고 싶다면 --coverage 옵션을 붙인다.
flutter test --coverage테스트가 많아질수록 이 명령어들은 귀찮은 절차가 아니라 안전벨트가 된다. 기능을 하나 고쳤는데 로그인, 검색, 오프라인 캐시가 동시에 깨지는 일을 막아 주기 때문이다.
clean은 최후의 망치처럼 사용하기
Flutter 개발을 하다 보면 이상한 순간이 온다. 분명 코드는 맞는데 빌드가 실패한다. 패키지를 지웠는데 계속 남아 있는 것처럼 보인다. Android에서는 안 되고 iOS에서는 된다. 어제는 됐는데 오늘은 안 된다.
이때 많은 개발자가 flutter clean을 떠올린다.
flutter clean
flutter pub getflutter clean은 build/와 .dart_tool/ 같은 빌드 관련 산출물을 삭제한다. 그래서 캐시가 꼬였을 때 효과가 있다. 하지만 모든 문제를 해결하는 마법 주문은 아니다. 실행 후에는 패키지를 다시 받아야 하므로 보통 flutter pub get과 함께 사용한다.
자주 사용하는 정리 루틴은 다음과 같다.
flutter clean
flutter pub get
flutter runiOS에서 CocoaPods 문제가 함께 있다면 iOS 디렉터리에서 pods를 다시 설치해야 할 수도 있다.
cd ios
pod install
cd ..
flutter run다만 습관적으로 매번 clean을 실행하면 개발 속도가 느려진다. 평소에는 pub get, analyze, test를 먼저 확인하고, 캐시나 빌드 산출물이 의심될 때 clean을 꺼내자. 망치는 필요할 때 강력하지만, 나사를 조일 때마다 망치를 들 필요는 없다.
플랫폼별 배포 빌드 명령어
이제 앱을 사용자에게 전달할 차례다. Flutter는 플랫폼별로 다른 산출물을 만든다. Android는 APK 또는 AAB, iOS는 IPA, Web은 정적 파일 묶음이 대표적이다.
| 플랫폼 | 명령어 | 산출물 | 기본 위치 |
|---|---|---|---|
| Android APK | flutter build apk --release | .apk | build/app/outputs/flutter-apk/app-release.apk |
| Android App Bundle | flutter build appbundle --release | .aab | build/app/outputs/bundle/release/app-release.aab |
| iOS 앱 빌드 | flutter build ios --release | iOS 앱 번들 | build/ios/iphoneos/Runner.app |
| iOS IPA | flutter build ipa --release | .ipa | build/ios/ipa/*.ipa |
| Web | flutter build web --release | HTML/CSS/JS 정적 파일 | build/web |
Android에 직접 설치해서 테스트할 때는 APK가 편하다.
flutter build apk --releaseGoogle Play에 올릴 때는 일반적으로 AAB를 사용한다.
flutter build appbundle --releaseiOS는 Xcode 설정, 인증서, 프로비저닝 프로파일이 함께 맞아야 한다. App Store Connect나 TestFlight 업로드를 생각한다면 IPA 빌드를 사용한다.
flutter build ipa --release웹 배포 파일은 다음 명령어로 만든다.
flutter build web --release빌드 결과물은 build/web에 생성된다. 이 폴더를 Firebase Hosting, GitHub Pages, Netlify, Vercel 같은 정적 호스팅에 올릴 수 있다.
Debug 빌드와 Release 빌드는 다르게 봐야 한다. Debug 빌드는 개발 편의를 위해 디버깅 정보와 Hot Reload 환경을 포함한다. Release 빌드는 최적화되어 크기와 성능이 실제 사용자 환경에 가깝다. "출시 전에 마지막으로 한 번만 확인하자"라는 순간에는 반드시 Release 빌드로 확인하자.
상황별 빠른 처방전
앱 개발 중에는 명령어를 문법별로 찾기보다 상황별로 찾는 일이 더 많다. 아래 표는 실제 작업 중 자주 만나는 장면을 기준으로 정리한 빠른 처방전이다.
| 상황 | 실행할 명령어 |
|---|---|
| 새 Flutter 앱 만들기 | flutter create my_app |
| 조직 도메인 포함해 앱 만들기 | flutter create --org com.mycompany my_app |
| 개발 환경 확인 | flutter doctor -v |
| 연결 기기 확인 | flutter devices |
| Chrome에서 실행 | flutter run -d chrome |
| Android 에뮬레이터에서 실행 | flutter run -d emulator-5554 |
| 패키지 설치 | flutter pub get |
| 패키지 추가 | flutter pub add package_name |
| 개발용 패키지 추가 | flutter pub add dev:package_name |
| 패키지 제거 | flutter pub remove package_name |
| 오래된 패키지 확인 | flutter pub outdated |
| 패키지 업그레이드 | flutter pub upgrade |
| 코드 분석 | flutter analyze |
| 코드 포맷 | dart format . |
| 전체 테스트 | flutter test |
| 커버리지 포함 테스트 | flutter test --coverage |
| 캐시 정리 | flutter clean && flutter pub get |
| Android APK 빌드 | flutter build apk --release |
| Android AAB 빌드 | flutter build appbundle --release |
| iOS IPA 빌드 | flutter build ipa --release |
| Web 빌드 | flutter build web --release |
명령어를 많이 아는 것보다 중요한 것은 실행 시점을 아는 것이다. flutter doctor는 환경이 의심될 때, flutter analyze는 코드 품질이 의심될 때, flutter clean은 캐시가 의심될 때, flutter test는 기능이 깨졌는지 확인할 때 사용한다.
실전 앱의 기본 패키지 지도
Flutter 생태계는 넓다. pub.dev에는 수많은 패키지가 있고, 같은 문제를 해결하는 패키지도 여러 개다. 입문자에게 가장 어려운 선택은 "무엇을 써야 하는가"다. 여기서는 이 책에서 배운 실전 앱 구조를 기준으로 기본 패키지 지도를 정리한다.
| 분야 | 추천 패키지 | 주 용도 |
|---|---|---|
| 라우팅 | go_router | 선언적 라우팅, 인증 기반 리다이렉트, 중첩 라우팅 |
| 상태관리 | flutter_riverpod | 상태관리, 의존성 주입, 비동기 상태 처리 |
| REST API | http | 가볍고 단순한 HTTP 통신 |
| REST API | dio | 인터셉터, 취소 토큰, 파일 업로드 등 고급 HTTP 통신 |
| 불변 모델 | freezed | 불변 객체, union/sealed 스타일 모델링 |
| JSON 직렬화 | json_serializable | fromJson, toJson 코드 생성 |
| 간단 저장소 | shared_preferences | 테마, 온보딩 여부 같은 간단한 설정값 저장 |
| 보안 저장소 | flutter_secure_storage | 토큰, 민감 정보 저장 |
| 로컬 DB | drift | SQLite 기반 구조화 데이터 저장 |
| 이미지 캐싱 | cached_network_image | 네트워크 이미지 캐싱과 로딩/에러 UI |
| Firebase 초기화 | firebase_core | Firebase 앱 초기화 |
| Firebase 인증 | firebase_auth | 이메일, 소셜 로그인, 익명 로그인 등 |
| Firebase DB | cloud_firestore | 문서형 실시간 데이터베이스 |
| Firebase 파일 | firebase_storage | 이미지, 첨부 파일 업로드 |
| Firebase 알림 | firebase_messaging | FCM 푸시 알림 |
| 오류 추적 | firebase_crashlytics | 실제 사용자 환경의 크래시 수집 |
패키지를 고를 때는 인기도만 보지 말고 세 가지를 함께 보자. 최근 업데이트가 있는지, 문서와 예제가 충분한지, 현재 앱의 복잡도에 맞는지다. 작은 앱에 너무 무거운 도구를 넣으면 개발이 느려지고, 큰 앱에 너무 단순한 도구만 쓰면 유지보수가 어려워진다.
라우팅: go_router
작은 앱에서는 Navigator.push만으로도 충분하다. 하지만 로그인 여부에 따라 첫 화면이 달라지고, 하단 탭마다 독립적인 탐색 스택이 있고, 웹에서 URL까지 맞춰야 한다면 이야기가 달라진다. 이때 go_router가 좋은 선택이 된다.
설치 명령어는 간단하다.
flutter pub add go_routergo_router는 화면 이동을 코드 여기저기 흩뿌리는 대신, 앱의 경로 구조를 한곳에서 선언하게 해 준다. 예를 들어 /login, /notes, /notes/:id, /settings처럼 앱의 지도를 먼저 그려 놓고, UI에서는 그 경로로 이동한다.
인증 기반 앱에서는 리다이렉트가 특히 중요하다. 사용자가 로그인하지 않았는데 /notes로 들어가려 하면 /login으로 보내고, 이미 로그인한 사용자가 /login에 접근하면 /notes로 보내는 식이다. 이 구조를 잘 잡아 두면 "뒤로 가기 했더니 로그인 화면이 다시 나와요" 같은 문제를 줄일 수 있다.
상태관리와 의존성 주입: flutter_riverpod
flutter_riverpod은 상태관리뿐 아니라 의존성 주입에도 많이 사용된다. 의존성 주입은 쉽게 말해 "필요한 객체를 직접 만들지 않고 바깥에서 넣어 주는 방식"이다. 예를 들어 화면이 ApiClient()를 직접 생성하는 대신, provider를 통해 NoteRepository를 받아 쓰게 만들면 테스트와 교체가 쉬워진다.
설치 명령어는 다음과 같다.
flutter pub add flutter_riverpodRiverpod 공식 문서에서는 Flutter 앱에 flutter_riverpod을 추가하고, 앱 루트를 ProviderScope로 감싼 뒤 provider를 읽는 기본 흐름을 안내한다.3 pub.dev의 flutter_riverpod 설명에서도 Riverpod은 비동기 코드의 로딩과 오류 상태 처리를 돕고, 로직을 UI에서 분리하며, 테스트 가능하고 확장 가능한 코드를 만들 수 있게 한다고 소개한다.4
가장 기본적인 구조는 다음과 같다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return MaterialApp(
home: Scaffold(
body: Center(child: Text('count: $count')),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Icon(Icons.add),
),
),
);
}
}실전에서는 단순 카운터보다 API, 인증, 로컬 저장소, Repository를 provider로 묶는 일이 많다.
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient();
});
final noteRepositoryProvider = Provider<NoteRepository>((ref) {
final apiClient = ref.watch(apiClientProvider);
return NoteRepository(apiClient);
});이렇게 해 두면 화면은 NoteRepository가 정확히 어떻게 만들어지는지 몰라도 된다. 나중에 REST API에서 Firebase로 바꾸거나, 테스트에서 가짜 Repository로 교체할 때도 구조가 훨씬 편해진다.
API 통신: http와 dio
REST API를 연결할 때 가장 먼저 고민하는 패키지는 http와 dio다. 둘 다 훌륭하지만 성격이 다르다.
http는 단순하고 가볍다. GET, POST, PUT, DELETE 요청을 직접 다루기 좋고, 입문자가 HTTP의 기본 흐름을 이해하기에 적합하다.
flutter pub add httpimport 'dart:convert';
import 'package:http/http.dart' as http;
Future<List<dynamic>> fetchNotes() async {
final response = await http.get(
Uri.parse('https://example.com/notes'),
);
if (response.statusCode != 200) {
throw Exception('노트 목록을 불러오지 못했습니다.');
}
return jsonDecode(response.body) as List<dynamic>;
}dio는 실전 앱에서 필요한 고급 기능이 풍부하다. 요청/응답 인터셉터, 인증 토큰 자동 추가, 파일 업로드, 요청 취소, 타임아웃 설정 등을 구조적으로 관리하기 쉽다.
flutter pub add dio앱이 작고 API가 단순하다면 http로 충분하다. 로그인 토큰 갱신, 공통 헤더, 에러 변환, 파일 업로드가 많다면 dio가 편하다.
모델링과 JSON: freezed, json_serializable
처음에는 JSON을 직접 Map<String, dynamic>으로 다뤄도 된다. 하지만 모델 필드가 늘어나고 API 응답이 많아지면 직접 작성한 fromJson, toJson은 실수의 온상이 된다. 필드명을 바꿨는데 JSON 키를 수정하지 않거나, nullable 처리를 놓치거나, 복사 메서드를 매번 손으로 작성하게 된다.
이때 freezed와 json_serializable을 함께 사용하면 좋다.
flutter pub add freezed_annotation json_annotation
flutter pub add dev:build_runner
flutter pub add dev:freezed
flutter pub add dev:json_serializable예를 들어 노트 모델은 다음처럼 작성할 수 있다.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'note.freezed.dart';
part 'note.g.dart';
@freezed
abstract class Note with _$Note {
const factory Note({
required String id,
required String title,
required String content,
required DateTime updatedAt,
@Default(<String>[]) List<String> tags,
}) = _Note;
factory Note.fromJson(Map<String, dynamic> json) => _$NoteFromJson(json);
}그다음 코드 생성기를 실행한다.
dart run build_runner build --delete-conflicting-outputs파일 변경을 감시하고 자동 생성하려면 다음 명령어를 사용한다.
dart run build_runner watch --delete-conflicting-outputs이 조합의 장점은 단순히 코드가 줄어드는 데 있지 않다. 모델을 불변으로 다루고, JSON 변환을 자동화하고, copyWith로 상태를 안전하게 갱신할 수 있다는 데 있다. 상태관리와 궁합이 매우 좋다.
데이터 저장과 캐싱 패키지 선택하기
앱에는 서버에 저장할 데이터도 있고, 기기에 잠깐 저장할 데이터도 있다. 모든 데이터를 같은 방식으로 저장하면 안 된다. 테마 설정과 로그인 토큰, 오프라인 노트 캐시는 성격이 완전히 다르다.
| 용도 | 추천 패키지 | 예시 |
|---|---|---|
| 간단한 설정값 | shared_preferences | 다크 모드 여부, 온보딩 완료 여부, 마지막 선택 탭 |
| 민감 정보 | flutter_secure_storage | 액세스 토큰, 리프레시 토큰, 사용자 식별 정보 |
| 구조화된 로컬 DB | drift | 노트 목록, 검색 가능한 캐시, 오프라인 데이터 |
| 네트워크 이미지 캐싱 | cached_network_image | 프로필 이미지, 첨부 이미지, 썸네일 |
shared_preferences는 작고 단순한 값에 적합하다.
flutter pub add shared_preferences예를 들어 온보딩을 한 번 봤는지 저장하는 정도라면 충분하다. 하지만 인증 토큰처럼 민감한 값은 여기에 저장하지 않는 것이 좋다.
토큰과 민감 정보는 flutter_secure_storage를 사용한다.
flutter pub add flutter_secure_storage구조화된 데이터가 많다면 drift를 고려하자.
flutter pub add drift sqlite3_flutter_libs path_provider path
flutter pub add dev:drift_dev build_runnerdrift는 SQLite를 Dart답게 사용할 수 있게 도와준다. 노트, 태그, 첨부파일, 동기화 상태처럼 관계가 있는 데이터를 저장하기 좋다.
이미지는 cached_network_image가 자주 쓰인다.
flutter pub add cached_network_image네트워크 이미지를 매번 새로 내려받으면 데이터도 낭비되고 화면도 느려진다. 캐싱 패키지를 사용하면 이미 본 이미지를 재사용하고, 로딩 중 UI와 에러 UI도 더 쉽게 처리할 수 있다.
Firebase 패키지 조합하기
Firebase는 하나의 거대한 패키지가 아니라 기능별 패키지들의 조합에 가깝다. 먼저 firebase_core로 Firebase를 초기화하고, 필요한 기능을 하나씩 붙인다.
| 목적 | 패키지 | 역할 |
|---|---|---|
| 기본 초기화 | firebase_core | Firebase 앱 연결의 출발점 |
| 로그인 | firebase_auth | 이메일/비밀번호, 소셜 로그인, 익명 로그인 |
| 문서형 데이터 | cloud_firestore | 컬렉션과 문서 기반 데이터 저장 |
| 파일 업로드 | firebase_storage | 이미지, 첨부 파일 저장 |
| 푸시 알림 | firebase_messaging | FCM 기반 알림 수신 |
| 크래시 수집 | firebase_crashlytics | 출시 후 오류 추적 |
기본 설치 예시는 다음과 같다.
flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutter pub add firebase_storage
flutter pub add firebase_messaging
flutter pub add firebase_crashlytics앱 시작 시 Firebase를 초기화한다.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}로그인 기반 앱에서는 사용자별 데이터 분리가 중요하다. Firestore에 노트를 저장한다면 대략 다음과 같은 경로를 생각할 수 있다.
users/{uid}/notes/{noteId}이 구조는 보안 규칙을 작성할 때도 자연스럽다. 현재 로그인한 사용자의 uid와 경로의 uid가 같을 때만 읽고 쓰게 만들 수 있기 때문이다.
Firebase 패키지는 강력하지만, 많이 붙일수록 초기 설정과 플랫폼별 설정도 늘어난다. 처음부터 모든 기능을 설치하지 말고, 인증이 필요하면 firebase_auth, 문서 데이터가 필요하면 cloud_firestore, 이미지 업로드가 필요하면 firebase_storage처럼 목적에 맞게 추가하자.
추천 패키지 설치 세트
프로젝트를 새로 만들 때마다 패키지를 하나씩 기억하기 어렵다. 아래 조합은 이 책에서 만든 개인 지식 노트 앱 같은 실전 앱을 시작할 때 참고할 만한 기본 세트다.
라우팅과 상태관리 중심의 최소 세트는 다음과 같다.
flutter pub add go_router flutter_riverpodREST API 앱 기본 세트는 다음과 같다.
flutter pub add go_router flutter_riverpod dio
flutter pub add freezed_annotation json_annotation
flutter pub add dev:build_runner dev:freezed dev:json_serializable로컬 저장과 캐싱까지 포함하면 다음처럼 확장할 수 있다.
flutter pub add shared_preferences flutter_secure_storage cached_network_image
flutter pub add drift sqlite3_flutter_libs path_provider path
flutter pub add dev:drift_dev build_runnerFirebase 기반 앱이라면 다음 조합을 많이 사용한다.
flutter pub add firebase_core firebase_auth cloud_firestore firebase_storage
flutter pub add firebase_messaging firebase_crashlytics여기서 중요한 것은 "추천 세트를 무조건 다 설치하라"가 아니다. 패키지는 앱에 들어가는 가구와 같다. 언젠가 필요할 것 같다는 이유로 모든 가구를 들이면 집이 좁아진다. 지금 필요한 기능부터 설치하고, 앱이 성장할 때 하나씩 추가하자.
패키지 선택 기준
좋은 패키지는 앱 개발 속도를 크게 높인다. 반대로 맞지 않는 패키지는 몇 달 뒤 발목을 잡는다. 패키지를 고를 때는 다음 기준을 확인하자.
| 기준 | 확인할 것 |
|---|---|
| 유지보수 | 최근 업데이트 날짜, 이슈 응답, Flutter 최신 버전 대응 |
| 안정성 | 다운로드 수, 좋아요 수, pub.dev 점수, 실제 사용 사례 |
| 문서화 | README, 예제 코드, API 문서, 마이그레이션 가이드 |
| 플랫폼 지원 | Android, iOS, Web, macOS, Windows, Linux 지원 여부 |
| 의존성 무게 | 불필요하게 많은 하위 패키지를 끌고 오지 않는지 |
| 라이선스 | 상업적 사용이 가능한 라이선스인지 |
특히 멀티플랫폼 앱을 만든다면 플랫폼 지원 여부를 꼭 확인해야 한다. 모바일에서는 잘 되는 패키지가 Web에서는 제한적일 수 있고, Desktop에서는 추가 설정이 필요할 수 있다. 카메라, 파일, 위치, 알림처럼 네이티브 기능에 가까운 패키지일수록 이 차이가 커진다.
팀 프로젝트에서 명령어를 표준화하기
혼자 개발할 때는 머릿속에 명령어를 넣어 두어도 된다. 하지만 팀 프로젝트에서는 "각자 알아서 실행"이 위험해질 수 있다. 누군가는 flutter pub upgrade를 자주 하고, 누군가는 몇 달째 같은 lock 파일을 쓰고, 누군가는 코드 생성 명령어를 잊어버린다.
이럴 때는 README에 자주 쓰는 명령어를 명확히 적어 두자.
## Development
```bash
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter analyze
flutter test
flutter runBuild
flutter build appbundle --release
flutter build ipa --release
더 나아가 Makefile이나 스크립트 도구를 사용할 수도 있다.
```makefile
setup:
flutter pub get
dart run build_runner build --delete-conflicting-outputs
check:
flutter analyze
flutter test
build-android:
flutter build appbundle --release그러면 팀원은 긴 명령어를 외우지 않아도 된다.
make setup
make check
make build-android명령어 표준화는 사소해 보이지만 협업의 피로를 크게 줄인다. 좋은 팀은 코드를 잘 짜는 팀이기도 하지만, 반복 작업을 덜 헷갈리게 만드는 팀이기도 하다.
마지막으로 남겨 두는 개인 치트시트
책을 덮기 전에, 가장 자주 쓸 명령어만 다시 압축해 보자. 이 표는 북마크해 두어도 좋다.
| 하고 싶은 일 | 명령어 |
|---|---|
| Flutter 상태 확인 | flutter doctor -v |
| 새 앱 만들기 | flutter create my_app |
| 앱 실행 | flutter run |
| 기기 목록 보기 | flutter devices |
| 패키지 받기 | flutter pub get |
| 패키지 추가 | flutter pub add package_name |
| 패키지 삭제 | flutter pub remove package_name |
| 패키지 업데이트 확인 | flutter pub outdated |
| 코드 분석 | flutter analyze |
| 코드 포맷 | dart format . |
| 테스트 실행 | flutter test |
| 코드 생성 | dart run build_runner build --delete-conflicting-outputs |
| 빌드 캐시 정리 | flutter clean && flutter pub get |
| Android 출시 빌드 | flutter build appbundle --release |
| iOS 출시 빌드 | flutter build ipa --release |
| Web 출시 빌드 | flutter build web --release |
이제 여러분은 Flutter 앱을 만드는 데 필요한 큰 길을 모두 지나왔다. Dart 문법에서 시작해 위젯, 레이아웃, Material 3, 입력, 라우팅, 상태관리, 아키텍처, API, 로컬 저장소, Firebase, 알림, 네이티브 기능, 반응형 UI, 애니메이션, 테스트, 성능, 보안, 배포까지 왔다.
마지막에 남는 것은 결국 반복이다. 좋은 앱은 한 번에 완성되지 않는다. 만들고, 실행하고, 분석하고, 테스트하고, 고치고, 다시 빌드한다. 오늘 정리한 명령어와 패키지는 그 반복을 더 빠르고 안전하게 만들어 주는 도구다.
터미널을 열고 다음 명령어를 입력하는 순간, 여러분은 더 이상 예제만 따라 하는 독자가 아니다.
flutter create my_next_app이제 다음 앱은 여러분의 차례다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
