F5
Ctrl
+ S
command
+ S
Ctrl
+ A
command
+ A
Ctrl
+ X
command
+ X
shift
+ enter
Ctrl
+ Alt
+ L
option
+ command
+ L
Tab
Shift
+ Tab
Ctrl
+ /
command
+ /
[수업 목표]
[목차]
VSCode에서 아래와 같이 네모 모양의 Stop
버튼을 눌러 기존에 실행한 앱을 종료해주세요.
View
→ Command Palette
를 선택해주세요.
명령어를 검색하는 팝업창이 뜨면, flutter
라고 입력한 뒤 Flutter: New Project
를 선택해주세요.
Application
을 선택해주세요.
프로젝트를 저장할 폴더를 선택하는 화면이 나오면 flutter
폴더를 선택한 뒤 Select a folder to create the project in
버튼을 눌러 주세요.
프로젝트 이름을 mymemo_admob
으로 입력해주세요.
만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요. (팝업이 안보이시면 넘어가주세요!)
💡 아래 팝업에 대한 자세한 사항은 [링크](https://stackoverflow.com/questions/67914668/vs-code-do-you-trust-the-authors-of-the-files-in-this-folder)를 참고해주세요.다음과 같이 프로젝트가 생성됩니다.
불필요한 힌트 숨기기
코드스니펫을 복사해서 analysis_options.yaml
파일의 24번째 라인 뒤에 붙여 넣고 저장해주세요.
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
main.dart
파일을 열어보시면 파란 실선이 있습니다. const
라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다.
💡 상수로 만들면 어떤 이점이 있나요?
상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다.
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
provider
패키지와 shared_preferences
패키지를 시작하는 코드에서 사용하고 있으므로 패키지 설치부터 진행하도록 하겠습니다. View
→ Terminal
을 선택해주세요.
아래 코드스니펫을 복사해서 터미널에 붙여넣고 실행해 주세요.
flutter pub add provider && flutter pub add shared_preferences
pubspec.yaml
을 열어서 39, 40번째 라인에 provider
와 shared_preferences
가 있으면 설치가 잘 되신 겁니다.
lib
폴더 내에 memo_service.dart
라는 이름의 파일을 새로 만들어주세요.
아래와 같이 빈 파일이 생성되면 됩니다.
아래 코드스니펫을 복사해서, main.dart
와 memo_service.dart
의 기존 코드를 모두 지우고 붙여 넣은 뒤 저장해 주세요.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
});
String content;
Map toJson() {
return {'content': content};
}
factory Memo.fromJson(json) {
return Memo(content: json['content']);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
VSCode 우측 하단에 Chrome (web-javascript)
를 클릭해주세요.
(에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed7e56bc-d81f-457f-8db9-d94e8d6e4f66/Untitled.png)
Start Pixel 2 API 29 mobile emulator
를 선택해주세요. macOS의 경우 iOS 에뮬레이터로 진행하셔도 무방합니다.
잠시 기다리면 에뮬레이터가 실행됩니다.
VSCode 우측 상단에 아래 화살표를 눌러 Run Without Debugging
을 눌러주세요.
에뮬레이터에 아래와 같이 메모장 화면이 나오면 완료!
💡 위와 같이 메모를 보여주는 `HomePage`와 메모를 작성하는 `DetailPage` 두 페이지로 구성된 앱입니다.기본적인 레이아웃과 페이지 이동 기능, CRUD 및 로컬 저장 기능도 구현 되어있습니다!
이제 여기에 AdMob 을 활용해 배너 광고를 구현해 보도록 하겠습니다.
링크를 클릭해 AdMob 사이트로 들어가주세요. 시작하기
버튼을 눌러줍니다.
구글 계정을 사용해 AdMob 에 가입해줍니다.
국가와 시간대를 선택하고, 약관에 동의한 후 AdMob 계정 만들기
를 눌러주세요.
이메일 팁 수신 여부는 원하는대로 체크하면 됩니다. 예 또는 아니오를 체크하고 계속해서 AdMob 사용
버튼을 눌러주세요.
그러면 아래와 같이 AdMob 관리자 페이지가 뜨는 것을 볼 수 있습니다! 시작하기
버튼을 눌러주세요.
아래와 같은 화면이 뜨면 각각 Android
, 아니오
를 선택하고, 계속
을 눌러줍니다.
앱 이름은 mymemo 로 작성해주세요 (원하시는 이름으로 설정하시면 됩니다)
아래와 같이 추가가 완료되었습니다. 완료
버튼을 눌러 다시 원래 페이지로 돌아가주세요.
왼쪽의 앱 설정
탭에 들어가면 앱 ID 가 있습니다.
main 함수에 앱을 처음 시작할 때 MobileAds 를 초기화/구동하는 코드를 추가하겠습니다.
아래 코드스니펫을 복사해 main.dart
12번째 줄 맨 뒤에 붙여넣어주세요
MobileAds.instance.initialize();
오류가 나는 것은 import 를 하지 않았기 때문이겠죠!
13번째 줄의 MobileAds
를 클릭한 뒤 Quick Fix(Ctrl/Cmd + .
)를 눌러 Import library 'package:google_mobile_ads/google_mobile_ads.dart’
를 선택해주세요.
에러가 해결되었습니다.
HomePage 에 배너 광고를 만들어주겠습니다.
아래 코드스니펫을 복사해 main.dart
46번째 줄 맨 뒤에 붙여넣어주세요.
final BannerAd myBanner = BannerAd(
// Test 광고 ID, 광고 승인받은 후 생성한 광고 unit ID 로 바꾸기
adUnitId: Platform.isAndroid
? 'ca-app-pub-3940256099942544/6300978111' // Android ad unit ID
: 'ca-app-pub-3940256099942544/2934735716', // iOS ad unit ID
size: AdSize.fullBanner,
request: AdRequest(),
listener: BannerAdListener(
onAdFailedToLoad: (ad, error) {
ad.dispose();
},
),
);
에러를 해결하겠습니다. 49번째 줄의 Platform 을 클릭한 뒤, Quick Fix(Ctrl/Cmd + .
)를 눌러 Import library 'dart:io’
를 선택해주세요.
첫줄에 import 문이 추가되며 에러가 해결됩니다.
광고를 화면에 띄우기 위해서는 load 해오는 코드를 위젯을 처음 그려줄 때 실행해야 합니다.
StatefulWidget
에서 initState
라는 함수 안에 코드를 작성하면, 위젯이 처음 그려질 때 해당 코드가 실행됩니다.
61번째 줄 맨 끝에서 엔터를 두 번 눌러주시고 ini
까지 작성해주세요. 아래과 같이 initState() { … }
가 자동완성 목록에 뜹니다. 이를 눌러주세요.
그럼 아래와 같이 빈 initState 함수가 생성됩니다.
💡 `super.initState()` 는 기존에 initState 에 지정된 로직들을 실행하겠다는 의미입니다. 여기 아래에 우리가 추가적으로 로직을 작성해주면, State 가 처음 만들어질 때 원하는 동작을 실행할 수 있겠죠!아래 코드스니펫을 복사해 66번째 줄 맨 뒤에 붙여넣어줍니다.
myBanner.load();
마이메모 앱에는 bottomNavigatoinBar
가 따로 없으니 해당 위치에 배너를 표시해주겠습니다.
아래 코드스니펫을 복사해 main.dart
129번째 줄 맨 뒤에 붙여넣어주세요.
bottomNavigationBar: Container(
alignment: Alignment.center,
width: myBanner.size.width.toDouble(),
height: myBanner.size.height.toDouble(),
child: AdWidget(ad: myBanner),
),
main.dart
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
MobileAds.instance.initialize();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final BannerAd myBanner = BannerAd(
// Test 광고 ID, 광고 승인받은 후 생성한 광고 unit ID 로 바꾸기
adUnitId: Platform.isAndroid
? 'ca-app-pub-3940256099942544/6300978111' // Android ad unit ID
: 'ca-app-pub-3940256099942544/2934735716', // iOS ad unit ID
size: AdSize.fullBanner,
request: AdRequest(),
listener: BannerAdListener(
onAdFailedToLoad: (ad, error) {
ad.dispose();
},
),
);
void initState() {
// TODO: implement initState
super.initState();
myBanner.load();
}
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
bottomNavigationBar: Container(
alignment: Alignment.center,
width: myBanner.size.width.toDouble(),
height: myBanner.size.height.toDouble(),
child: AdWidget(ad: myBanner),
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
memo_service.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
});
String content;
Map toJson() {
return {'content': content};
}
factory Memo.fromJson(json) {
return Memo(content: json['content']);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
아래 가이드를 따라 Android 와 iOS, 각각 실 기기에 앱을 설치해보겠습니다.
(iOS 에 앱을 설치하려면 macOS 가 필요해요! Windows 를 이용하시는 분들은 Android 기기에만 설치할 수 있습니다)
앱을 설치할 Android 기기를 USB 케이블로 PC 와 연결해주세요.
기기에서 개발자 옵션
을 활성화합니다. 설정
화면으로 이동해 휴대전화 정보
탭으로 이동해주세요.
소프트웨어 정보
탭으로 이동합니다.
여기서 빌드번호
를 연속해서 여러번 탭 해주세요.
계속 탭하다보면 아래와 같이 개발 설정 완료 ~단계 전입니다.
라는 내용이 화면에 표시됩니다. 마저 탭 해주세요.
개발자 모드를 켰습니다.
라는 내용이 화면에 표시되면, 다시 뒤로 가기를 눌러 설정 화면으로 돌아가주세요.
이제 설정 화면에 개발자 옵션
이 추가된 것을 확인할 수 있습니다. 눌러서 들어가주세요.
USB 디버깅
을 활성화해주세요.
이제 기기에 앱을 설치할 준비를 완료했습니다.
이제 VS Code 우측 하단을 누르면 연결한 핸드폰 이름이 목록에 뜨는 것을 볼 수 있습니다. 설치할 기기를 선택해주세요.
터미널을 열고 flutter run --release
라고 입력하고 엔터를 눌러주세요.
아래와 같이 기기를 선택하는 옵션이 나오면 해당 번호를 입력해주시면 됩니다. (기기가 하나만 연결되어있다면 뜨지 않을 수도 있습니다)
💡 **디버그 모드** (Hot Reload 가능)로 앱을 실행하고 싶다면, 이전에 해왔듯이 `Run without debugging` 을 눌러주시면 됩니다. 이 경우, Hot Reload 등을 활용해 변경사항을 바로바로 반영할 수 있으나, 케이블 연결을 끊었을 때 앱을 사용하실 수 없습니다.지금처럼 릴리즈 모드로 설치하면 케이블 연결을 끊어도 앱을 계속 사용할 수 있습니다.
아래와 같은 에러가 발생할 겁니다.
android/app/build.gradle
파일을 열어주세요. 아래와 같이 50번째 줄의 minSdkVersion
을 19로 수정해줍니다.
다시 flutter run --release
를 실행해주면…
앱 설치가 완료되었습니다!
이제 터미널에서 아래 명령어를 실행합니다.
flutter build appbundle
아래와 같은 메시지가 터미널에 뜬다면 성공적으로 빌드가 완료된 것입니다.
build/app/outputs/bundle/release/
경로로 들어가시면 app-release.aab
파일을 확인할 수 있습니다.
해당 파일을 탐색기에서 열어주세요.
Play Console 에 접속하셔서 (구글 개발자 계정을 이미 생성하신 후여야 합니다) 앱 만들기
버튼을 클릭합니다.
아래와 같이 세부정보를 설정한 후 앱 만들기
버튼을 클릭합니다
앱이 생성되었습니다! 왼쪽 옵션에서 프로덕션
을 선택하고 새 버전 만들기
를 클릭해주세요.
App Bundle 옵션에 아까 생성한 app-release.aab
파일을 드래그 앤 드롭해 업로드해줍니다.
아래와 같이 파일 목록이 보인다면 성공적으로 업로드가 진행된 것입니다.
이제 맨 아래로 내려가 저장 -> 버전 검토
를 클릭해줍니다.
아래와 같은 오류는 각각 해당 대시보드로 이동해 설정을 마저 진행해줍니다.
설정이 끝나면 프로덕션 트랙으로 출시 시작
버튼을 눌러 제출을 진행해주세요.
이전 강의 바로가기
전체 강의 바로가기
Copyright ⓒ TeamSparta All rights reserved.