📱 모바일 개발자라면 누구나 한 번쯤 꿈꿔본 것: “하나의 코드로 Android와 iOS 모두에 배포하기”
Flutter는 이 꿈을 현실로 만들어주는 강력한 프레임워크다.
🎯 이 글은 누구를 위한 것인가요?
Android/iOS 네이티브 개발자인데 크로스 플랫폼으로 전환하고 싶다
웹 개발자인데 모바일 앱 개발에 도전해보고 싶다
Flutter는 관심 있는데 Dart 언어가 생소해서 망설이고 있다
실무에서 바로 사용 가능한 실용적인 Dart 문법을 배우고 싶다
개발자를 위해, Dart의 핵심만 골라 정리했다.
🚀 Dart를 배워야 하는 이유
1. Flutter의 심장, Dart
// 이 간단한 코드가 Android와 iOS 모두에서 동작합니다!
void main() {
runApp(MyApp());
}
Dart는 Google이 개발한 언어로, Flutter의 핵심이다. Java, Kotlin, Swift에 익숙하다면 Dart 문법이 친숙하게 느껴질 것이다.
2. 생산성의 혁신
기존에는 이랬죠:
Android: Java/Kotlin + XML
iOS: Swift/Objective-C + Storyboard
결과: 같은 기능을 두 번 개발
이제는:
하나의 Dart 코드 → Android + iOS 동시 배포
Hot Reload: 코드 수정 후 1초 만에 결과 확인
결과: 개발 시간 50% 단축
📚 Dart 핵심 문법 마스터하기
1. 변수와 데이터 타입: 스마트한 타입 추론
변수 선언의 3가지 방법
// 1. var: 타입 추론 (가장 자주 사용)
var userName = 'kimdev'; // String으로 자동 추론
var userAge = 28; // int로 자동 추론
var isActive = true; // bool로 자동 추론
// 2. 명시적 타입 (팀 컨벤션이나 명확성이 필요할 때)
String userEmail = 'kim@example.com';
int projectCount = 5;
bool isOnline = false;
// 3. dynamic: 모든 타입 허용 (신중하게 사용)
dynamic response = {'status': 'success'};
response = 'Error occurred'; // 타입 변경 가능
💡 실무 팁: var를 기본으로 사용하되, API 응답처럼 타입이 불확실한 경우에만 dynamic을 사용하자.
final vs const: 언제 무엇을 사용할까?
// final: 런타임에 한 번 설정 (API 응답값 등)
final String userToken = await authService.getToken(); // ✅ OK
final DateTime now = DateTime.now(); // ✅ OK
// const: 컴파일 타임 상수 (설정값, 고정값 등)
const String appName = 'MyAwesomeApp'; // ✅ OK
const int maxRetryCount = 3; // ✅ OK
// const DateTime now = DateTime.now(); // ❌ 에러!
💡 실무 팁: 설정값은 const, API에서 받아온 값은 final을 사용하자.
late 키워드: 지연 초기화의 마법
class UserService {
late String token; // 나중에 초기화
Future<void> initialize() async {
token = await getTokenFromStorage();
print('Token loaded: $token');
}
}
// 사용법
final userService = UserService();
await userService.initialize(); // 이제 token 사용 가능
💡 실무 팁: SharedPreferences나 SecureStorage에서 값을 불러올 때 유용합니다.
2. 컬렉션: 데이터 관리의 핵심
List: 순서가 있는 데이터
// 기본 리스트
var fruits = ['apple', 'banana', 'orange'];
var numbers = <int>[1, 2, 3, 4, 5];
// 실무에서 자주 사용하는 패턴
List<String> todoList = [];
todoList.add('Flutter 공부하기');
todoList.addAll(['프로젝트 시작하기', '코드 리뷰하기']);
// 함수형 프로그래밍 스타일 (매우 유용!)
var completedTasks = todoList
.where((task) => task.contains('완료'))
.map((task) => task.toUpperCase())
.toList();
print('할 일 개수: ${todoList.length}');
Map: 키-값 쌍의 데이터
// API 응답 데이터 처리에 필수!
var user = {
'id': 1,
'name': 'Kim MinJun',
'email': 'kim@example.com',
'isActive': true,
};
// 타입 안전한 방법
Map<String, dynamic> apiResponse = {
'status': 'success',
'data': {
'users': ['user1', 'user2'],
'count': 2,
}
};
// 안전한 접근 방법
String status = apiResponse['status'] ?? 'unknown';
List users = apiResponse['data']?['users'] ?? [];
Set: 중복 없는 데이터
// 태그 시스템, 카테고리 관리에 유용
var tags = <String>{'flutter', 'dart', 'mobile'};
tags.add('ios');
tags.add('flutter'); // 중복 추가되지 않음
print(tags); // {flutter, dart, mobile, ios}
// 실무 예시: 사용자 권한 관리
Set<String> userPermissions = {'read', 'write'};
bool canDelete = userPermissions.contains('delete');
3. 함수: 재사용 가능한 코드 블록
기본 함수와 Arrow 함수
// 전통적인 함수
String getWelcomeMessage(String name) {
return 'Welcome, $name!';
}
// Arrow 함수 (한 줄일 때 깔끔)
String getWelcomeMessage(String name) => 'Welcome, $name!';
// 실무에서 자주 사용하는 패턴
bool isValidEmail(String email) => email.contains('@') && email.contains('.');
int calculateAge(DateTime birthDate) {
final now = DateTime.now();
return now.year - birthDate.year;
}
실무에서 바로 쓰는 Flutter 프로젝트 초기 설정 가이드
더 이상 프로젝트 중간에 “아, 이걸 처음에 설정했어야 했는데…” 하지 마세요!
안녕하세요! 모바일 개발의 새로운 패러다임인 Flutter로 첫 프로젝트를 시작하려고 하시나요?
Android/iOS 네이티브 개발을 해보신 분들이나 웹 프론트엔드에서 모바일로 확장을 고려하고 계신 분들에게 “프로젝트 시작 전에 이것만은 꼭 확인하자!”는 실전 가이드를 준비했습니다.
제가 여러 Flutter 프로젝트를 진행하면서 겪었던 시행착오를 바탕으로, 프로젝트 초기에 놓치기 쉬운 핵심 설정들을 정리해봤습니다. 이 글을 따라하시면 나중에 “아, 처음에 이걸 설정했어야 했는데…”라는 후회를 하지 않으실 거예요! 😊
🎯 왜 초기 설정이 중요할까?
Flutter 프로젝트에서 초기 설정을 제대로 하지 않으면:
🔄 중간에 패키지 의존성 충돌로 며칠을 삽질하게 됩니다
📱 플랫폼별 설정 누락으로 배포 직전에 당황하게 됩니다
🔐 권한 설정 실수로 앱스토어 리젝을 당하게 됩니다
⚡ 성능 최적화를 나중에 하려고 하면 리팩토링 지옥에 빠집니다
특히 안드로이드나 iOS에서 넘어오신 분들은 “Flutter는 크로스 플랫폼이니까 설정이 간단하겠지?”라고 생각하시는데, 오히려 두 플랫폼을 모두 고려해야 하기 때문에 더 신경 써야 할 부분이 많습니다.
📋 체크리스트 개요
이 글에서 다룰 핵심 체크 포인트들입니다:
Android 설정 – Manifest와 Gradle 설정
iOS 설정 – info.plist와 Xcode 프로젝트 설정
pubspec.yaml 완벽 설정 – 의존성부터 리소스까지
필수 패키지 미리 세팅 – 나중에 후회하지 않을 패키지들
권한 및 보안 설정 – 앱스토어 승인을 위한 필수 사항들
각 단계별로 왜 필요한지, 어떻게 설정하는지, 주의사항은 무엇인지 상세히 알아보겠습니다.
1️⃣ Android 설정: 탄탄한 기반 다지기
📱 Android Manifest 설정
Android Manifest는 앱의 신분증 같은 역할을 합니다. android/app/src/main/AndroidManifest.xml 파일에서 핵심 설정들을 확인해봅시다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.your_app">
<!-- 인터넷 권한 (API 통신 필수) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 카메라 권한 (카메라 기능 사용 시) -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 저장소 권한 (파일 업로드/다운로드 시) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 위치 권한 (지도 기능 사용 시) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:label="Your App Name" <!-- 앱 이름 -->
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" <!-- 앱 아이콘 -->
android:usesCleartextTraffic="true"> <!-- HTTP 통신 허용 (개발 시에만) -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- 딥링크 설정 -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="yourapp.com" />
</intent-filter>
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
</activity>
<!-- Firebase 설정 (푸시 알림 사용 시) -->
<service
android:name=".java.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
💡 Pro Tip: 권한은 처음부터 다 추가하지 마세요! 필요한 기능이 생길 때마다 추가하는 것이 좋습니다. Google Play Store에서는 불필요한 권한을 요청하는 앱을 좋지 않게 봅니다.
🔧 build.gradle 설정
android/app/build.gradle 파일은 앱의 빌드 설정을 담당합니다.
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Firebase 사용 시 추가
apply plugin: 'com.google.gms.google-services'
android {
compileSdkVersion 34 // 최신 SDK 버전 사용
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "com.example.your_app" // 고유한 패키지명
minSdkVersion 21 // Android 5.0 이상 지원
targetSdkVersion 34 // 최신 타겟 SDK
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
// MultiDex 활성화 (패키지가 많을 때 필요)
multiDexEnabled true
// 프로가드 설정
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
signingConfigs {
release {
// 릴리즈 빌드를 위한 키스토어 설정
keyAlias localProperties.getProperty('keyAlias')
keyPassword localProperties.getProperty('keyPassword')
storeFile localProperties.getProperty('storeFile') ? file(localProperties.getProperty('storeFile')) : null
storePassword localProperties.getProperty('storePassword')
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true // 코드 난독화 활성화
shrinkResources true // 불필요한 리소스 제거
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug" // 디버그 버전 구분
debuggable true
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Firebase 사용 시
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-crashlytics'
// MultiDex 지원
implementation 'com.android.support:multidex:1.0.3'
}
⚠️ 주의사항:
minSdkVersion은 21 이상으로 설정하세요 (Android 5.0). 더 낮으면 최신 Flutter 기능들을 사용할 수 없습니다.
compileSdkVersion과 targetSdkVersion은 최신 버전을 사용하세요.
키스토어 정보는 절대 코드에 하드코딩하지 마세요!
🔑 키스토어 설정 (배포 준비)
배포를 위해서는 앱 서명용 키스토어가 필요합니다.
# 키스토어 생성
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
# android/key.properties 파일 생성
storePassword=your_store_password
keyPassword=your_key_password
keyAlias=upload
storeFile=/Users/your_username/upload-keystore.jks
2️⃣ iOS 설정: 애플 생태계 정복하기
🍎 info.plist 설정
iOS 설정의 핵심인 ios/Runner/Info.plist 파일을 살펴봅시다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 앱 기본 정보 -->
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Your App Name</string> <!-- 홈 화면에 표시될 앱 이름 -->
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>your_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<!-- 최소 iOS 버전 -->
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<!-- UI 설정 -->
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<!-- 지원 기기 방향 -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!-- 권한 설명 메시지 (필수!) -->
<key>NSCameraUsageDescription</key>
<string>이 앱은 사진 촬영을 위해 카메라에 접근합니다.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>이 앱은 사진 저장을 위해 사진 라이브러리에 접근합니다.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>이 앱은 위치 기반 서비스 제공을 위해 위치 정보를 사용합니다.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>이 앱은 백그라운드에서도 위치 기반 서비스를 제공하기 위해 위치 정보를 사용합니다.</string>
<key>NSMicrophoneUsageDescription</key>
<string>이 앱은 음성 녹음을 위해 마이크에 접근합니다.</string>
<!-- App Transport Security -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/> <!-- 프로덕션에서는 false로 설정 -->
<key>NSExceptionDomains</key>
<dict>
<key>your-api-domain.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
</dict>
</dict>
<!-- Deep Link 설정 -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>your-app-scheme</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>
<!-- App Tracking Transparency (iOS 14.5+) -->
<key>NSUserTrackingUsageDescription</key>
<string>이 앱은 더 나은 광고 경험을 제공하기 위해 추적 권한을 요청합니다.</string>
</dict>
</plist>
🚨 중요: iOS에서는 권한 사용 이유를 반드시 명시해야 합니다. 애매한 설명은 앱스토어 리뷰에서 리젝당할 수 있습니다!
🔨 Xcode 프로젝트 설정
Xcode에서 직접 설정해야 할 항목들:
Bundle Identifier: 고유한 식별자 설정
Deployment Target: 최소 iOS 버전 (12.0 이상 권장)
Signing & Capabilities: 개발자 계정 연결
App Transport Security: HTTPS 보안 설정
Background Modes: 백그라운드 실행 권한
# Xcode에서 프로젝트 열기
open ios/Runner.xcworkspace
💡 Pro Tip: Xcode에서 설정하는 내용들은 자동으로 관련 파일들에 반영됩니다. 가능하면 Xcode GUI를 통해 설정하세요.
3️⃣ pubspec.yaml: Flutter 프로젝트의 심장
📦 기본 설정
pubspec.yaml은 Flutter 프로젝트의 모든 의존성과 설정을 관리하는 핵심 파일입니다.
name: your_app
description: A new Flutter project.
publish_to: 'none' # 패키지로 배포하지 않음
# 버전 정보
version: 1.0.0+1
# 환경 설정
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.10.0"
# 의존성
dependencies:
flutter:
sdk: flutter
# UI 컴포넌트
cupertino_icons: ^1.0.6 # iOS 스타일 아이콘
# 상태 관리 (선택: riverpod OR bloc OR provider)
flutter_riverpod: ^2.4.9
# flutter_bloc: ^8.1.3
# provider: ^6.1.1
# 네트워킹
dio: ^5.4.0 # HTTP 클라이언트 (http보다 기능이 풍부)
retrofit: ^4.0.3 # REST API 클라이언트 생성
# 로컬 저장소
shared_preferences: ^2.2.2 # 간단한 키-값 저장
hive: ^2.2.3 # NoSQL 로컬 데이터베이스
hive_flutter: ^1.1.0
# 유틸리티
get_it: ^7.6.7 # 의존성 주입
injectable: ^2.3.2 # 코드 생성 기반 DI
auto_route: ^7.9.2 # 자동 라우팅 생성
# JSON 직렬화
json_annotation: ^4.8.1
freezed_annotation: ^2.4.1
# 환경 변수
flutter_dotenv: ^5.1.0
# 네이티브 기능
permission_handler: ^11.2.0 # 권한 관리
device_info_plus: ^9.1.1 # 디바이스 정보
package_info_plus: ^4.2.0 # 앱 정보
url_launcher: ^6.2.2 # 외부 링크 열기
# 이미지 & 미디어
cached_network_image: ^3.3.0 # 이미지 캐싱
image_picker: ^1.0.5 # 이미지 선택
photo_view: ^0.14.0 # 이미지 확대/축소
video_player: ^2.8.1 # 비디오 재생
# Firebase (필요한 것만 선택)
firebase_core: ^2.24.2
firebase_auth: ^4.15.3
cloud_firestore: ^4.13.6
firebase_storage: ^11.5.6
firebase_messaging: ^14.7.10
firebase_analytics: ^10.7.4
firebase_crashlytics: ^3.4.8
# 소셜 로그인
google_sign_in: ^6.1.6
sign_in_with_apple: ^5.0.0
# flutter_facebook_auth: ^6.0.3
# UI 라이브러리
flutter_svg: ^2.0.9 # SVG 이미지
lottie: ^2.7.0 # Lottie 애니메이션
shimmer: ^3.0.0 # 로딩 효과
flutter_staggered_grid_view: ^0.7.0 # 그리드 뷰
# 유틸리티 위젯
gap: ^3.0.1 # 간격 위젯
flutter_screenutil: ^5.9.0 # 반응형 UI
# 기타
intl: ^0.19.0 # 국제화
equatable: ^2.0.5 # 값 비교 유틸리티
# 개발 의존성
dev_dependencies:
flutter_test:
sdk: flutter
# 린팅
flutter_lints: ^3.0.1
very_good_analysis: ^5.1.0 # 더 엄격한 린트 규칙
# 코드 생성
build_runner: ^2.4.7
freezed: ^2.4.6
json_serializable: ^6.7.1
retrofit_generator: ^8.0.4
injectable_generator: ^2.4.1
auto_route_generator: ^7.3.2
hive_generator: ^2.0.1
# 테스팅
mockito: ^5.4.4
integration_test:
sdk: flutter
# Flutter 설정
flutter:
uses-material-design: true
# 리소스 파일
assets:
- assets/images/
- assets/icons/
- assets/animations/
- .env
- .env.dev
- .env.prod
# 폰트
fonts:
- family: Pretendard
fonts:
- asset: assets/fonts/Pretendard-Regular.ttf
weight: 400
- asset: assets/fonts/Pretendard-Medium.ttf
weight: 500
- asset: assets/fonts/Pretendard-SemiBold.ttf
weight: 600
- asset: assets/fonts/Pretendard-Bold.ttf
weight: 700
# 상태 관리
flutter_riverpod: ^2.4.9 # 또는 bloc, provider 중 선택
# 네트워킹
dio: ^5.4.0
# 로컬 저장소
shared_preferences: ^2.2.2
# 권한 관리
permission_handler: ^11.2.0
# 환경 변수
flutter_dotenv: ^5.1.0
# JSON 처리
json_annotation: ^4.8.1
freezed_annotation: ^2.4.1
🛒 E-커머스/소셜 앱
# 인증
firebase_auth: ^4.15.3
google_sign_in: ^6.1.6
sign_in_with_apple: ^5.0.0
# 이미지 처리
cached_network_image: ^3.3.0
image_picker: ^1.0.5
# 푸시 알림
firebase_messaging: ^14.7.10
# 결제
# in_app_purchase: ^3.1.11
📍 위치 기반 서비스
# 위치
geolocator: ^10.1.0
geocoding: ^2.1.1
# 지도
google_maps_flutter: ^2.5.0
🎵 미디어 앱
# 미디어
video_player: ^2.8.1
audio_players: ^5.2.1
camera: ^0.10.5+5
# 파일 처리
file_picker: ^6.1.1
path_provider: ^2.1.2
📚 패키지 관리 팁
버전 호환성 확인: flutter pub deps 명령어로 의존성 트리 확인
정기적인 업데이트: flutter pub upgrade 하되 메이저 업데이트는 신중히
불필요한 패키지 제거: 사용하지 않는 패키지는 과감히 제거
라이선스 확인: 상업적 사용 가능한 라이선스인지 확인
# 패키지 추가
flutter pub add package_name
# 개발 의존성 추가
flutter pub add --dev package_name
# 특정 버전 지정
flutter pub add package_name:^1.0.0
# 의존성 확인
flutter pub deps
# 오래된 패키지 확인
flutter pub outdated
5️⃣ 권한 및 보안 설정: 앱스토어 승인의 지름길
🔐 iOS App Tracking Transparency (iOS 14.5+)
iOS 14.5부터는 사용자 추적을 위한 명시적 권한 요청이 필요합니다:
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:permission_handler/permission_handler.dart';
class AppInitializer {
static Future<void> initialize() async {
WidgetsFlutterBinding.ensureInitialized();
// App Tracking Transparency 권한 요청
if (Platform.isIOS) {
await _requestTrackingPermission();
}
// 알림 권한 요청
await _requestNotificationPermission();
// 기타 초기화 작업...
}
static Future<void> _requestTrackingPermission() async {
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
if (status == TrackingStatus.notDetermined) {
// 커스텀 다이얼로그 표시 (선택사항)
await _showTrackingDialog();
// 시스템 권한 요청
await AppTrackingTransparency.requestTrackingAuthorization();
}
}
static Future<void> _showTrackingDialog() async {
// 사용자에게 추적 권한의 필요성을 설명하는 커스텀 다이얼로그
return showDialog(
context: navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: Text('개인화된 광고'),
content: Text(
'더 나은 앱 경험을 위해 다른 회사의 앱과 웹사이트에서 회원님의 활동을 추적하도록 허용하시겠습니까?'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('계속'),
),
],
),
);
}
static Future<void> _requestNotificationPermission() async {
final status = await Permission.notification.status;
if (status.isDenied) {
await Permission.notification.request();
}
}
}
// main.dart
void main() async {
await AppInitializer.initialize();
runApp(MyApp());
}
## Android 배포 체크리스트
- [ ] 키스토어 생성 및 설정 완료
- [ ] 프로가드 규칙 설정
- [ ] 앱 서명 확인
- [ ] 권한 최소화
- [ ] APK/AAB 크기 최적화
- [ ] 64비트 아키텍처 지원 확인
## iOS 배포 체크리스트
- [ ] Apple Developer 계정 등록
- [ ] Bundle ID 등록
- [ ] 인증서 및 프로비저닝 프로파일 생성
- [ ] App Store Connect 앱 등록
- [ ] 권한 사용 이유 명시
- [ ] ATS (App Transport Security) 설정
## 공통 체크리스트
- [ ] 앱 아이콘 설정 (모든 사이즈)
- [ ] 스플래시 스크린 설정
- [ ] 앱 이름 및 설명 다국어 지원
- [ ] 개인정보 보호정책 URL 설정
- [ ] 테스트 디바이스에서 최종 확인
- [ ] 성능 최적화 확인
- [ ] 크래시 없는지 확인
Android/iOS 개발자가 Flutter로 전환할 때 반드시 알아야 할 Dart 언어의 핵심 개념들
들어가며
안녕하세요! 모바일 개발 경험이 있으시거나 웹 프론트엔드에서 모바일로 영역을 확장하려는 개발자분들을 위한 Dart 언어 가이드를 준비했습니다.
Flutter를 배우기 위해서는 먼저 Dart 언어를 이해해야 합니다. 하지만 걱정하지 마세요! Java/Kotlin, Swift, JavaScript를 사용해보신 분이라면 Dart는 매우 친숙하게 느껴질 것입니다.
이 글에서는 실제 Flutter 앱 개발에서 자주 사용되는 Dart의 핵심 개념들을 실무 예제와 함께 설명드리겠습니다.
1. Dart 언어 소개
왜 Dart인가?
Flutter팀이 Dart를 선택한 이유는 명확합니다:
AOT(Ahead-of-Time) 컴파일: 네이티브 수준의 성능
JIT(Just-in-Time) 컴파일: 개발 중 Hot Reload 지원
가비지 컬렉션: 메모리 관리 자동화
단일 스레드 모델: 복잡한 동시성 문제 해결
다른 언어와의 비교
// Java/Kotlin 스타일과 매우 유사
class User {
String name;
int age;
User(this.name, this.age); // 생성자 단축 문법
}
// JavaScript처럼 동적 타이핑도 지원
var data = {'name': 'John', 'age': 30};
2. 변수와 타입 시스템
변수 선언의 베스트 프랙티스
Dart는 강타입 언어이지만 타입 추론을 지원하여 개발 편의성을 제공합니다.
// 타입 추론 활용 (권장)
var userName = 'John Doe'; // String으로 추론
var userAge = 25; // int로 추론
var isActive = true; // bool로 추론
// 명시적 타입 선언 (API 경계에서 권장)
String getUserName() => userName;
int calculateAge(DateTime birthDate) =>
DateTime.now().year - birthDate.year;
// 컬렉션 타입 명시
List<String> tags = ['flutter', 'dart', 'mobile'];
Map<String, dynamic> apiResponse = {'status': 'success', 'data': []};
final vs const: 실무에서의 활용
class AppConfig {
// 컴파일 타임 상수 - 빌드 시점에 결정
static const String appName = 'MyAwesomeApp';
static const int maxRetryCount = 3;
static const Duration requestTimeout = Duration(seconds: 30);
// 런타임 상수 - 앱 실행 중 한 번만 설정
final String deviceId;
final DateTime appStartTime;
AppConfig()
: deviceId = _generateDeviceId(),
appStartTime = DateTime.now();
static String _generateDeviceId() {
// 디바이스 고유 ID 생성 로직
return DateTime.now().millisecondsSinceEpoch.toString();
}
}
// 사용 예
void main() {
final config = AppConfig();
print('앱 이름: ${AppConfig.appName}');
print('시작 시간: ${config.appStartTime}');
}
late 키워드: 지연 초기화 패턴
class ApiService {
late final Dio _dio;
late final String _baseUrl;
// 초기화 메서드에서 late 변수들을 설정
Future<void> initialize() async {
_baseUrl = await _loadBaseUrlFromConfig();
_dio = Dio(BaseOptions(
baseUrl: _baseUrl,
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 10),
));
}
Future<String> _loadBaseUrlFromConfig() async {
// 설정 파일이나 환경 변수에서 URL 로드
return 'https://api.example.com';
}
Future<Map<String, dynamic>> get(String endpoint) async {
final response = await _dio.get(endpoint);
return response.data;
}
}
3. 함수와 메서드
함수형 프로그래밍 스타일
Dart는 함수를 일급 객체로 취급하여 함수형 프로그래밍 패턴을 지원합니다.
// 고차 함수 활용
List<T> processData<T>(
List<T> data,
bool Function(T) filter,
T Function(T) transform,
) {
return data
.where(filter)
.map(transform)
.toList();
}
// 실사용 예제
class UserService {
List<User> filterActiveUsers(List<User> users) {
return processData(
users,
(user) => user.isActive && user.lastLoginDays < 30,
(user) => user.copyWith(displayName: user.name.toUpperCase()),
);
}
}
// 익명 함수와 클로저
class EventHandler {
final List<VoidCallback> _listeners = [];
void addListener(VoidCallback callback) => _listeners.add(callback);
void fireEvent() {
for (final listener in _listeners) {
listener();
}
}
// 클로저를 활용한 이벤트 리스너
VoidCallback createLoggingListener(String tag) {
return () => print('[$tag] Event fired at ${DateTime.now()}');
}
}
// 이 간단한 코드가...
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://example.com/image.png'),
const Text('Hello Flutter'),
],
),
)
// 실제로는 이렇게 복잡한 위젯 트리를 생성합니다
Container
├── ColoredBox (color 속성 때문에 자동 생성)
│ └── Row
│ ├── RawImage (Image.network가 내부적으로 사용)
│ └── RichText (Text가 내부적으로 사용)
개발자 팁: flutter inspector를 사용하면 실제 위젯 트리를 볼 수 있습니다.
Layout Phase (레이아웃 단계)
박스 제약조건(Box Constraints) 시스템을 사용한 레이아웃 계산:
// Flutter의 레이아웃 원칙
// "Constraints go down, sizes go up, parent sets position"
class CustomWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 200, // 부모에게 원하는 크기 전달
height: 100,
child: Text('Hello'), // 자식은 부모 제약조건 내에서 크기 결정
);
}
}
제약조건의 종류:
Tight Constraints: 정확한 크기 지정 (width: 100, height: 50)
// 위젯은 불변(immutable) 객체
class MyWidget extends StatelessWidget {
final String title;
const MyWidget({required this.title, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(title); // 매번 새로운 Text 위젯 반환
}
}
특징:
불변 객체 (매번 새로 생성)
설정값만 보관 (데이터)
빠른 생성과 폐기
Element Tree (엘리먼트 트리)
// Element는 위젯의 인스턴스를 관리
abstract class Element {
Widget get widget; // 현재 위젯
RenderObject? get renderObject; // 렌더 객체 참조
void update(Widget newWidget) {
// 위젯이 바뀌어도 Element는 재사용
}
}
특징:
위젯과 렌더 객체 사이의 중재자
생명주기 관리 (mount, unmount)
BuildContext의 실체
Render Tree (렌더 트리)
// RenderObject는 실제 레이아웃과 페인팅 담당
class RenderFlex extends RenderBox {
@override
void performLayout() {
// 자식들의 크기와 위치 계산
}
@override
void paint(PaintingContext context, Offset offset) {
// 실제 그리기 명령 수행
}
}