Gonganjuck 개발일지 5주차

GongAn Juck·2023년 1월 29일
0
post-thumbnail

5주차 - 구글 애드몹 & 배포

  • PDF 파일 5주차-구글_애드몹__배포.pdf
  • 단축키 모음
    • 새로고침 F5
    • 저장
      • Windows: Ctrl + S
      • macOS: command + S
    • 전체선택
      • Windows: Ctrl + A
      • macOS: command + A
    • 잘라내기
      • Windows: Ctrl + X
      • macOS: command + X
    • 콘솔창 줄바꿈
      • shift + enter
    • 코드정렬
      • Windows: Ctrl + Alt + L
      • macOS: option + command + L
    • 들여쓰기
      • Tab
      • 들여쓰기 취소 : Shift + Tab
    • 주석
      • Windows: Ctrl + /
      • macOS: command + /

[수업 목표]

  • 마이메모 앱에 구글 애드몹 적용해 수익화 하기
  • 내 핸드폰에 앱 설치해보기
  • 구글 플레이스토어에 앱 배포하기

[목차]

01. 프로젝트 준비

💡 마이메모에 구글 AdMob 을 적용한 앱을 새로 만들어보겠습니다. 각자 숙제를 진행하며 코드가 달라졌을테니, 새로 프로젝트를 생성해 같은 코드로 시작합시다.
  • 1) Flutter 프로젝트 생성
    1. VSCode에서 아래와 같이 네모 모양의 Stop 버튼을 눌러 기존에 실행한 앱을 종료해주세요.

    2. ViewCommand Palette를 선택해주세요.

      💡 `Command Palette` 단축키 window : `Ctrl + Shift + P` macOS : `Cmd + Shift + P`

    3. 명령어를 검색하는 팝업창이 뜨면, flutter라고 입력한 뒤 Flutter: New Project를 선택해주세요.

    4. Application을 선택해주세요.

    5. 프로젝트를 저장할 폴더를 선택하는 화면이 나오면 flutter 폴더를 선택한 뒤 Select a folder to create the project in 버튼을 눌러 주세요.

    6. 프로젝트 이름을 mymemo_admob 으로 입력해주세요.

      만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요. (팝업이 안보이시면 넘어가주세요!)

      💡 아래 팝업에 대한 자세한 사항은 [링크](https://stackoverflow.com/questions/67914668/vs-code-do-you-trust-the-authors-of-the-files-in-this-folder)를 참고해주세요.

    7. 다음과 같이 프로젝트가 생성됩니다.

    8. 불필요한 힌트 숨기기

      코드스니펫을 복사해서 analysis_options.yaml 파일의 24번째 라인 뒤에 붙여 넣고 저장해주세요.

      💡 아래 내용은 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정하는 과정입니다.
      • [코드스니펫] analysis_options.yaml
        	
            prefer_const_constructors: false
            prefer_const_literals_to_create_immutables: false

      • 어떤 의미인지 궁금하신 분들을 위해
        • main.dart 파일을 열어보시면 파란 실선이 있습니다.
        • 파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다. 12번째 라인에 마우스를 올리면 아래와 같이 설명이 뜹니다. 위젯이 변경될 일이 없기 때문에 const라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다. 💡 상수로 만들면 어떤 이점이 있나요? 상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다. 아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다. ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
        • 지금은 학습 단계이니 눈에 띄지 않도록 해주도록 하겠습니다.
    9. provider 패키지와 shared_preferences 패키지를 시작하는 코드에서 사용하고 있으므로 패키지 설치부터 진행하도록 하겠습니다. ViewTerminal을 선택해주세요.

      아래 코드스니펫을 복사해서 터미널에 붙여넣고 실행해 주세요.

      • [코드스니펫] provider, shared_preferences 패키지 설치
        flutter pub add provider && flutter pub add shared_preferences

      pubspec.yaml을 열어서 39, 40번째 라인에 providershared_preferences 가 있으면 설치가 잘 되신 겁니다.

    10. lib 폴더 내에 memo_service.dart 라는 이름의 파일을 새로 만들어주세요.

      아래와 같이 빈 파일이 생성되면 됩니다.

    11. 아래 코드스니펫을 복사해서, main.dartmemo_service.dart 의 기존 코드를 모두 지우고 붙여 넣은 뒤 저장해 주세요.

      • [코드스니펫] main.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),
                      ),
                    ),
                  ],
                );
              },
            );
          }
        }
      • [코드스니펫] 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();
          }
        }
  • 2) 에뮬레이터 실행하기
    1. VSCode 우측 하단에 Chrome (web-javascript)를 클릭해주세요.
      (에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)

      ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed7e56bc-d81f-457f-8db9-d94e8d6e4f66/Untitled.png)
    2. Start Pixel 2 API 29 mobile emulator를 선택해주세요. macOS의 경우 iOS 에뮬레이터로 진행하셔도 무방합니다.

      잠시 기다리면 에뮬레이터가 실행됩니다.

    3. VSCode 우측 상단에 아래 화살표를 눌러 Run Without Debugging을 눌러주세요.

      💡 `Start Debugging`으로 실행해도 무방합니다. 디버깅 모드는 특정 라인에서 앱 실행을 멈추고 해당 변수에 어떤 값이 들어있는지 볼 수 있지만 `Run without debugging`이 실행 속도가 더 빨라 안내를 위와 같이 드렸습니다 🙂

      에뮬레이터에 아래와 같이 메모장 화면이 나오면 완료!

      💡 위와 같이 메모를 보여주는 `HomePage`와 메모를 작성하는 `DetailPage` 두 페이지로 구성된 앱입니다.

      기본적인 레이아웃과 페이지 이동 기능, CRUD 및 로컬 저장 기능도 구현 되어있습니다!
      이제 여기에 AdMob 을 활용해 배너 광고를 구현해 보도록 하겠습니다.

02. 구글 애드몹 연결하기

💡 마이메모에 구글 AdMob 을 적용하겠습니다. 실제 광고가 올라가기 위해서는 심사가 필요하니, 테스트 ID 를 활용해 앱에 띄워보겠습니다.
  • 구글 애드몹이란 💡 **AdMob** 은 Advertising on Mobile 의 줄임말로, 구글에서 제공하는 **모바일 광고 네트워크 및 수익 창출 플랫폼** 입니다. 웹에서 널리 사용되는 AdSense 의 모바일 버전이라고 생각하시면 됩니다. 앱에 코드를 연동하면, 자동으로 사용자들에게 타겟팅 된 광고를 띄워주며, 이로 인해 발생한 광고 수익을 분배해줍니다. 여기에 광고 네트워크 최적화, 광고 실적 및 사용자 참여도 분석 등의 기능도 제공합니다. 광고주를 직접 찾고 계약하는 수고없이 코드 연동만으로 수익을 낼 수 있다는 점이 장점입니다. 💡 아래 사진에서 볼 수 있듯이 현재 대부분의 앱은 무료로 제공되고 있습니다. 유료 앱에 대한 소비자들의 허들이 그만큼 높다는 반증이겠죠! ![[https://www.businessofapps.com/news/the-paid-for-app-model-is-over/](https://www.businessofapps.com/news/the-paid-for-app-model-is-over/)](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a4bf440a-c3ce-4543-8524-99b34d047396/Untitled.png) [https://www.businessofapps.com/news/the-paid-for-app-model-is-over/](https://www.businessofapps.com/news/the-paid-for-app-model-is-over/) AdMob 과 같은 광고 플랫폼을 활용하면 무료 앱에서도 트래픽에 따라 수익을 낼 수 있습니다. [Mobile App Monetization - Google AdMob](https://admob.google.com/home/?utm_source=firebase&utm_medium=et&utm_campaign=firebase-docs&utm_content=2017Q1) 이외에도 아래와 같은 다양한 수익 창출 전략이 있습니다. 앱의 성격에 따라 적절한 수익화 전략을 세워야 합니다. [5가지 앱 수익 창출 전략 | Google AdMob](https://admob.google.com/intl/ko/home/resources/5-app-monetization-strategies-to-grow-and-monetize-your-app/) 1. 앱의 무료 버전과 유료 버전을 동시에 제공 2. 인앱 구매 모델을 적용한 무료 앱 3. 정기 결제 모델을 적용한 무료 앱 4. 유료 앱 모델 5. 파트너십 모델 💡 **AdMob** 에서는 아래와 같은 유형의 광고들을 지원합니다. 1. 앱 오프닝 광고 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e1dbb6e2-1680-4130-9b03-08f3edabcf47/Untitled.png) 2. 배너 광고 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63ebf8ae-7330-4456-ab29-51752eabb43c/Untitled.png) 기기 화면의 상단이나 하단에 표시되는 직사각형 광고입니다. 사용자가 앱과 상호작용하는 동안 배너 광고가 화면에 표시되며 일정 시간이 지나면 자동으로 새로고침될 수 있습니다. 3. 전면 광고 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/14adafe2-ae23-4cbe-bc62-3ab64e6f026a/Untitled.png) 사용자가 닫을 때까지 게재되며 앱의 인터페이스를 완전히 덮는 전체 화면 광고입니다. 게임에서 다음 레벨로 넘어가거나 작업을 완료한 직후처럼 앱 이용이 잠시 중단될 때 자연스럽게 광고가 게재되는 것이 좋습니다. 4. 네이티브 광고 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8e9cdfa3-3af9-42ed-b70e-085f1c054fef/Untitled.png) 앱의 디자인과 스타일에 어울리는 개인 맞춤 광고입니다. 광고 배치 방법 및 위치를 정할 수 있으므로 광고 레이아웃과 앱 디자인 간의 일관성 유지가 가능합니다. 5. 보상형 광고 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/40015862-6e80-46ac-b43b-01ca1673e6ff/Untitled.png) 사용자에게 짧은 동영상 시청, 플레이어블 광고 상호작용, 설문조사 응답 등 광고 참여에 대한 리워드를 제공하는 광고 형식입니다. 무료 게임 사용자로부터 수익을 창출하는 데 효과적입니다. 💡 우리는 이 중 **2) 배너 광고** 를 앱에 적용해보겠습니다. ![배너 광고 적용 예시: 하단에 광고 표시](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/54720581-4b9f-4c8e-b061-a5fc138dd9c7/Untitled.png) 배너 광고 적용 예시: 하단에 광고 표시
  • 1) AdMob 가입 및 설정
    1. 링크를 클릭해 AdMob 사이트로 들어가주세요. 시작하기 버튼을 눌러줍니다.

    2. 구글 계정을 사용해 AdMob 에 가입해줍니다.

      국가와 시간대를 선택하고, 약관에 동의한 후 AdMob 계정 만들기 를 눌러주세요.

    3. 이메일 팁 수신 여부는 원하는대로 체크하면 됩니다. 예 또는 아니오를 체크하고 계속해서 AdMob 사용 버튼을 눌러주세요.

    4. 그러면 아래와 같이 AdMob 관리자 페이지가 뜨는 것을 볼 수 있습니다! 시작하기 버튼을 눌러주세요.

    5. 아래와 같은 화면이 뜨면 각각 Android, 아니오 를 선택하고, 계속을 눌러줍니다.

    6. 앱 이름은 mymemo 로 작성해주세요 (원하시는 이름으로 설정하시면 됩니다)

    7. 아래와 같이 추가가 완료되었습니다. 완료 버튼을 눌러 다시 원래 페이지로 돌아가주세요.

    8. 왼쪽의 앱 설정 탭에 들어가면 앱 ID 가 있습니다.

      💡 아래에서 코드에 붙여넣어 줄테니 우선 페이지를 열어둡니다.

  • 2) AdMob 설치 💡 코드에 AdMob 을 적용합니다. 다시 VS Code를 열어주세요. 1. `google_mobile_ads` 설치 아래 코드스니펫을 복사해 새 탭에서 열어주세요 - [**[코드스니펫] google_mobile_ads / install**](https://pub.dev/packages/google_mobile_ads/install) ```dart https://pub.dev/packages/google_mobile_ads/install ``` ![Screen Shot 2022-09-23 at 6.24.48 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a2650b83-4719-44d8-b8b9-10dff1d4c9af/Screen_Shot_2022-09-23_at_6.24.48_PM.png) 표시한 부분을 클릭해 텍스트를 복사합니다. 복사한 `flutter pub add google_mobile_ads` 를 터미널에 붙여넣어주세요 ![Screen Shot 2022-09-23 at 6.29.41 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b2514c09-d9e8-4731-bfe8-4b87fdfb92ae/Screen_Shot_2022-09-23_at_6.29.41_PM.png) `pubspec.yaml` 파일을 열어보면 41번째 줄에 `google_mobile_ads` 가 잘 추가된 것을 확인할 수 있습니다. ![Screen Shot 2022-09-23 at 6.30.09 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/822f647f-cd76-4353-8ef0-5b2581c95f97/Screen_Shot_2022-09-23_at_6.30.09_PM.png) 💡 google_mobile_ads 패키지 설치를 마쳤습니다. Android, iOS 각각에 추가 설정이 필요하니 아래 과정을 마저 따라가주세요. 2. Android 설정을 진행하겠습니다. **`android/app/src/main/AndroidManifest.xml`** 파일을 열어주세요. ![Screen Shot 2022-09-23 at 6.32.16 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23dc25a4-fe10-4cb6-8f2c-60fd1b5ea01d/Screen_Shot_2022-09-23_at_6.32.16_PM.png) 💡 같은 이름의 파일이 다른 폴더(debug, profile)에도 있으니 이들을 열지 않도록 주의해주세요! 아래 코드스니펫을 복사해 `AndroidManifest.xml` 의 32번째 줄 맨 뒤에 붙여넣어주세요 - **[코드스니펫] AndroidManifest.xml / meta-data** ``` ``` ![Screen Shot 2022-09-23 at 6.35.43 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/053b3a6a-9b1a-467b-888c-845f385cbb4f/Screen_Shot_2022-09-23_at_6.35.43_PM.png) `여기에 APP ID` 라고 쓰여있는 부분에 아까 생성해준 앱 ID 를 붙여넣어줍니다. ![Screen Shot 2022-09-22 at 9.38.50 PM copy.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8746a8e2-e370-4439-9900-06e2f4809b4a/Screen_Shot_2022-09-22_at_9.38.50_PM_copy.png) ![Screen Shot 2022-09-23 at 6.39.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e86204cf-e1bf-48c5-a02e-263e8199f87e/Screen_Shot_2022-09-23_at_6.39.05_PM.png) 3. 이번엔 iOS 설정을 진행하겠습니다. `**ios/Runner/Info.plist**` 파일을 열어주세요 ![Screen Shot 2022-09-23 at 6.37.06 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7c6761d7-2e83-4e05-ad31-e66983d91533/Screen_Shot_2022-09-23_at_6.37.06_PM.png) 아래 코드스니펫을 복사해 49번째 줄 맨 뒤에 붙여넣어줍니다. - **[코드스니펫] Info.plist** ```dart GADApplicationIdentifier 여기에 APP ID ``` ![Screen Shot 2022-09-23 at 6.40.43 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c7094387-16ea-426d-ae2b-351187462ca4/Screen_Shot_2022-09-23_at_6.40.43_PM.png) `여기에 APP ID` 라고 쓰여있는 부분에 아까 생성해준 앱 ID 를 붙여넣어줍니다. ![Screen Shot 2022-09-22 at 9.38.50 PM copy.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8746a8e2-e370-4439-9900-06e2f4809b4a/Screen_Shot_2022-09-22_at_9.38.50_PM_copy.png) ![Screen Shot 2022-09-23 at 6.41.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5a916aa5-ca74-48bb-958f-123280d27c59/Screen_Shot_2022-09-23_at_6.41.35_PM.png) 💡 AdMob 패키지를 설치하고, Android, iOS 세팅을 완료했습니다.
  • 3) AdMob 코드 추가
    1. main 함수에 앱을 처음 시작할 때 MobileAds 를 초기화/구동하는 코드를 추가하겠습니다.

      아래 코드스니펫을 복사해 main.dart 12번째 줄 맨 뒤에 붙여넣어주세요

      • [코드스니펫] MobileAds 인스턴스 초기화
        
          MobileAds.instance.initialize();
        

      오류가 나는 것은 import 를 하지 않았기 때문이겠죠!

      13번째 줄의 MobileAds 를 클릭한 뒤 Quick Fix(Ctrl/Cmd + .)를 눌러 Import library 'package:google_mobile_ads/google_mobile_ads.dart’ 를 선택해주세요.

      에러가 해결되었습니다.

    2. HomePage 에 배너 광고를 만들어주겠습니다.

      아래 코드스니펫을 복사해 main.dart 46번째 줄 맨 뒤에 붙여넣어주세요.

      • [코드스니펫] HomePage 에 BannerAd 추가
        
          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’ 를 선택해주세요.

      💡 `dart:html` 을 고르면 안됩니다! 꼭 `dart:io` 를 선택해주세요

      첫줄에 import 문이 추가되며 에러가 해결됩니다.

    3. 광고를 화면에 띄우기 위해서는 load 해오는 코드를 위젯을 처음 그려줄 때 실행해야 합니다.

      StatefulWidget 에서 initState 라는 함수 안에 코드를 작성하면, 위젯이 처음 그려질 때 해당 코드가 실행됩니다.

      61번째 줄 맨 끝에서 엔터를 두 번 눌러주시고 ini 까지 작성해주세요. 아래과 같이 initState() { … } 가 자동완성 목록에 뜹니다. 이를 눌러주세요.

      그럼 아래와 같이 빈 initState 함수가 생성됩니다.

      💡 `super.initState()` 는 기존에 initState 에 지정된 로직들을 실행하겠다는 의미입니다. 여기 아래에 우리가 추가적으로 로직을 작성해주면, State 가 처음 만들어질 때 원하는 동작을 실행할 수 있겠죠!

      아래 코드스니펫을 복사해 66번째 줄 맨 뒤에 붙여넣어줍니다.

      • [코드스니펫] myBanner 로드하기
        
            myBanner.load();

      💡 이제 배너의 load 까지 마쳤습니다. 배너를 포함한 위젯을 HomePage 의 맨 아래에 보여줍시다.
    4. 마이메모 앱에는 bottomNavigatoinBar 가 따로 없으니 해당 위치에 배너를 표시해주겠습니다.

      아래 코드스니펫을 복사해 main.dart 129번째 줄 맨 뒤에 붙여넣어주세요.

      • [코드스니펫] bottomNavigationBar 자리에 광고 배너 넣기
        
                  bottomNavigationBar: Container(
                    alignment: Alignment.center,
                    width: myBanner.size.width.toDouble(),
                    height: myBanner.size.height.toDouble(),
                    child: AdWidget(ad: myBanner),
                  ),

      💡 광고가 잘 뜨는 것을 확인할 수 있습니다

  • 참고) 테스트 광고 ID 를 실제 광고 ID 로 대체하기 💡 구글 AdMob 광고는 승인이 이뤄져야 앱에 표시할 수 있습니다. 또, 하나의 기기에서 지나치게 많은 접속이 이뤄질 경우 이를 어뷰징으로 판단해 불이익이 가해질 수 있습니다. 이 때문에 개발 과정에서는 `app ID` 나 `ad unit ID` 에 테스트용 ID 를 사용합니다. 아래는 테스트용 `app ID` 와 각 광고 유형별 `ad unit ID` 입니다. **Android app ID/ad unit ID** | Item | app ID/ad unit ID | | --- | --- | | AdMob app ID | ca-app-pub-3940256099942544~3347511713 | | Banner | ca-app-pub-3940256099942544/6300978111 | | Interstitial | ca-app-pub-3940256099942544/1033173712 | | Rewarded | ca-app-pub-3940256099942544/5224354917 | **iOS app ID/ad unit ID** | Item | app ID/ad unit ID | | --- | --- | | AdMob app ID | ca-app-pub-3940256099942544~1458002511 | | Banner | ca-app-pub-3940256099942544/2934735716 | | Interstitial | ca-app-pub-3940256099942544/4411468910 | | Rewarded | ca-app-pub-3940256099942544/1712485313 | 💡 우리 앱에도 실제 광고를 등록하기 위한 절차를 알려드리겠습니다. 1. AdMob 페이지에 들어가 좌측 탭에서 `광고 단위` 를 클릭하고, `시작하기` 를 눌러줍니다. ![Screen Shot 2022-09-22 at 9.40.03 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e499786a-03a6-40d6-a186-873ae36d5d8b/Screen_Shot_2022-09-22_at_9.40.03_PM.png) 2. 원하는 광고 유형을 선택합니다. ![Screen Shot 2022-09-22 at 9.40.12 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/041c18fb-38f9-44e6-9671-e1445f96ab76/Screen_Shot_2022-09-22_at_9.40.12_PM.png) 3. 광고 단위 이름을 작성합니다. 여러분이 각각의 광고를 구분하기 위한 이름이므로, 적절히 작성해주시면 됩니다. ![Screen Shot 2022-09-22 at 9.41.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb1a7bd5-f405-4252-a42f-5b3b69ea9e0b/Screen_Shot_2022-09-22_at_9.41.02_PM.png) 4. 아래 동그라미 친 부분이 바로 `app unit ID` 입니다. 이를 복사해서 ![Screen Shot 2022-09-22 at 9.41.13 PM copy.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/94309825-ca70-4601-8309-04e7bb7c3cca/Screen_Shot_2022-09-22_at_9.41.13_PM_copy.png) 작성해준 `BannerAd` 객체의 `adUnitId` 속성에 붙여넣어주시면 됩니다. ![Screen Shot 2022-09-23 at 7.15.55 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/148972dc-06b8-4d21-afaf-1022cfe7cb42/Screen_Shot_2022-09-23_at_7.15.55_PM.png)
  • 최종 코드
    • 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();
        }
      }

03. 내 핸드폰에 앱 설치하기

💡 내가 만든 앱을 내 핸드폰에 설치하고 사용할 수 있다면 좋겠죠! 앱을 플레이스토어나 앱스토어에 배포하기 전에도 직접 컴퓨터와 연결해 앱을 설치할 수 있습니다.

아래 가이드를 따라 Android 와 iOS, 각각 실 기기에 앱을 설치해보겠습니다.
(iOS 에 앱을 설치하려면 macOS 가 필요해요! Windows 를 이용하시는 분들은 Android 기기에만 설치할 수 있습니다)

  • 1) Android 기기에 설치하기
    1. 앱을 설치할 Android 기기를 USB 케이블로 PC 와 연결해주세요.

    2. 기기에서 개발자 옵션 을 활성화합니다. 설정 화면으로 이동해 휴대전화 정보 탭으로 이동해주세요.

      소프트웨어 정보 탭으로 이동합니다.

      여기서 빌드번호 를 연속해서 여러번 탭 해주세요.

      계속 탭하다보면 아래와 같이 개발 설정 완료 ~단계 전입니다. 라는 내용이 화면에 표시됩니다. 마저 탭 해주세요.

      개발자 모드를 켰습니다. 라는 내용이 화면에 표시되면, 다시 뒤로 가기를 눌러 설정 화면으로 돌아가주세요.

      이제 설정 화면에 개발자 옵션 이 추가된 것을 확인할 수 있습니다. 눌러서 들어가주세요.

      USB 디버깅 을 활성화해주세요.

      이제 기기에 앱을 설치할 준비를 완료했습니다.

    3. 이제 VS Code 우측 하단을 누르면 연결한 핸드폰 이름이 목록에 뜨는 것을 볼 수 있습니다. 설치할 기기를 선택해주세요.

    4. 터미널을 열고 flutter run --release 라고 입력하고 엔터를 눌러주세요.

      아래와 같이 기기를 선택하는 옵션이 나오면 해당 번호를 입력해주시면 됩니다. (기기가 하나만 연결되어있다면 뜨지 않을 수도 있습니다)

      💡 **디버그 모드** (Hot Reload 가능)로 앱을 실행하고 싶다면, 이전에 해왔듯이 `Run without debugging` 을 눌러주시면 됩니다. 이 경우, Hot Reload 등을 활용해 변경사항을 바로바로 반영할 수 있으나, 케이블 연결을 끊었을 때 앱을 사용하실 수 없습니다.

      지금처럼 릴리즈 모드로 설치하면 케이블 연결을 끊어도 앱을 계속 사용할 수 있습니다.

      아래와 같은 에러가 발생할 겁니다.

      android/app/build.gradle 파일을 열어주세요. 아래와 같이 50번째 줄의 minSdkVersion 을 19로 수정해줍니다.

      다시 flutter run --release 를 실행해주면…

      앱 설치가 완료되었습니다!

  • 2) iOS 기기에 설치하기 💡 iOS 기기에 앱을 설치하려면 macOS 가 필요하며, Xcode 가 설치된 상태여야 합니다. (Windows 에서는 설치할 수 없습니다) 1. 앱을 설치할 iPhone 을 맥북에 USB 케이블로 연결해주세요. 아래와 같이 `이 컴퓨터를 신뢰하겠습니까?` 팝업이 뜨면 `신뢰` 를 눌러주세요. ![IMG_0098.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/df1caa34-e4dd-4d10-b7b1-deb335c73b8e/IMG_0098.jpg) 이제 VS Code 우측 하단을 누르면 연결한 iPhone이 목록에 뜨는 것을 볼 수 있습니다. ![Screen Shot 2022-09-23 at 1.13.54 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/02197e6a-fae7-4fce-bf4a-9269215e5cbb/Screen_Shot_2022-09-23_at_1.13.54_PM.png) 연결한 iPhone 을 선택해주세요 ![Screen Shot 2022-09-23 at 1.13.31 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9b00a22d-3bae-4918-a048-53e60d5ef273/Screen_Shot_2022-09-23_at_1.13.31_PM.png) 2. Xcode 설정 (`Signing &Capabilities`) 설치에 앞서 Xcode 를 열어 해당 프로젝트에 관한 몇가지 설정을 진행해야 합니다. 아래와 같이 VS Code 에서 `ios` 폴더를 우클릭하고, `Open in Xcode` 를 눌러주세요 ![Screen Shot 2022-09-23 at 1.05.30 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9b717de0-5cb8-442f-997e-21a136c69750/Screen_Shot_2022-09-23_at_1.05.30_PM.png) Xcode 가 열렸다면 아래 사진처럼 `폴더 아이콘` → `Runner` → `Signing & Capabilities` 로 들어가주세요. ![Screen Shot 2022-09-23 at 1.07.31 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/446bb740-a778-4882-a8d0-1828e4ff81e4/Screen_Shot_2022-09-23_at_1.07.31_PM.png) 아래와 같이 `Team` 을 클릭해 옵션을 열고, `Add an Account` 를 눌러주세요. ![Screen Shot 2022-09-23 at 1.08.36 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8aab162c-51c3-40ea-88b1-1ca27018b3c2/Screen_Shot_2022-09-23_at_1.08.36_PM.png) 각자의 애플 아이디로 로그인해주세요 (애플 개발자 계정을 등록하지 않았어도 괜찮습니다. 애플 개발자 계정은 앱스토어에 배포할 때 필요해요!) ![Screen Shot 2022-09-23 at 1.09.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/14abe516-f4ef-4063-88ac-6feeba11e37e/Screen_Shot_2022-09-23_at_1.09.02_PM.png) 로그인하면 아래와 같이 계정이 추가됩니다. `x` 버튼을 눌러 창을 닫아주세요. ![Screen Shot 2022-09-23 at 1.09.23 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/78327c3a-7323-4dbc-bbf7-77e1018222ba/Screen_Shot_2022-09-23_at_1.09.23_PM.png) 위 메뉴에서 연결한 iPhone 을 선택해주세요. ![Screen Shot 2022-09-23 at 2.58.00 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c6aad0c-cc13-4176-b78c-ec5c0fb8997e/Screen_Shot_2022-09-23_at_2.58.00_PM.png) 다시 Team 을 눌러 아까 추가한 계정을 선택해주세요! ![Screen Shot 2022-09-23 at 2.59.44 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bfa3d05d-bd69-4d9a-9e22-e766d098215f/Screen_Shot_2022-09-23_at_2.59.44_PM.png) 중간중간 아래와 같은 창이 뜨면 맥북 로그인 할 때 쓰는 비밀번호를 입력하고 `Allow` 내지는 `Always Allow` 를 클릭해주시면 됩니다. (창이 여러번 뜹니다) ![Screen Shot 2022-09-23 at 2.36.42 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/974917d4-ff68-468f-b25d-882310456d9c/Screen_Shot_2022-09-23_at_2.36.42_PM.png) 3. 이제 VS Code 에서 터미널을 열고 `flutter run --release` 라고 입력하고 엔터를 눌러주세요. 아래와 같이 기기를 선택하는 옵션이 나오면 해당 번호를 입력해주시면 됩니다. (기기가 하나만 연결되어있다면 뜨지 않을 수도 있습니다) ![Screen Shot 2022-09-23 at 3.08.52 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/063f4d98-509f-44a9-8263-a6ad448d44d6/Screen_Shot_2022-09-23_at_3.08.52_PM.png) 💡 **디버그 모드** (Hot Reload 가능)로 앱을 실행하고 싶다면, 이전에 해왔듯이 `Run without debugging` 을 눌러주시면 됩니다. 이 경우, Hot Reload 등을 활용해 변경사항을 바로바로 반영할 수 있으나, 케이블 연결을 끊었을 때 앱을 사용하실 수 없습니다. 지금처럼 **릴리즈 모드**로 설치하면 케이블 연결을 끊어도 앱을 계속 사용할 수 있습니다. 4. 핸드폰에서 “신뢰하는 사용자” 등록 iPhone 화면에 아래와 같은 메시지가 뜨는 것을 확인할 수 있습니다. ![IMG_0099.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8b2d982b-3a58-4d4d-8107-149ed6f2e17d/IMG_0099.png) `**설정**` 화면으로 이동해, `일반` 탭으로 이동합니다. ![IMG_0101.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1ed40901-0a19-42c2-8f30-db74c7b4418c/IMG_0101.png) `VPN 및 기기 관리` 로 이동합니다. ![IMG_0102.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb18d425-85de-4eee-a89d-9d183d69ca53/IMG_0102.png) 아래 `개발자 앱` 탭의 “신뢰하지 않음” 표시된 옵션을 클릭해주세요 ![IMG_0103.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6e9afb1-acf5-4244-9c99-e8cb23f3a219/IMG_0103.png) 아래 파란 글씨를 클릭한 뒤 ![IMG_0104.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/15ddd487-6931-4aea-946f-7eb12b26dbf0/IMG_0104.png) 아래 다이얼로그가 나타나면 `신뢰` 를 눌러주세요. ![IMG_0105.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ea387989-5403-4bc5-ac04-536a0545a5ad/IMG_0105.png) 아래와 같은 화면이 나오면 설정이 완료된 것입니다. ![IMG_0106.PNG](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aa369874-240d-4669-9360-7cddca8e5e7f/IMG_0106.png) 다시 터미널에 `flutter run --release` 을 입력하면 핸드폰에서 앱이 설치되는 것을 확인할 수 있습니다. 5. 이제 iPhone 에서 앱을 사용할 수 있습니다. ![IMG_B522FC97EB43-1.jpeg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a6a4e500-004c-4961-b3d9-4b2d9a6f2093/IMG_B522FC97EB43-1.jpeg)

04. 구글 플레이스토어에 앱 배포하기

💡 구글 Play Store 에 우리가 만든 마이메모 앱을 배포해보겠습니다.
  • 1) 구글 플레이 개발자 계정 생성 (유료 결제 필수) 💡 먼저 구글 플레이 개발자 계정을 생성해야 합니다. 아래 링크를 참고해 계정을 생성해주세요. (등록 수수료 미화 25달러가 청구되며, 한번 등록하면 영구적으로 라이센스를 사용할 수 있습니다) **[Google Play 개발자 계정 등록 방법](https://support.google.com/googleplay/android-developer/answer/6112435?hl=ko#zippy=%2C%EB%8B%A8%EA%B3%84-google-play-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B3%84%EC%A0%95-%EB%A7%8C%EB%93%A4%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B0%B0%ED%8F%AC-%EA%B3%84%EC%95%BD%EC%97%90-%EB%8F%99%EC%9D%98%ED%95%98%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EB%93%B1%EB%A1%9D-%EC%88%98%EC%88%98%EB%A3%8C-%EA%B2%B0%EC%A0%9C%ED%95%98%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EA%B3%84%EC%A0%95-%EC%84%B8%EB%B6%80%EC%A0%95%EB%B3%B4-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0)**
  • 2) 디지털 서명(keystore) 파일 만들기 💡 Google Play 스토어에 출시하기 위해서는 반드시 앱에 디지털 서명을 해야 합니다. 이는 다른 사람이 내 앱 설치 파일을 변조하는 것을 막기 위해서 입니다. 우리는 아래 절차를 진행하면서 1. 앱 개발자에 대한 정보를 서명한 암호화 된 파일(KeyStore)을 생성해서 내 컴퓨터에 두고 2. 배포 버전 파일을 만들때마다 이 서명 파일(KeyStore)을 사용해서 디지털 서명을 진행하도록 하겠습니다. 매 업데이트 버전의 설치 파일(.aab)도 원래 버전과 같은 서명 파일로 서명이 되어야 합니다 1. 아래 명령어를 터미널에서 실행해주세요 - **macOS** ```yaml keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key ``` - **Windows** ```yaml keytool -genkey -v -keystore c:/Users/**USER_NAME**/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key -storetype JKS ``` - 위 `**USER_NAME**` 부분에 반드시 본인의 Windows **계정명**을 입력해주세요 `로컬디스크 (C:) → 사용자` 폴더에 들어가시면 **계정명**을 확인할 수 있습니다. 2. `key.jks` 파일을 생성해주겠습니다 keystore password 를 입력해주시고 (**꼭 기억하셔야 합니다! 별도의 메모장에 기록해주세요**) 엔터를 눌러주세요. ![Screen Shot 2022-04-24 at 8.13.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bc9fa733-027d-4f6f-952f-a9eff7faddce/Screen_Shot_2022-04-24_at_8.13.35_PM.png) 아래와 같은 질문들에 나머지 값들은 모두 빈 값으로 남기고 진행하셔도 좋습니다. ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fb7c39a-3d42-4f7a-92e7-7823fa6e073a/Untitled.png) 마지막에 `yes` 를 **직접 입력**해주시면 사용자 폴더에 `key.jks` 파일이 생성되는 것을 확인할 수 있습니다. 💡 이 파일은 항상 개인적으로 보관하세요. **절대 공개된 저장소(ex. Github)에 올리지 마세요** ![Screen Shot 2022-04-24 at 7.54.10 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6e61bd8a-8f50-47e7-914a-e6608fa5f8a3/Screen_Shot_2022-04-24_at_7.54.10_PM.png) 💡 이제 서명 파일을 생성해서 컴퓨터에 저장했습니다.
  • 3) 코드 수정하기 💡 배포 버전 파일을 만들때마다 이 서명 파일(KeyStore)을 사용해서 디지털 서명을 진행하도록 코드를 수정하겠습니다. 1. `android` 폴더에 `key.properties` 라는 이름의 빈 파일을 생성합니다 ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6e4a5bce-34c3-47b2-8e96-d4568018d2c1/Untitled.png) 2. 생성한 `key.properties` 파일에 아래 내용을 추가합니다 ```yaml storePassword=**key.jks 만들 때 입력한 비밀번호 (keystore password)** keyPassword=**key.jks 만들 때 입력한 비밀번호 (keystore password)** keyAlias=key storeFile=**key.jks** 파일 위치, 예) MacOS: /Users//key.jks Windows: C:\Users\\key.jks ``` ![Screen Shot 2022-04-24 at 8.18.57 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ab301677-c039-4f4f-b0db-b4b67f971d8d/Screen_Shot_2022-04-24_at_8.18.57_PM.png) 3. `key.properties` 파일 역시 **절대로 공개된 저장소 (ex. github)에 올라가서는 안됩니다**. `.gitignore` 를 수정해주겠습니다. 맨 아래줄에 `key.properties` 를 추가해주세요. ![Screen Shot 2022-04-24 at 8.20.32 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/20a51008-058b-4199-89dc-e6a089053646/Screen_Shot_2022-04-24_at_8.20.32_PM.png) 4. `build.gradle` 파일을 수정해주겠습니다. `android/app/build.gradle` 파일을 열어주세요. (`android/build.gradle` 이 아닙니다!) ![Screen Shot 2022-04-24 at 8.22.15 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/94652198-dba2-4507-9d4b-5b0d4a8e20d5/Screen_Shot_2022-04-24_at_8.22.15_PM.png) 5. 26번째 줄 맨 뒤에 아래의 코드스니펫을 복사해 붙여넣어주세요. - **[코드스니펫] keystoreProperties** ```yaml def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } ``` ![Screen Shot 2022-09-23 at 5.06.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1c84191e-057b-4385-a05a-76c5e0176fcb/Screen_Shot_2022-09-23_at_5.06.02_PM.png) 6. 53번째 줄의 `applicationId` 를 수정해주세요. 플레이스토어에 올라가는 **앱의 고유한 ID** 이므로, 타 앱과 구분되는 값을 넣어줘야 합니다. `com.example.mymemo_admob` 중간의 `example` 을 각자 생성하신 구글 개발자 계정 이름으로 설정해준다면 중복을 막을 수 있겠죠! 반영해준다면 아래와 같을 겁니다. ```yaml applicationId "com.<개발자 계정 이름>.mymemo_admob" ``` ![Screen Shot 2022-09-23 at 5.06.29 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8e3c08ae-0967-442f-967d-24f7ccdb36f1/Screen_Shot_2022-09-23_at_5.06.29_PM.png) 7. 62~68 번째 줄 (buildTypes 부분)을 지우고 아래 코드스니펫을 복사해 붙여넣어주세요 - **[코드스니펫] signingConfigs, buildTypes** ```yaml signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } } ``` ![Screen Shot 2022-09-23 at 5.08.57 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6509f163-e804-47b1-bf58-c989ff1fb37f/Screen_Shot_2022-09-23_at_5.08.57_PM.png) 💡 이제 `flutter run --release` 를 실행할 때마다 생성한 서명 파일로 서명이 일어납니다.
  • 4) 버전 정보 업데이트 💡 지금은 따로 건드릴 필요 없지만, 추후 앱을 업데이트하는 경우 버전 정보를 바꿔줘야합니다. 아래 블로그 글을 참고해주세요! [**플러터 출시한 앱 버전 관리하기**](https://bebesoft.tistory.com/46)
  • 5) 설치파일(.aab) 생성 및 업로드, 앱 출시
    1. 이제 터미널에서 아래 명령어를 실행합니다.

      flutter build appbundle

      아래와 같은 메시지가 터미널에 뜬다면 성공적으로 빌드가 완료된 것입니다.

    2. build/app/outputs/bundle/release/ 경로로 들어가시면 app-release.aab 파일을 확인할 수 있습니다.

      해당 파일을 탐색기에서 열어주세요.

    3. Play Console 에 접속하셔서 (구글 개발자 계정을 이미 생성하신 후여야 합니다) 앱 만들기 버튼을 클릭합니다.

    4. 아래와 같이 세부정보를 설정한 후 앱 만들기 버튼을 클릭합니다

    5. 앱이 생성되었습니다! 왼쪽 옵션에서 프로덕션 을 선택하고 새 버전 만들기 를 클릭해주세요.

    6. App Bundle 옵션에 아까 생성한 app-release.aab 파일을 드래그 앤 드롭해 업로드해줍니다.

      아래와 같이 파일 목록이 보인다면 성공적으로 업로드가 진행된 것입니다.

    7. 이제 맨 아래로 내려가 저장 -> 버전 검토 를 클릭해줍니다.

      아래와 같은 오류는 각각 해당 대시보드로 이동해 설정을 마저 진행해줍니다.

    8. 설정이 끝나면 프로덕션 트랙으로 출시 시작 버튼을 눌러 제출을 진행해주세요.

      💡 제출된 앱은 7일 정도의 심사 기간을 거친 뒤 플레이스토어에 출시됩니다. 고생 많으셨습니다 :)

05. 숙제 - 다양한 유형의 광고 추가하기

  • 1) 앱을 켤 때 실행되는 전면 광고 추가하기
    • 1) 구현 목표 💡 앱을 켤 때 전면 광고가 표시되도록 합니다. [Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5bf93775-713a-4722-919c-c56db5a3e074/Untitled.mp4)
    • 2) 숙제 답안 💡 노란색 하이라이트로 변경 사항을 표시해뒀습니다 - `main.dart` ```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}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { 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(); }, ), ); InterstitialAd? interstitialAd; @override void initState() { // TODO: implement initState super.initState(); myBanner.load(); InterstitialAd.load( adUnitId: Platform.isAndroid ? 'ca-app-pub-3940256099942544/1033173712' : 'ca-app-pub-3940256099942544/4411468910', request: AdRequest(), adLoadCallback: InterstitialAdLoadCallback( onAdLoaded: (ad) { interstitialAd = ad; interstitialAd!.show(); }, onAdFailedToLoad: (error) {}, ), ); } @override Widget build(BuildContext context) { return Consumer( builder: (context, memoService, child) { // memoService로 부터 memoList 가져오기 List 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(); @override Widget build(BuildContext context) { MemoService memoService = context.read(); 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` ```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 memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; createMemo({required String content}) { Memo memo = Memo(content: content); memoList.add(memo); notifyListeners(); // Consumer의 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(); } } ```

이전 강의 바로가기

4주차 - API 사용법 익히기 (왓챠피디아)

전체 강의 바로가기

[스파르타코딩클럽] 앱개발 종합반


Copyright ⓒ TeamSparta All rights reserved.

profile
Wish Coding Master

0개의 댓글