한림대 경제학과 개쌕기 아닌 씹ㅅㄲ 김진영 영화감독 그것이 알고싶다. 영화 "B컷"개봉 

 
 

 

별로 알고 싶지 않을 사람인지는 모르지만.
 
한림대 경제학과일때 꽈보지 이쁘장한거 방송어쩌고 보지에 찝적대고
 
아버지가 어떤 개씩긴지.
 
졸업하자 마자 MBC에 PD로 실습시키고
 
    순풍산부인과? 과를 골라도 산부인과? 왜? 모 보고싶어서?
 
  그때 당시 인기를 끌던 "꽃보다 할배" 인기에 편승하여
 
  "꽃할배 수사대"라는 걸 남의 밥상에 수저 얻는거 가르치고
 
    꽃할배 수사대로 "꽃보다 할배" 욕먹이고
 
    다시 영화 감독한다며
 
    당시 인기가 있던 "가문의 영광" 시리즈를 도용
 
    비슷한 아류작인 "위험한 상견례"라는 가족 영화를 기획 날려먹고
 
  평생을 남의 인기에 편승해서 돈벌려고 하는 영화감독 창작자이다.
 
  2022 대선에 편승 결과를 보자

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

  • PDF 파일

    4주차-API_사용법익히기(왓챠피디아).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 + /

[수업 목표]

  • API 이해하기
  • 비동기 이해하기
  • API를 이용한 앱 만들기
  • Provider 를 이용한 상태관리 익숙해지기
  • WebView 사용해보기

[목차]


01. API 이해하기

  • 1) API는 무엇인가요?

    Untitled

    <aside>
    💡 아래 이미지와 같이 사용 설명서를 작성해 두었는데 이를 **API 문서**라고 부릅니다. API를 이해하는데 필요한 배경 지식을 배워보도록 하겠습니다.

    </aside>

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/775c9eae-15a7-4ec8-8460-872d1be881c9/Untitled.png)


<aside>
💡 그 외에도 `인스타그램 API`, `Youtube API`, `Kakao API`와 같이 검색해보시면 많은 서비스들이 API를 제공하고 있는 것을 보실 수 있습니다. (API는 유료도 있고, 무료도 있습니다)

- **[[코드스니펫] Public APIs](https://github.com/public-apis/public-apis)**

    ```dart
    https://github.com/public-apis/public-apis
    ```

</aside>
  • 2) API를 이해하기 위한 배경 지식

    • 클라이언트와 서버

      Untitled

    • 프로토콜(Protocol)

      http.png

    • HTTP

      • 요청(Request)

        • URL : 목적지

          출처 - [https://sitechecker.pro/](https://sitechecker.pro/)

          출처 - https://sitechecker.pro/

        • 메소드(method) : 원하는 액션

          • GET : 조회

          • POST : 생성 / 수정 / 삭제

          • [코드스니펫] HTTP 요청 메소드

              https://developer.mozilla.org/ko/docs/Web/HTTP/Methods
        • 파라미터(Parameter)

          Untitled

          실제 예제를 보도록 하겠습니다. 코드스니펫을 복사해서 새 탭에서 열어주세요.

            <aside>
            💡 아래와 같이 `hello`가 검색된 영어 사전 페이지가 뜹니다.
            주소창을 자세히 보면 `query=hello`라고 검색어가 동일하게 있는 것을 확인하실 수 있습니다.

            </aside>

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5cd59c04-73e0-4835-a87f-5d2ed76b52de/Untitled.png)

            <aside>
            💡 주소창에 `hello`를 `good`으로 변경한 뒤 엔터를 눌러보면 검색창에 `good`이 작성되어 있는 것을 보실 수 있습니다.

            이와 같이 네이버 서버에 내가 알고 싶은 단어를 `query`라는 키에 값으로 전달하여 결과를 받아올 수 있습니다.

            </aside>

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4eccae2b-8d7c-4853-9727-6aa11471031e/Untitled.png)

    - 응답(Response)
        - 웹페이지

            <aside>
            💡 웹 브라우저 주소창에 `naver.com`이라고 입력하면 네이버 웹페이지를 응답해줍니다.

            </aside>

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd350dfc-c579-47d0-84ba-f17fc10c0152/Untitled.png)

        - 데이터

            <aside>
            💡 개발자가 아닌 이상 평소에 볼 일이 없지만 웹페이지가 아닌 데이터만 응답해주기도 합니다.

            </aside>

            코드스니펫을 복사해 새 탭에서 접속해주세요.

            - [**[코드스니펫] json 데이터 응답 URL**](https://jsonplaceholder.typicode.com/posts)

                ```dart
                https://jsonplaceholder.typicode.com/posts
                ```


            <aside>
            💡 데이터를 보내줄 때에도 정해진 형식에 따라 주는데, 보통 `JSON`이라는 형식을 많이 사용합니다.

            </aside>

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cfe18aff-da00-43a9-a7be-cff6184a29a8/Untitled.png)

            - 크롬 JSON Viewer 확장 프로그램 설치

                <aside>
                💡 JSON 구조를 조금 더 쉽게 파악할 수 있도록 도와주는 크롬 확장 프로그램을 설치해 보도록 하겠습니다.

                </aside>

                코드스니펫을 복사해서 새 탭에서 열어주세요.

                - **[[코드스니펫] JSON Viewer 확장 프로그램 URL](https://chrome.google.com/webstore/detail/jsonview/gmegofmjomhknnokphhckolhcffdaihd?hl=ko)**

                    ```dart
                    https://chrome.google.com/webstore/detail/jsonview/gmegofmjomhknnokphhckolhcffdaihd?hl=ko
                    ```

                1. `Chrome에 추가` 버튼을 눌러주세요.

                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/276bbe51-2df3-4a77-862d-2835f367844d/Untitled.png)

                2. `확장 프로그램 추가` 버튼을 눌러서 설치를 진행해주세요.

                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/775d4ffb-9a25-4f1f-aa8c-2df5eca4719a/Untitled.png)

                3. JSON 샘플 웹페이지를 새로고침 해주시면 아래와 같이 좀 더 가독성 있게 JSON 구조를 볼 수 있습니다.

                    ![Screen Shot 2022-09-18 at 6.46.32 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a00eaad3-598a-48ac-8d11-945349a45da6/Screen_Shot_2022-09-18_at_6.46.32_PM.png)


            <aside>
            💡 JSON은 Dart 문법 시간에 배운 **문자열, 숫자, 배열, 맵(딕셔너리)** 형태 구조로 이루어져 있어 쉽게 익힐 수 있습니다.

            자세한 사용 방법은, 실습하면서 배워보도록 하겠습니다.

            </aside>

        - 상태 코드

            <aside>
            💡 상태 코드란 HTTP 응답시 요청이 성공했는지 실패했는지 한 번에 알 수 있는 약속된 숫자입니다.

            </aside>

            <aside>
            💡 자주 사용하는 상태코드

            200 : 성공
            4xx : 잘못된 요청
            5xx : 서버 문제로 실패

            좀 더 자세한 상태코드 종류는 아래 링크를 참고해 주세요.

            - **[[코드스니펫] HTTP 상태코드](https://developer.mozilla.org/ko/docs/Web/HTTP/Status)**

                ```dart
                https://developer.mozilla.org/ko/docs/Web/HTTP/Status
                ```

            </aside>


    ![_http (1).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c13ec2d-379c-4fc3-a46c-50cb451dc2a7/_http_(1).png)
  • 3) Google Book API 문서 읽어보기

    그럼 직접 API 문서를 읽어볼까요? 아래 코드스니펫을 복사해서 새 탭에서 열면 구글 책 검색 API 문서가 열립니다.

<aside>
💡 **요청(Request)**

- URL : `https://www.googleapis.com/books/v1/volumes`
- method : GET
- Parameter : `q=검색어`

![bookApi (1).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c691e45b-6c91-421c-9d0c-0934dca56b7a/bookApi_(1).png)

</aside>

<aside>
💡 **응답(Response)**

- JSON

![bookApiResponse.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8a170df0-5846-422f-8537-30f84495a4ac/bookApiResponse.png)

</aside>
  • 4) Google Book API 사용해보기

    q라는 파라미터에 고양이 라는 단어를 검색하여 책 데이터를 가져왔습니다.

    Untitled

  • [코드스니펫] DartPad google 책 검색 API

      https://dartpad.dev/?id=1519f6b1a7ac26cf42e4e302750650e0

    Run 버튼을 눌러 보면 Console에 JSON 응답이 출력되는 것을 볼 수 있습니다.

    Screen Shot 2022-09-18 at 7.05.42 PM.png

02. 비동기 이해하기

  • 1) 동기 & 비동기

    비동기 방식이 소요시간은 더 짧습니다.

    비동기 방식이 소요시간은 더 짧습니다.

    http (1).png

  • 2) async & await

    코드스니펫을 복사해 새 탭에서 붙여넣어 DartPad를 열어주세요.

<aside>
💡 console을 통해 실행 순서를 보면, `print("2");`의 경우 1초 뒤 실행되기 때문에 **비동기** 방식으로 실행되어 1 → 3 → 2 순서대로 실행된 것을 볼 수 있습니다.

</aside>

![Screen Shot 2022-09-18 at 7.25.47 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/809b2f90-9be0-4de6-b914-9dd36351a865/Screen_Shot_2022-09-18_at_7.25.47_PM.png)

<aside>
💡 위 코드를 **동기** 방식으로 실행해 1 → 2 → 3 순서대로 출력되도록 만들어 보겠습니다. 코드스니펫을 복사해서 새 탭에 붙여넣어 DartPad를 열어주세요.

- **[[코드스니펫] DartPad async & await](https://dartpad.dev/?id=83d6c5a955cc9230d3f44ea5106236a7)**

    ```dart
    https://dartpad.dev/?id=83d6c5a955cc9230d3f44ea5106236a7
    ```


![Screen Shot 2022-09-18 at 7.26.18 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1c50e0ca-cc6f-490d-bca8-7be11c43ce32/Screen_Shot_2022-09-18_at_7.26.18_PM.png)

</aside>

<aside>
💡 **비동기** 코드인 7번째 줄 앞에 `await`을 붙이고, 해당 코드가 속해있는 `main`함수의 소괄호와 중괄호 사이에 `async`라고 적어주면 **비동기** 방식으로 실행되는 코드를 **동기** 방식으로 실행할 수 있습니다.

![async & await.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b94b2f90-89c9-46b8-b7fd-dd65473a2c70/async__await.png)

</aside>
  • 3) Future

    코드스니펫을 복사해 새 탭에서 붙여넣어 DartPad를 열어주세요.

![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/abee377c-58b1-456b-b175-1c45e2f2f4fd/Untitled.png)

<aside>
💡 `async`가 붙은 함수는 내부에 코드 실행이 완료되기를 기다리는 `await` 코드가 있을 수 있기 때문에, 미래에 언젠간 값을 반환한다는 의미로 반환 값의 타입을 `Future<반환타입>` 형태로 작성해 줍니다.

![future (1).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/30ef794c-2efd-4d92-b66a-4976bc6c1fd6/future_(1).png)

실행 순서를 보면 9번째 줄에 `await`이 되어있으므로, 해당 코드가 끝날 때까지 기다립니다. 따라서 13번째 반환 값은 9번 째 줄이 끝난 미래에 반환이 되므로 8번째 줄에 반환 타입을 `Future<String>` 이라고 표시한다고 이해하시면 됩니다.

</aside>

<aside>
💡 해당 함수를 호출하는 쪽에서도 `await`을 함수 앞에 붙여주면 **동기** 방식으로 결과가 응답될 때까지 기다리기 때문에 `Future`를 벗겨낸 타입으로 반환 값을 받을 수 있습니다.

![future (2).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9d1e291a-87c2-448a-92e7-2a80bb2b730f/future_(2).png)

</aside>
  • 4) 요약

    배운 이론들을 활용해 프로젝트에서 API를 사용해 보도록 하겠습니다.

03. 프로젝트 준비 (왓챠피디아)

  • 1) Flutter 프로젝트 생성

    1. VSCode를 실행해주세요.

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

      Untitled

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

      Untitled

    4. Application을 선택해주세요.

      Untitled

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

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

      Screen Shot 2022-09-18 at 9.00.52 PM.png

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

      Untitled

    7. 다음과 같이 프로젝트가 생성되고, 다음으로 불필요한 힌트를 숨기도록 하겠습니다.

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

      • [코드스니펫] analysis_options.yaml

        
              prefer_const_constructors: false
              prefer_const_literals_to_create_immutables: false
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)

    - 어떤 의미인지 궁금하신 분들을 위해
        - `main.dart` 파일을 열어보시면 파란 실선이 있습니다.

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)

        - 파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.

            12번째 라인에 마우스를 올리면 아래와 같이 설명이 뜹니다.

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)

            위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다.

            <aside>
            💡 상수로 만들면 어떤 이점이 있나요?

            상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다.

            </aside>

            아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.

            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)

        - 지금은 학습 단계이니 눈에 띄지 않도록 해주도록 하겠습니다.
8. Provider 패키지를 시작하는 코드에서 사용하고 있으므로 패키지 설치부터 진행하도록 하겠습니다. `View` → `Terminal`을 선택해주세요.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/db3f319b-cb6e-4e78-96ce-52685882adbe/Untitled.png)

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

    - **[코드스니펫] provider 패키지 설치**

        ```dart
        flutter pub add provider
        ```


    ![Screen Shot 2022-09-18 at 9.52.23 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/28d0f310-52b4-4da2-b179-4890f65467eb/Screen_Shot_2022-09-18_at_9.52.23_PM.png)

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

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89172053-32a7-489d-837d-e51351df92bf/Untitled.png)

9. 아래 `lib/main.dart` 코드스니펫을 복사해서 기존 내용을 모두 지우고, `main.dart` 파일에 붙여 넣고 저장해 주세요.
    - **[코드스니펫]  main.dart**

        ```dart
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';

        import 'book_service.dart';

        void main() {
          runApp(
            MultiProvider(
              providers: [
                ChangeNotifierProvider(create: (context) => BookService()),
              ],
              child: const MyApp(),
            ),
          );
        }

        class MyApp extends StatelessWidget {
          const MyApp({Key? key}) : super(key: key);

          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: HomePage(),
            );
          }
        }

        class HomePage extends StatefulWidget {
          HomePage({Key? key}) : super(key: key);

          @override
          State<HomePage> createState() => _HomePageState();
        }

        class _HomePageState extends State<HomePage> {
          var bottomNavIndex = 0;
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: [
                SearchPage(),
                LikedBookPage(),
              ].elementAt(bottomNavIndex),
              bottomNavigationBar: BottomNavigationBar(
                selectedItemColor: Colors.black,
                unselectedItemColor: Colors.grey,
                showUnselectedLabels: true,
                selectedFontSize: 12,
                unselectedFontSize: 12,
                iconSize: 28,
                type: BottomNavigationBarType.fixed,
                onTap: (value) {
                  setState(() {
                    bottomNavIndex = value;
                  });
                },
                items: [
                  BottomNavigationBarItem(
                    icon: Icon(Icons.search),
                    label: '검색',
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(Icons.star),
                    label: '좋아요',
                  ),
                ],
                currentIndex: bottomNavIndex,
              ),
            );
          }
        }

        class SearchPage extends StatelessWidget {
          SearchPage({super.key});

          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                backgroundColor: Colors.white,
                toolbarHeight: 80,
                title: TextField(
                  onSubmitted: (value) {},
                  cursorColor: Colors.grey,
                  decoration: InputDecoration(
                    prefixIcon: Icon(Icons.search, color: Colors.grey),
                    hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
                    border: OutlineInputBorder(
                      borderSide: BorderSide(color: Colors.white),
                      borderRadius: BorderRadius.all(Radius.circular(10)),
                    ),
                    focusedBorder: OutlineInputBorder(
                      borderSide: BorderSide(color: Colors.grey),
                      borderRadius: BorderRadius.all(Radius.circular(10)),
                    ),
                  ),
                ),
              ),
              body: Center(
                child: Text("검색"),
              ),
            );
          }
        }

        class LikedBookPage extends StatelessWidget {
          const LikedBookPage({super.key});

          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: Center(
                child: Text("좋아요"),
              ),
            );
          }
        }
        ```

10. 그리고 `lib` 폴더 밑에 `book.dart`와 `book_service.dart` 파일도 만들어주신 뒤 아래 코드스니펫을 붙여 넣어주세요.

    <aside>
    💡 `Book` 클래스는 책에 대한 정보를 담을 클래스 입니다.

    `BookService`는 `Book`에 대한 CRUD를 담당하는 클래스 입니다.

    </aside>

    - **[코드스니펫]  book.dart**

        ```dart
        class Book {}
        ```

    - **[코드스니펫]  book_service.dart**

        ```dart
        import 'package:flutter/material.dart';

        import 'book.dart';

        class BookService extends ChangeNotifier {
          List<Book> bookList = []; // 책 목록
        }
        ```


    최종적으로 `lib` 폴더 밑에 아래와 같이 파일들이 있으면 됩니다.

    ![Screen Shot 2022-09-18 at 9.38.21 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b4bf8f96-ef8e-4164-9d2b-d25cd968b318/Screen_Shot_2022-09-18_at_9.38.21_PM.png)
  • 2) 에뮬레이터 실행하기

    1. VSCode 우측 하단에 Chrome (web-javascript)를 클릭해주세요.
      (에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)

      Untitled

    2. Start Pixel 2 API 29 mobile emulator를 선택해주세요.

      Untitled

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

      Untitled

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

      Untitled

      에뮬레이터에 아래와 같이 탭으로 전환되는 화면이 나오면 준비 완료!

      Untitled

04. API 연결하기

  • 구현 목표

    Untitled

  • 1) 통신 패키지 dio 설치하기

    1. 코드스니펫을 복사해 새 탭에서 열어주세요.

    2. dio 패키지의 Installing 페이지가 열리면 flutter pub add dio 우측 아이콘을 눌러서 복사해주세요.

      Untitled

    3. VSCode ViewTerminal을 선택해주세요.

      Untitled

    4. Terminal창에 복사한 flutter pub add dio를 붙여넣고 엔터를 눌러 실행해 주세요.
      아래와 같이 나오면 정상적으로 작동한 것입니다.

      Untitled

    5. pubspec.yaml 파일을 열어서 아래와 같이 40번째 라인에 dio가 있으면 설치가 완료된 것입니다.

      Untitled

  • 2) dio 사용법

    • 아래 코드는 GET 메소드로 URL로 요청을 보내는 코드입니다. dio 패키지는 매우 직관적이고 쉽게 사용할 수 있습니다.

        main() {
            Dio().get("URL");
        }
    • HTTP 요청은 응답까지 시간이 걸리기 때문에 비동기 코드입니다. 따라서 동기 방식으로 작동하게 하려면 아래와 같이 async & await을 추가해야 합니다.

        main() async {
            Response result = await Dio().get("URL");
            print(result.data); // data 안에 응답 내용이 들어 있습니다.
        }
  • 3) Google Book API 응답값 자세히 보기

    1. 일단 API를 호출해 보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.
![Screen Shot 2022-09-18 at 11.49.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f722ad7b-42b5-4cae-948d-b4d0112d1de0/Screen_Shot_2022-09-18_at_11.49.35_PM.png)

<aside>
💡 위와 같이 GET방식으로 해당 URL로 요청시 JSON 응답이 옵니다.  
`thumbnail` 이라고 적힌 부분에 파란 링크를 클릭해보시면 책 표지 사진이 나옵니다.

![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1171cf1d-9e93-46aa-bd43-7605fd043ef4/Untitled.png)

</aside>

<aside>
💡 Google Book API에 대한 자세한 사항은 아래 코드스니펫 링크를 참고해주세요.

- **[[코드스니펫] Google Book API 문서](https://developers.google.com/books/docs/v1/reference/volumes/list)**

    ```dart
    https://developers.google.com/books/docs/v1/reference/volumes/list
    ```

</aside>

1. 주소창에 `q=고양이` 라고 되어있는 부분을 `q=dog`로 변경해 보면 아래와 같은 결과가 나옵니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8ad130cb-92db-4864-b2cf-c56eba35b8de/Untitled.png)

    `totalItems` 라는 요소 안에 조회된 책의 전채 개수가 나오는 것을 확인할 수 있습니다.

    `items` 안에 있는 토글 버튼(화살표 표시) 를 눌러 요소를 두개 접어봅시다. `items` 안에 각각의 책에 대한 정보가 마치 `Map` 과 같은 형태로 담겨 있습니다.

    ![Screen Shot 2022-09-19 at 12.01.06 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ad41601b-38c4-434b-80fb-8abd97b0bcc0/Screen_Shot_2022-09-19_at_12.01.06_AM.png)

    <aside>
    💡 위 JSON 데이터의 구조는 Dart 문법에서 나오는 `List`와 `Map`의 조합과 같다고 보셔도 좋습니다.

    | 자료형 | 설명 | 예시 |
    | --- | --- | --- |
    | List | 배열 | [1, 2, 3] |
    | Map | {key : value} 형태의 자료형 | {
      'name': '철수',
      'age': 20
    } |
    </aside>

    <aside>
    💡 예를 들어 3번째 책의 `title` 을 가져오고 싶다면 아래와 같은 문법으로 데이터에 접근하면 됩니다.

    ```dart
    // data 에 위 json 데이터가 담겨온다고 가정
    String title = data['items'][2]['volumeInfo']['title'] 

    print(title) // "Everyday Dog"
    ```

    </aside>
  • 4) Book 클래스 수정하기

    1. json 데이터의 값들을 활용해 Book 객체를 만들고 이를 화면에 띄워주는 과정이 필요하겠죠.

      먼저 Book 클래스가 id, title, subtitle, thumbnail, previewLink 요소를 가질 수 있게끔 book.dart 파일을 수정해줍니다. 코드스니펫을 복사해 book.dart 파일 내의 내용을 모두 지우고 붙여넣어주세요.

      • [코드스니펫] book.dart

          class Book {
            String id;
            String title;
            String subtitle;
            String thumbnail; // 썸네일 이미지 링크
            String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
        
            Book({
              required this.id,
              required this.title,
              required this.subtitle,
              required this.thumbnail, 
              required this.previewLink, 
            });
          }
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8c770840-ef46-4dbf-9606-bc3e5a3f735c/Untitled.png)
  • 5) Google Book API 호출하기

    1. 키보드에서 완료 버튼을 누르면, TextField 에 입력된 문자열을 인자로 받는 onSubmitted 함수가 실행됩니다.

      Untitled

    2. BookService 내에 검색 로직을 추가한 뒤, 이를 onSubmitted 에서 호출하도록 하겠습니다.

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

      • [코드스니펫] 책 목록 search 함수

        
            void search(String q) async {
              if (q.isNotEmpty) {
                Response res = await Dio().get(
                  "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
                );
                List items = res.data["items"];
                print(items);
              }
            }
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6883dea5-fbac-4b66-b500-decdb75b40e7/Untitled.png)

3. 에러를 해결하겠습니다. 10번째 줄의 `Dio()` 을 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 눌러 `Import library ‘package:dio/dio.dart’` 를 선택해주세요.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/725da2df-eb4e-4a8e-aa89-2bbb683c6f97/Untitled.png)

4. 첫번째 줄에 Import 구문이 추가되며 에러가 해결되었습니다. 아까 설치해준 `Dio` 패키지를 이제 사용할 수 있습니다.

    ![Screen Shot 2022-09-19 at 1.49.06 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/55679400-1d20-455b-b18a-d1d0689e6b47/Screen_Shot_2022-09-19_at_1.49.06_AM.png)

5. `SearchPage` 로 돌아와  `BookService` 의 `search` 함수를 사용할 수 있도록 해봅시다. 

    저번 시간에 배운 `Provider` 내용을 복기해봅시다. Service 내의 변수나 함수에 접근하기 위해서는 아래 두가지 방법을 사용합니다.

    <aside>
    💡 Provider 에서 Service 를 사용하는 방법

    1. `Consumer<클래스명>` : 클래스 정보 갱신시 화면을 새로고침 해야 할 때 사용
    2. `context.read<클래스명>` : 1회성으로 클래스 접근할 때 사용 (화면 새로고침이 필요 없을 때)
    </aside>

    우리는 검색이 일어날 때마다 화면에 책 목록을 다시 그려줄 것이므로, `Consumer` 를 사용해야 합니다.

    `main.dart` 의 79번째 줄의 `Scaffold` 를 누르고, 왼쪽의 전구를 클릭해 `Wrap with Builder` 를 선택합니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d76c4285-7faf-48bf-bfc7-77155363ea95/Untitled.png)

    아래와 같이 `Scaffold` 위젯을 `Builder` 위젯이 감쌉니다. `Consumer` 위젯과 형식이 비슷한 `Builder` 를 사용해 모양을 잡아줬습니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/945394a9-d0d2-4e15-a04b-799356ca346e/Untitled.png)

6. 아래 이미지와 같이 79번째 라인의 `Builder`를 `Consumer<BookService>`로 변경해주세요. 

    `context` 뒤에 `bookService` 와 `child` 도 추가해줍니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e172af43-21dc-4931-badb-859a84bfa6c8/Untitled.png)

    그리고 줄 정렬을 예쁘게 해주기 위해 105번째 라인의 중괄호와 소괄호 사이에 콤마(`,`)를 찍은 뒤 저장해주세요.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/505edd08-2861-4d71-86d6-c0263d2c040b/Untitled.png)

    저장하면 코드가 정렬됩니다.

    <aside>
    💡 **복습:** `Consumer<BookService>`는 위젯 트리를 타고 올라가 **Provider**로 등록된 **BookService**를 찾습니다. 찾은 **BookService**를 80번째 라인의 두 번째 파라미터인 `bookService` 에 담아줍니다.
    우리는 `bookService` 변수를 통해 **BookService** 안에 있는 변수에 접근해 수정하고, 화면을 새로고침할 수 있습니다.

    </aside>

    <aside>
    💡 `BookService`에서 값을 변경하고 `notifyListeners();`를 호출하면, 해당 서비스를 `Consumer`로 등록한 모든 위젯의 `builder` 함수가 재호출 되면서 화면이 갱신 됩니다.

    </aside>

7. 86번째 줄 `onSubmitted` 의 중괄호 사이에 아래 코드스니펫을 붙여넣습니다.
    - **[코드스니펫] TextField 의 onSubmitted**

        ```dart
        bookService.search(value);
        ```


    저장하면 아래 이미지와 같이 코드가 정렬됩니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1260462-b5e2-4366-9dd5-9afb0d152096/Untitled.png)

8. 이제 키보드에 값을 입력하고 Debug Console 을 확인해볼까요? VS Code 에서 `View → Debug Console` 를 눌러 콘솔을 켜줍니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/25bb78ef-a5f0-4bde-b411-db7bc6775fd1/Untitled.png)

    아래와 같이 dog 라는 키워드를 입력하고 검색을 해보면 콘솔에 리스트가 출력되는 것을 확인할 수 있습니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3ff8ec6-b13c-4c28-b29d-deb8d2dd6eb6/Untitled.png)

    이 리스트의 정체는 무엇일까요? 아까 **BookService** 에 만들어둔 `search` 함수를 찬찬히 뜯어봅시다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64154232-0b2d-445f-8f9c-2f0c809f573f/Untitled.png)

    응답으로 온 데이터 (`res.data`) 에 `items` 라는 Key 로 Value 를 가져오는 코드네요!

    `res.data` 에 담겨오는 데이터는 바로…

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e4040b39-0799-4785-9d80-0d7b3cece952/Untitled.png)

    아까 보았던 **json 데이터**입니다. `totalItems` 아래에 `items` 에 있는 데이터가 우리가 만들어둔 `items` (`book_service.dart` 14번째 줄) 변수에 List 형식으로 담긴 것입니다.
  • 6) JSON 파싱(parsing)

    1. 콘솔에서는 JSON 구조가 잘 안보이니, 샘플을 보면서 원하는 데이터만 뽑아보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.

    ![items 에서 맨 위 두개의 요소는 토글 버튼을 눌러 접어뒀습니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e4040b39-0799-4785-9d80-0d7b3cece952/Untitled.png)

    items 에서 맨 위 두개의 요소는 토글 버튼을 눌러 접어뒀습니다.

    보시는 바와 같이 큰 `Map(맵)` 내에 items 라는 `key` 의 `value` 로 `List(배열)` 가 있고, 이 안에 다시 `Map(맵)`이 들어 있는 형태입니다.

    <aside>
    💡 List 안에 여러개의 `Map`이 들어있습니다. 따라서 **반복문**을 통해 해당 `List`에 있는 모든 `Map`을 각각 `Book` 클래스의 객체로 만들어 `bookList` 에 추가해주면 되겠군요!

    </aside>

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fb6daa22-f1c6-444d-b43b-25ab9c6ab172/Untitled.png)

2. 데이터를 잘 가져오는 데 성공했으니, 이제는 이를 사용하기 편하게 `Book` 클래스의 객체로 만들겠습니다. 위에서 `book.dart` 에 아래와 같이 `Book` 클래스를 만들었습니다. 코드를 다시 볼까요?

    ![Screen Shot 2022-09-19 at 2.49.25 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01adf851-c3fb-47c5-80f2-c89b62727396/Screen_Shot_2022-09-19_at_2.49.25_AM.png)

    Book 클래스의 생성자(8번째 줄)를 보면 어떤 값들을 넣어줘야 하는지 알 수 있습니다. `id`, `title`, `subtitle`, `thumbnail`, `previewLink` 에 값을 넣어 Book 클래스의 객체를 만들어줍시다. 

    아래 코드스니펫을 복사해 `book_service.dart` 의 15번째 줄을 지우고 붙여넣어주세요.

    - **[코드스니펫] for 문 / Book 객체를 만들고 bookList 에 추가하는 함수**

        ```dart

              for (Map<String, dynamic> item in items) {
                Book book = Book(
                  id: item['id'],
                  title: item['volumeInfo']['title'] ?? "",
                  subtitle: item['volumeInfo']['subtitle'] ?? "",
                  thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
                      "https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
                  previewLink: item['volumeInfo']['previewLink'] ?? "",
                );
                bookList.add(book);
              }
        ```


    ![?? 는 해당 변수에 담긴 값이 null일 경우, 즉 값이 없을 경우 뒤에 오는 값을 사용하겠다는 뜻입니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e2a12f29-d6a8-4ba3-9d23-f32e7ad59a0f/Untitled.png)

    ?? 는 해당 변수에 담긴 값이 null일 경우, 즉 값이 없을 경우 뒤에 오는 값을 사용하겠다는 뜻입니다.

    `item` 이라는 변수에 담긴 `Map` 에서는 key 값으로 데이터들을 가져올 수 있습니다. 

    아래 json 데이터의 `items` 리스트 안에 포함된 각각의 요소가 위 코드의 `item` 변수에 들어갔다고 생각하시면 됩니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/92325eba-d64c-475c-837e-8f00d3d89d7d/Untitled.png)

    만들어진 Book 객체는 25번째 줄의 `bookList.add` 를 통해 `bookList` 에 추가됩니다.

3. 매번 검색을 할 때마다 `bookList` 에 담긴 값을 모두 비워줘야 해당 검색어에 대한 결과만을 볼 수 있겠죠. 아래 코드스니펫을 복사해 `book_service.dart` 9번째 줄 맨 뒤에 붙여넣습니다.
    - **[코드스니펫] bookList 비우기**

        ```dart

            bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기

        ```


    ![Screen Shot 2022-09-19 at 3.34.27 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3b9d33a1-ded3-45f4-bc71-e77d6494239b/Screen_Shot_2022-09-19_at_3.34.27_AM.png)


<aside>
💡 `BookService`에서 Google Book API 데이터를 가져와 `bookList`에 저장하는 것까지 완성했습니다.

</aside>
  • 7) 검색 결과 보여주기

    1. ListView.builder 를 사용해 bookList 내의 요소들을 화면에 보여주도록 하겠습니다.

      아래 코드스니펫을 복사해 main.dart 파일의 104-106 번째 줄을 지우고 붙여넣어주세요.

      • [코드스니펫] SearchPage / ListView.builder

          body: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: ListView.separated(
                        itemCount: bookService.bookList.length,
                        separatorBuilder: (context, index) {
                          return Divider();
                        },
                        itemBuilder: (context, index) {
                          Book book = bookService.bookList.elementAt(index);
                          return ListTile();
                        },
                      ),
                    ),
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/44ac70cb-05dc-4a2c-8a7f-c5b51d604413/Untitled.png)

2. 112번째 줄에 있는 Book 을 클릭해 아래 사진처럼 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import  library ‘book.dart’` 를 선택합니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/586a27ce-98b9-466f-8d6e-0ee18394bcaf/Untitled.png)

    4번째 줄에 Import 문이 생기며 에러가 사라졌습니다.

    ![Screen Shot 2022-09-19 at 10.04.08 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4ea57bdd-bd69-4a49-bb92-930f88bc23ca/Screen_Shot_2022-09-19_at_10.04.08_AM.png)

3. 이제 ListTile 내에 Book의 내용을 보여주도록 하겠습니다. ListTile 의 소괄호 사이에  `title: Text(book.title)` 라고 입력해보세요.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e16c593-35ef-43cf-b2c7-7d9ee7f36a4d/Untitled.png)

    위와 같이 검색 결과가 에뮬레이터에 잘 표시되는 것을 확인할 수 있습니다.

4. 이제 나머지 요소들도 화면에 보여주도록 하겠습니다. 아래 코드스니펫을 복사해 114번째 줄을 **지우고** 붙여넣어주세요.
    - **[코드스니펫] ListTile**

        ```dart
        return ListTile(
                          onTap: () {},
                          leading: Image.network(
                            book.thumbnail,
                            fit: BoxFit.fitHeight,
                          ),
                          title: Text(
                            book.title,
                            style: TextStyle(fontSize: 16),
                          ),
                          subtitle: Text(
                            book.subtitle,
                            style: TextStyle(color: Colors.grey),
                          ),
                          trailing: IconButton(
                            onPressed: () {},
                            icon: Icon(Icons.star_border),
                          ),
                        );
        ```


    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a8a676df-6cd0-465b-adfe-2aa4a85c46e8/Untitled.png)

    `ListTile` 의 각 요소는 아래와 같이 배치됩니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7639eb36-5b37-43b8-87d0-7cf97013552e/Untitled.png)

    다른 검색어를 입력해보세요. 검색이 잘 구현되었나요? 

    화면에 검색 결과가 뜨지 않는군요. 이는 검색을 통해 `bookList` 는 수정되었지만, 이후 화면을 **새로고침하지 않아서** 생기는 문제입니다.

    `notifyListeners();` 를 `book_service.dart` 의 29번째 줄 아래에 추가해줍니다. 
    (위치에 주의해주세요!)

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4a23b383-94e7-4805-a5f7-bea5a93d6d36/Untitled.png)

    <aside>
    💡 `notifyListeners();` 는 해당 `Service` 의 `Consumer` 로 등록된 모든 위젯의 `builder` 함수를 재호출해 화면을 새로고침합니다.

    </aside>

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/036862cd-218a-401b-93e4-afe0c590ee1c/Untitled.png)

5. 이제 검색 결과는 잘 나오지만, `Debug Console` 에 아래와 같은 에러가 발생하고 있습니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0af5da55-36c1-4648-8497-80b1ca2ed385/Untitled.png)

    이는 `BookService` 의 `search` 함수에서 이전 데이터를 지워줄 때 순간적으로 `bookList` 가 비어서 생기는 문제입니다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/de463a28-727c-43f5-ae14-a37fd7fb1641/Untitled.png)

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

    - **[코드스니펫] bookList 가 비어있을 때 처리**

        ```dart

                        if (bookService.bookList.isEmpty) return SizedBox();
        ```


    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/19dad2a7-7193-41d0-97da-874a0efaffde/Untitled.png)

    `bookList` 가 비어있을 때는 `elementAt` 함수를 호출하지 않고 바로 return 하도록 해 `bookList` 의 `range` 를 벗어나는 인덱스로 접근이 일어나지 않도록 합니다.

    이제 에러 없이 잘 동작합니다!

05. 좋아요 구현하기

  • 구현 목표

  • 1) 파일 분리

    1. main.dart 의 115번째 줄에 있는 ListTile 을 우클릭해 Refactor(Ctrl + Shift + R)을 선택하고,

      Screen Shot 2022-09-19 at 11.05.03 AM.png

      Extract Widget 을 선택해주세요.

      Untitled

      위젯의 이름을 입력하는 칸이 나오면 아래와 같이 BookTile 이라고 적은 후 엔터를 눌러줍니다.

      Screen Shot 2022-09-19 at 11.07.03 AM.png

      ListTile 내에 있던 코드가 BookTile 이라는 이름의 별도의 위젯으로 분리된 것을 확인할 수 있습니다.

      Untitled

  • 2) BookService 에 좋아요 기능 추가

    1. 먼저 좋아요 한 책들을 담는 List 를 만들겠습니다.

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

      • [코드스니펫] likedBookList

        
            List<Book> likedBookList = [];
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/08c2094c-88ff-45c7-a30f-d36353445eba/Untitled.png)

2. 이제 **좋아요 아이콘을 누르면 호출될 함수**를 추가하겠습니다. 

    <aside>
    💡 해당 함수에서 구현되어야 할 기능은 아래와 같습니다.

    - **좋아요가 눌러져 있지 않은 경우 (`likedBookList` 에 없는 경우)
    ⇒** 좋아요 추가 (`likedBookList` 에 추가)
    - **좋아요가 이미 눌러져있다면 (`likedBookList` 에 있는 경우)
    ⇒** 좋아요 취소 (`likedBookList` 에서 제거)
    </aside>

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

    - **[코드스니펫] toggleLikeBook**

        ```dart

          void toggleLikeBook({required Book book}) {
            String bookId = book.id;
            if (likedBookList.map((book) => book.id).contains(bookId)) {
              likedBookList.removeWhere((book) => book.id == bookId);
            } else {
              likedBookList.add(book);
            }
            notifyListeners();
          }
        ```


    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/730739ac-1558-4e4b-9567-ce6efc17d47b/Untitled.png)

    <aside>
    💡 위 코드들에 대한 설명입니다.

    `**likedBookList.map((book) => book.id)**` : likedBookList 의 요소들을 순회하며 id 들만 뽑아 새로 iterable(List와 비슷) 을 만듭니다.

    `**likedBookList.removeWhere((book) => book.id == bookId)`** : toggleLikeBook 함수가 인자로 받는 book 과 id 가 같은 Book 이 likedBookList 내에 있다면 제거합니다.

    `**notifyListeners`** : 화면을 새로고침합니다. 

    </aside>

    <aside>
    💡 왜 `**likedBookList.contains(book)`** 과 같이 작성하지 않을까요?

    클래스를 통해 생성된 두개의 객체는 처음에 대입해주는 값들(Book 에서는 title, subtitle 등)이 같더라도, 서로 다른 존재입니다.

    코드로 작성해보면 아래와 같습니다.

    ```dart
    var book1 = Book(id: "1", title: "톰 소여의 모험");
    var book2 = Book(id: "1", title: "톰 소여의 모험");

    print(book1 == book2) // false
    ```

    우리가 작성한 코드에서도 똑같은 일이 일어납니다. 서로 다른 객체로 인식하기 때문에 contains 는 항상 false 를 반환할 수밖에 없습니다.

    ```dart
    var likedBookList = [Book(id: "1", title: "톰 소여의 모험")];

    var book2 = Book(id: "1", title: "톰 소여의 모험");

    print(likedBookList.contains(book2)) // false
    ```

    이를 해결하기 위해서 `==` 연산자를 덮어씌워 비교 로직을 새로 작성하거나, `Equatable` 등의 플러그인 등을 사용하기도 합니다. 핵심은 두개의 객체가 같은 데이터를 가리키고 있는지를 어떤 식으로 알아낼 것이냐는 점입니다.

    우리는 Google Book API 가 제공하는 id (고유한 값입니다) 를 이용해서 비교 로직을 작성했습니다.

    `**likedBookList.map((book) => book.id)**` 와 같이 id 들이 담긴 iterable(리스트와 유사합니다) 를 만들고, 이것이 우리가 좋아요 버튼을 누른 Book 의 id 를 포함하는지 비교해, 이미 좋아요가 눌린 책인지 아닌지를 구분할 수 있겠죠!

    </aside>

3. `ListTile` 의 trailing 에 있는 좋아요 버튼을 누르면 `toggleLikeBook` 함수를 호출하고, 좋아요 여부에 따라 아이콘의 색깔을 바꿔주도록 하겠습니다.

    먼저 위에서 분리한 `BookTile` 위젯에서 `BookService` 를 사용할 수 있도록 해야합니다. 다시 Provider 사용법을 복기해볼까요?

    <aside>
    💡 Provider 에서 Service 를 사용하는 방법

    1. `Consumer<클래스명>` : 클래스 정보 갱신시 화면을 새로고침 해야 할 때 사용
    2. `context.read<클래스명>` : 1회성으로 클래스 접근할 때 사용 (화면 새로고침이 필요 없을 때)
    </aside>

    우리가 만든 `BookTile` 은 좋아요를 누를 때마다 trailing 에 있는 아이콘의 색깔이 바뀌어야 합니다. 즉 새로고침이 일어나는 위젯입니다.

    하지만, `BookTile` 을 포함하고 있는 **SearchPage** 가 `Consumer`로 감싸져 있기 때문에, `BookTile` 은 별개의 `Consumer` 로 감싸지 않더라도 함께 새로고침이 일어나게 됩니다.

    그러므로, `BookTile` 에서는 `context.read` 만 사용해서 BookService 에 접근해주겠습니다.

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

    - **[코드스니펫] context.read<BookService>**

        ```dart

            BookService bookService = context.read<BookService>();

        ```


    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9673abd3-e54a-4e1f-8ce8-76fa61a8665a/Untitled.png)

    이제 `bookService` 라는 이름으로 `BookService` 클래스에 있는 변수와 함수를 사용할 수 있습니다.

    아래 코드스니펫을 복사해 `main.dart` 의 152, 153번째 줄을 지우고 붙여넣어주세요.

    - **[코드스니펫] ListTile trailing 아이콘**

        ```dart
        onPressed: () {
                  bookService.toggleLikeBook(book: book);
                },
                icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
                    ? Icon(
                        Icons.star,
                        color: Colors.amber,
                      )
                    : Icon(Icons.star_border),
        ```


    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fe835df4-045e-4fdc-b019-acd55bed32e4/Untitled.png)

    <aside>
    💡 위 코드들에 대한 설명입니다.

    `**bookService.toggleLikeBook(book: book)**` : BookService 에 생성해준 toggleLikeBook 에 해당 book 데이터를 인자로 넘겨 호출해줍니다.

    `**bookService.likedBookList.map((book) => book.id).contains(book.id)`** : bookService 에 있는 likedBookList(좋아요 한 책들 담겨있는 리스트) 가 해당 book 을 담고있는지, 즉 이미 좋아요를 누른 상태인지 확인합니다.

    아래 삼항연산자 (조건 ? a : b) 를 통해 좋아요가 눌러진 상태라면 노란색 별 아이콘을 보여줍니다.

    </aside>

    아래와 같이 좋아요를 누를 때 화면이 잘 갱신되는 것을 볼 수 있습니다.

    ![Screen Shot 2022-09-19 at 12.06.51 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12e8db01-a01f-449c-8174-436c43da5c97/Screen_Shot_2022-09-19_at_12.06.51_PM.png)
  • 3) LikedBookPage 에서 좋아요 한 책들 모아보기

    1. 먼저 LikedBookPage 에서도 likedBookList 의 변화에 따라 화면이 갱신되도록 해야합니다.

      Consumer 를 사용해 Scaffold 를 감싸주겠습니다. 171번째 줄의 Scaffold 를 클릭한 뒤 Quick Fix(Ctrl/Cmd + .)를 눌러 Wrap with Builder 를 선택해주세요.

      Untitled

    2. 아래와 같이 Scaffold 위젯을 Builder 위젯이 감쌉니다. Consumer 위젯과 형식이 비슷한 Builder 를 사용해 모양을 잡아줬습니다.

      Untitled

    3. 아래 이미지와 같이 79번째 라인의 BuilderConsumer<BookService>로 변경해주세요.

      context 뒤에 bookServicechild 도 추가해줍니다.

      Untitled

      그리고 줄 정렬을 예쁘게 해주기 위해 177번째 라인의 중괄호와 소괄호 사이(위 화살표)에 콤마(,)를 찍은 뒤 저장해주세요. 저장하면 코드가 정렬됩니다.

      Screen Shot 2022-09-19 at 12.45.27 PM.png

    4. 아래 코드스니펫을 복사해 main.dart 의 174-176 번째 줄을 지우고 붙여넣어주세요. 위 SearchPage 에 작성한 코드를 그대로 가져왔습니다.

      • [코드스니펫] LikedBookPage / BookTile

          body: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: ListView.separated(
                        itemCount: bookService.bookList.length,
                        separatorBuilder: (context, index) {
                          return Divider();
                        },
                        itemBuilder: (context, index) {
                          if (bookService.bookList.isEmpty) return SizedBox();
                          Book book = bookService.bookList.elementAt(index);
                          return BookTile(book: book);
                        },
                      ),
                    ),
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/548eb9ab-6c0c-449f-b847-943299ca4d7b/Untitled.png)

    좋아요 한 책만 모아보기 위해서는 위에 밑줄친 `bookList` 대신에 `likedBookList` 를 사용하면 되겠죠! 모두 아래와 같이 수정해봅시다.

    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3d1e194e-4014-45c6-9e51-76f63128f334/Untitled.png)

    이제 좋아요 탭에서 아래와 같이 좋아요 누른 책들을 모아볼 수 있습니다.

    ![Screen Shot 2022-09-19 at 12.49.08 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1f969888-e359-4b43-8895-506eaad7bb27/Screen_Shot_2022-09-19_at_12.49.08_PM.png)

06. 책 상세 페이지 WebView로 보여주기

  • 구현 목표

  • 1) WebView 란?

    대표적인 예시가 네이버 앱입니다. 앱에 들어가보시면 네이버 모바일 웹사이트와 거의 동일한 UI 를 볼 수 있습니다. 껍데기 부분만 앱으로 만들고 내부 위젯에 WebView 를 사용해 웹사이트를 그대로 띄우는 방식으로 구현되어 있다는 것을 알 수 있겠죠!

    Untitled

    이외에도 당근마켓, 토스와 같은 앱들도 내부 기능 일부를 WebView 를 활용해 구현했습니다.

  • 2) webview_flutter 패키지 설치하기

    1. 해당 패키지 사이트는 아래와 같습니다.

    2. 강의 영상과 조금 다른 부분이니 주의해서 봐주세요! webview_flutter 패키지가 업데이트 되면서 아래 강의 내용대로 진행하면 에러가 뜹니다. 이전 버전 패키지를 설치함으로서 이 에러를 해결해주도록 하겠습니다

      아래 코드스니펫을 복사해주세요.

      • [코드스니펫] webview_flutter 3.0.4 버전 설치 스크립트

          flutter pub add webview_flutter:^3.0.4
    3. VSCode ViewTerminal을 선택해주세요.

      Untitled

    4. Terminal창에 복사한 flutter pub add webview_flutter:^3.0.4 를 붙여넣고 엔터를 눌러 실행해 주세요.
      아래와 같이 나오면 정상적으로 작동한 것입니다.

      Screenshot 2022-12-22 at 5.12.45 PM.png

    5. pubspec.yaml 파일을 열어서 아래와 같이 41번째 라인에 webview_flutter: ^3.0.4가 있으면 설치가 완료된 것입니다.

      Screen Shot 2022-09-19 at 1.01.36 PM.png

      이제 아래부터는 강의 내용과 같습니다!

    6. android/app/build.gradle 파일을 열고(경로에 유의하세요!)
      50 번째 줄의 flutter.minSdkVersion19 로 수정해줍니다.

      Untitled

       android {
           defaultConfig {
               minSdkVersion 19
           }
       }

      아래와 같이 작성해주면 됩니다.

      Untitled

    7. webview_flutter 를 앱에서 사용하기 위해서는 앱을 재설치해야합니다.

      먼저 앱을 정지하고

      Untitled

      main.dart 를 열고 우측 상단에서 Run Without Debugging 을 클릭해 다시 앱을 설치 / 실행해줍니다.

      Untitled

  • <aside>
    💡 WebView 를 앱에서 사용할 준비가 완료되었습니다.
    
    </aside>
    • 3) WebViewPage 위젯 만들기

      1. webview_flutter 의 설치가 끝났으니, 이제 각 Book 의 previewLink 로 이동하는 기능을 구현해보겠습니다.

        Untitled

        먼저 WebViewPage 라는 StatelessWidget 을 하나 만들어주겠습니다. 아래 코드스니펫을 복사해 192번째 줄 맨 뒤에 붙여넣어주세요.

        • [코드스니펫] WebViewPage

          
            class WebViewPage extends StatelessWidget {
              WebViewPage({super.key, required this.url});
          
              String url;
          
              @override
              Widget build(BuildContext context) {
                return Scaffold(
                  appBar: AppBar(
                    backgroundColor: Colors.grey,
                    title: Text(url),
                  ),
                  body: WebView(initialUrl: url),
                );
              }
            }
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f549f171-9a60-42a5-868b-b94c08e27e62/Untitled.png)
    
    2. 206번째 줄에 에러가 발생했군요. 에러가 발생한 `WebView` 를 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 누르고, `Import library ‘package:webview_flutter/webview_flutter.dart` 를 선택해주세요.
    
        ![Screen Shot 2022-09-19 at 3.17.21 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e59b7f90-e05c-4a4d-8193-bc4267b127e1/Screen_Shot_2022-09-19_at_3.17.21_PM.png)
    
        3번째 줄에 Import 문이 추가되면서 에러가 해결됩니다.
    
        ![Screen Shot 2022-09-19 at 3.17.51 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f3026c0d-bd4d-49d8-a5c1-62d8c1f18457/Screen_Shot_2022-09-19_at_3.17.51_PM.png)
    
    3. 코드를 자세히 뜯어볼까요? 
    
        ![Screen Shot 2022-09-19 at 3.18.28 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/be60e694-dae6-46c9-ac11-0cbe5470d9d9/Screen_Shot_2022-09-19_at_3.18.28_PM.png)
    
        **WebViewPage** 위젯으로 이동하면서 url 이라는 인자를 넘겨받고, 이를 `WebView` 위젯에 다시 인자로 넘겨 웹페이지를 `Scaffold` 의 body 내에 띄워주는 코드입니다.
    • 4) 웹 URL 연결하기

      아래 코드스니펫을 복사해 ListTileonTap 함수 중괄호 사이 (139번째 줄) 에 붙여넣어줍니다.

      • [코드스니펫] BookTile / ListTile / onTap

        
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => WebViewPage(
                        url: book.previewLink.replaceFirst("http", "https"),
                      ),
                    ),
                  );
        
    ![Screen Shot 2022-09-19 at 3.20.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/54280695-d3b7-4957-b6c6-06e86cb57f87/Screen_Shot_2022-09-19_at_3.20.05_PM.png)
    
    `ListTile` 을 누르면 방금 만든 **WebViewPage** 로 이동하도록 했습니다. 이 때 해당 book의 `previewLink` 을 인자로 전달해, 해당 링크를 웹뷰로 열도록 합니다.
    
    <aside>
    💡 Book 의 `previewLink` 의 경우 http 로 시작하기 때문에 보안 정책상 웹뷰로 열리지 않습니다. 이 때문에 `replaceFirst`를 사용해 해당 url 의 http 를 https 로 바꿔주었습니다.
    
    http, https 에 대한 보다 자세한 내용이 궁금하시다면 아래 링크를 참고해주세요. 
    [**HTTP와 HTTPS 차이점**](https://brunch.co.kr/@hyoi0303/10)
    
    </aside>
    
    이제 에뮬레이터에서 검색 후에 각각의 BookTile 을 클릭해봅시다. 
    
    아래와 같이 앱 내에서 해당 웹페이지를 잘 보여주는 것을 확인할 수 있습니다.
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ab54991c-1b5a-4ae9-9855-91967b4aadb4/Untitled.gif)
    • 최종 코드

      • main.dart

          import 'package:flutter/material.dart';
          import 'package:provider/provider.dart';
          import 'package:webview_flutter/webview_flutter.dart';
        
          import 'book.dart';
          import 'book_service.dart';
        
          void main() {
            runApp(
              MultiProvider(
                providers: [
                  ChangeNotifierProvider(create: (context) => BookService()),
                ],
                child: const MyApp(),
              ),
            );
          }
        
          class MyApp extends StatelessWidget {
            const MyApp({Key? key}) : super(key: key);
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
        
          class HomePage extends StatefulWidget {
            HomePage({Key? key}) : super(key: key);
        
            @override
            State<HomePage> createState() => _HomePageState();
          }
        
          class _HomePageState extends State<HomePage> {
            var bottomNavIndex = 0;
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: [
                  SearchPage(),
                  LikedBookPage(),
                ].elementAt(bottomNavIndex),
                bottomNavigationBar: BottomNavigationBar(
                  selectedItemColor: Colors.black,
                  unselectedItemColor: Colors.grey,
                  showUnselectedLabels: true,
                  selectedFontSize: 12,
                  unselectedFontSize: 12,
                  iconSize: 28,
                  type: BottomNavigationBarType.fixed,
                  onTap: (value) {
                    setState(() {
                      bottomNavIndex = value;
                    });
                  },
                  items: [
                    BottomNavigationBarItem(
                      icon: Icon(Icons.search),
                      label: '검색',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.star),
                      label: '좋아요',
                    ),
                  ],
                  currentIndex: bottomNavIndex,
                ),
              );
            }
          }
        
          class SearchPage extends StatelessWidget {
            SearchPage({super.key});
        
            @override
            Widget build(BuildContext context) {
              return Consumer<BookService>(
                builder: (context, bookService, child) {
                  return Scaffold(
                    appBar: AppBar(
                      backgroundColor: Colors.white,
                      toolbarHeight: 80,
                      title: TextField(
                        onSubmitted: (value) {
                          bookService.search(value);
                        },
                        cursorColor: Colors.grey,
                        decoration: InputDecoration(
                          prefixIcon: Icon(Icons.search, color: Colors.grey),
                          hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
                          border: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.white),
                            borderRadius: BorderRadius.all(Radius.circular(10)),
                          ),
                          focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey),
                            borderRadius: BorderRadius.all(Radius.circular(10)),
                          ),
                        ),
                      ),
                    ),
                    body: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: ListView.separated(
                        itemCount: bookService.bookList.length,
                        separatorBuilder: (context, index) {
                          return Divider();
                        },
                        itemBuilder: (context, index) {
                          if (bookService.bookList.isEmpty) return SizedBox();
                          Book book = bookService.bookList.elementAt(index);
                          return BookTile(book: book);
                        },
                      ),
                    ),
                  );
                },
              );
            }
          }
        
          class BookTile extends StatelessWidget {
            const BookTile({
              Key? key,
              required this.book,
            }) : super(key: key);
        
            final Book book;
        
            @override
            Widget build(BuildContext context) {
              BookService bookService = context.read<BookService>();
        
              return ListTile(
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => WebViewPage(
                        url: book.previewLink.replaceFirst("http", "https"),
                      ),
                    ),
                  );
                },
                leading: Image.network(
                  book.thumbnail,
                  fit: BoxFit.fitHeight,
                ),
                title: Text(
                  book.title,
                  style: TextStyle(fontSize: 16),
                ),
                subtitle: Text(
                  book.subtitle,
                  style: TextStyle(color: Colors.grey),
                ),
                trailing: IconButton(
                  onPressed: () {
                    bookService.toggleLikeBook(book: book);
                  },
                  icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
                      ? Icon(
                          Icons.star,
                          color: Colors.amber,
                        )
                      : Icon(Icons.star_border),
                ),
              );
            }
          }
        
          class LikedBookPage extends StatelessWidget {
            const LikedBookPage({super.key});
        
            @override
            Widget build(BuildContext context) {
              return Consumer<BookService>(
                builder: (context, bookService, child) {
                  return Scaffold(
                    body: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: ListView.separated(
                        itemCount: bookService.likedBookList.length,
                        separatorBuilder: (context, index) {
                          return Divider();
                        },
                        itemBuilder: (context, index) {
                          if (bookService.likedBookList.isEmpty) return SizedBox();
                          Book book = bookService.likedBookList.elementAt(index);
                          return BookTile(book: book);
                        },
                      ),
                    ),
                  );
                },
              );
            }
          }
        
          class WebViewPage extends StatelessWidget {
            WebViewPage({super.key, required this.url});
        
            String url;
        
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.grey,
                  title: Text(url),
                ),
                body: WebView(initialUrl: url),
              );
            }
          }
      • book.dart

          class Book {
            String id;
            String title;
            String subtitle;
            String thumbnail; // 썸네일 이미지 링크
            String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
        
            Book({
              required this.id,
              required this.title,
              required this.subtitle,
              required this.thumbnail,
              required this.previewLink,
            });
          }
      • book_service.dart

          import 'package:dio/dio.dart';
          import 'package:flutter/material.dart';
        
          import 'book.dart';
        
          class BookService extends ChangeNotifier {
            List<Book> bookList = []; // 책 목록
            List<Book> likedBookList = [];
        
            void toggleLikeBook({required Book book}) {
              String bookId = book.id;
              if (likedBookList.map((book) => book.id).contains(bookId)) {
                likedBookList.removeWhere((book) => book.id == bookId);
              } else {
                likedBookList.add(book);
              }
              notifyListeners();
            }
        
            void search(String q) async {
              bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
        
              if (q.isNotEmpty) {
                Response res = await Dio().get(
                  "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
                );
                List items = res.data["items"];
        
                for (Map<String, dynamic> item in items) {
                  Book book = Book(
                    id: item['id'],
                    title: item['volumeInfo']['title'] ?? "",
                    subtitle: item['volumeInfo']['subtitle'] ?? "",
                    thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
                        "https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
                    previewLink: item['volumeInfo']['previewLink'] ?? "",
                  );
                  bookList.add(book);
                }
              }
              notifyListeners();
            }
          }

    07. 숙제 - 왓챠피디아 추가기능 구현

    • 1) Google Book API 에서 제공하는 다른 정보 보여주기

      • 1) 구현 목표

    • 2) 숙제 답안

      • main.dart

          import 'package:flutter/material.dart';
          import 'package:provider/provider.dart';
          import 'package:webview_flutter/webview_flutter.dart';
        
          import 'book.dart';
          import 'book_service.dart';
        
          void main() {
            runApp(
              MultiProvider(
                providers: [
                  ChangeNotifierProvider(create: (context) => BookService()),
                ],
                child: const MyApp(),
              ),
            );
          }
        
          class MyApp extends StatelessWidget {
            const MyApp({Key? key}) : super(key: key);
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
        
          class HomePage extends StatefulWidget {
            HomePage({Key? key}) : super(key: key);
        
            @override
            State<HomePage> createState() => _HomePageState();
          }
        
          class _HomePageState extends State<HomePage> {
            var bottomNavIndex = 0;
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: [
                  SearchPage(),
                  LikedBookPage(),
                ].elementAt(bottomNavIndex),
                bottomNavigationBar: BottomNavigationBar(
                  selectedItemColor: Colors.black,
                  unselectedItemColor: Colors.grey,
                  showUnselectedLabels: true,
                  selectedFontSize: 12,
                  unselectedFontSize: 12,
                  iconSize: 28,
                  type: BottomNavigationBarType.fixed,
                  onTap: (value) {
                    setState(() {
                      bottomNavIndex = value;
                    });
                  },
                  items: [
                    BottomNavigationBarItem(
                      icon: Icon(Icons.search),
                      label: '검색',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.star),
                      label: '좋아요',
                    ),
                  ],
                  currentIndex: bottomNavIndex,
                ),
              );
            }
          }
        
          class SearchPage extends StatelessWidget {
            SearchPage({super.key});
        
            @override
            Widget build(BuildContext context) {
              return Consumer<BookService>(
                builder: (context, bookService, child) {
                  return Scaffold(
                    appBar: AppBar(
                      backgroundColor: Colors.white,
                      toolbarHeight: 80,
                      title: TextField(
                        onSubmitted: (value) {
                          bookService.search(value);
                        },
                        cursorColor: Colors.grey,
                        decoration: InputDecoration(
                          prefixIcon: Icon(Icons.search, color: Colors.grey),
                          hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
                          border: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.white),
                            borderRadius: BorderRadius.all(Radius.circular(10)),
                          ),
                          focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey),
                            borderRadius: BorderRadius.all(Radius.circular(10)),
                          ),
                        ),
                      ),
                    ),
                    body: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: ListView.separated(
                        itemCount: bookService.bookList.length,
                        separatorBuilder: (context, index) {
                          return Divider();
                        },
                        itemBuilder: (context, index) {
                          if (bookService.bookList.isEmpty) return SizedBox();
                          Book book = bookService.bookList.elementAt(index);
                          return BookTile(book: book);
                        },
                      ),
                    ),
                  );
                },
              );
            }
          }
        
          class BookTile extends StatelessWidget {
            const BookTile({
              Key? key,
              required this.book,
            }) : super(key: key);
        
            final Book book;
        
            @override
            Widget build(BuildContext context) {
              BookService bookService = context.read<BookService>();
        
              return ListTile(
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => WebViewPage(
                        url: book.previewLink.replaceFirst("http", "https"),
                      ),
                    ),
                  );
                },
                leading: Image.network(
                  book.thumbnail,
                  fit: BoxFit.fitHeight,
                ),
                title: Text(
                  book.title,
                  style: TextStyle(fontSize: 16),
                ),
                subtitle: Text(
                  "${book.authors.join(", ")}\n${book.publishedDate}",
                  style: TextStyle(color: Colors.grey),
                ),
                trailing: IconButton(
                  onPressed: () {
                    bookService.toggleLikeBook(book: book);
                  },
                  icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
                      ? Icon(
                          Icons.star,
                          color: Colors.amber,
                        )
                      : Icon(Icons.star_border),
                ),
              );
            }
          }
        
          class LikedBookPage extends StatelessWidget {
            const LikedBookPage({super.key});
        
            @override
            Widget build(BuildContext context) {
              return Consumer<BookService>(
                builder: (context, bookService, child) {
                  return Scaffold(
                    body: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 12),
                      child: ListView.separated(
                        itemCount: bookService.likedBookList.length,
                        separatorBuilder: (context, index) {
                          return Divider();
                        },
                        itemBuilder: (context, index) {
                          if (bookService.likedBookList.isEmpty) return SizedBox();
                          Book book = bookService.likedBookList.elementAt(index);
                          return BookTile(book: book);
                        },
                      ),
                    ),
                  );
                },
              );
            }
          }
        
          class WebViewPage extends StatelessWidget {
            WebViewPage({super.key, required this.url});
        
            String url;
        
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.grey,
                  title: Text(url),
                ),
                body: WebView(initialUrl: url),
              );
            }
          }
      • book.dart

          class Book {
            String id;
            String title;
            String subtitle;
            List authors;
            String publishedDate;
            String thumbnail; // 썸네일 이미지 링크
            String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
        
            Book({
              required this.id,
              required this.title,
              required this.subtitle,
              required this.authors,
              required this.publishedDate,
              required this.thumbnail,
              required this.previewLink,
            });
          }
      • book_service.dart

          import 'package:dio/dio.dart';
          import 'package:flutter/material.dart';
        
          import 'book.dart';
        
          class BookService extends ChangeNotifier {
            List<Book> bookList = []; // 책 목록
            List<Book> likedBookList = [];
        
            void toggleLikeBook({required Book book}) {
              String bookId = book.id;
              if (likedBookList.map((book) => book.id).contains(bookId)) {
                likedBookList.removeWhere((book) => book.id == bookId);
              } else {
                likedBookList.add(book);
              }
              notifyListeners();
            }
        
            void search(String q) async {
              bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
        
              if (q.isNotEmpty) {
                Response res = await Dio().get(
                  "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
                );
                List items = res.data["items"];
        
                for (Map<String, dynamic> item in items) {
                  Book book = Book(
                    id: item['id'],
                    title: item['volumeInfo']['title'] ?? "",
                    subtitle: item['volumeInfo']['subtitle'] ?? "",
                    authors: item['volumeInfo']['authors'] ?? [],
                    publishedDate: item['volumeInfo']['publishedDate'] ?? "",
                    thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
                        "https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
                    previewLink: item['volumeInfo']['previewLink'] ?? "",
                  );
                  bookList.add(book);
                }
              }
              notifyListeners();
            }
          }
  • 2) 좋아요 누른 책 목록 기기에 저장하기

    • 1) 구현 목표

  • 2) 숙제 답안

    • main.dart

        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import 'package:shared_preferences/shared_preferences.dart';
        import 'package:webview_flutter/webview_flutter.dart';
      
        import 'book.dart';
        import 'book_service.dart';
      
        late SharedPreferences prefs;
      
        void main() async {
          WidgetsFlutterBinding.ensureInitialized();
          prefs = await SharedPreferences.getInstance();
          runApp(
            MultiProvider(
              providers: [
                ChangeNotifierProvider(create: (context) => BookService()),
              ],
              child: const MyApp(),
            ),
          );
        }
      
        class MyApp extends StatelessWidget {
          const MyApp({Key? key}) : super(key: key);
      
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: HomePage(),
            );
          }
        }
      
        class HomePage extends StatefulWidget {
          HomePage({Key? key}) : super(key: key);
      
          @override
          State<HomePage> createState() => _HomePageState();
        }
      
        class _HomePageState extends State<HomePage> {
          var bottomNavIndex = 0;
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: [
                SearchPage(),
                LikedBookPage(),
              ].elementAt(bottomNavIndex),
              bottomNavigationBar: BottomNavigationBar(
                selectedItemColor: Colors.black,
                unselectedItemColor: Colors.grey,
                showUnselectedLabels: true,
                selectedFontSize: 12,
                unselectedFontSize: 12,
                iconSize: 28,
                type: BottomNavigationBarType.fixed,
                onTap: (value) {
                  setState(() {
                    bottomNavIndex = value;
                  });
                },
                items: [
                  BottomNavigationBarItem(
                    icon: Icon(Icons.search),
                    label: '검색',
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(Icons.star),
                    label: '좋아요',
                  ),
                ],
                currentIndex: bottomNavIndex,
              ),
            );
          }
        }
      
        class SearchPage extends StatelessWidget {
          SearchPage({super.key});
      
          @override
          Widget build(BuildContext context) {
            return Consumer<BookService>(
              builder: (context, bookService, child) {
                return Scaffold(
                  appBar: AppBar(
                    backgroundColor: Colors.white,
                    toolbarHeight: 80,
                    title: TextField(
                      onSubmitted: (value) {
                        bookService.search(value);
                      },
                      cursorColor: Colors.grey,
                      decoration: InputDecoration(
                        prefixIcon: Icon(Icons.search, color: Colors.grey),
                        hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
                        border: OutlineInputBorder(
                          borderSide: BorderSide(color: Colors.white),
                          borderRadius: BorderRadius.all(Radius.circular(10)),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderSide: BorderSide(color: Colors.grey),
                          borderRadius: BorderRadius.all(Radius.circular(10)),
                        ),
                      ),
                    ),
                  ),
                  body: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 12),
                    child: ListView.separated(
                      itemCount: bookService.bookList.length,
                      separatorBuilder: (context, index) {
                        return Divider();
                      },
                      itemBuilder: (context, index) {
                        if (bookService.bookList.isEmpty) return SizedBox();
                        Book book = bookService.bookList.elementAt(index);
                        return BookTile(book: book);
                      },
                    ),
                  ),
                );
              },
            );
          }
        }
      
        class BookTile extends StatelessWidget {
          const BookTile({
            Key? key,
            required this.book,
          }) : super(key: key);
      
          final Book book;
      
          @override
          Widget build(BuildContext context) {
            BookService bookService = context.read<BookService>();
      
            return ListTile(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => WebViewPage(
                      url: book.previewLink.replaceFirst("http", "https"),
                    ),
                  ),
                );
              },
              leading: Image.network(
                book.thumbnail,
                fit: BoxFit.fitHeight,
              ),
              title: Text(
                book.title,
                style: TextStyle(fontSize: 16),
              ),
              subtitle: Text(
                "${book.authors.join(", ")}\n${book.publishedDate}",
                style: TextStyle(color: Colors.grey),
              ),
              trailing: IconButton(
                onPressed: () {
                  bookService.toggleLikeBook(book: book);
                },
                icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
                    ? Icon(
                        Icons.star,
                        color: Colors.amber,
                      )
                    : Icon(Icons.star_border),
              ),
            );
          }
        }
      
        class LikedBookPage extends StatelessWidget {
          const LikedBookPage({super.key});
      
          @override
          Widget build(BuildContext context) {
            return Consumer<BookService>(
              builder: (context, bookService, child) {
                return Scaffold(
                  body: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 12),
                    child: ListView.separated(
                      itemCount: bookService.likedBookList.length,
                      separatorBuilder: (context, index) {
                        return Divider();
                      },
                      itemBuilder: (context, index) {
                        if (bookService.likedBookList.isEmpty) return SizedBox();
                        Book book = bookService.likedBookList.elementAt(index);
                        return BookTile(book: book);
                      },
                    ),
                  ),
                );
              },
            );
          }
        }
      
        class WebViewPage extends StatelessWidget {
          WebViewPage({super.key, required this.url});
      
          String url;
      
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                backgroundColor: Colors.grey,
                title: Text(url),
              ),
              body: WebView(initialUrl: url),
            );
          }
        }
    • book.dart

        class Book {
          String id;
          String title;
          String subtitle;
          List authors;
          String publishedDate;
          String thumbnail; // 썸네일 이미지 링크
          String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
      
          Book({
            required this.id,
            required this.title,
            required this.subtitle,
            required this.authors,
            required this.publishedDate,
            required this.thumbnail,
            required this.previewLink,
          });
      
          Map toJson() {
            return {
              "id": id,
              "title": title,
              "subtitle": subtitle,
              "authors": authors,
              "publishedDate": publishedDate,
              "thumbnail": thumbnail,
              "previewLink": previewLink,
            };
          }
      
          factory Book.fromJson(json) {
            return Book(
              id: json['id'],
              title: json['title'],
              subtitle: json['subtitle'],
              authors: json['authors'],
              publishedDate: json['publishedDate'],
              thumbnail: json['thumbnail'],
              previewLink: json['previewLink'],
            );
          }
        }
    • book_service.dart

        import 'dart:convert';
      
        import 'package:dio/dio.dart';
        import 'package:flutter/material.dart';
      
        import 'book.dart';
        import 'main.dart';
      
        class BookService extends ChangeNotifier {
          BookService() {
            loadLikedBookList();
          }
      
          List<Book> bookList = []; // 책 목록
          List<Book> likedBookList = [];
      
          void toggleLikeBook({required Book book}) {
            String bookId = book.id;
            if (likedBookList.map((book) => book.id).contains(bookId)) {
              likedBookList.removeWhere((book) => book.id == bookId);
            } else {
              likedBookList.add(book);
            }
            notifyListeners();
            saveLikedBookList();
          }
      
          void search(String q) async {
            bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
      
            if (q.isNotEmpty) {
              Response res = await Dio().get(
                "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
              );
              List items = res.data["items"];
      
              for (Map<String, dynamic> item in items) {
                Book book = Book(
                  id: item['id'],
                  title: item['volumeInfo']['title'] ?? "",
                  subtitle: item['volumeInfo']['subtitle'] ?? "",
                  authors: item['volumeInfo']['authors'] ?? [],
                  publishedDate: item['volumeInfo']['publishedDate'] ?? "",
                  thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
                      "https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
                  previewLink: item['volumeInfo']['previewLink'] ?? "",
                );
                bookList.add(book);
              }
            }
            notifyListeners();
          }
      
          saveLikedBookList() {
            List likedBookJsonList =
                likedBookList.map((book) => book.toJson()).toList();
            // [{"content": "1"}, {"content": "2"}]
      
            String jsonString = jsonEncode(likedBookJsonList);
            // '[{"content": "1"}, {"content": "2"}]'
      
            prefs.setString('likedBookList', jsonString);
          }
      
          loadLikedBookList() {
            String? jsonString = prefs.getString('likedBookList');
            // '[{"content": "1"}, {"content": "2"}]'
      
            if (jsonString == null) return; // null 이면 로드하지 않음
      
            List likedBookJsonList = jsonDecode(jsonString);
            // [{"content": "1"}, {"content": "2"}]
      
            likedBookList =
                likedBookJsonList.map((json) => Book.fromJson(json)).toList();
          }
        }
  • 3주차 - 패키지 사용법 익히기 & 앱의 기능 만들기 (마이메모)

    • PDF 파일

      3주차-패키지사용법_익히기__앱의_기능_만들기(마이메모).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 + /

    [수업 목표]

    • 패키지 사용법 익히기
    • Create / Read / Update / Delete 기능 구현하기
    • 상태 관리의 필요성 이해하기
    • Provider 사용법 익히기
    • shared_preferences 사용법 익히기

    [목차]


    01. 패키지(Package)

    • 1) 패키지란?

      • Flutter는 pub.dev라는 사이트에서 패키지들을 검색하실 수 있습니다. 코드스니펫을 복사해서 새 탭에서 띄워주세요.

        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9e74b84f-b51e-4789-9a74-c38325a6f3c8/Untitled.png)
    
    - 밑으로 조금 내려와 보면 Flutter Favorites라고 인증된 신뢰할 만한 패키지 목록이 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/68298554-45dc-4a21-8796-49647462169c/Untitled.png)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7a7eb553-ac5a-45dd-ab1e-5bf60e361959/Untitled.png)
    <aside>
    💡 처음에는 패키지 설치 방법 및 사용 설명서를 읽는데 집중해주세요!
    
    모든 문서가 영어로 되어 있지만, 계속 보다 보면 나오는 용어들이 계속 나오는 것을 보실 수 있습니다.
    
    </aside>
    
    ![Pub.dev.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6eaff257-24ce-4f9f-957f-249dd7030249/Pub.dev.png)
    
    <aside>
    💡 Readme와 Example 탭을 보면 해당 패키지의 사용법을 익힐 수 있습니다.
    
    </aside>
    • 3) 패키지 추천 사이트

      • Awesome Flutter : Flutter에 관련된 모든 자료를 모아둔 Github 문서입니다.

        Untitled

      • Flutter Gems : 카테고리별 Flutter 패키지 모음

        Untitled

    <aside>
    💡 직접 프로젝트를 진행하면서 패키지 사용법을 배워보도록 하겠습니다.
    
    </aside>

    02. 마이메모 앱 만들기

    • 완성본

      Untitled

    • CRUD는 가장 기본이 되는 데이터 처리 기능입니다.

      게시판 기능을 만든다면 아래 CRUD 기능이 필수적으로 제공되어야 합니다.

      1. 글 쓰기(Create)
      2. 글 읽기(Read)
      3. 글 수정(Update)
      4. 글 삭제(Delete)

      유저 정보를 다루는 과정도 CRUD로 표현하면 다음과 같습니다.

      1. 회원 가입(Create)

      2. 프로필 보여주기(Read)

      3. 회원 정보 수정(Update)

      4. 회원 탈퇴(Delete)

        이와 같이 CRUD는 다루는 데이터의 종류만 바뀌고 항상 기본적으로 구현하는 데이터 처리 기능입니다.

    • 1) 프로젝트 준비

      • Flutter 프로젝트 생성

        1. VSCode에서 아래와 같이 네모 모양의 Stop 버튼을 눌러 기존에 실행한 앱을 종료해주세요.

          Untitled

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

          Untitled

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

          Untitled

        4. Application을 선택해주세요.

          Untitled

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

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

          Screen Shot 2022-09-07 at 8.04.46 PM.png

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

          Untitled

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

          Screen Shot 2022-09-07 at 8.06.51 PM.png

        8. 불필요한 힌트 숨기기

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

          • [코드스니펫] analysis_options.yaml

            
                  prefer_const_constructors: false
                  prefer_const_literals_to_create_immutables: false
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
    
            - 어떤 의미인지 궁금하신 분들을 위해
                - `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
    
                - 파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
    
                    12번째 라인에 마우스를 올리면 아래와 같이 설명이 뜹니다.
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
    
                    위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다.
    
                    <aside>
                    💡 상수로 만들면 어떤 이점이 있나요?
    
                    상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다.
    
                    </aside>
    
                    아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
    
                - 지금은 학습 단계이니 눈에 띄지 않도록 해주도록 하겠습니다.
        9. 아래 코드스니펫을 복사해서, `main.dart`의 기존 코드를 모두 지우고 붙여 넣은 뒤 저장해 주세요.
            - **[코드스니펫]  main.dart**
    
                ```dart
                import 'package:flutter/cupertino.dart';
                import 'package:flutter/material.dart';
    
                void main() {
                  runApp(const MyApp());
                }
    
                class MyApp extends StatelessWidget {
                  const MyApp({super.key});
    
                  @override
                  Widget build(BuildContext context) {
                    return MaterialApp(
                      debugShowCheckedModeBanner: false,
                      home: HomePage(),
                    );
                  }
                }
    
                // 홈 페이지
                class HomePage extends StatelessWidget {
                  const HomePage({super.key});
    
                  @override
                  Widget build(BuildContext context) {
                    return Scaffold(
                      appBar: AppBar(
                        title: Text("mymemo"),
                      ),
                      body: Center(child: Text("메모를 작성해주세요")),
                      floatingActionButton: FloatingActionButton(
                        child: Icon(Icons.add),
                        onPressed: () {
                          // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
                          Navigator.push(
                            context,
                            MaterialPageRoute(builder: (_) => DetailPage()),
                          );
                        },
                      ),
                    );
                  }
                }
    
                // 메모 생성 및 수정 페이지
                class DetailPage extends StatelessWidget {
                  DetailPage({super.key});
    
                  TextEditingController contentController = TextEditingController();
    
                  @override
                  Widget build(BuildContext context) {
                    return Scaffold(
                      appBar: AppBar(
                        actions: [
                          IconButton(
                            onPressed: () {
                              // 삭제 버튼 클릭시
                            },
                            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) {
                            // 텍스트필드 안의 값이 변할 때
                          },
                        ),
                      ),
                    );
                  }
                }
                ```
    
    - 에뮬레이터 실행하기
        1. VSCode 우측 하단에 `Chrome (web-javascript)`를 클릭해주세요.
        (에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5d0db764-7e21-4261-8c19-94162c4d0ba8/Untitled.png)
    
        2. `Start Pixel 2 API 29 mobile emulator`를 선택해주세요. macOS의 경우 iOS 에뮬레이터로 진행하셔도 무방합니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5e0e7243-7d95-4b57-99df-08fd28e1b4d7/Untitled.png)
    
            잠시 기다리면 에뮬레이터가 실행됩니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf5640d3-c9a9-4105-bd40-9dcb309412f5/Untitled.png)
    
        3. VSCode 우측 상단에 **아래 화살표**를 눌러 `Run Without Debugging`을 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fba923c5-b4b0-4ddb-b381-ae8686c0315c/Untitled.png)
    
            <aside>
            💡 `Start Debugging`으로 실행해도 무방합니다. 디버깅 모드는 특정 라인에서 앱 실행을 멈추고 해당 변수에 어떤 값이 들어있는지 볼 수 있지만 `Run without debugging`이 실행 속도가 더 빨라 안내를 위와 같이 드렸습니다 🙂
    
            </aside>
    
            에뮬레이터에 아래와 같이 **메모장** 화면이 나오면 완료!
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1092421d-a876-4748-aa68-7370d5a1cb3b/Untitled.png)
    
            <aside>
            💡 위와 같이 메모를 보여주는 `HomePage`와 메모를 작성하는 `DetailPage` 두 페이지로 구성된 앱입니다.
    
            기본적인 레이아웃과 페이지 이동 기능은 구현되어 있고, CRUD 기능을 함께 구현해 보도록 하겠습니다.
    
            </aside>
    • 2) 메모 목록 조회(Read)

    1. HomePage메모의 유무, 메모의 개수라는 상태에 따라 다른 화면을 갱신해야 하므로 StatefulWidget으로 변경해줍니다.

      21번째 라인에 StatelessWidget을 클릭한 뒤 Quick Fix(Ctrl/Cmd + .)를 누른 뒤 Convert to StatefulWidget을 선택해주세요.

      Untitled

      아래와 같이 _HomePageState라는 상태 클래스가 추가되었습니다.

      Untitled

    2. 메모 목록을 가지고 있을 상태 변수 memoList를 추가해 줍니다.

      아래 코드스니펫을 복사해서 28번째 라인 맨 뒤에 붙여 넣어 주세요.

      • [코드스니펫] HomePage / memoList 상태 변수

        
              List<String> memoList = ['장보기 목록: 사과, 양파']; // 전체 메모 목록
        
        <aside>
        💡 `memoList`는 `String`만 받는 배열로 선언하였습니다.
        미리 `memoList`에 `'장보기 목록: 사과, 양파'` 라는 항목을 하나 넣어 두었습니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/46fc74b3-4b74-48d5-8eb8-f4530e9f546d/Untitled.png)
    
    3. `memoList`에 항목이 있을 때 `body`에 해당 항목이 보이도록 해봅시다.
    
        코드스니펫을 복사해서 37번째 라인을 교체해 주세요.
    
        - **[코드스니펫] HomePage / memoList 조건문**
    
            ```dart
            body: memoList.isEmpty
                      ? Center(child: Text("메모를 작성해 주세요"))
                      : Center(child: Text('메모가 존재합니다!')),
            ```
    
    
        <aside>
        💡 `조건 ? true : false` 형태의 조건문을 사용하여 조건에 따라 위젯을 다르게 보여줄 수 있습니다. (이를 “삼항 연산자” 라고 합니다.)
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a52ec0e1-e3de-4ccb-affe-afc086f9a0c3/Untitled.png)
    
        미리 `memoList`에 `'장보기 목록: 사과, 양파’`라는 항목을 하나 넣어 두었기 때문에 `memoList.isEmpty`는 `false`가 되고, `메모가 존재합니다!` 라는 문구가 에뮬레이터에 나오는 것을 보실 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/204c4470-92f2-4ebf-821b-6b5601a53f13/Untitled.png)
    
    4. `memoList`가 존재하는 경우, `ListView`를 이용해 보여주도록 코드스니펫을 복사해서 39번째 라인을 교체해 주세요.
        - **[코드스니펫] HomePage / ListView**
    
            ```dart
            : ListView.builder(
                          itemCount: memoList.length, // memoList 개수 만큼 보여주기
                          itemBuilder: (context, index) {
                            String memo = memoList[index]; // index에 해당하는 memo 가져오기
                            return ListTile(
                              // 메모 고정 아이콘
                              leading: IconButton(
                                icon: Icon(CupertinoIcons.pin),
                                onPressed: () {
                                  print('$memo : pin 클릭 됨');
                                },
                              ),
                              // 메모 내용 (최대 3줄까지만 보여주도록)
                              title: Text(
                                memo,
                                maxLines: 3,
                                overflow: TextOverflow.ellipsis,
                              ),
                              onTap: () {
                                // 아이템 클릭시
                                print('$memo : 클릭 됨');
                              },
                            );
                          },
                        ),
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aa6d810c-cacf-42a1-8ec9-e3f71fd0c928/Untitled.png)
    
        <aside>
        💡 `ListTile` 위젯을 활용하면 박스 안에 여러 영역에 다른 위젯을 손쉽게 배치할 수 있습니다.
    
        ![ListTile.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2a86c971-bcf5-4882-a07f-37a674357946/ListTile.png)
    
        좀 더 상세한 설명은 아래 공식 문서를 참고해 주세요.
    
        - **[[코드스니펫] ListTile 공식문서 URL](https://api.flutter.dev/flutter/material/ListTile-class.html)**
    
            ```dart
            https://api.flutter.dev/flutter/material/ListTile-class.html
            ```
    
        </aside>
    
        저장해주시면 에뮬레이터에 메모가 추가된 것을 볼 수 있습니다.
    
        ![simulator_screenshot_5FAD2CD3-E9AC-4DCD-B347-58A12C2F36AB.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e67a7ca-f59f-4be8-b089-a5e4c0042c09/simulator_screenshot_5FAD2CD3-E9AC-4DCD-B347-58A12C2F36AB.png)
    
        <aside>
        💡 클릭시 print문은 `view` → `Debug Console`을 선택하셔서 보실 수 있습니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d7d99dcc-642e-4282-80df-8b8331584a01/Untitled.png)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23627feb-d8e8-43e1-89b1-e1c313210f5c/Untitled.png)
    
    5. `memoList` 상태 변수에 `, '새 메모'` 를 추가해 봅시다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e886bb08-2a5d-4f4d-b75d-0d6591ac1217/Untitled.png)
    
        <aside>
        💡 저장을 해도 에뮬레이터에 반영이 안되실 겁니다.
        아직 우리는 변경된 상태에 따라 화면을 새로고침 해주는 코드를 작성하지 않았습니다.
        따라서 `Restart` 를 해야 앱에 반영됩니다.
    
        </aside>
    
        우측 상단에 `Restart` 버튼을 눌러주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fed3dbef-6b58-4dfd-a519-419a3db55550/Untitled.png)
    
        앱에 새로 추가된 메모가 보이는 것을 보실 수 있습니다.
    
        ![Screen Shot 2022-09-07 at 11.02.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/31a55655-af59-48d9-9dde-f5ca2027ead9/Screen_Shot_2022-09-07_at_11.02.35_PM.png)
    
        여기까지 메모 목록 조회 기능 완성!
    • 3) 메모 개별 조회 (Read)

    1. 메모 목록에서 ListTile 을 눌렀을 때 DetailPage 로 이동하도록 합니다.

      아래 코드스니펫을 복사해서 59번째 라인을 지우고 붙여넣어주세요.

      • [코드스니펫] ListTile onTap / DetailPage 로 이동

          Navigator.push(
                                context,
                                MaterialPageRoute(builder: (_) => DetailPage()),
                              );
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/61e61367-b85b-4169-8c80-811d65988e2f/Untitled.png)
    
        <aside>
        💡 메모 목록의 메모 하나를 누르면 DetailPage 로 잘 넘어가는 것을 확인할 수 있습니다.
    
        </aside>
    
    2. 이제 `DetailPage` 에서 해당 메모 내용을 볼 수 있도록 해야겠죠!
    
        `memoList` 와 해당 메모의 `index` 를 `DetailPage` 로 넘겨주겠습니다. 
    
        (`memoList[index]` 와 같은 식으로 개별 메모의 내용을 불러오기 위함입니다)
    
        우선 `DetailPage` 가 메모의 내용을 변수에 담아 받도록 해야합니다. 아래 코드스니펫을 복사해서 83번째 라인 뒤에 붙여넣어주세요.
    
        - **[코드스니펫] ListTile onTap / DetailPage 로 이동**
    
            ```dart
    
              final List<String> memoList;
              final int index;
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89a8640a-f80b-4bd4-bdf8-4d77a5fa949a/Untitled.png)
    
        83번째 라인에 빨간 밑줄이 그어졌네요! 이는 우리가 선언한 `memoList` 와 `index` 라는 변수에 아무것도 담지 않아 생긴 일입니다. 
    
        <aside>
        💡 빨간 밑줄이 그어진 코드에 마우스를 올려보면 보다 자세한 에러 메시지를 확인할 수 있습니다
    
        </aside>
    
        ![Screen Shot 2022-09-12 at 4.11.53 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0930cb3c-6dda-4117-ae2f-28964cb38360/Screen_Shot_2022-09-12_at_4.11.53_PM.png)
    
        아래 사진처럼 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Add final field formal parameters` 를 클릭해줍니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/62fc6605-6198-4b1c-a7a4-fe5892871acf/Untitled.png)
    
        아래와 같이 생성자에 넘겨받는 변수를 추가해줌으로서 문제가 해결되었습니다!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e76e8c3-b1b1-448b-bc8a-8a68611f6026/Untitled.png)
    
        그러자 이번엔 `HomePage` 에서 에러가 발생했네요! (61번째 줄, 73번째 줄)
    
        <aside>
        💡 아래 사진처럼 VS Code 우측의 빨간 부분을 클릭해서 문제가 발생한 코드 줄을 빠르게 확인할 수 있습니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/03eae13b-dd60-4d91-9dea-74b5892b86e1/Untitled.png)
    
        61번째 줄에서 마우스를 에러가 난 부분 위에 올려보면, `memoList` 와 `index` 변수를 받아야 하는데 넘겨주지 않아서 생긴 문제라는 것을 확인할 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/36c0f8f5-8e35-4196-880c-2a6cbfbbda46/Untitled.png)
    
        `Quick Fix(Ctrl/Cmd + .)`를 누른 뒤 `Add required argument ‘index’` 와 `Add required argument ‘memoList’` 를 클릭해줍니다.
    
        ![Screen Shot 2022-09-12 at 4.16.56 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9d970a5c-3522-41e0-a626-9633f19c70dd/Screen_Shot_2022-09-12_at_4.16.56_PM.png)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fe8b75c-4a48-4f56-b68a-b37fbcb31588/Untitled.png)
    
        index 뒤 `null` 이 들어간 자리에 `index` 라고 작성해 위에서 만든 `index` 변수를 대입해줍니다.
    
        memoList 뒤에도 마찬가지로 `memoList` 변수를 그대로 대입해줍니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fc0df8e-5ce3-495f-9a38-297be4098a8c/Untitled.png)
    
        `DetailPage` 괄호가 닫히는 부분(위 체크 표시) 에 쉼표를 찍어서 코드 정렬도 맞춰줍시다.
    
        ![Screen Shot 2022-09-12 at 4.24.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1026ebcc-e93a-4bb0-a14c-1d9fce463ec5/Screen_Shot_2022-09-12_at_4.24.05_PM.png)
    
        나머지 에러도 마저 해결해보겠습니다.
    
        아래 코드스니펫을 복사해 76번째~79번째 줄을 지우고 붙여넣어줍니다.
    
        - **[코드스니펫] 메모 생성 / floatingActionButton onPressed**
    
            ```dart
            String memo = ''; // 빈 메모 내용 추가
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (_) => DetailPage(
                            index: memoList.indexOf(memo),
                            memoList: memoList,
                          ),
                        ),
                      );
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b3199c26-e897-4f03-bd5f-1ad945c031a3/Untitled.png)
    
    3. 넘겨받은 변수에 담긴 내용을 화면에 표시해줍니다.
    
        `DetailPage` 에서 받은 `memoList` 의 `index` 번째에 있는 메모 내용을 화면에 띄워줘야겠죠! 
    
        아래 코드스니펫을 복사해 102번째 줄 맨 뒤에 붙여넣어줍니다.
    
        - **[코드스니펫] commentController 의 text 값 설정**
    
            ```dart
    
                contentController.text = memoList[index];
    
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7f66f71d-fa75-4aca-b5dd-3bd1a57c825e/Untitled.png)
    
        이제 `DetailPage` 에 해당 메모의 내용이 보이는 것을 확인하실 수 있습니다!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1d60e22-5141-4f8c-8aeb-d15d8e459ebd/Untitled.png)
    • 4) 메모 작성(Create)

    1. 우측 하단의 FloatingActionButton 을 클릭할 때마다 앞에서 만들어준 memoList 변수에 빈 메모를 추가 해주겠습니다.

      지금은 해당 버튼을 누를 때 아래와 같은 에러가 발생할텐데요.

      Screen Shot 2022-09-12 at 4.34.50 PM.png

      이는 우리가 새로운 메모를 memoList 에 추가하지 않았기 때문에 memoList.indexOf(memo)-1 을 반환해서 생기는 문제입니다.

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

      • [코드스니펫] memoList 에 memo 추가

        
                    memoList.add(memo);
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d4e62b4f-5996-415b-a2d5-42cd96a3b83b/Untitled.png)
    
        이제 해당 버튼을 누를 때마다 `memoList` 에 빈 메모가 추가되고, `DetailPage` 로 이동하겠죠!
    
        <aside>
        💡 생성한 메모의 index 를 `indexOf` 를 통해 찾아줍니다. 
        `memoList.length - 1` 로 작성해도 같은 결과가 나옵니다
        (add 는 배열의 맨 뒤에 요소를 추가하므로)
    
        </aside>
    
    2. 화면 상에 잘 나타나는지 확인해봅시다. 
    
        우측 하단의 `FloatingActionButton` 을 누르면 내용이 빈 메모의 `DetailPage`로 잘 넘어갑니다. 
    
        다시 `HomePage` 로 돌아와 보세요.
    
        ![Screen Shot 2022-09-08 at 2.39.59 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/77174237-2dff-4d4c-beb1-9537d703ac33/Screen_Shot_2022-09-08_at_2.39.59_AM.png)
    
        메모가 새로 생성이 되지 않는군요.
    
        `memoList` 에는 변화가 생겼지만, 화면을 새로 그려주지 않아서 생기는 문제입니다. 
        (`Hot Reload` 를 한 번 실행해주면 새로 생성된 메모가 보일거에요)
    
        <aside>
        💡 이럴 때는 `setState` 를 활용해 변화한 상태(변수)에 맞게 화면을 새로 그려주면 됩니다.
    
        </aside>
    
        아래 코드스니펫을 복사해 77번째 줄을 지우고 붙여넣어주세요. `memoList` 에 `memo` 를 추가하는 코드를 `setState` 로 감싸줬습니다.
    
        - **[코드스니펫] setState**
    
            ```dart
            setState(() {
                        memoList.add(memo);
                      });
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4dce72fc-434e-4d94-8bc1-23332bd18ddb/Untitled.png)
    
        이제 정상적으로 추가된 빈 메모가 화면에 나타나는 것을 볼 수 있습니다.
    • 5) 메모 수정 (Update)

    1. DetailPage 에 있는 TextFieldonChange 에 들어가는 함수를 수정하면, 텍스트가 변할 때마다 특정한 동작을 할 수 있습니다.

      먼저, 우측 하단의 FloatingActionButton을 클릭하여 DetailPage로 이동해 주세요.

      Untitled

      • iOS 에뮬레이터에서 키보드를 보이게 하는 방법

        Untitled

        simulator_screenshot_7A27686D-DC81-4E6A-A974-50A4A89576A0.png

        133번째 줄에 `print(value)` 를 입력하고 `TextField` 의 값을 변경해보면
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c20d3969-7f49-452a-8b40-2e683b853962/Untitled.png)
    
        `TextField` 에 내용을 적는대로 콘솔에 프린트 되는 것을 볼 수 있습니다. 이제 `memoList` 에 해당 내용을 추가해주면 되겠네요! 
    
        아래 코드스니펫을 복사해 133번째 줄을 지우고 붙여넣어줍니다.
    
        - **[코드스니펫] TextField onChanged /  memoList[index] 의 값 설정**
    
            ```dart
            memoList[index] = value;
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7b32a817-4ae7-49c8-85f4-d0aa474152c5/Untitled.png)
    
        자 그럼 메모를 작성하고 돌아와볼까요? `HomePage` 에 변화가 생겼는지 확인해봅시다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12182fc3-bc81-49f3-a1c8-f8cf8e98b266/Untitled.png)
    
        메모 내용이 변경되지 않는군요! 
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cce61e92-1a2c-4056-b048-8b36d97e7d3c/Untitled.png)
    
        `Hot Reload` 를 실행해야 정상적으로 변경된 메모 내용이 뜨는 것을 확인할 수 있습니다. 이는 HomePage 의 화면이 갱신되지 않았다는 것인데요.
    
        <aside>
        💡 기존에 우리는 `StatefulWidget` 의 `setState` 를 활용해 **해당 위젯의 화면**을 갱신할 수 있었습니다. 그러나 위 경우는 `DetailPage` 에서 발생한 변화에 따라 `HomePage` 의 화면을 갱신해줘야 합니다.
    
        </aside>
    
        <aside>
        💡 `HomePage` 의 `setState` 함수를 통으로 `DetailPage` 로 넘겨주는 방법도 있지만, 보다 편리하게 코드를 작성하기 위해서는 **상태 관리(State Management)** 라는 것을 사용하게 됩니다.
    
        우선 메모 삭제를 구현한 후, 아래에서 이에 대해 보다 자세히 다뤄보겠습니다.
    
        </aside>
    • 6) 메모 삭제 (Delete)

    1. 삭제 버튼 클릭 이벤트는 112번째 줄, DetailPage 의 우상단에 있는 IconButton 위젯의 onPressed의 함수로 받을 수 있습니다.

      Untitled

      먼저 삭제 버튼을 누르는 경우 Dialog를 띄워보도록 하겠습니다.

      코드스니펫을 복사해 113번째 라인 맨 뒤에 붙여넣어주세요.

      • [코드스니펫] DetailPage / showDialog

        
                        showDialog(
                          context: context,
                          builder: (context) {
                            return AlertDialog(
                              title: Text("정말로 삭제하시겠습니까?"),
                            );
                          },
                        );
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f0333d17-7044-46c2-88c2-6db40c8e8496/Untitled.png)
    
        삭제 버튼을 눌러서 `Dialog`를 확인해보세요
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f84a2a93-7d23-4b9e-80d0-6463fdf86384/Untitled.png)
    
        <aside>
        💡 Dialog 배경을 클릭하여 끌 수 있습니다.
    
        </aside>
    
    2. `AlertDialog`에 `취소`와 `확인` 버튼을 추가해 보도록 하겠습니다.
    
        코드스니펫을 복사해 118번째 라인 뒤에 붙여 넣어주세요.
    
        - **[코드스니펫] DetailPage / Dialog actions**
    
            ```dart
    
                                actions: [
                                  // 취소 버튼
                                  TextButton(
                                    onPressed: () {
                                      Navigator.pop(context);
                                    },
                                    child: Text("취소"),
                                  ),
                                  // 확인 버튼
                                  TextButton(
                                    onPressed: () {
                                      Navigator.pop(context);
                                    },
                                    child: Text(
                                      "확인",
                                      style: TextStyle(color: Colors.pink),
                                    ),
                                  ),
                                ],
            ```
    
    
        <aside>
        💡 `AlertDialog`의 `actions`는 배열로 원하는 위젯을 넣을 수 있는 파라미터 입니다.
    
        </aside>
    
        <aside>
        💡 버튼을 클릭하는 경우 `Navigator.pop(context);`를 호출하여 `Dialog`를 종료할 수 있습니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/661d9525-c0d1-49f7-a577-a1d7a6d16029/Untitled.png)
    
        ![Screen Shot 2022-09-12 at 6.31.28 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/da8304ff-d203-404c-8f18-11931aec5805/Screen_Shot_2022-09-12_at_6.31.28_PM.png)
    
    3. `확인` 버튼 클릭 시 `MemoList`에서 항목을 삭제해 보도록 하겠습니다.
    
        코드스니펫을 복사해 130번째 줄을 지우고 붙여넣어주세요.
    
        - **[코드스니펫] HomePage / delete memo**
    
            ```dart
            memoList.removeAt(index); // index에 해당하는 항목 삭제
                                      Navigator.pop(context); // 팝업 닫기
                                      Navigator.pop(context); // HomePage 로 가기
            ```
    
    
        <aside>
        💡 `List.removeAt(index)`를 이용하면 배열에서 index에 해당하는 항목을 삭제할 수 있습니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ef4c648c-8295-44ae-9585-dd7787622b4f/Untitled.png)
    
    4. 저장 후 다이얼로그를 다시 띄우고 `확인` 버튼을 누르면..
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9da3d5a0-cc5c-49bb-af18-ee65ee35478a/Untitled.png)
    
        이번에도 “메모 수정” 때와 마찬가지로, Hot Reload 를 활용해 화면을 새로고침 해줘야 변경사항이 반영되는 것을 확인하실 수 있습니다. 
    
        <aside>
        💡 위와 같은 상황에서는 `DetailPage` 에서 발생한 변화에 따라 `HomePage` 의 화면을 갱신해줘야 합니다.
    
        서로 다른 페이지에서 변수를 공유하고, 변수의 값을 바꾸며 여러 페이지의 화면을 한꺼번에 갱신해주기 위해 **상태 관리(State Management)** 라는 것을 배우고 적용해보겠습니다.
    
        </aside>
    • main.dart 완성 코드

        import 'package:flutter/cupertino.dart';
        import 'package:flutter/material.dart';
      
        void main() {
          runApp(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<HomePage> createState() => _HomePageState();
        }
      
        class _HomePageState extends State<HomePage> {
          List<String> memoList = ['장보기 목록: 사과, 양파', '새 메모']; // 전체 메모 목록
      
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: Text("mymemo"),
              ),
              body: memoList.isEmpty
                  ? Center(child: Text("메모를 작성해 주세요"))
                  : ListView.builder(
                      itemCount: memoList.length, // memoList 개수 만큼 보여주기
                      itemBuilder: (context, index) {
                        String memo = memoList[index]; // index에 해당하는 memo 가져오기
                        return ListTile(
                          // 메모 고정 아이콘
                          leading: IconButton(
                            icon: Icon(CupertinoIcons.pin),
                            onPressed: () {
                              print('$memo : pin 클릭 됨');
                            },
                          ),
                          // 메모 내용 (최대 3줄까지만 보여주도록)
                          title: Text(
                            memo,
                            maxLines: 3,
                            overflow: TextOverflow.ellipsis,
                          ),
                          onTap: () {
                            // 아이템 클릭시
                            Navigator.push(
                              context,
                              MaterialPageRoute(
                                builder: (_) => DetailPage(
                                  index: index,
                                  memoList: memoList,
                                ),
                              ),
                            );
                          },
                        );
                      },
                    ),
              floatingActionButton: FloatingActionButton(
                child: Icon(Icons.add),
                onPressed: () {
                  // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
                  String memo = ''; // 빈 메모 내용 추가
                  setState(() {
                    memoList.add(memo);
                  });
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (_) => DetailPage(
                        index: memoList.indexOf(memo),
                        memoList: memoList,
                      ),
                    ),
                  );
                },
              ),
            );
          }
        }
      
        // 메모 생성 및 수정 페이지
        class DetailPage extends StatelessWidget {
          DetailPage({super.key, required this.memoList, required this.index});
      
          final List<String> memoList;
          final int index;
      
          TextEditingController contentController = TextEditingController();
      
          @override
          Widget build(BuildContext context) {
            contentController.text = memoList[index];
      
            return Scaffold(
              appBar: AppBar(
                actions: [
                  IconButton(
                    onPressed: () {
                      // 삭제 버튼 클릭시
                      showDialog(
                        context: context,
                        builder: (context) {
                          return AlertDialog(
                            title: Text("정말로 삭제하시겠습니까?"),
                            actions: [
                              // 취소 버튼
                              TextButton(
                                onPressed: () {
                                  Navigator.pop(context);
                                },
                                child: Text("취소"),
                              ),
                              // 확인 버튼
                              TextButton(
                                onPressed: () {
                                  memoList.removeAt(index); // index에 해당하는 항목 삭제
                                  Navigator.pop(context); // 팝업 닫기
                                  Navigator.pop(context); // HomePage 로 가기
                                },
                                child: Text(
                                  "확인",
                                  style: TextStyle(color: Colors.pink),
                                ),
                              ),
                            ],
                          );
                        },
                      );
                    },
                    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) {
                    // 텍스트필드 안의 값이 변할 때
                    memoList[index] = value;
                  },
                ),
              ),
            );
          }
        }

    03. 상태 관리 패키지 Provider 준비하기

    • 상태 관리(State Management)의 필요성

      Untitled

    • 1) Provider 패키지 추가

      1. 코드스니펫을 복사해서 새 탭에서 열어주세요.

      2. Provider 패키지 Install 탭이 아래와 같이 나오면, flutter pub add provider 명령어 옆에 아이콘을 눌러 복사해 주세요.

        Untitled

      3. ViewTerminal을 열어 주세요.

        Untitled

        아래와 같이 복사한 코드를 붙여넣고 엔터를 눌러주세요.

        Untitled

        그러면 아래와 같이 실행이 됩니다.

        Untitled

      4. pubspec.yaml 파일에 39번째 라인에 아래와 같이 Provider가 있으면 정상 설치 완료입니다.

        Untitled

    • 2) MemoService 만들기

      1. lib 폴더에서 우클릭을 해주신 뒤 New File을 선택해주세요.

        Untitled

      2. 파일 이름은 memo_service.dart로 짓도록 하겠습니다.

        Untitled

      3. 코드스니펫을 복사해 memo_service.dart파일에 붙여 넣어주세요.

        • [코드스니펫] MemoService / init

            import 'package:flutter/material.dart';
          
            import 'main.dart';
          
            // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
            class Memo {
              Memo({
                required this.content,
              });
          
              String content;
            }
          
            // Memo 데이터는 모두 여기서 관리
            class MemoService extends ChangeNotifier {
              List<Memo> memoList = [
                Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
                Memo(content: '새 메모'), // 더미(dummy) 데이터
              ];
            }
        <aside>
        💡 **MemoService**의 `memoList`가 변경되는 경우 해당 값을 보여주는 화면들을 갱신시켜 주는 기능을 구현하기 위해 `ChangeNotifier` 클래스의 기능을 물려 받았습니다.
    
        `ChangeNotifier`를 상속 받은 경우 `notifylisteners();`를 호출하여 위젯들을 갱신하는 기능을 사용할 수 있습니다. 자세한 사용법은 뒤쪽에서 알아보도록 하겠습니다.
    
        </aside>
    
        <aside>
        💡 앞으로 `Memo` 관련 데이터는 모두 **MemoService** 에서 담당할 예정이므로, 이전에 HomePage에서 구현했던 `memoList` 배열을 **MemoService** 에 추가했습니다.
    
        17, 18번째 `Memo`는 바로 테스트를 진행할 수 있도록 넣은 가짜(dummy) 데이터입니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/831f2d05-82e5-4307-a4ff-095cf682151a/Untitled.png)
    • 3) Provider 패키지 설정

      1. 코드스니펫을 복사해 main.dart 파일의 5번째 라인을 지우고 붙여넣어주세요.

        • [코드스니펫] Provider init

            runApp(
                MultiProvider(
                  providers: [
                    ChangeNotifierProvider(create: (context) => MemoService()),
                  ],
                  child: const MyApp(),
                ),
              );
        <aside>
        💡 `MultiProvider`로 `MyApp`을 감싸서 모든 위젯들의 최상단에 `provider`들을 등록해줍니다. `MultiProvider`는 위젯트리 꼭대기에 여러 `Service` 들을 등록할 수 있도록 만들 때 사용합니다.
    
        </aside>
    
        <aside>
        💡 **MemoService**는 `memoList` 값이 변하는 경우 `HomePage`에 변경사항을 알려주도록 구현해야 하므로 `ChangeNotifierProvider`로 **Provider**에 등록해줍니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/33619a81-8d46-402f-ac51-c5fe41edc020/Untitled.png)
    
    2. **Provider**와 **MemoService**가 `Import` 되지 않아 에러가 발생합니다.
    
        6번째 라인에 `MultiProvider`를 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 눌르고 `Import 'package:provider/provider.dart';`를 선택해주세요.
    
        <aside>
        💡 만약 `import 'package:provider/provider.dart';`가 안보이신다면 `pubspec.yaml` 파일을 열고 저장 단축키를 눌러주세요.
    
        저장 단축키
        window : `Ctrl + S`
        macOS : `Cmd + S`
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8580c991-b81d-4001-90d3-7e21e45d081e/Untitled.png)
    
    3. 마찬가지로 9번째 라인 **MemoService**를 클릭하신 뒤 `import 'memo_service.dart';`를 선택해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0135ea61-101f-4f4d-8de6-305f851db553/Untitled.png)
    
        **Provider** 설정이 완료 되었습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b44f89cb-ce78-4833-b8f8-cb8ee4ef104f/Untitled.png)
    
    4. `Restart`를 클릭해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c26d3381-5b05-433e-8429-a25e46733820/Untitled.png)
    
        이제 `Provider` 를 사용할 준비가 되었습니다!

    04. 마이메모 앱에 Provider 적용하기

    • 1) 메모 목록 조회(Read)

    1. HomePage 전체 위젯을 Consumer로 감싸주겠습니다. main.dart 파일에 43번째 라인에 Scaffold를 우클릭하여 Refactor(Ctrl + Shift + R)을 선택하고, Wrap with Builder를 선택해주세요.

      Untitled

      아래와 같이 Scaffold 위젯을 Builder 위젯이 감싸집니다.

      Consumer 위젯과 형식이 비슷한 Builder 를 사용해 형식을 잡아줬습니다.

      Untitled

    2. 아래 이미지와 같이 43번째 라인에 BuilderConsumer<MemoService>로 변경해주세요.

      Untitled

    3. 에러를 해결하겠습니다.

      아래 이미지와 같이 ,memoService, child 를 43번째 라인의 context 뒤에 추가해주세요.

      Untitled

      그리고 줄 정렬을 예쁘게 해주기 위해 103번째 라인의 중괄호와 소괄호 사이에 콤마(,)를 찍은 뒤 저장해주세요.

      Untitled

      저장하면 코드가 정렬됩니다.

    4. 코드스니펫을 복사해 44번째 라인 뒤에 붙여넣어 주세요.

      • [코드스니펫] MemoService에서 memoList 가져오기

        
                  // memoService로 부터 memoList 가져오기
                  List<Memo> memoList = memoService.memoList;
        
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f52d5fa9-d3bf-4749-97c2-84112f9b6c87/Untitled.png)
    
    5. `Provider` 를 사용해 모든 `Memo` 데이터는 `MemoService` 에서 관리하기로 했습니다. 
    
        39번째 줄에 있는 `memoList` 는 더이상 사용하지 않으므로 지워줍니다.
    
        지우고 난 후 코드는 아래와 같습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a4375405-ca68-4ff3-95cd-40ff0df47381/Untitled.png)
    
    6. 에러들을 해결하겠습니다.
    
        <aside>
        💡 에러를 모두 해결하고 나면 자연스럽게 **메모 목록 조회(Read)** 기능이 구현되어 있을 겁니다!
    
        </aside>
    
        이전에는 `String` 자료형의 메모 데이터를 사용했다면, 이번엔 `memo_service.dart` 에서 `Memo` 라는 클래스로 데이터의 틀을 잡고, 각각의 메모 데이터를 객체의 형태로 만들어 활용한다는 점을 유의해주세요.
    
        이렇게 데이터의 형식(자료형)이 바뀌었기 때문에 코드상에 에러가 발생합니다. 55번째 줄의 에러 위에 마우스를 올려봅시다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ff555c78-1f86-42c0-a36b-f27a37fc52ee/Untitled.png)
    
        `String` 자료형의 변수인 `memo` 에 `Memo` 자료형의 값이 들어갈 수 없다는 뜻이군요!
    
        Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `‘Change ‘String’ to ‘Memo’ type annotation` 을 눌러 변수의 타입(자료형)을 바꿔줍니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d556b38b-9f54-4db7-ae9f-7556f474eba9/Untitled.png)
    
        코드가 아래와 같이 바뀌면서 에러가 사라지는 것을 볼 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5978d022-8b8e-4df5-81d2-81cd5152713a/Untitled.png)
    
        66번째 줄의 `memo` 를 `memo.content` 로 바꿔줍니다.
    
        <aside>
        💡 `Text` 위젯은 `String` 자료형의 값을 받아서 보여주는 위젯입니다. 메모의 내용인 `content`를 넘겨줘야겠죠.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b2e67e54-d29e-44e2-b7a1-c343738c3f4b/Untitled.png)
    
        72-80 번째 줄을 지워줍니다. **메모 개별 조회 (Read)** 기능은 아래에서 다시 구현하겠습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6b4bba1-52df-44af-81f8-cefb933930d5/Untitled.png)
    
        80-92 번째 줄을 지워줍니다. **메모 생성 (Create)** 기능도 아래에서 새로 구현하겠습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ecce9a27-36f2-4061-b729-56fb45871976/Untitled.png)
    
    7. `Restart`를 클릭해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c26d3381-5b05-433e-8429-a25e46733820/Untitled.png)
    
        지금 화면에 보이는 메모 목록은 우리가 생성해준 더미 데이터들입니다. 
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0c74725c-5ac3-41d4-89a7-cb243aa72de8/Untitled.png)
    
        `Provider` 를 활용한 **메모 목록 조회 기능(Read)** 을 구현했습니다!
    • 2) 메모 개별 조회 (Read)

      1. 이제 메모 데이터를 MemoService 에서 관리하니, DetailPagememoList 를 따로 가지고 있을 필요가 없겠죠.

        아래 코드스니펫을 복사해 main.dart 90-92번째 라인을 모두 지우고 붙여넣습니다.

        • [코드스니펫] DetailPage / 파라미터

            DetailPage({super.key, required this.index});
          
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd990739-5a05-4fbe-bdf9-42dff52aabaf/Untitled.png)
    
    2. `memoService` 에서 `memoList` 를 가져와 `index` 번째에 있는 메모를 뽑아보겠습니다.
    
        코드스니펫을 복사해 `main.dart` 98번째 라인을 지우고 붙여넣어주세요. 
    
        - **[코드스니펫] DetailPage / context.read 로 memo 1개 조회**
    
            ```dart
            MemoService memoService = context.read<MemoService>();
                Memo memo = memoService.memoList[index];
    
                contentController.text = memo.content;
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/87501087-8236-4773-b4ac-87f54e46141b/Untitled.png)
    
        <aside>
        💡 `context.read<클래스명>();`를 이용하면 위젯 트리 상단에 있는 Provider로 등록한 클래스에 접근할 수 있습니다.
    
        변화에 따라 화면을 새로 그려줄 필요가 있을 때는 `Consumer` 를 사용하며, 화면을 새로고침할 필요없이 MemoService 의 변수나 함수만 이용하고자 한다면 `context.read<클래스명>()` 를 사용해도 됩니다.
    
        </aside>
    
    3. `main.dart` 125번째 라인, 157번째 라인에 있는 에러를 각각 없애보겠습니다. 
    
        아래에서 메모의 수정 및 삭제를 따로 구현할 것이기 때문에 지금은 코드를 모두 주석처리 해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b82a2e83-02c0-4217-bd24-6200214e89bd/Untitled.png)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/733b6f3d-b91d-42b2-b0b4-61e530c90434/Untitled.png)
    
    4. `ListTile` 을 클릭시 `DetailPage` 로 이동하도록 하겠습니다. 
    
        `main.dart` 의 71번째 라인 뒤에 아래 코드스니펫을 복사해 붙여넣어주세요.
    
        - **[코드스니펫] HomePage 에서 DetailPage 로 이동하는 Navigator**
    
            ```dart
    
                                    Navigator.push(
                                      context,
                                      MaterialPageRoute(
                                        builder: (_) => DetailPage(
                                          index: index,
                                        ),
                                      ),
                                    );
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aabfe72e-6353-4771-b545-54fd4db6ee19/Untitled.png)
    
        **메모 개별 조회(Read)** 기능이 구현되었습니다. `ListTile` 을 눌러 상세페이지로 이동해보세요!
    
        ![Screen Shot 2022-09-13 at 4.41.46 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/20af2c23-98bd-4290-ba7b-55f93ea7be28/Screen_Shot_2022-09-13_at_4.41.46_AM.png)
    • 3) 메모 작성 (Create)

      1. 먼저 코드스니펫을 복사해 MemoServiceMemo 를 추가하는 함수를 구현해봅시다. memo_service.dart 의 19번째 라인 맨 뒤에 붙여넣습니다.

        • [코드스니펫] MemoService / createMemo

          
              createMemo({required String content}) {
                Memo memo = Memo(content: content);
                memoList.add(memo);
              }
        <aside>
        💡 **MemoService**에 `memoList`에 대한 CRUD 기능을 담당하는 함수를 추가할 예정입니다. 그 중 Create를 담당하는 `createMemo` 함수를 생성하였습니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c584b852-baa2-4db3-84a2-cfb725acc092/Untitled.png)
    
        <aside>
        💡 `String` 자료형의 `content` 변수를 받아 `Memo`를 생성하고, 이를 `memoList` 맨 뒤에 추가하는 로직을 작성했습니다.
    
        </aside>
    
    2. `HomePage` 에서 `FloatingActionButton` 을 누를 때, 방금 만든 `createMemo` 함수를 호출해 내용이 비어있는 메모가 추가되도록 하겠습니다.
    
        아래 코드스니펫을 복사해 `main.dart` 의 87번째 라인 맨 뒤에 붙여넣습니다
    
        - **[코드스니펫] HomePage / createMemo 호출**
    
            ```dart
    
                          memoService.createMemo(content: '');
                          Navigator.push(
                            context,
                            MaterialPageRoute(
                              builder: (_) => DetailPage(
                                index: memoService.memoList.length - 1,
                              ),
                            ),
                          );
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b7306c0c-3194-4004-97c0-3f562b5b79cb/Untitled.png)
    
        <aside>
        💡 `memoList` 의 맨 뒤에 memo가 추가되었으므로, `memoList.length - 1` 의 인덱스로 해당 memo에 접근합니다.
    
        </aside>
    
    3. 저장한 뒤 에뮬레이터에서 `FloatingActionButton` 를 눌러 메모를 추가해봅시다.
    
        <aside>
        💡 메모를 추가했지만 `HomePage` 에는 아무런 변화가 없습니다.
        `Hot Reload` 를 해보면 비로소 메모가 추가되는 것을 확인할 수 있는데요. 이는 `memoList` 에 memo 가 추가되었지만 화면의 갱신이 일어나지 않았다는 것을 의미합니다.
    
        </aside>
    
    4. 이를 해결하기 위해 코드스니펫을 복사해 **MemoService**에 23번째 라인 맨 뒤에 붙여 넣어주세요.
        - **[코드스니펫] notifyListeners**
    
            ```dart
    
                notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/66f3a459-46a5-42d7-b186-247a49975361/Untitled.png)
    
        <aside>
        💡 **MemoService**에서 `notifyListeners();`를 호출해야 `Consumer<MemoService>`의 `builder` 에 있는 함수를 호출해 화면이 갱신됩니다.
    
        </aside>
    
        이제 에뮬레이터에서 `FloatingActionButton` 를 누를 때마다 `HomePage` 에 빈 메모가 추가되는 것을 확인할 수 있습니다.
    • 4) 메모 수정 (Update)

      1. 먼저 MemoService 에 메모를 수정하는 로직을 추가하겠습니다. 아래 코드스니펫을 복사해 memo_service.dart 의 25번째 라인 맨 뒤에 붙여넣습니다

        • [코드스니펫] MemoService / updateMemo

          
              updateMemo({required int index, required String content}) {
                Memo memo = memoList[index];
                memo.content = content;
                notifyListeners();
              }
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b88fd71-7b53-40d9-a126-b25730ecb281/Untitled.png)
    
        <aside>
        💡 `index` 와 `content` 변수를 받아 `memoList` 의 index 번째 memo 의 `content` 를 바꿔주는 로직을 작성했습니다.
        또, `notifyListeners()` 를 호출해 `Consumer<MemoService>` 의 `builder` 부분에 있는 위젯을 다시 그려줍니다.
    
        </aside>
    
    2. `DetailPage` 에서 `TextField` 에 텍스트를 입력할 때마다 메모 수정이 일어나도록 하겠습니다. 
    아래 코드스니펫을 복사해 `main.dart` 의 174번째 라인을 지우고 붙여넣습니다
        - **[코드스니펫] DetailPage / TextField onChange**
    
            ```dart
            memoService.updateMemo(index: index, content: value);
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1debffca-b607-4396-b4ca-4fc9af942580/Untitled.png)
    
        <aside>
        💡 `TextField` 의 값이 변할 때, 즉 타이핑할때마다 `memoService` 의 `updateMemo` 를 호출하도록 했습니다.
    
        </aside>
    
    3. `DetailPage` 에서 메모를 작성하고 `HomePage` 로 돌아오면 변경 내역이 잘 반영되어있는 것을 확인할 수 있습니다.
    • 5) 메모 삭제 (Delete)

      1. 먼저 MemoService 에 메모를 삭제하는 로직을 추가하겠습니다. 아래 코드스니펫을 복사해 memo_service.dart 의 31번째 라인 맨 뒤에 붙여넣습니다.

        • [코드스니펫] MemoService / deleteMemo

          
              deleteMemo({required int index}) {
                memoList.removeAt(index);
                notifyListeners();
              }
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/532b94be-5756-4b30-89ab-b22b4be58437/Untitled.png)
    
        <aside>
        💡 `index` 변수를 받아 `memoList` 의 index 번째 memo 를 삭제하는 로직을 작성했습니다.
        또, `notifyListeners()` 를 호출해 `Consumer<MemoService>` 의 `builder` 부분에 있는 위젯을 다시 그려줍니다.
    
        </aside>
    
    2. `DetailPage` 에서 삭제 버튼을 누르면 뜨는 `Dialog` 가 뜨고, 여기서 `확인`을 누르면 삭제가 일어나도록 해봅시다. 아래 코드스니펫을 복사해 `main.dart` 의 142번째 라인을 지우고 붙여넣습니다
        - **[코드스니펫] DetailPage / TextButton onPressed**
    
            ```dart
            memoService.deleteMemo(index: index);
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6ec414aa-b3a9-4cbb-97b9-14f85dbcca75/Untitled.png)
    
        <aside>
        💡 삭제 버튼을 누르면 뜨는 `Dialog` 가 뜨고, 여기서 `확인`을 누르면 삭제가 일어나며, 뒤로가기를 두번 (Dialog 닫기 → DetailPage 닫고 HomePage 로 이동) 실행합니다.
    
        </aside>
    
    3. `DetailPage` 에서 삭제 버튼을 눌러 구현이 잘 되었는지 확인합니다.
    • 6) Provider 사용법 정리

      • 전역적으로 사용되는 데이터를 담당할 서비스로 만들고, 해당 데이터에 대한 CRUD를 모두 해당 서비스에서 구현합니다.

          class MemoService extends ChangeNotifier {
        
            ...
        
            void deleteMemo({required int index}) {
              memoList.removeAt(index);
              notifyListeners(); // Consumer의 builder 함수를 호출하여 화면 갱신
            }
          }
      • Provider 패키지를 이용하여 최상단 위젯 서비스를 등록해 줍니다.

        Untitled

      • 위젯트리 꼭대기에 있는 Provider로 등록한 클래스에 접근 방법

      • Consumer<클래스명> : 클래스 정보 갱신시 함께 새로고침 할 때 사용

        Untitled

      • context.read<클래스명>() : 화면을 새로고침할 필요 없이, 일회성으로 서비스의 변수나 함수에 접근하고 싶은 경우에 사용

        Untitled

    • 7) 리팩토링

      1. showDeleteDialog 메소드(함수)로 분리하기

        126번째 줄에 showDialog를 클릭한 뒤 왼쪽에 전구(💡) 아이콘을 선택하고, Extract Method를 선택해 주세요.

        Untitled

        메소드(함수)의 이름을 입력하라고 뜨면 showDeleteDialog라고 입력한 뒤 엔터를 누르고 저장(Ctrl/Cmd + S)를 눌러주세요.

        Untitled

        그러면 아래와 같이 126번째 줄이 생성된 함수를 호출하도록 바뀝니다.

        Untitled

        그리고 153번째 라인으로 내려가보면 showDeleteDialog라는 함수가 생성되어 있는 것을 볼 수 있습니다.

        Untitled

      2. 153번째 줄을 보면 함수의 반환 타입이 Future<dynamic> 이라고 되어있는데, 이 부분을 void로 변경하고, 155번째 줄에 있는 return을 삭제해주세요.

        Untitled

      3. 저장한 뒤, 메모를 하나 삭제해보면 정상적으로 작동하는 것을 확인할 수 있습니다.

    • 최종 코드

      • main.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
          import 'package:provider/provider.dart';
        
          import 'memo_service.dart';
        
          void main() {
            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<HomePage> createState() => _HomePageState();
          }
        
          class _HomePageState extends State<HomePage> {
            @override
            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();
        
            @override
            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 'package:flutter/material.dart';
        
          import 'main.dart';
        
          // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
          class Memo {
            Memo({
              required this.content,
            });
        
            String content;
          }
        
          // Memo 데이터는 모두 여기서 관리
          class MemoService extends ChangeNotifier {
            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 부분을 호출해서 화면 새로고침
            }
        
            updateMemo({required int index, required String content}) {
              Memo memo = memoList[index];
              memo.content = content;
              notifyListeners();
            }
        
            deleteMemo({required int index}) {
              memoList.removeAt(index);
              notifyListeners();
            }
          }

    05. shared_preferences 를 이용해 메모 데이터 기기에 저장하기

    • shared_preferences 패키지를 사용하는 이유

    • 1) shared_preferences 패키지 설치

      1. 패키지를 설치해 보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.

        `flutter pub add shared_preferences` 우측에 아이콘을 눌러 복사해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c859621-73d6-42cb-86eb-99d54bfeac18/Untitled.png)
    
    2. `View` → `Terminal`을 열고 복사한 내용을 붙여 넣은 뒤 엔터를 눌러 실행해 주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9ad5c331-063e-4fce-81d8-0148093ce7b1/Untitled.png)
    
        `pubspec.yaml`파일을 열어서 40번째 라인에 `shared_preferences`를 확인해 주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4342608-4cf6-4955-a4c6-c7dd417fc167/Untitled.png)
    
    3. 패키지 설치가 끝나셨다면 우측 상단의 정지 버튼을 눌러 앱을 끈 후
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed82c5f1-f39f-48a9-a11f-5ea54ca69fa5/Untitled.png)
    
        `Run without debugging` 을 눌러 앱을 다시 설치해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c40bd256-eaf2-48ff-ba16-2f51c5da6701/Untitled.png)
    
        앱이 재설치가 된 후에 `shared_preferences` 를 사용할 수 있습니다.
    • 2) shared_preferences 사용법

      1. 사용 준비
        SharedPreferences를 이용하려면 먼저 SharedPreferences 인스턴스를 가져와야합니다.

         // 인스턴스 생성
         SharedPreferences prefs = await SharedPreferences.getInstance();
      2. 값 저장하기
        SharedPreferences는 데이터를 Key와 Value로 구성된 Map 형태로 데이터를 저장합니다. 저장시 사용되는 Key원하는 이름으로 정하시면 됩니다.

         prefs.setString("username", "John Doe");
      3. 값 불러오기
        저장할 때 사용한 username 이라는 Key를 이용해 다시 값을 가져올 수 있습니다. 저장된 값이 없는 경우 null을 반환하는 점 유의해 주세요.

         String? value = prefs.getString("username"); 
  • 3) 기기에 memoList 데이터 저장하기

    1. 먼저 전역으로 prefs라는 변수를 선언해 코드 어디서든 SharedPreferences 객체를 사용할 수 있도록 합니다. 아래 코드스니펫을 복사해 main.dart 의 7번째 줄을 지우고 붙여넣어주세요.

      • [코드스니펫] 전역 변수 prefs 선언 / main 함수 수정

          late SharedPreferences prefs;
        
          void main() async {
            WidgetsFlutterBinding.ensureInitialized();
            prefs = await SharedPreferences.getInstance();
  •     ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/67734020-ec2c-41f2-990d-419ef1f0cc28/Untitled.png)
    
        7번째 라인에 `SharedPreferences`을 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library…` 를 선택해주세요. `shared_preferences` 패키지를 import 하지 않아 발생하는 문제입니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fb816b4-9425-4df6-828b-a8f9e60b5305/Untitled.png)
    
        아래와 같이 import 가 되면 에러가 해결됩니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a31272db-f17e-48b6-abc0-e2e027d5851b/Untitled.png)
    
    2. `Memo` 객체를 `Map` 자료형으로 바꿔주는 함수 **toJson**, `Map` 자료형에서 다시 `Memo` 객체를 복원하는 함수 **fromJson** 을 `Memo` 클래스 내에 작성해줍니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 11번째 라인 맨 마지막에 붙여넣어주세요.
        - **[코드스니펫] Memo toJson, fromJson**
    
            ```dart
    
              Map toJson() {
                return {'content': content};
              }
    
              factory Memo.fromJson(json) {
                return Memo(content: json['content']);
              }
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c8132838-be7a-42de-8395-f5e01ff2cbfc/Untitled.png)
    
    3. **MemoService** 에 `memoList` 를 기기에 저장하는 함수 **saveMemoList** 와, 저장된 `memoList` 를 다시 복원하는 함수 **loadMemoList** 를 추가합니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 44번째 라인 뒤에 붙여넣어주세요.
        - **[코드스니펫] saveMemoList / loadMemoList**
    
            ```dart
    
              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();
              }
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e500d5a7-5c03-45c9-a223-9dcaae341ef9/Untitled.png)
    
        50번째 라인에 에러가 발생한 부분에서 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library 'dart:convert'` 를 눌러 에러를 해결해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c9fe2dbe-b948-4bca-bc8c-edfbf1af9b8b/Untitled.png)
    
        <aside>
        💡 각 과정을 거치며 데이터가 어떻게 변하는지 주석으로 작성해뒀습니다. 대략적인 흐름은 아래와 같습니다.
    
        1) `saveMemoList` 
    
        `**List<Memo>**` ⇒ (toJson) ⇒  `**List<Map>**` ⇒ (jsonEncode) ⇒  **`String`**
    
        2) `loadMemoList`
    
        `**String**` ⇒ (jsonDecode) ⇒ `**List<Map>**` ⇒ (fromJson) ⇒ `**List<Memo>**`
    
        </aside>
    
    4. 메모를 생성, 변경, 삭제할 때마다 메모를 저장하도록 합니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 `MemoService` 클래스 함수들에 있는 `notifyListeners()` 뒤에 **모두** 붙여넣어줍니다.
        - **[코드스니펫] memoList 기기에 저장**
    
            ```dart
    
                saveMemoList();
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/49bda047-bee9-4cd3-9919-c6d3e7e98da2/Untitled.png)
    
    5. **MemoService** 를 시작할 때 저장된 메모를 불러와서 `memoList` 변수에 담아주도록 생성자를 수정해줍니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 25번째 줄 맨 뒤에 붙여넣어주세요.
        - **[코드스니펫] memoList 불러오기**
    
            ```dart
    
              MemoService() {
                loadMemoList();
              }
    
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d0034163-f49b-4ab4-9769-27c1c4a932ba/Untitled.png)
    
    6. 자 이제 메모를 생성하고, 우측 상단의 `Restart` 버튼을 눌러주세요. 앱을 재부팅하는 것과 동일한 동작입니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3804f24-5072-4989-9dae-d34b77c9b50d/Untitled.png)
    
        메모가 삭제되지 않고 잘 남아있는 것을 확인할 수 있습니다. 이는 메모 데이터가 `shared_preferences` 를 통해 기기에 저장되고, 불러와지기 때문입니다. 
    • 최종 코드

      • 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});
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
        
          // 홈 페이지
          class HomePage extends StatefulWidget {
            const HomePage({super.key});
        
            @override
            State<HomePage> createState() => _HomePageState();
          }
        
          class _HomePageState extends State<HomePage> {
            @override
            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();
        
            @override
            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();
            }
          }

    06. 숙제 - 마이메모 추가 기능 구현

    • 1) 메모 핀 기능 구현

    • 2) 숙제 답안

      • 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});
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
        
          // 홈 페이지
          class HomePage extends StatefulWidget {
            const HomePage({super.key});
        
            @override
            State<HomePage> createState() => _HomePageState();
          }
        
          class _HomePageState extends State<HomePage> {
            @override
            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(memo.isPinned
                                      ? CupertinoIcons.pin_fill
                                      : CupertinoIcons.pin),
                                  onPressed: () {
                                    memoService.updatePinMemo(index: index);
                                  },
                                ),
                                // 메모 내용 (최대 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();
        
            @override
            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,
              this.isPinned = false,
            });
        
            String content;
            bool isPinned;
        
            Map toJson() {
              return {
                'content': content,
                'isPinned': isPinned,
              };
            }
        
            factory Memo.fromJson(json) {
              return Memo(
                content: json['content'],
                isPinned: json['isPinned'] ?? false,
              );
            }
          }
        
          // 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();
            }
        
            updatePinMemo({required int index}) {
              Memo memo = memoList[index];
              memo.isPinned = !memo.isPinned;
              memoList = [
                ...memoList.where((element) => element.isPinned),
                ...memoList.where((element) => !element.isPinned)
              ];
              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) 구현 목표

  • 2) 숙제 답안

    • 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});
      
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: HomePage(),
            );
          }
        }
      
        // 홈 페이지
        class HomePage extends StatefulWidget {
          const HomePage({super.key});
      
          @override
          State<HomePage> createState() => _HomePageState();
        }
      
        class _HomePageState extends State<HomePage> {
          @override
          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(memo.isPinned
                                    ? CupertinoIcons.pin_fill
                                    : CupertinoIcons.pin),
                                onPressed: () {
                                  memoService.updatePinMemo(index: index);
                                },
                              ),
                              // 메모 내용 (최대 3줄까지만 보여주도록)
                              title: Text(
                                memo.content,
                                maxLines: 3,
                                overflow: TextOverflow.ellipsis,
                              ),
                              trailing: Text(memo.updatedAt == null
                                  ? ""
                                  : memo.updatedAt.toString().substring(0, 19)),
                              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();
      
          @override
          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,
            this.isPinned = false,
            this.updatedAt,
          });
      
          String content;
          bool isPinned;
          DateTime? updatedAt;
      
          Map toJson() {
            return {
              'content': content,
              'isPinned': isPinned,
              'updatedAt': updatedAt?.toIso8601String(),
            };
          }
      
          factory Memo.fromJson(json) {
            return Memo(
              content: json['content'],
              isPinned: json['isPinned'] ?? false,
              updatedAt:
                  json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt']),
            );
          }
        }
      
        // 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, updatedAt: DateTime.now());
            memoList.add(memo);
            notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
            saveMemoList();
          }
      
          updateMemo({required int index, required String content}) {
            Memo memo = memoList[index];
            memo.content = content;
            memo.updatedAt = DateTime.now();
            notifyListeners();
            saveMemoList();
          }
      
          updatePinMemo({required int index}) {
            Memo memo = memoList[index];
            memo.isPinned = !memo.isPinned;
            memoList = [
              ...memoList.where((element) => element.isPinned),
              ...memoList.where((element) => !element.isPinned)
            ];
            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();
          }
        }
  • 3) 빈 메모 삭제

    • 1) 구현 목표

  • 2) 숙제 답안

    • 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});
      
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: HomePage(),
            );
          }
        }
      
        // 홈 페이지
        class HomePage extends StatefulWidget {
          const HomePage({super.key});
      
          @override
          State<HomePage> createState() => _HomePageState();
        }
      
        class _HomePageState extends State<HomePage> {
          @override
          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(memo.isPinned
                                    ? CupertinoIcons.pin_fill
                                    : CupertinoIcons.pin),
                                onPressed: () {
                                  memoService.updatePinMemo(index: index);
                                },
                              ),
                              // 메모 내용 (최대 3줄까지만 보여주도록)
                              title: Text(
                                memo.content,
                                maxLines: 3,
                                overflow: TextOverflow.ellipsis,
                              ),
                              trailing: Text(memo.updatedAt == null
                                  ? ""
                                  : memo.updatedAt.toString().substring(0, 19)),
                              onTap: () async {
                                // 아이템 클릭시
                                await Navigator.push(
                                  context,
                                  MaterialPageRoute(
                                    builder: (_) => DetailPage(
                                      index: index,
                                    ),
                                  ),
                                );
                                if (memo.content.isEmpty) {
                                  memoService.deleteMemo(index: index);
                                }
                              },
                            );
                          },
                        ),
                  floatingActionButton: FloatingActionButton(
                    child: Icon(Icons.add),
                    onPressed: () async {
                      // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
                      memoService.createMemo(content: '');
                      await Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (_) => DetailPage(
                            index: memoService.memoList.length - 1,
                          ),
                        ),
                      );
                      if (memoList[memoService.memoList.length - 1].content.isEmpty) {
                        memoService.deleteMemo(index: memoList.length - 1);
                      }
                    },
                  ),
                );
              },
            );
          }
        }
      
        // 메모 생성 및 수정 페이지
        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<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,
            this.isPinned = false,
            this.updatedAt,
          });
      
          String content;
          bool isPinned;
          DateTime? updatedAt;
      
          Map toJson() {
            return {
              'content': content,
              'isPinned': isPinned,
              'updatedAt': updatedAt?.toIso8601String(),
            };
          }
      
          factory Memo.fromJson(json) {
            return Memo(
              content: json['content'],
              isPinned: json['isPinned'] ?? false,
              updatedAt:
                  json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt']),
            );
          }
        }
      
        // 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, updatedAt: DateTime.now());
            memoList.add(memo);
            notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
            saveMemoList();
          }
      
          updateMemo({required int index, required String content}) {
            Memo memo = memoList[index];
            memo.content = content;
            memo.updatedAt = DateTime.now();
            notifyListeners();
            saveMemoList();
          }
      
          updatePinMemo({required int index}) {
            Memo memo = memoList[index];
            memo.isPinned = !memo.isPinned;
            memoList = [
              ...memoList.where((element) => element.isPinned),
              ...memoList.where((element) => !element.isPinned)
            ];
            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주차 - 다양한 위젯을 활용해 화면 그리기

    • PDF 파일

      2주차-다양한_위젯을_활용해_화면_그리기.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 + /

    [수업 목표]

    • Flutter의 Widget 이해하기
    • 화면 그리는 위젯 이해하기
    • 당근마켓 만들기

    [목차]


    01. Flutter Widget 이해하기

    • 1) StatelessWidget

      1. StatelessWidget 생김새

        stateless (1).png

        • extends StatelessWidget : StatelessWidget의 기능을 물려받습니다.
        • 생성자 : 클래스 이름과 동일한 함수
        • build 함수 : 화면에 보여줄 자식 위젯을 반환
      2. 코드스니펫을 복사한 뒤 주소창에 붙여 넣어, DartPad로 접속하여 StatelessWidget을 배워봅시다.

        <aside>
        💡 **StatelessWidget**의 **생김새** 및 **실행 순서**만 집중해주세요!
    
        </aside>
    
        `Run` 버튼을 누르면 우측에 `hello Stateless Widget` 이라는 문구가 표시됩니다.
    
        ![Screen Shot 2022-09-01 at 12.37.09 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/42501e38-82f0-442a-867e-ec2ab58635f8/Screen_Shot_2022-09-01_at_12.37.09_AM.png)
    
        9번째 줄에 `MyApp` 클래스가 직접 만든 **StatelessWidget** 위젯입니다.
    
        ![Screen Shot 2022-09-01 at 12.41.14 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dcc19904-977a-45a2-bd13-285be1c8537b/Screen_Shot_2022-09-01_at_12.41.14_AM.png)
    
        참고: [Key 란 무엇인가](https://nsinc.tistory.com/214)
    
    3. 실행 순서
    
        좌측 하단에 `Console` 버튼을 클릭해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7e528288-600b-4e8e-a1b1-0a2ac6a0e812/Untitled.png)
    
        1. 처음에 3번째 줄 `main()` 함수가 호출되어 Console에 `시작`이 출력됩니다.
        2. MyApp 위젯이 첫 번째 위젯으로 등록되고 `build` 함수가 호출 되면서 Console에 `build 호출`이 출력됩니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2ec5bad2-6cf2-4a07-9f83-753e01ad67c7/Untitled.png)
    
    
        <aside>
        💡 화면에 보이는 첫 번째 위젯은 일반적으로 **MaterialApp** 또는 **CupertinoApp** 위젯으로 시작합니다.
    
        </aside>
    • 2) StatefulWidget

      1. StatefulWidget 생김새

        기본적으로 2개의 클래스로 구성되어 있습니다.

        stateful (2).png

        • MyApp : StatefulWidget의 기능을 물려받은 클래스

        • _MyAppState : MyApp 상태를 가진 클래스(실질적인 내용은 여기에 들어가요!)

          _stateful.png

        • 화면을 그리는 build 함수상태 클래스 (_MyAppState) 에 있습니다.

      2. 코드스니펫을 복사한 뒤 주소창에 붙여 넣어, DartPad로 접속하여 StatefulWidget을 배워봅시다.

        <aside>
        💡 **StatefulWidget**의 **생김새** 및 **실행 순서**만 집중해주세요!
    
        </aside>
    
        `Run` 버튼을 누르면 우측에 `1`이 표시됩니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4b296df2-cda6-495b-9f5f-fd5033a36e30/Untitled.png)
    
    3. 실행 순서
    
        좌측 하단에 Console 버튼을 클릭해 주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/71b1ba4f-f307-41f1-97ee-1793e5175058/Untitled.png)
    
        4번째 줄의 `main 함수`와 23번째 줄의 `build 함수`가 차례대로 실행되어 아래와 같이 출력됩니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1711ccdb-fc11-4c16-81fa-a5b289191ad3/Untitled.png)
    
        우측에 파란 버튼을 누르면 화면에는 숫자가 2로 변경된 것을 볼 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfc42de8-6de9-45f1-b283-5fd7290863fc/Untitled.png)
    
        Console을 보면 `클릭 됨` 과 `build 호출`이 추가되어 있습니다.
    
        <aside>
        💡 버튼 클릭시 실행 순서는 다음과 같습니다.
    
        1. 34번째 `print("클릭 됨");` 출력
        2. 38번째 number 1 증가
        3. 37번째 라인의 `setState`로 인해 화면 갱신 (= build 함수 호출)
        4. build 함수가 호출되어 24번째 `print("build 호출");` 출력
        </aside>
    
        <aside>
        💡 **StatefulWidget** 위젯에서 `setState()`를 호출하면 `build()` 함수가 다시 실행되면서 화면이 갱신됩니다.
    
        </aside>
    • 3) Navigation (화면 이동)

      • 다음 페이지로 이동하기

          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => SecondPage()), // 이동하려는 페이지
          );
      • 현재 화면 종료

          Navigator.pop(context); // 종료
      • 코드스니펫을 복사한 뒤 주소창에 붙여 넣으면, DartPad로 접속합니다.

        ![Routing.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0d93233f-58cb-448a-bb05-7b6f4aa6c438/Routing.png)
    
    
    <aside>
    💡 화면이 많아지는 경우, [Named Route](https://docs.flutter.dev/cookbook/navigation/named-routes) 방식을 사용하기도 합니다.
    
    </aside>

    02. 프로젝트 준비

    • 1) Flutter 프로젝트 생성하기

      1. Visual Studio Code(VSCode)를 실행해 주세요.

      2. ViewCommand Palette 버튼을 클릭해주세요.

        Untitled

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

        Untitled

      4. Application을 선택해주세요.

        Untitled

      5. 프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔 flutter 폴더를 선택한 뒤 Select a folder to create the project in 버튼을 눌러 주세요.

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

        Screen Shot 2022-09-04 at 11.17.06 PM.png

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

        Untitled

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

        Screen Shot 2022-09-05 at 7.27.13 PM.png

      8. 아래 main.dart 코드스니펫을 복사해서 기존 코드를 모두 지운 뒤, main.dart 파일에 붙여 넣고 저장해주세요.

        • [코드스니펫] main.dart

            import 'package:flutter/material.dart';
          
            void main() {
              runApp(MyApp());
            }
          
            class MyApp extends StatelessWidget {
              const MyApp({Key? key}) : super(key: key);
          
              @override
              Widget build(BuildContext context) {
                return MaterialApp(
                  debugShowCheckedModeBanner: false,
                  home: Scaffold(),
                );
              }
            }
    • 2) VSCode Dart 세팅

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

        Untitled

      2. 아래와 같이 dart recommend라고 검색한 뒤 Dart: Use Recommended Settings를 선택해 주세요. 그러면 자동으로 저장 시 자동 줄 정렬해 주는 기능과 같이 편의 기능 설정이 적용됩니다.

        Untitled

  • 다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.

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

    • [코드스니펫] analysis_options.yaml

      
            prefer_const_constructors: false
            prefer_const_literals_to_create_immutables: false
  •     ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
    
        <aside>
        💡 상세 내용은 아래를 참고해 주세요.
    
        - 어떤 의미인지 궁금하신 분들을 위해
    
            `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
    
            파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
    
            12번째 라인에 마우스를 올리면 아래와 같이 설명이 보입니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
    
            위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수(변하지 않는 수)로 선언하라는 힌트입니다. 상수로 만들면 화면을 새로고침 할 때, 상수로 선언된 위젯들은 새로고침을 할 필요가 없어서 성능상 이점이 있습니다.
    
            아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
    
            지금은 학습 단계이니 눈에 띄지 않도록 `analysis_options.yaml` 파일에 아래와 같이 설정하여 힌트를 숨기도록 하겠습니다.
    
            ```dart
                prefer_const_constructors: false
                prefer_const_literals_to_create_immutables: false
            ```
    
        </aside>
    • 3) 에뮬레이터 실행하기

      1. VSCode 우측 하단에 Chrome (web-javascript)를 클릭해주세요.

        Untitled

      2. Start Pixel 2 API 29 mobile emulator를 선택해주세요.

        Untitled

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

        Untitled

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

        Untitled

        에뮬레이터에 아래와 같이 흰 화면이 나오면 정상적으로 실행이 완료된 것입니다!

        [Android]

        Untitled

        [iOS]

        Screen Shot 2022-09-01 at 1.58.31 AM.png

        Untitled

    03. 당근마켓 화면 만들기

    • 최종 완성 이미지
    ![simulator_screenshot_4030D76F-D1E6-40EA-BE1A-761CA515E677.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d6ab797b-808d-4665-80ee-9b4f8035ae49/simulator_screenshot_4030D76F-D1E6-40EA-BE1A-761CA515E677.png)
    
    ![ezgif-4-9067a9c5c7.webp](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb313c25-f041-4f70-b4d9-7b91c8a17e61/ezgif-4-9067a9c5c7.webp)
    • 1) HomePage 만들기

      1. main.dart 파일 19번째 라인에 st라고 입력한 뒤, 자동 완성으로 추천되는 Flutter Stateless Widget 을 선택해주세요.

        Screen Shot 2022-09-01 at 1.52.19 AM.png

      2. 그러면 아래와 같이 StatelessWidget 클래스가 완성됩니다.

        Screen Shot 2022-09-04 at 11.37.27 PM.png

      3. HomePage라고 이름을 적어주세요. 그러면 클래스의 이름과 생성자(constructor)에 아래와 같이 작성이 됩니다.

        Screen Shot 2022-09-04 at 11.42.47 PM.png

      4. 아래 코드스니펫을 복사해서 24번째 라인을 지우고 붙여 넣어 저장해주세요.
        (window : ctrl + s / macOS : cmd + s)

        • [코드스니펫] HomePage Scaffold

            return Scaffold(
                body: Center(child: Text("home page")),
            );
        ![Screen Shot 2022-09-05 at 1.17.07 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ce61b0d3-b543-4139-8a5a-e7845b64bb6e/Screen_Shot_2022-09-05_at_1.17.07_AM.png)
    
    5. 앱 실행시 14번째 라인에 `home: Scaffold()`를 `home: HomePage()`로 변경한 뒤 저장해주세요.
    
        ![Screen Shot 2022-09-05 at 1.17.38 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b69d28e4-1c9d-4101-851b-2017934c9d62/Screen_Shot_2022-09-05_at_1.17.38_AM.png)
    
    6. 이제 에뮬레이터에 `HomePage`가 뜨는 것을 볼 수 있습니다.
    
        ![Screen Shot 2022-09-01 at 1.57.55 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d297b20a-6ca8-4a47-b590-5290a9f88617/Screen_Shot_2022-09-01_at_1.57.55_AM.png)
    
    
    <aside>
    💡 내용 요약
    
    앱을 시작할 때 `MaterialApp`으로 앱을 시작하고, `home`이라는 **이름지정 매개변수(named parameter)**에 첫 번째 페이지 위젯을 만들어 전달합니다.
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/46d02d2b-5fce-4384-a49e-bd1a012fa8d6/Untitled.png)
    
    </aside>
    • 2) appBar 만들기

      AppBar의 영역에 대한 명칭은 아래와 같습니다.

      Untitled

      우리는 아래와 같이 leadingactions 에 각각 아이콘과 이미지를 넣어주면 되겠군요!

      Untitled

      • leading & actions 아이콘 버튼 만들기

        1. 아래 코드스니펫을 복사해서 24번째 return Scaffold( 뒤에 붙여 넣어주세요!

          • [코드스니펫] AppBar 아이콘

                              appBar: AppBar(
                      leading: Row(
                        children: [
                          SizedBox(width: 16),
                          Text(
                            '중앙동',
                            style: TextStyle(
                              color: Colors.black,
                              fontWeight: FontWeight.bold,
                              fontSize: 20,
                            ),
                          ),
                          Icon(
                            Icons.keyboard_arrow_down_rounded,
                            color: Colors.black,
                          ),
                        ],
                      ),
                      leadingWidth: 100,
                      actions: [
                        IconButton(
                          onPressed: () {},
                          icon: Icon(CupertinoIcons.search, color: Colors.black),
                        ),
                        IconButton(
                          onPressed: () {},
                          icon: Icon(Icons.menu_rounded, color: Colors.black),
                        ),
                        IconButton(
                          onPressed: () {},
                          icon: Icon(CupertinoIcons.bell, color: Colors.black),
                        ),
                      ],
                    ),
            ![Screen Shot 2022-09-05 at 2.07.08 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9a8d700e-75d2-4a0c-9744-8dadb5913dc3/Screen_Shot_2022-09-05_at_2.07.08_AM.png)
    
        2. 47번째 라인에 `CupertinoIcons`가 빨간 줄로 그어져 있습니다. 마우스를 올려보면 아래와 같이 `CupertinoIcons` 위젯이 인식이 안된다고 나옵니다.
    
            <aside>
            💡 **빨간 줄**은 문제가 있다는 표시입니다. 이 상태에서는 저장을 해도 앱에 반영이 되지 않으니 항상 해결을 하고 넘어가야 합니다.
    
            </aside>
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1131e670-a5c6-4f0a-85e6-7902fbb6f773/Untitled.png)
    
            위에 `Quick Fix`를 클릭해주세요.
    
            <aside>
            💡 에러가 있는 곳을 클릭하신 뒤 단축키를 눌러 Quick Fix를 바로 실행하실 수도 있어요.
    
            window : `ctrl + .`
            macOS : `cmd + .`
    
            - window 단축키가 안되는 경우
    
                `ctrl + .`을 눌렀을 때 `·` 가 입력되고 단축키가 먹히지 않는 경우가 있습니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c71b281e-23ca-4efd-a6fe-94260d9290e7/Untitled.png)
    
                `win + space` 를 눌서 `한컴 입력기`가 아닌 `Microsoft 입력기`를 선택해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d8676cf6-948c-4662-8bc4-873c135425b8/Untitled.png)
    
                한컴 입력기를 삭제하는 방법은 아래 링크를 참고해 주세요.
    
                - **[[코드스니펫] window 한컴 입력기 삭제방법](https://www.lesstif.com/life/ms-ide-75956246.html)**
    
                    ```dart
                    https://www.lesstif.com/life/ms-ide-75956246.html
                    ```
    
            </aside>
    
            아래와 같이 `import library 'package:flutter/cupertino.dart';`를 선택해주세요
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ca6288c-4cb4-43d5-a401-7f10ee216d6b/Untitled.png)
    
            그러면 맨 위에 이런 구문이 추가된 것을 보실 수 있습니다.
            `import 'package:flutter/cupertino.dart';` 
    
            <aside>
            💡 `cupertino.dart` 에는 아이콘이나 위젯들이 미리 정의되어 있습니다.
            우리는 이를 가져다 쓰기 위해 import 해준 것입니다.
    
            </aside>
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/06583a4e-4290-4de8-a277-e23599d34626/Untitled.png)
    
        3. 저장하면 에뮬레이터에 AppBar와 아이콘이 출력됩니다.
    
            ![Screen Shot 2022-09-05 at 2.13.58 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6333c7cc-57e5-4514-a8fc-6d602294601a/Screen_Shot_2022-09-05_at_2.13.58_AM.png)
    
        4. AppBar 의 색상과 그림자를 변경해보겠습니다.
    
            26번째 줄 맨 뒤에 엔터를 눌러 빈 라인을 추가한 뒤, 코드스니펫을 복사해서 붙여 넣어 주세요. 
    
            - **[코드스니펫] AppBar backgroundColor, elevation**
    
                ```dart
                backgroundColor: Colors.white,
                elevation: 0.5,
                ```
    
    
            각각 `AppBar`의 속성으로 들어가는 값들 이기 때문에 쉼표로 구분해줍니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/17696096-88e0-4725-a99a-7c1ee6e20c80/Untitled.png)
    
            <aside>
            💡 AppBar를 완성했습니다!
    
            </aside>
    • 3) body 만들기 - 레이아웃

      • 레이아웃 나누기

        Untitled

        1. 62번째 줄에 Center를 클릭한 뒤 마우스 우클릭Refactor를 선택합니다.

          Untitled

        2. Wrap with Row을 선택합니다.

          Screen Shot 2022-09-05 at 2.37.07 AM.png

          Center 위젯이 아래와 같이 Row 위젯으로 감싸집니다.

          Screen Shot 2022-09-05 at 2.37.27 AM.png

        3. 64번째 라인을 삭제하고 아래 코드 스니펫을 붙여넣어주세요.

          • [코드스니펫] 레이아웃

                                  // 이미지 들어갈 자리
                        Column(
                          children: [
                            // 'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.'
                            // '봉천동 · 6분 전'
                            // '100만원'
                            Row(
                              children: [
                                // 빈 칸
                                // 하트 아이콘
                                // '1'
                              ],
                            ),
                          ],
                        ),
            ![Screen Shot 2022-09-05 at 2.46.03 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/398473da-ae01-43eb-b560-a7d7d00ef573/Screen_Shot_2022-09-05_at_2.46.03_AM.png)
    
            - 먼저 `Row` 안에 가로 방향으로 이미지와 나머지 요소들의 `Column` 을 배치해줍니다.
            - `Column` 안에 각각의 텍스트 요소들을 넣어줍니다.
            - 마지막 줄에 나오는 하트 아이콘과 숫자는 다시 `Row` 로 묶어줍니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/41ce8a5c-8ea9-41c3-9cb8-a0d64ea28888/Untitled.png)
    
            <aside>
            💡 디자인을 보고 큰 단위에서부터 차근차근 Row 와 Column 을 사용해 요소들을 배치하는 연습이 필요합니다.
    
            레이아웃을 잡았으니, `children`에 위젯들을 하나씩 넣어봅시다.
    
            </aside>
    
            <aside>
            💡 이 둘만으로도 많은 레이아웃을 만들어낼 수 있으나, 여러 요소들을 겹치게 표현하기 위해서는 Stack 이라는 클래스를 사용해야 합니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cafe8d72-ce81-401f-bc4b-cbb3f2abf5e7/Untitled.png)
    
            자세한 사용법과 Column 및 Row 와의 비교는 아래의 글을 참고해주세요!
    
            [[Flutter] Stack과 Positioned Class](https://ahang.tistory.com/24)
    
            </aside>
    
    - 이미지 만들기
    
        <aside>
        💡 이번에는 인터넷에 있는 고양이 사진 URL을 `Image.network()` 위젯을 이용해 가져오겠습니다.
    
        </aside>
    
        1. 코드스니펫을 복사해서 64번째 라인의 주석을 지우고 붙여 넣어 주세요. 
            - **[코드스니펫] body 이미지**
    
                ```dart
                                    // CilpRRect 를 통해 이미지에 곡선 border 생성
                          ClipRRect(
                            borderRadius: BorderRadius.circular(8),
                            // 이미지
                            child: Image.network(
                              'https://cdn2.thecatapi.com/images/6bt.jpg',
                              width: 100,
                              height: 100,
                              fit: BoxFit.cover,
                            ),
                          ),
                ```
    
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2a2c497b-3217-476b-985b-cf56c72fce71/Untitled.png)
    
            <aside>
            💡 우리는 정사각형의 위젯에 이미지를 채워서 보여주려 합니다.  
            `fit: BoxFit.cover`라고 넣어주면 이미지의 비율을 유지하면서 고정된 폭과 높이에 맞추어 이미지를 적절히 잘라서 보여줍니다.
    
            ![폭과 높이를 넘어가는 이미지 부분은 모두 잘립니다](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d085ff5d-8600-439b-bb91-0ba3d81be3e6/Untitled.png)
    
            폭과 높이를 넘어가는 이미지 부분은 모두 잘립니다
    
            [BoxFit 이 더 궁금하다면?](https://api.flutter.dev/flutter/painting/BoxFit.html)
    
            </aside>
    
    - 텍스트 만들기
    
        <aside>
        💡 아래와 같이 텍스트를 세로로 배치해보겠습니다. `Column` 위젯 속에 `Text` 위젯들을 배치하면 되겠죠!
    
        ![Screen Shot 2022-09-05 at 3.31.56 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5c33048e-13ea-443a-80ba-052f4db1cfb8/Screen_Shot_2022-09-05_at_3.31.56_AM.png)
    
        </aside>
    
        1. 코드스니펫을 복사해서 77, 78, 79번째 라인을 모두 지우고 붙여 넣어 주세요.
            - **[코드스니펫] 텍스트 세로 배치**
    
                ```dart
                                                Text(
                                  'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                                  style: TextStyle(
                                    fontSize: 16,
                                    color: Colors.black,
                                  ),
                                  softWrap: false,
                                  maxLines: 2,
                                  overflow: TextOverflow.ellipsis,
                                ),
                                SizedBox(height: 2),
                                Text(
                                  '봉천동 · 6분 전',
                                  style: TextStyle(
                                    fontSize: 12,
                                    color: Colors.black45,
                                  ),
                                ),
                                SizedBox(height: 4),
                                Text(
                                  '100만원',
                                  style: TextStyle(
                                    fontSize: 14,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                ```
    
    
            ![simulator_screenshot_196B932F-273E-4490-A02B-486EDD508ADB.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b3eb7c74-1d52-4226-8deb-b8524264dfa7/simulator_screenshot_196B932F-273E-4490-A02B-486EDD508ADB.png)
    
            위와 같이 overflow가 발생할 겁니다! 이는 지정된 사이즈를 위젯이 넘어갔다는 뜻인데요.
    
            이는 `Column` 위젯의 폭이 설정되지 않았기 때문에 발생하는 문제입니다. `Expanded` 위젯을 통해 이 문제를 해결해봅시다.
    
            75번째 줄의 `Column` 위젯에 마우스 커서를 가져다두면 왼쪽에 전구 모양 아이콘이 생길텐데요. 이를 눌러봅시다. (마우스 우클릭해서 나오는 `Refactor` 와 같은 옵션입니다!)
    
            ![Screen Shot 2022-09-05 at 10.49.41 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/72357c1b-9673-4296-acc3-521c7ed87d8c/Screen_Shot_2022-09-05_at_10.49.41_AM.png)
    
            `Wrap with widget` 을 눌러주시면 아래와 같이 `widget` 으로 `Column` 이 쌓이게 됩니다. widget 자리에 우리가 원하는 위젯 이름을 넣어주면 됩니다.
    
            ![Screen Shot 2022-09-05 at 10.50.59 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6922308b-447f-4db2-b8ca-94348fd18710/Screen_Shot_2022-09-05_at_10.50.59_AM.png)
    
            `Expanded` 라고 입력해줍니다.
    
            ![Screen Shot 2022-09-05 at 10.52.03 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c2930ec8-7a33-438c-a806-ea07b2ce4c86/Screen_Shot_2022-09-05_at_10.52.03_AM.png)
    
            `Expanded` 위젯으로 `Column` 을 감싸면 해당 위젯이 차지할 수 있는 공간을 최대한 차지하게 됩니다. 남는 공간 만큼을 위젯의 폭으로 사용할 수 있는 것이죠. 위젯의 폭이 설정되니 overflow 도 해결됩니다. 저장하고 확인해볼까요?
    
            ![Screen Shot 2022-09-05 at 10.52.43 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/09029cb1-5c99-4189-9fd4-73d4eed67045/Screen_Shot_2022-09-05_at_10.52.43_AM.png)
    
            오른쪽에 뜨던 overflow 가 사라졌습니다!
    
            <aside>
            💡 `Expanded` 위젯은 child 위젯이 차지할 수 있는 공간을 최대한 차지하도록 그 크기를 지정해주는 위젯입니다.
    
            보다 자세한 내용은 아래 링크를 참고해주세요!
            [[플러터]Flexible과 Expanded 위젯](https://mike123789-dev.tistory.com/entry/%ED%94%8C%EB%9F%AC%ED%84%B0Flexible%EA%B3%BC-Expanded-%EC%9C%84%EC%A0%AF)
    
            </aside>
    • 4) body 만들기 - 요소 정렬

      • crossAxisAlignment 설정하기

        이제 이미지와 텍스트의 정렬을 맞춰주면 되겠군요.

        RowColumn 은 주축, 부축을 기준으로 정렬할 수 있는데요. 주축을 main axis, 부축을 cross axis 라고 합니다. 아래 사진을 참고해주세요!

        Untitled

        Untitled

        내부 요소들의 정렬은 MainAxisAlignment.center 와 같은 식으로 설정해줍니다.

        Alignment 방법들을 시각화하면 아래와 같습니다.

        출처: [https://morioh.com/p/9c7541691c2c](https://morioh.com/p/9c7541691c2c)

        출처: https://morioh.com/p/9c7541691c2c

        출처: [https://morioh.com/p/9c7541691c2c](https://morioh.com/p/9c7541691c2c)

        출처: https://morioh.com/p/9c7541691c2c

        Untitled

        위와 같이 배치되어 있는 요소들을 같은 선상에 맞추려면, RowCrossAxisAlignment 를 설정해주면 되겠죠!

        코드스니펫을 복사해서 62번째 라인 뒤에 붙여 넣어 요소들을 위로 정렬해 보도록 하겠습니다.

        • [코드스니펫] Row CrossAxisAlignment

          
            crossAxisAlignment: CrossAxisAlignment.start,
        ![Screen Shot 2022-09-05 at 3.23.32 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1efe54fb-6370-49ae-939c-313e3546476b/Screen_Shot_2022-09-05_at_3.23.32_PM.png)
    
        아래 사진처럼 요소들이 위쪽 시작점에 붙는 것을 볼 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4cb7fb62-c4ac-43a2-a7ba-896e8b5ae70b/Untitled.png)
    
        마찬가지로 코드스니펫을 복사해서 77번째 라인 뒤에 붙여 넣어 요소들을 왼쪽으로 정렬해 보도록 하겠습니다.
    
        - **[코드스니펫] Column CrossAxisAlignment**
    
            ```dart
    
            crossAxisAlignment: CrossAxisAlignment.start,
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2a0cead6-1afb-4d7d-aca9-4052d64756ac/Untitled.png)
    
        텍스트 요소들이 왼쪽 시작점에 붙는 것을 볼 수 있습니다.
    
        <aside>
        💡 **MainAxisAlignment, CrossAxisAlignment** 를 설정해서 `Row`, `Column` 내 요소들을 정렬할 수 있습니다.
    
        </aside>
    
    - `SizedBox` 를 이용해 위젯간 간격 설정하기
    
        <aside>
        💡 아래 사진처럼 이미지와 텍스트가 너무 붙어있을 때는 어떻게 하는 게 좋을까요?
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/122c3ceb-a8ac-48f5-94c6-a079661f11c4/Untitled.png)
    
        이미지와 텍스트 사이에 빈 `SizedBox` 위젯을 넣어 간격을 조정할 수 있습니다.
    
        </aside>
    
        코드스니펫을 복사해서 75번째 라인 뒤에 붙여 넣어 `SizedBox` 를 추가해주겠습니다.
    
        - **[코드스니펫] SizedBox**
    
            ```dart
    
            SizedBox(width: 12),
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/14b8e1cb-1b32-4ee0-aeef-27669e7efed4/Untitled.png)
    
        저장하면 아래와 같이 요소 사이에 간격이 추가된 것을 확인할 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e04f335c-8780-47e1-af9d-240b8ef562f3/Untitled.png)
    
    - 좋아요 버튼 추가하기
    
        <aside>
        💡 오른쪽 아래에 좋아요 버튼을 추가하기 위해 `Row` 위젯과 `Spacer` 위젯을 활용해봅시다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cc2d9b5d-fb6d-4dff-b7c2-6a3abaf650a1/Untitled.png)
    
        </aside>
    
        코드스니펫을 복사해서 109번째, 110번째, 111번째 줄에 있는 주석을 지우고 `Row` 의 `children` 안에 붙여 넣어줍니다.
    
        - **[코드스니펫] 좋아요 아이콘, 숫자**
    
            ```dart
    
                                                Spacer(),
                                GestureDetector(
                                  onTap: () {},
                                  child: Row(
                                    children: [
                                      Icon(
                                        CupertinoIcons.heart,
                                        color: Colors.black54,
                                        size: 16,
                                      ),
                                      Text(
                                        '1',
                                        style: TextStyle(color: Colors.black54),
                                      ),
                                    ],
                                  ),
                                )
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f1cc1318-9ccd-4e84-8125-0cd7c63c27f4/Untitled.png)
    
        좋아요 아이콘과 숫자가 오른쪽 끝에 배치된 것을 볼 수 있습니다.
        (`GestureDetector` 는 추후에 좋아요 버튼 클릭을 구현하기 위해 우선 추가했습니다.)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6c5330b6-4c0b-465e-88cd-6ed421c690d9/Untitled.png)
    
        <aside>
        💡 `Spacer` 위젯은 빈 공간을 차지하는 위젯입니다.
    
        </aside>
    
        피드 1개를 완성했습니다!
    • 5) floatingActionButton 만들기

      아래 코드스니펫을 복사해 132번째 라인 뒤에 붙여 넣습니다.

      • [코드스니펫] FloatingActionButton

        
                      floatingActionButton: FloatingActionButton(
                  onPressed: () {},
                  backgroundColor: Color(0xFFFF7E36),
                  elevation: 1,
                  child: Icon(
                    Icons.add_rounded,
                    size: 36,
                  ),
                ),
    ![Screen Shot 2022-09-05 at 4.10.04 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/20190587-ca31-4b00-9265-f7dd448e2a52/Screen_Shot_2022-09-05_at_4.10.04_PM.png)
    
    저장하면 아래와 같이 `FloatingActionButton` 이 생성된 것을 볼 수 있습니다.
    
    ![Screen Shot 2022-09-05 at 4.10.28 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6fc0c60c-9b78-400d-a2d4-4ee5a1928342/Screen_Shot_2022-09-05_at_4.10.28_PM.png)
    
    <aside>
    💡 코드가 너무 길 때는 body 요소가 어디에서 끝나는지 닫는 괄호를 찾기가 힘들 수 있습니다.
    
    이럴 때는 아래와 같은 VS Code 의 접어두기 기능이 유용합니다.
    
    ![Screen Shot 2022-09-05 at 4.12.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/506744f3-ec9d-4e26-94c6-6c7eba000484/Screen_Shot_2022-09-05_at_4.12.05_PM.png)
    
    해당 버튼을 누르면 아래와 같이 해당 요소가 접힙니다. 새로운 요소를 추가하고 싶다면, 이 아래에 추가해주면 되겠죠.
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ce88fec3-4874-4c0d-87ad-79e424039f27/Untitled.png)
    
    </aside>
    • 6) bottomNavigationBar 만들기

      아래 코드스니펫을 복사해 141번째 라인 뒤에 붙여 넣습니다.

      • [코드스니펫] BottomNavigationBar

        
                      bottomNavigationBar: BottomNavigationBar(
                  fixedColor: Colors.black,
                  unselectedItemColor: Colors.black,
                  showUnselectedLabels: true,
                  selectedFontSize: 12,
                  unselectedFontSize: 12,
                  iconSize: 28,
                  type: BottomNavigationBarType.fixed,
                  items: [
                    BottomNavigationBarItem(
                      icon: Icon(Icons.home_filled),
                      label: '홈',
                      backgroundColor: Colors.white,
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.my_library_books_outlined),
                      label: '동네생활',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.fmd_good_outlined),
                      label: '내 근처',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(CupertinoIcons.chat_bubble_2),
                      label: '채팅',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(
                        Icons.person_outline,
                      ),
                      label: '나의 당근',
                    ),
                  ],
                  currentIndex: 0,
                ),
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/684b79a1-a894-4aa7-9d3e-3991693eec1d/Untitled.png)
    
    저장하면 아래와 같이 `BottomNavigationBar` 이 생성된 것을 볼 수 있습니다.
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9a7b616c-1a15-48e5-8df6-e38d11723772/Untitled.png)
    • main.dart 최종 코드

        import 'package:flutter/cupertino.dart';
        import 'package:flutter/material.dart';
      
        void main() {
          runApp(MyApp());
        }
      
        class MyApp extends StatelessWidget {
          const MyApp({Key? key}) : super(key: key);
      
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: HomePage(),
            );
          }
        }
      
        class HomePage extends StatelessWidget {
          const HomePage({super.key}); // 생성자
      
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                backgroundColor: Colors.white,
                elevation: 0.5,
                leading: Row(
                  children: [
                    SizedBox(width: 16),
                    Text(
                      '중앙동',
                      style: TextStyle(
                        color: Colors.black,
                        fontWeight: FontWeight.bold,
                        fontSize: 20,
                      ),
                    ),
                    Icon(
                      Icons.keyboard_arrow_down_rounded,
                      color: Colors.black,
                    ),
                  ],
                ),
                leadingWidth: 100,
                actions: [
                  IconButton(
                    onPressed: () {},
                    icon: Icon(CupertinoIcons.search, color: Colors.black),
                  ),
                  IconButton(
                    onPressed: () {},
                    icon: Icon(Icons.menu_rounded, color: Colors.black),
                  ),
                  IconButton(
                    onPressed: () {},
                    icon: Icon(CupertinoIcons.bell, color: Colors.black),
                  ),
                ],
              ),
              body: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // CilpRRect 를 통해 이미지에 곡선 border 생성
                  ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    // 이미지
                    child: Image.network(
                      'https://cdn2.thecatapi.com/images/6bt.jpg',
                      width: 100,
                      height: 100,
                      fit: BoxFit.cover,
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                          style: TextStyle(
                            fontSize: 16,
                            color: Colors.black,
                          ),
                          softWrap: false,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                        SizedBox(height: 2),
                        Text(
                          '봉천동 · 6분 전',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.black45,
                          ),
                        ),
                        SizedBox(height: 4),
                        Text(
                          '100만원',
                          style: TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Row(
                          children: [
                            Spacer(),
                            GestureDetector(
                              onTap: () {},
                              child: Row(
                                children: [
                                  Icon(
                                    CupertinoIcons.heart,
                                    color: Colors.black54,
                                    size: 16,
                                  ),
                                  Text(
                                    '1',
                                    style: TextStyle(color: Colors.black54),
                                  ),
                                ],
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: () {},
                backgroundColor: Color(0xFFFF7E36),
                elevation: 1,
                child: Icon(
                  Icons.add_rounded,
                  size: 36,
                ),
              ),
              bottomNavigationBar: BottomNavigationBar(
                fixedColor: Colors.black,
                unselectedItemColor: Colors.black,
                showUnselectedLabels: true,
                selectedFontSize: 12,
                unselectedFontSize: 12,
                iconSize: 28,
                type: BottomNavigationBarType.fixed,
                items: [
                  BottomNavigationBarItem(
                    icon: Icon(Icons.home_filled),
                    label: '홈',
                    backgroundColor: Colors.white,
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(Icons.my_library_books_outlined),
                    label: '동네생활',
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(Icons.fmd_good_outlined),
                    label: '내 근처',
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(CupertinoIcons.chat_bubble_2),
                    label: '채팅',
                  ),
                  BottomNavigationBarItem(
                    icon: Icon(
                      Icons.person_outline,
                    ),
                    label: '나의 당근',
                  ),
                ],
                currentIndex: 0,
              ),
            );
          }
        }

    04. 파일 분리

    • 최종 파일 구조

      Untitled

      • main.dart : 앱 시작

      • home_page.dart : 첫 번째 페이지 레이아웃

      • feed.dart : HomePage에서 body

        Screen Shot 2022-09-05 at 4.45.51 PM.png

    • 1) home_page.dart 파일 분리

      1. lib 폴더를 선택한 뒤 마우스 우클릭으로 New File을 선택해주세요.

        Untitled

      2. 이름을 home_page.dart로 지어주세요.

        Untitled

      3. main.dart 파일에 20번째 라인부터 마지막 줄 까지 HomePage 위젯이므로 잘라서 home_page.dart 파일로 옮겨주세요!

        • 처음 옮기고 나면 아래와 같이 빨간줄 투성이로 에러가 발생하는데, MaterialCupertino import가 안 돼서 발생하는 에러입니다.

          Screen Shot 2022-09-05 at 4.56.09 PM.png

        • 첫 번째 줄에 StatelessWidget을 클릭하신 뒤 Quick Fix(window : Ctrl + . / macOS : Cmd + .)를 누르면 아래와 같이 나옵니다. 여기서 import 'package:flutter/cupertino.dart';를 선택해주세요.

          Untitled

        • Cupertino 패키지가 import 되고도 에러가 발생하는데, 빨간 줄이 있는 곳에서 Quick Fix(window : Ctrl + . / macOS : Cmd + .)를 눌러주신 뒤 import 'package:flutter/material.dart';를 선택해주세요.

          Untitled

        • 그러면 빨간 줄이 모두 없어졌습니다. Ctrl + S 또는 Cmd + S를 눌러 저장해주세요.

          Screen Shot 2022-09-05 at 4.58.04 PM.png

      4. 다음 main.dart 파일을 보면 15번째 라인에 에러가 있습니다.

        • HomePage 위젯을 별도로 옮겼기 때문에 발생하는 에러로 Quick Fix(window : Ctrl + . / macOS : Cmd + .) 눌러서 import library 'home_page.dart'를 선택해줍니다.

          Untitled

        • 4번째 라인에 home_page 위젯을 불러오는 코드가 추가 되며 문제가 해결되었습니다.

          Screen Shot 2022-09-05 at 4.58.58 PM.png

        • 저장을 하시면 에뮬레이터에서 정상적으로 결과가 나오는 것을 보실 수 있습니다.

    • 2) feed.dart 파일 분리

      1. home_page.dart에 46번째 라인 Row 위젯을 우클릭하여 Refactor 메뉴를 선택해주세요.

        Screen Shot 2022-09-05 at 5.07.55 PM.png

      2. Extract Widget을 선택 해주세요. 그러면 해당 위젯을 별도의 StatelessWidget 클래스로 분리할 수 있습니다.

        Screen Shot 2022-09-05 at 5.08.12 PM.png

      3. 위젯 이름을 Feed라고 입력해주세요.

        Untitled

      4. 그러면 아래 사진과 같이 46번째 라인에 Feed() 인스턴스가 입력되고, 95번째 라인에 Feed 클래스가 생성됩니다.

        Untitled

      5. lib 폴더를 우클릭한 뒤 New File을 선택해주세요.

        Untitled

      6. 파일 이름은 feed.dart라고 입력해주세요.

        Untitled

      7. home_page.dart에 95번째 라인에 있는 Feed 클래스를 잘라내어 feed.dart 파일로 옮겨주세요.

        Screen Shot 2022-09-05 at 5.11.04 PM.png

    1. `feed.dart` 파일에 1번째 `StatelessWidget`을 클릭한 뒤 Quick Fix(window : `Ctrl + .` / macOS : `Cmd + .`)을 눌러 `cupertino`를 `import`해 주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/51b66047-25af-4cc8-8dbb-a83f6a9d8ff1/Untitled.png)
    
        33번째 줄에 빨간 줄이 있는 `Colors`을 선택한 뒤 Quick Fix(window : `Ctrl + .` / macOS : `Cmd + .`)을 눌러 `material`을 `import`하고 저장해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a9fbdbc5-bd6a-4247-84ed-bc2e63e527c4/Untitled.png)
    
        그럼 `feed.dart` 파일의 모든 에러가 해결됩니다.
    
    2. `home_page.dart`에서 46번째 라인에 `Feed`도 Quick Fix(window : `Ctrl + .` / macOS : `Cmd + .`) 눌러서를 눌러서 `import` 한 뒤 저장해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4e2c84ae-94fe-4f2f-8470-5623941311fb/Untitled.png)
    
    3. VSCode 우측 상단에 `Restart` 버튼을 눌러보면 정상적으로 작동하는 것을 볼 수 있습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b9202832-289a-47f5-bfcb-933c95484e81/Untitled.png)
    
    
    <aside>
    💡 이와 같이 기능을 변경하거나 추가하지 않고, 코드만 관리하기 쉽게 변경하는 과정을 리팩토링(refactoring)이라고 부릅니다.
    
    더 큰 앱을 만들수록 코드의 복잡도가 올라가므로 주기적인 리팩토링을 통해 복잡도를 낮춰줘야 프로젝트가 손을 떠나지 않습니다.
    
    </aside>
    • 최종 코드

      • main.dart 전체 파일

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          import 'home_page.dart';
        
          void main() {
            runApp(MyApp());
          }
        
          class MyApp extends StatelessWidget {
            const MyApp({Key? key}) : super(key: key);
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
      • home_page.dart 전체 파일

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          import 'feed.dart';
        
          class HomePage extends StatelessWidget {
            const HomePage({super.key}); // 생성자
        
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.white,
                  elevation: 0.5,
                  leading: Row(
                    children: [
                      SizedBox(width: 16),
                      Text(
                        '중앙동',
                        style: TextStyle(
                          color: Colors.black,
                          fontWeight: FontWeight.bold,
                          fontSize: 20,
                        ),
                      ),
                      Icon(
                        Icons.keyboard_arrow_down_rounded,
                        color: Colors.black,
                      ),
                    ],
                  ),
                  leadingWidth: 100,
                  actions: [
                    IconButton(
                      onPressed: () {},
                      icon: Icon(CupertinoIcons.search, color: Colors.black),
                    ),
                    IconButton(
                      onPressed: () {},
                      icon: Icon(Icons.menu_rounded, color: Colors.black),
                    ),
                    IconButton(
                      onPressed: () {},
                      icon: Icon(CupertinoIcons.bell, color: Colors.black),
                    ),
                  ],
                ),
                body: Feed(),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {},
                  backgroundColor: Color(0xFFFF7E36),
                  elevation: 1,
                  child: Icon(
                    Icons.add_rounded,
                    size: 36,
                  ),
                ),
                bottomNavigationBar: BottomNavigationBar(
                  fixedColor: Colors.black,
                  unselectedItemColor: Colors.black,
                  showUnselectedLabels: true,
                  selectedFontSize: 12,
                  unselectedFontSize: 12,
                  iconSize: 28,
                  type: BottomNavigationBarType.fixed,
                  items: [
                    BottomNavigationBarItem(
                      icon: Icon(Icons.home_filled),
                      label: '홈',
                      backgroundColor: Colors.white,
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.my_library_books_outlined),
                      label: '동네생활',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.fmd_good_outlined),
                      label: '내 근처',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(CupertinoIcons.chat_bubble_2),
                      label: '채팅',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(
                        Icons.person_outline,
                      ),
                      label: '나의 당근',
                    ),
                  ],
                  currentIndex: 0,
                ),
              );
            }
          }
      • feed.dart 전체 파일

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          class Feed extends StatelessWidget {
            const Feed({
              Key? key,
            }) : super(key: key);
        
            @override
            Widget build(BuildContext context) {
              return Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // CilpRRect 를 통해 이미지에 곡선 border 생성
                  ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    // 이미지
                    child: Image.network(
                      'https://cdn2.thecatapi.com/images/6bt.jpg',
                      width: 100,
                      height: 100,
                      fit: BoxFit.cover,
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                          style: TextStyle(
                            fontSize: 16,
                            color: Colors.black,
                          ),
                          softWrap: false,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                        SizedBox(height: 2),
                        Text(
                          '봉천동 · 6분 전',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.black45,
                          ),
                        ),
                        SizedBox(height: 4),
                        Text(
                          '100만원',
                          style: TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Row(
                          children: [
                            Spacer(),
                            GestureDetector(
                              onTap: () {},
                              child: Row(
                                children: [
                                  Icon(
                                    CupertinoIcons.heart,
                                    color: Colors.black54,
                                    size: 16,
                                  ),
                                  Text(
                                    '1',
                                    style: TextStyle(color: Colors.black54),
                                  ),
                                ],
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              );
            }
          }

    05. 좋아요 구현하기 & 피드 리스트 만들기

    • 최종 모습

      ezgif-4-89b3150141.gif

    • 1) 좋아요 표시하기

        1. FeedStatefulWidget로 변환하기

          feed.dart 파일을 열고, 4번째 줄에 StatelessWidget을 클릭한 뒤 Refactor(window : ctrl + . / macOS : Cmd + .)를 누르고 아래와 같은 팝업이 뜨면 Convert to StatefulWidget을 선택한 뒤 저장해 주세요.

          Untitled

          StatefulWidget이 되면서 상태를 관리하는 _FeedState 클래스가 추가됩니다

          Untitled

        1. isFavorite 상태 추가하기

          코드스니펫을 복사해서 아래 이미지와 같이 13번째 줄에 맨 뒤에 추가하고 저장해주세요.

          • [코드스니펫] isFavorite 선언

                 // 좋아요 여부
               bool isFavorite = false;
            
        <aside>
        💡 `isFavorite` 변수는 좋아요 여부를 나타내는 상태 변수입니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6f1dfb5b-6a34-4547-8780-e3d5c769d989/Untitled.png)
    
    - 3. 하트 버튼 클릭시 상태 변경하기
    
        <aside>
        💡 버튼 클릭 이벤트를 코드의 어느 부분에서 받을 수 있는지 흐름을 잘 알고 있어야합니다.
    
        </aside>
    
        하트 버튼을 클릭하는 경우 68번째 줄의 `onTap` 함수가 실행되므로, 코드스니펫을 복사해 68번째 라인에 `onTap` 함수의 중괄호 안에 붙여넣고 저장해 주세요.
    
        - **[코드스니펫] heart 버튼 클릭**
    
            ```dart
    
                                            // 화면 갱신
                            setState(() {
                              isFavorite = !isFavorite; // 좋아요 토글
                            });
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9795f8f4-1526-48af-9eb2-5b9d20ba6ffa/Untitled.png)
    
        아래와 같이 추가해주면, 클릭시 `isFavorite` 변수의 값이 반전되면서 화면이 갱신 됩니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f4926ae3-1283-4f5f-b43e-fac6b43260db/Untitled.png)
    
        <aside>
        💡 클릭시 상태 변경하기 : 71번째 줄
    
        ```dart
        isFavorite = !isFavorite;
        ```
    
        - `!isFavorite` : `!`는 not의 의미로 `isFavorite`가 true라면 false로 반환 해줍니다.
        </aside>
    
        <aside>
        💡 화면 갱신 : 71번째 줄을 감싸고 있는 70 ~ 72번째 줄
    
        ```dart
        setState(() {
            // 안쪽 코드 실행 후 화면 갱신
        })
        ```
    
        - `setState()` 호출시 StatefulWidget의 `build()`함수를 호출해 화면을 갱신시켜 줍니다.
        </aside>
    
    - 4. `isFavorite` 상태에 따라 하트 색상 바꾸기
    
        <aside>
        💡 `isFavorite` 상태에 따라 하트의 색상을 바꾸고, 아이콘도 채워지도록 해보겠습니다.
    
        </aside>
    
        코드스니펫을 복사해서 77, 78번째 라인을 지우고, 붙여 넣은 뒤 저장해주세요.
    
        - **[코드스니펫] heart 색상 변경**
    
            ```dart
                                      isFavorite
                                          ? CupertinoIcons.heart_fill
                                          : CupertinoIcons.heart,
                                      color: isFavorite ? Colors.pink : Colors.black,
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed5e4af0-d244-48a1-81b4-48b02d1933a2/Untitled.png)
    
        <aside>
        💡 위젯에 전달하는 값을 조건에 따라 다르게 보여줄 때 아래와 같이 한 줄 if문을 사용합니다.
    
        ```dart
        조건 ? 반환값1 : 반환값2
        ```
    
        - `조건`이 `true`인 경우 `반환값1`이 할당되고, `false`인 경우에는 `반환값2`가 할당됩니다.
        </aside>
    
        에뮬레이터에서 `좋아요 버튼`을 누르면 버튼 색상이 바뀌는 것을 볼 수 있습니다.
    
        ![Screen Shot 2022-09-05 at 5.35.19 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3467f6ba-e7e8-4ec3-8269-e27cde5f38ea/Screen_Shot_2022-09-05_at_5.35.19_PM.png)
    • 2) 피드 리스트 만들기

        1. ListView 위젯

          출처 - [dribbble](https://dribbble.com/shots/5027549/attachments/5027549-Get-Wheels-List-Styles?mode=media)

          출처 - dribbble

          • DartPad에서 ListView 위젯을 사용해 보도록 하겠습니다. 코드스니펫을 복사한 뒤 주소창에 붙여 넣어주세요.

            ![Screen Shot 2022-09-01 at 12.34.29 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cddba52c-f407-42fd-950f-7935e4185878/Screen_Shot_2022-09-01_at_12.34.29_PM.png)
    
            ```dart
            ListView(
                children: [
                    Text("0"),
                    Text("1"),
                    Text("2"),
                    ...
                ]
            );
            ```
    
            <aside>
            💡 위와 같이 `children`을 직접 작성할 수도 있지만 `ListView.builder()`를 이용하면 적은 코드로 `itemCount` 만큼 화면을 그릴 수 있습니다.
    
            ```dart
            ListView.builder(
              itemCount: 100, // 전체 아이템 개수
              itemBuilder: (context, index) { // index는 0 부터 99까지 증가
                    return Text("$index"); // 100번 실행
              }
            ),
            ```
    
            </aside>
    
        - 코드스니펫을 복사한 뒤 주소창에 붙여 넣으면, DartPad로 접속합니다.
            - **[[코드스니펫] DartPad ListView.builder 학습](https://dartpad.dev/?id=fd5b0e646af4d9ebd0618b08111e0fd2)**
    
                ```dart
                https://dartpad.dev/?id=fd5b0e646af4d9ebd0618b08111e0fd2
                ```
    
    
            ![Screen Shot 2022-09-01 at 12.35.42 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b07a1849-7eeb-474f-9ff3-4be4f1c0b2bc/Screen_Shot_2022-09-01_at_12.35.42_PM.png)
    
    - 2. `ListView.builder` 적용하기
    
        <aside>
        💡 `ListView.builder` 를 활용해 원하는 개수만큼의 Feed 를 보여주도록 하겠습니다.
    
        </aside>
    
        `home_page.dart` 의 48번째 줄에 있는 `Feed` 를 여러개 반복해서 보여주겠습니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/31511111-07c6-467d-bb7c-f44e3b22735b/Untitled.png)
    
        `Feed`를 마우스 우클릭하고 Refactor(window : `ctrl + .` / macOS : `Cmd + .`) 를 눌러줍니다. (왼쪽의 전구 아이콘을 눌러도 됩니다)
    
        `Wrap with Builder` 옵션을 선택해주세요.
    
        ![Screen Shot 2022-09-05 at 6.06.23 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cd5e1edf-1eed-447e-905d-368662fe21ef/Screen_Shot_2022-09-05_at_6.06.23_PM.png)
    
        48 번째 줄의 Builder 를 `ListView.builder` 로 바꿔줍니다.
    
        ![Screen Shot 2022-09-05 at 6.07.07 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6c7f2d2d-a437-47c4-91ed-ac158ac334d1/Screen_Shot_2022-09-05_at_6.07.07_PM.png)
    
        50번째 줄 중괄호와 소괄호 사이에 쉼표를 추가해 정렬을 맞춰줍니다.
    
        `builder` 를 `itemBuilder` 로 바꾸고, context 뒤에 `index` 를 써줍니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fa0906f-8d56-4f83-811d-5397709c4a4d/Untitled.png)
    
        저장하고 앱을 스크롤 해보면 피드가 여러 개로 늘어난 것을 보실 수 있습니다. (무한대로 늘어나 있습니다!)
    
        ![simulator_screenshot_5C2F3D9A-389E-408F-B16C-7B83FCB17424.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f259b857-661f-4f7a-95db-b3dd5f7d8a3f/simulator_screenshot_5C2F3D9A-389E-408F-B16C-7B83FCB17424.png)
    
        `ListView.builder` 에서는 itemCount 라는 속성을 조정해 자식 위젯이 그려지는 개수를 조절할 수 있습니다. 48번째 줄 맨 뒤에 아래 코드 스니펫을 추가해줍니다.
    
        - **[코드스니펫] itemCount**
    
            ```dart
    
            itemCount: 10,
            ```
    
    
        <aside>
        💡 이제 딱 10개의 피드만 나타나는 것을 확인할 수 있습니다.
    
        </aside>
    
    - 3. `ListView.separated` 적용하기
    
        <aside>
        💡 `ListView.separated` 를 활용해 각각의 Feed 사이에 `Divider` 를 추가해주겠습니다.
    
        </aside>
    
        48 번째 줄의 builder 를 separated 로 바꿔줍니다. 
    
        ![Screen Shot 2022-09-05 at 6.09.55 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/79ee2909-3ea9-4350-8ac3-d904879ea1d6/Screen_Shot_2022-09-05_at_6.09.55_PM.png)
    
        52번째 줄 맨 뒤에 아래 코드스니펫을 복사한 후 붙여넣어줍니다.
    
        - **[코드스니펫] separatorBuilder**
    
            ```dart
                            separatorBuilder: (context, index) {
                      return Divider();
                    },
            ```
    
    
        ![Screen Shot 2022-09-05 at 6.12.01 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9205206c-b0a3-4a3e-ba19-6e2931add851/Screen_Shot_2022-09-05_at_6.12.01_PM.png)
    
        <aside>
        💡 각각의 요소 사이에 `Divider` (구분선) 이 추가된 것을 볼 수 있습니다.
    
        </aside>
    
    - 4. Padding 추가하기
    
        <aside>
        💡 `Padding` 을 조절해 Feed 를 보기 좋게 수정해봅시다.
    
        </aside>
    
        48번째 줄의 ListView 를 마우스 우클릭하고 Refactor (window : `ctrl + .` / macOS : `Cmd + .`) 를 눌러줍니다. 
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e52227d3-1cdd-4b0c-9aa8-b2a9c70c845f/Untitled.png)
    
        `Wrap with Padding` 을 선택해주세요.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5703917d-08e8-4a5d-b3cb-545b11f30705/Untitled.png)
    
        49번째 줄을 지우고 아래 코드스니펫을 붙여넣어주세요
    
        - **[코드스니펫] ListView horizontal padding**
    
            ```dart
            padding: const EdgeInsets.symmetric(horizontal: 16),
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1f15c1a7-019c-4c74-867e-0d59f9e681d2/Untitled.png)
    
        <aside>
        💡 가로 방향 패딩이 추가된 것을 볼 수 있습니다.
    
        </aside>
    
        53번째 `Feed()`를 마우스 우클릭하고 Refactor (window : `ctrl + .` / macOS : `Cmd + .`) 를 눌러줍니다. 
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/45157278-0715-4ed4-b70f-0e4f2f430c21/Untitled.png)
    
        `Wrap with Padding` 을 선택합니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ad055937-e596-4952-ac8c-3a2959d74ada/Untitled.png)
    
        54번째 줄을 지우고 아래 코드스니펫을 붙여넣어주세요
    
        - **[코드스니펫] Feed vertical padding**
    
            ```dart
            padding: const EdgeInsets.symmetric(vertical: 12),
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/07a5143f-c971-4029-8b0b-3db297ea8547/Untitled.png)
    
        <aside>
        💡 이제 각 피드 사이의 간격도 보기 좋게 설정되었습니다.
    
        </aside>
    • 최종 코드

      • main.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          import 'home_page.dart';
        
          void main() {
            runApp(MyApp());
          }
        
          class MyApp extends StatelessWidget {
            const MyApp({Key? key}) : super(key: key);
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
      • home_page.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          import 'feed.dart';
        
          class HomePage extends StatelessWidget {
            const HomePage({super.key}); // 생성자
        
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.white,
                  elevation: 0.5,
                  leading: Row(
                    children: [
                      SizedBox(width: 16),
                      Text(
                        '중앙동',
                        style: TextStyle(
                          color: Colors.black,
                          fontWeight: FontWeight.bold,
                          fontSize: 20,
                        ),
                      ),
                      Icon(
                        Icons.keyboard_arrow_down_rounded,
                        color: Colors.black,
                      ),
                    ],
                  ),
                  leadingWidth: 100,
                  actions: [
                    IconButton(
                      onPressed: () {},
                      icon: Icon(CupertinoIcons.search, color: Colors.black),
                    ),
                    IconButton(
                      onPressed: () {},
                      icon: Icon(Icons.menu_rounded, color: Colors.black),
                    ),
                    IconButton(
                      onPressed: () {},
                      icon: Icon(CupertinoIcons.bell, color: Colors.black),
                    ),
                  ],
                ),
                body: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: ListView.separated(
                    itemCount: 10,
                    itemBuilder: (context, index) {
                      return Padding(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                        child: Feed(),
                      );
                    },
                    separatorBuilder: (context, index) {
                      return Divider();
                    },
                  ),
                ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {},
                  backgroundColor: Color(0xFFFF7E36),
                  elevation: 1,
                  child: Icon(
                    Icons.add_rounded,
                    size: 36,
                  ),
                ),
                bottomNavigationBar: BottomNavigationBar(
                  fixedColor: Colors.black,
                  unselectedItemColor: Colors.black,
                  showUnselectedLabels: true,
                  selectedFontSize: 12,
                  unselectedFontSize: 12,
                  iconSize: 28,
                  type: BottomNavigationBarType.fixed,
                  items: [
                    BottomNavigationBarItem(
                      icon: Icon(Icons.home_filled),
                      label: '홈',
                      backgroundColor: Colors.white,
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.my_library_books_outlined),
                      label: '동네생활',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.fmd_good_outlined),
                      label: '내 근처',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(CupertinoIcons.chat_bubble_2),
                      label: '채팅',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(
                        Icons.person_outline,
                      ),
                      label: '나의 당근',
                    ),
                  ],
                  currentIndex: 0,
                ),
              );
            }
          }
      • feed.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          class Feed extends StatefulWidget {
            const Feed({
              Key? key,
            }) : super(key: key);
        
            @override
            State<Feed> createState() => _FeedState();
          }
        
          class _FeedState extends State<Feed> {
            // 좋아요 여부
            bool isFavorite = false;
        
            @override
            Widget build(BuildContext context) {
              return Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // CilpRRect 를 통해 이미지에 곡선 border 생성
                  ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    // 이미지
                    child: Image.network(
                      'https://cdn2.thecatapi.com/images/6bt.jpg',
                      width: 100,
                      height: 100,
                      fit: BoxFit.cover,
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                          style: TextStyle(
                            fontSize: 16,
                            color: Colors.black,
                          ),
                          softWrap: false,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                        SizedBox(height: 2),
                        Text(
                          '봉천동 · 6분 전',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.black45,
                          ),
                        ),
                        SizedBox(height: 4),
                        Text(
                          '100만원',
                          style: TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Row(
                          children: [
                            Spacer(),
                            GestureDetector(
                              onTap: () {
                                // 화면 갱신
                                setState(() {
                                  isFavorite = !isFavorite; // 좋아요 토글
                                });
                              },
                              child: Row(
                                children: [
                                  Icon(
                                    isFavorite
                                        ? CupertinoIcons.heart_fill
                                        : CupertinoIcons.heart,
                                    color: isFavorite ? Colors.pink : Colors.black,
                                    size: 16,
                                  ),
                                  Text(
                                    '1',
                                    style: TextStyle(color: Colors.black54),
                                  ),
                                ],
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              );
            }
          }

    06. 피드마다 각각 다른 이미지 보여주기

    • 최종 모습

      simulator_screenshot_12C862BD-B8FD-4784-AAE1-355E352AAC09.png

    • 1) Feed 위젯 수정하기

      1. feed.dart 파일에 8번째 줄에 아래 코드를 붙여 넣어 이미지 URL을 담을 변수를 만들어 줍니다.

        • [코드스니펫] feed.dart / imageUrl 변수 선언

          
            final String imageUrl; // 이미지를 담을 변수
          
        <aside>
        💡 `imageUrl`은 한 번 전달받은 뒤 변경되지 않기 때문에 앞에 `final` 키워드를 붙여줍니다.
    
        </aside>
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1f175007-d9de-4928-9688-e3ab69c401fd/Untitled.png)
    
    2. Feed 위젯의 생성자를 호출 할 때, `imageUrl`을 전달받아 방금 생성한 변수에 할당하도록 만들어주겠습니다. 아래 코드스니펫을 복사해서 6번째 줄에 붙여 넣어 주세요.
        - **[코드스니펫] feed.dart / imageUrl 변수 주입**
    
            ```dart
    
                required this.imageUrl,
            ```
    
    
        <aside>
        💡 `required` : 필수 전달 매개 변수로 만들어줍니다.
        `this.imageUrl` : 많은 Feed 인스턴스 중 현재 인스턴스의 `imageUrl`
    
        </aside>
    
        ![_constructor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/95dcd08e-baed-4513-a9e0-d237452aa3fa/_constructor.png)
    
    3. 전달 받은 `imageUrl`을 화면에 보여줍시다.
    
        아래 코드 스니펫을 복사해서 30번째 줄을 지우고, 붙여 넣은 뒤 저장해주세요.
    
        - **[코드스니펫] feed.dart / imageUrl 사용하기**
    
            ```dart
            widget.imageUrl, // 10번째 줄의 imageUrl 가져오기
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e9115cd0-cde3-4593-9e57-4375901aadba/Untitled.png)
    
        <aside>
        💡 **StatefulWidget**의 상태 클래스에서 `widget.변수명`으로 전달받은 변수에 접근할 수 있습니다.
    
        ![_stateful.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b8ea0dec-ab36-4580-8ca0-3837737e5e8d/_stateful.png)
    
        </aside>
    
    
    <aside>
    💡 Feed 위젯 이미지 url를 전달 받을 준비가 완료되었습니다.
    
    </aside>
    • 2) 데이터 전달하기

      1. 화면에 보여줄 데이터를 HomePage 위젯에 추가해 봅시다.

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

        • [코드스니펫] home_page.dart / images 추가

          
                final List<String> images = [
                  "https://cdn2.thecatapi.com/images/6bt.jpg",
                  "https://cdn2.thecatapi.com/images/ahr.jpg",
                  "https://cdn2.thecatapi.com/images/arj.jpg",
                  "https://cdn2.thecatapi.com/images/brt.jpg",
                  "https://cdn2.thecatapi.com/images/cml.jpg",
                  "https://cdn2.thecatapi.com/images/e35.jpg",
                  "https://cdn2.thecatapi.com/images/MTk4MTAxOQ.jpg",
                  "https://cdn2.thecatapi.com/images/MjA0ODM5MQ.jpg",
                  "https://cdn2.thecatapi.com/images/AuY1uMdmi.jpg",
                  "https://cdn2.thecatapi.com/images/AKUofzZW_.png",
                ];
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2775e36c-ea68-4761-98de-75d224e42751/Untitled.png)
    
    2. `images`의 개수만큼 Feed를 보여주도록 수정해봅시다. 63번째 줄의 `itemCount : 10,` 을 삭제하고 아래 코드스니펫을 붙여 넣어주세요.
        - **[코드스니펫] home_page.dart /  itemCount**
    
            ```dart
            itemCount: images.length, // 이미지 개수만큼 보여주기
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0f4b75d9-4a54-4a25-9b32-219174878609/Untitled.png)
    
    3. `Feed` 위젯에 `index`에 해당하는 image를 전달해 봅시다. 
    
        먼저 이미지 url 1개를 `image` 라는 이름의 변수에 저장합니다. 64번째 줄 맨 뒤에 아래 코드스니펫을 붙여 넣어주세요.
    
        - **[코드스니펫] home_page.dart /  image 변수 선언**
    
            ```dart
    
            final image = images[index]; // index에 해당하는 이미지
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b0a9d915-ae70-4661-b16b-f071a6c59539/Untitled.png)
    
        `Feed`의 `imageUrl`에 image 변수에 저장된 url 을 넘겨줍니다. 68번째 줄을 지우고 아래 코드스니페을 붙여 넣어주세요.
    
        - **[코드스니펫] home_page.dart /  Feed 에 imageUrl 전달**
    
            ```dart
            child: Feed(imageUrl: image), // imageUrl 전달
            ```
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e557d4c3-5e3b-442c-857e-b169e9081995/Untitled.png)
    
    4. 그리고 저장해주면 완성!
    
        ![Simulator Screen Shot - iPhone 13 - 2022-09-05 at 19.23.13.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4f389f36-c316-4845-a6af-e070678beebb/Simulator_Screen_Shot_-_iPhone_13_-_2022-09-05_at_19.23.13.png)
    
        <aside>
        💡 개발자들은 데이터를 보여주는 껍데기를 뷰(View)라고 부릅니다.
        View와 데이터를 분리하면 View를 재활용 할 수 있습니다.
    
        </aside>
    • 최종 코드

      • main.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          import 'home_page.dart';
        
          void main() {
            runApp(MyApp());
          }
        
          class MyApp extends StatelessWidget {
            const MyApp({Key? key}) : super(key: key);
        
            @override
            Widget build(BuildContext context) {
              return MaterialApp(
                debugShowCheckedModeBanner: false,
                home: HomePage(),
              );
            }
          }
      • home_page.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          import 'feed.dart';
        
          class HomePage extends StatelessWidget {
            const HomePage({super.key}); // 생성자
        
            @override
            Widget build(BuildContext context) {
              final List<String> images = [
                "https://cdn2.thecatapi.com/images/6bt.jpg",
                "https://cdn2.thecatapi.com/images/ahr.jpg",
                "https://cdn2.thecatapi.com/images/arj.jpg",
                "https://cdn2.thecatapi.com/images/brt.jpg",
                "https://cdn2.thecatapi.com/images/cml.jpg",
                "https://cdn2.thecatapi.com/images/e35.jpg",
                "https://cdn2.thecatapi.com/images/MTk4MTAxOQ.jpg",
                "https://cdn2.thecatapi.com/images/MjA0ODM5MQ.jpg",
                "https://cdn2.thecatapi.com/images/AuY1uMdmi.jpg",
                "https://cdn2.thecatapi.com/images/AKUofzZW_.png",
              ];
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.white,
                  elevation: 0.5,
                  leading: Row(
                    children: [
                      SizedBox(width: 16),
                      Text(
                        '중앙동',
                        style: TextStyle(
                          color: Colors.black,
                          fontWeight: FontWeight.bold,
                          fontSize: 20,
                        ),
                      ),
                      Icon(
                        Icons.keyboard_arrow_down_rounded,
                        color: Colors.black,
                      ),
                    ],
                  ),
                  leadingWidth: 100,
                  actions: [
                    IconButton(
                      onPressed: () {},
                      icon: Icon(CupertinoIcons.search, color: Colors.black),
                    ),
                    IconButton(
                      onPressed: () {},
                      icon: Icon(Icons.menu_rounded, color: Colors.black),
                    ),
                    IconButton(
                      onPressed: () {},
                      icon: Icon(CupertinoIcons.bell, color: Colors.black),
                    ),
                  ],
                ),
                body: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: ListView.separated(
                    itemCount: images.length, // 이미지 개수만큼 보여주기
                    itemBuilder: (context, index) {
                      final image = images[index]; // index에 해당하는 이미지
                      return Padding(
                        padding: const EdgeInsets.symmetric(vertical: 12),
                        child: Feed(imageUrl: image), // imageUrl 전달
                      );
                    },
                    separatorBuilder: (context, index) {
                      return Divider();
                    },
                  ),
                ),
                floatingActionButton: FloatingActionButton(
                  onPressed: () {},
                  backgroundColor: Color(0xFFFF7E36),
                  elevation: 1,
                  child: Icon(
                    Icons.add_rounded,
                    size: 36,
                  ),
                ),
                bottomNavigationBar: BottomNavigationBar(
                  fixedColor: Colors.black,
                  unselectedItemColor: Colors.black,
                  showUnselectedLabels: true,
                  selectedFontSize: 12,
                  unselectedFontSize: 12,
                  iconSize: 28,
                  type: BottomNavigationBarType.fixed,
                  items: [
                    BottomNavigationBarItem(
                      icon: Icon(Icons.home_filled),
                      label: '홈',
                      backgroundColor: Colors.white,
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.my_library_books_outlined),
                      label: '동네생활',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(Icons.fmd_good_outlined),
                      label: '내 근처',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(CupertinoIcons.chat_bubble_2),
                      label: '채팅',
                    ),
                    BottomNavigationBarItem(
                      icon: Icon(
                        Icons.person_outline,
                      ),
                      label: '나의 당근',
                    ),
                  ],
                  currentIndex: 0,
                ),
              );
            }
          }
      • feed.dart

          import 'package:flutter/cupertino.dart';
          import 'package:flutter/material.dart';
        
          class Feed extends StatefulWidget {
            const Feed({
              Key? key,
              required this.imageUrl,
            }) : super(key: key);
        
            final String imageUrl; // 이미지를 담을 변수
        
            @override
            State<Feed> createState() => _FeedState();
          }
        
          class _FeedState extends State<Feed> {
            // 좋아요 여부
            bool isFavorite = false;
        
            @override
            Widget build(BuildContext context) {
              return Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // CilpRRect 를 통해 이미지에 곡선 border 생성
                  ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    // 이미지
                    child: Image.network(
                      widget.imageUrl, // 10번째 줄의 imageUrl 가져오기
                      width: 100,
                      height: 100,
                      fit: BoxFit.cover,
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.',
                          style: TextStyle(
                            fontSize: 16,
                            color: Colors.black,
                          ),
                          softWrap: false,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                        SizedBox(height: 2),
                        Text(
                          '봉천동 · 6분 전',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.black45,
                          ),
                        ),
                        SizedBox(height: 4),
                        Text(
                          '100만원',
                          style: TextStyle(
                            fontSize: 14,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Row(
                          children: [
                            Spacer(),
                            GestureDetector(
                              onTap: () {
                                // 화면 갱신
                                setState(() {
                                  isFavorite = !isFavorite; // 좋아요 토글
                                });
                              },
                              child: Row(
                                children: [
                                  Icon(
                                    isFavorite
                                        ? CupertinoIcons.heart_fill
                                        : CupertinoIcons.heart,
                                    color: isFavorite ? Colors.pink : Colors.black,
                                    size: 16,
                                  ),
                                  Text(
                                    '1',
                                    style: TextStyle(color: Colors.black54),
                                  ),
                                ],
                              ),
                            )
                          ],
                        ),
                      ],
                    ),
                  ),
                ],
              );
            }
          }

    07. 숙제 - Shazam 클론 코딩

    • 1) 프로젝트 만들기

      1. ViewCommand Palette 버튼을 클릭해주세요.

        Untitled

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

        Untitled

      3. Application을 선택해주세요.

        Untitled

      4. 프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔 flutter 폴더를 선택한 뒤 Select a folder to create the project in 버튼을 눌러 주세요.

      5. 프로젝트 이름을 shazam 으로 입력해주세요.

        Screen Shot 2022-04-16 at 11.22.52 PM.png

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

        Untitled

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

        Untitled

      7. 아래 main.dart 코드스니펫을 복사해서 기존 코드를 모두 지운 뒤, main.dart 파일에 붙여 넣고 저장해주세요.

        • [코드스니펫] main.dart

            import 'package:flutter/material.dart';
          
            void main() {
              runApp(const MyApp());
            }
          
            class MyApp extends StatelessWidget {
              const MyApp({Key? key}) : super(key: key);
          
              // This widget is the root of your application.
              @override
              Widget build(BuildContext context) {
                return MaterialApp(
                  title: 'Shazam',
                  theme: ThemeData(
                    primarySwatch: Colors.blue,
                  ),
                  home: HomePage(),
                );
              }
            }
          
            class HomePage extends StatefulWidget {
              const HomePage({Key? key}) : super(key: key);
          
              @override
              State<HomePage> createState() => _HomePageState();
            }
          
            class _HomePageState extends State<HomePage> {
              @override
              Widget build(BuildContext context) {
                return DefaultTabController(
                  initialIndex: 1,
                  length: 3,
                  child: Builder(builder: (context) {
                    DefaultTabController.of(context)?.addListener(() {
                      setState(() {});
                    });
          
                    return Scaffold(
                      body: Stack(
                        children: [
                          TabBarView(
                            children: [
                              FirstTab(),
                              SecondTab(),
                              ThirdTab(),
                            ],
                          ),
                          SafeArea(
                            child: Padding(
                              padding:
                                  const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
                              child: Column(
                                children: [
                                  Container(
                                    alignment: Alignment.topCenter,
                                    child: TabPageSelector(
                                      color: DefaultTabController.of(context)?.index == 1
                                          ? Colors.blue[300]
                                          : Colors.grey[400],
                                      selectedColor:
                                          DefaultTabController.of(context)?.index == 1
                                              ? Colors.white
                                              : Colors.blue,
                                      indicatorSize: 8,
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ],
                      ),
                    );
                  }),
                );
              }
            }
          
            // 첫번째 페이지
            class FirstTab extends StatelessWidget {
              const FirstTab({Key? key}) : super(key: key);
          
              @override
              Widget build(BuildContext context) {
                const songs = [
                  {
                    'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
                    'title': '가을밤에 든 생각',
                    'artist': '잔나비',
                  },
                  {
                    'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
                    'title': '가을밤에 든 생각',
                    'artist': '잔나비',
                  },
                  {
                    'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
                    'title': '가을밤에 든 생각',
                    'artist': '잔나비',
                  },
                  {
                    'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
                    'title': '가을밤에 든 생각',
                    'artist': '잔나비',
                  },
                  {
                    'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
                    'title': '가을밤에 든 생각',
                    'artist': '잔나비',
                  },
                  {
                    'imageUrl': 'https://i.ytimg.com/vi/jAO0KXRdz_4/hqdefault.jpg',
                    'title': '가을밤에 든 생각',
                    'artist': '잔나비',
                  },
                ];
          
                return Center(child: Text('첫번째 페이지'));
              }
            }
          
            // 두번째 페이지
            class SecondTab extends StatelessWidget {
              const SecondTab({Key? key}) : super(key: key);
          
              @override
              Widget build(BuildContext context) {
                return Center(child: Text('두번째 페이지'));
              }
            }
          
            // 세번째 페이지
            class ThirdTab extends StatelessWidget {
              const ThirdTab({Key? key}) : super(key: key);
          
              @override
              Widget build(BuildContext context) {
                const chartData = {
                  'korea': [
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                  ],
                  'global': [
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                  ],
                  'newyork': [
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                    {
                      'imageUrl': 'https://i.ibb.co/xf2HpfG/dynamite.jpg',
                      'name': 'Dynamite',
                      'artist': 'BTS',
                    },
                  ],
                };
          
                return Center(child: Text('세번째 페이지'));
              }
            }
      8. 다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.

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

        • [코드스니펫] analysis_options.yaml

          
                prefer_const_constructors: false
                prefer_const_literals_to_create_immutables: false
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
    
        <aside>
        💡 상세 내용은 아래를 참고해 주세요.
    
        - 어떤 의미인지 궁금하신 분들을 위해
    
            `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
    
            파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
    
            12번째 라인에 마우스를 올리면 아래와 같이 설명이 보입니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
    
            위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수(변하지 않는 수)로 선언하라는 힌트입니다. 상수로 만들면 화면을 새로고침 할 때, 상수로 선언된 위젯들은 새로고침을 할 필요가 없어서 성능상 이점이 있습니다.
    
            아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
    
            지금은 학습 단계이니 눈에 띄지 않도록 `analysis_options.yaml` 파일에 아래와 같이 설정하여 힌트를 숨기도록 하겠습니다.
    
            ```dart
                prefer_const_constructors: false
                prefer_const_literals_to_create_immutables: false
            ```
    
        </aside>
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/69d7b3a1-e116-4bb9-a911-80fe461d4f50/Untitled.png)
    
    <aside>
    💡 만들고 싶은 페이지를 선택해서 정답을 참고하며 구현해 보세요.
    
    </aside>
    
    |  | 첫 번째 페이지 | 두 번째 페이지 | 세 번째 페이지 |
    | --- | --- | --- | --- |
    | 난이도 | ⭐️⭐️⭐️ | ⭐️ | ⭐️⭐️ |

    이전 강의 바로가기

    1주차 - Flutter 앱 개발 맛보기 & Dart 문법 익히기

    다음 강의 바로가기

    3주차 - 패키지 사용법 익히기 & 앱의 기능 만들기 (마이메모)


    Copyright ⓒ TeamSparta All rights reserved.

    1주차 - Flutter 앱 개발 맛보기 & Dart 문법 익히기

    • PDF 파일

      1주차-Flutter_앱_개발_맛보기__Dart_문법_익히기.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 + /

    [수업 목표]

    • Flutter 앱 개발 과정 이해하기
    • VSCode와 친해지기
    • Dart 문법 이해하기

    [목차]


    01. Why Flutter

    • 1) 앱 개발 방법

      • 네이티브 앱(Native App)

        • Android

          → Google에서 제공하는 Android SDK(Software Development Kit)를 이용하여 개발

          • 프로그래밍 언어 : Java / Kotlin
          • 개발 툴 : Android Studio
        • iOS

          → Apple에서 제공하는 iOS SDK(Software Development Kit)를 이용하여 개발

          • 프로그래밍 언어 : Objective-C / Swift
          • 개발 툴 : XCode
          • 특이사항 : macOS 에서만 개발이 가능

          출처 - [지식iN 앱을 Flutter로 개발하는 이유](https://d2.naver.com/helloworld/3384599)

          출처 - 지식iN 앱을 Flutter로 개발하는 이유

      • 크로스 플랫폼 앱(Cross Platform App)

        • React Native

          • 프로그래밍 언어 : JavaScript
          • 페이스북에서 출시한 오픈 소스 모바일 애플리케이션 프레임 워크
        • Flutter

          • 프로그래밍 언어 : Dart
          • 구글에서 출시한 오픈 소스 모바일 애플리케이션 프레임 워크
    • 2) 왜 크로스 플랫폼을 사용할까요?

      🛠 생산성이 월등하다.

      [출처 - 네이버 지식iN 앱을 Flutter로 만든 이유](https://d2.naver.com/helloworld/3384599)코

      출처 - 네이버 지식iN 앱을 Flutter로 만든 이유

    • 3) 왜 Flutter를 사용할까요?

      1. 🔥 Flutter가 훨씬 핫하다 (React Native 대비. 커뮤니티 & 자료 ⬆)

        → 개발을 할 때 훨씬 편하게 찾아볼 수 있다는 뜻입니다 :)

        출처 - [getstream.io](https://getstream.io/blog/flutter-vs-react-native-the-ultimate-comparison/)

        출처 - getstream.io

      2. 📚 공식 문서가 잘 되어 있다.

        [https://docs.flutter.dev/](https://docs.flutter.dev/)

        https://docs.flutter.dev/

      3. 👍 성능이 뛰어나다

        출처 - [Flutter vs Native vs React-Native: Examining performance](https://medium.com/swlh/flutter-vs-native-vs-react-native-examining-performance-31338f081980)

        출처 - Flutter vs Native vs React-Native: Examining performance

    02. 설치하기

    • Windows 사용자를 위한 프로그램 설치 방법
    - [잠깐] 혹시 Windows 7를 쓰신다면 아래 가이드를 따라주세요 :)
        - [링크](https://www.microsoft.com/ko-kr/p/powershell/9mz1snwt0n5d?activetab=pivot:overviewtab)에 접속하셔서 Power Shell을 설치해주세요.
    
    **[프레임워크 설치하기]**
    
    - 1. Flutter 설치하기
    
        <aside>
        💡 Flutter 설치 과정은 일반적인 프로그램 설치와는 조금 다릅니다. 일반적으로는 `.exe` 파일등을 실행해 프로그램을 설치하는데요.
    
        Flutter 설치는 아래와 같은 과정으로 진행됩니다.
    
        1. zip 파일을 다운로드 받아서 적절한 경로에 압축 해제
        2. 압축 해제한 flutter 폴더 경로를 환경변수에 등록 
        (flutter 라는 명령어가 이 경로에 있다는 것을 윈도에게 알리는 것과 같습니다)
        </aside>
    
        - 1) 다운로드
            1. 먼저 flutter 를 설치할 폴더를 만들겠습니다. 
    
                아래 사진에서 밑줄친 부분을 폴더 경로(주소)라고 하는데요. 
    
                여기에 **한글이나 공백이 들어가면** 인식을 못하고 에러가 발생합니다.
    
                ![Screenshot 2022-09-20 024333.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9b6779d0-a45f-40b8-b848-542e143236be/Screenshot_2022-09-20_024333.png)
    
                에러를 막기 위해 `C 드라이브` 바로 아래에 `src` 라는 이름의 폴더를 생성해줍니다.
    
                ![Screenshot 2022-09-20 024147.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2090724a-a484-4960-8c32-5fa8f3e49c3b/Screenshot_2022-09-20_024147.png)
    
            2. flutter 를 다운로드할 수 있는 [링크](https://docs.flutter.dev/get-started/install/windows)로 이동합니다.
    
                [Windows install](https://docs.flutter.dev/get-started/install/windows)
    
            3. 파란색 버튼(zip 파일 다운로드 링크)을 클릭합니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/962ad32c-bfeb-410e-bb13-a8117bebdc90/Untitled.png)
    
                <aside>
                💡 아래 경고 메시지를 꼭 확인해주세요
                1. 특수문자(한글 포함)나 공백이 설치 경로에 포함되면 안된다
                2. `C:\Program Files\` 밑에는 설치하면 안된다 (상위 권한 필요)
    
                </aside>
    
            4. 아까 만들어준 `C:\src` 폴더에 해당 zip 파일을 다운로드 해줍니다. 
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/22a72d03-2d30-4090-94e7-03b78ed87e80/Untitled.png)
    
                <aside>
                💡 zip 파일은 어디에 다운로드하셔도 상관 없습니다. 
                대신 압축을 풀어줄 때 만큼은 꼭 `C:\src` 폴더에 풀어주세요
    
                </aside>
    
            5. 압축을 해제해주세요!
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e70291ca-afad-49c1-b4ee-368d5f797f21/Untitled.png)
    
                누르면 아래와 같이 압축 해제 대상 폴더를 지정하는 화면이 나옵니다. 
                여기서 `flutter_windows_…` 이 부분을 지워주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/210a3788-ccb5-4728-b9ca-5823bec0fadd/Untitled.png)
    
                지우고 난 경로는 아래와 같습니다. 마저 압축 해제를 눌러줄까요?
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1bf07a7d-1a97-421a-837d-13248cf836e4/Untitled.png)
    
                압축을 모두 해제하는 데 10-20분 정도 걸립니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/187db94f-296d-4810-ab42-b87792dc7d1c/Untitled.png)
    
                압축 해제가 끝나고 나면 아래와 같이 flutter 라는 폴더가 생성되어 있을 것입니다. 
                (위에서 경로를 잘 지정해줬다면요!)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4af3a34-0d85-4dcc-be30-8c988e71a46c/Untitled.png)
    
                <aside>
                💡 간혹가다 위에서 경로 설정을 실수해 `flutter_windows_…` 이런 이름으로 해제된 경우가 있습니다.
                이 때는, 해당 폴더를 클릭해서 들어가면 비로소 `flutter` 라는 이름의 폴더를 보실 수 있습니다. 해당 `flutter` 폴더를 위 사진과 같이 밖으로 빼주세요! 
                (아래 환경 변수 설정을 원활히 하기 위함입니다)
    
                </aside>
    
        - 2) 환경 변수 설정
    
            <aside>
            💡 다운로드한 Flutter를 어디서든지 사용 가능하도록 설정하는 절차입니다.
    
            </aside>
    
            1. 압축이 해제된 `flutter` 폴더로 들어가 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4833238e-f729-4f8c-9071-961a744110e9/Untitled.png)
    
            2. `bin` 폴더로 들어가주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/160d2074-5c6c-447b-96b5-6f7b6a7b24b2/Untitled.png)
    
            3. 들어가시면 `dart`, `dart.bat`, `flutter`, `flutter.bat` 과 같은 파일들이 있습니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c811e337-c8c9-4e33-afa4-789a2b915b6e/Untitled.png)
    
                <aside>
                💡 이들이 바로 flutter 실행 파일입니다. cmd 로 이 폴더를 열고 `flutter` 라고 치면, 여기 있는 파일이 실행됩니다.
    
                지금은 플러터가 설치된 폴더 에서만 `flutter` 라는 명령어를 실행할 수 있습니다.
    
                우리는 이 flutter 실행 파일을 **컴퓨터 어디에서도 모두 접근 가능하게끔**, 즉 어디에서도 `flutter` 라는 명령어를 사용할 수 있게끔 해야 합니다.
    
                환경 변수에 flutter 가 위치한 **폴더 경로를 추가**하면, 시스템의 모든 경로에서 이 flutter 파일에 접근하고, 실행할 수 있습니다.
    
                </aside>
    
            4. 이를 위해 우선 해당 폴더 경로를 복사하겠습니다. 위 과정을 잘 따라오셨다면 경로는 아래와 같습니다.
    
                ```
                C:\src\flutter\bin
                ```
    
                선택된 경로를 복사해주세요. (`Ctrl + C`)
    
                ![Screenshot 2022-09-20 033732.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3be69e5a-d3cc-4dd2-ac50-0a6337570189/Screenshot_2022-09-20_033732.png)
    
            5. 윈도우에 `환경 변수` 라고 검색합니다. (띄어쓰기도 하셔야 해요!)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8ec7c12c-472c-495c-894c-a1053c69ad96/Untitled.png)
    
            6. 아래와 같은 창에서 `환경 변수` 버튼을 클릭해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4b070e0b-98ea-458e-a852-0eb81fe4d496/Untitled.png)
    
            7. `사용자 변수`에 `path`를 선택한 뒤, `편집` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/85218bab-1214-48fd-be84-88664017d379/Untitled.png)
    
            8. `새로 만들기(N)`을 선택하신 뒤 복사한 주소를 붙여 넣고 `확인` 버튼을 눌러주세요.
    
                ![Screenshot 2022-09-20 033954.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b5246c68-425b-4d7b-924c-d03b9c6a09df/Screenshot_2022-09-20_033954.png)
    
            9. 켜져있는 창들을 모두 `확인` 버튼을 눌러 닫아주세요.
    
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c2ab250-9830-4b25-b116-7dbd8c6c3757/Untitled.png)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/71c100c4-a2f5-4776-87df-d9cb79a55dc2/Untitled.png)
    
        - 3) 설치 확인
            1. `cmd`를 검색해서 `명령 프롬프트`를 실행해줍니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/383052bb-12ac-468a-8044-7f663e4eaac2/Untitled.png)
    
            2. 명령 프롬프트에 `flutter --version` 이라고 검색했을 때, 아래와 같은 문구가 뜨면 설치 완료!
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/37002ed1-e60e-4265-a98f-1da627f8faf7/Untitled.png)
    
                <aside>
                💡 이제 시스템의 모든 경로에서 `flutter` 라는 명령어를 사용할 수 있습니다!
    
                </aside>
    
    
    **[에디터 설치하기]**
    
    - 2. Visual Studio Code
    
        <aside>
        💡 Visual Studio Code (줄여서 VSCode) 앞으로 실제 코드를 작성할 편집 툴입니다.
    
        Flutter 개발은 Android Studio와 VSCode 둘 중 원하는 툴을 사용하여 개발할 수 있는데 VSCode가 더 가볍기 때문에 앞으로 수업은 VSCode에서 진행하도록 하겠습니다.
    
        </aside>
    
        - 1) VSCode 설치
            1. [링크](https://code.visualstudio.com/download)에 접속해 주세요.
            2. Window 버튼을 클릭합니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63572ab2-31fb-47de-8ee1-edb8a771ad70/Untitled.png)
    
            3. 내 문서에 저장합니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b8406fa7-10d5-4e0c-a62a-8c71df502976/Untitled.png)
    
            4. VSCodeUserSetup 파일을 실행해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/15e64328-4565-4b1e-9997-a1caf5b97980/Untitled.png)
    
            5. `동의합니다` 선택 후 `다음` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/26c4868e-98de-4a72-8234-0cf8cdd98c95/Untitled.png)
    
            6. 경로를 확인하시고, `다음` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ee0ad10c-50f8-4c5e-95ce-a10813d226b1/Untitled.png)
    
            7. `다음` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3af5829c-429a-423c-9ff8-f79400f451bd/Untitled.png)
    
            8. `바탕 화면에 바로가기 만들기`를 선택해주시고, `다음` 버튼을 클릭해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/85d38b8f-cd2c-4f29-9dd6-e590ea63fcd1/Untitled.png)
    
            9. `설치` 버튼을 클릭해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dd0dcbcc-e464-4f96-b883-1b225ae0ab10/Untitled.png)
    
            10. `종료` 버튼을 클릭해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6c6ba3eb-5b6b-4089-b70a-68dc787e9d3c/Untitled.png)
    
            11. 아래와 같이 Visual Studio Code가 실행되면, 설치 완료!
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb31a4ee-2ed8-48df-961e-1d856ff46188/Untitled.png)
    
                <aside>
                💡 우측 하단에 한국어로 변경하라는 팝업이 뜹니다.
                하지만 VSCode 사용법이나 대부분의 개발 자료는 영어로 되어 있기 때문에, 가급적 적용하지 않기를 권장 드립니다. (수업 자료도 영어 버전으로 되어있어요!)
    
                해당 알람을 다시 보지 않으려면 `우측 톱니바퀴 ⚙` 아이콘을 누른 뒤 `Don't Show Again`을 선택해 주세요. (만약 사라져서 버튼을 누르지 못했다면 다음번에 눌러주세요!)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1bbcc25a-bac1-412e-a0ee-a8302ce700e2/Untitled.png)
    
                </aside>
    
        - 2) Extension 설치
    
            <aside>
            💡 VSCode는 Flutter 뿐만 아니라 다양한 개발을 모두 할 수 있는 통합 에디터입니다. VSCode에서 Flutter 앱 개발을 하려면 VSCode에 Extension 탭에서 아래 목록의 Extension 들을 설치해야 합니다.
    
            **Flutter** : VSCode에서 Flutter 개발 환경 지원
            **Dart** : Flutter 개발 시 사용되는 Dart 개발 환경 지원
    
            </aside>
    
            1. 좌측에 extension 아이콘(동그라미)을 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/069b334c-7c6b-4eca-814c-24e159788880/Untitled.png)
    
            2. `flutter` 라고 검색한 뒤, 해당 익스텐션을 선택하고 `install` 버튼을 눌러 설치해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3fd60cf3-786d-427f-852f-08b92eae5795/Untitled.png)
    
            3. 위 `flutter` 익스텐션을 설치하면서 `dart` 익스텐션도 일반적으로 함께 설치가 됩니다.
    
                `dart` 라고 검색하신 뒤 혹시 설치가 안되었다면 해당 익스텐션도 같이 설치해주세요. 
    
                `uninstall`이라고 뜨신다면 이미 설치가 된 것이니 넘어가시면 됩니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b4bbc0d8-e9bc-4b7b-9fb8-ef0989d06ce0/Untitled.png)
    
    
    **[IDE 설치하기]** ** IDE : 소프트웨어 애플리케이션*
    
    - 3. Android Studio
        - 1) Android Studio 설치
            1. [링크](https://developer.android.com/studio)에 접속한 뒤, `Download Android Studio`를 클릭해주세요.
    
                ![Screenshot 2022-09-20 035221.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7ce46c8b-e17a-40d0-b310-104655399374/Screenshot_2022-09-20_035221.png)
    
            2. 팝업 창이 뜨면, 쭉 읽어보시고, 바닥까지 스크롤을 내려주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/11a77b36-7bdd-4011-bc10-522b6a25894a/Untitled.png)
    
            3. 약관에 동의하도록 체크하신 뒤, 다운로드 버튼을 눌러주세요.
    
                ![Screenshot 2022-09-20 035239.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/58b91dbe-952b-49ac-90f2-13e05d6d0c3d/Screenshot_2022-09-20_035239.png)
    
            4. 다운로드 폴더에 다운로드를 진행해주세요.
    
                ![Screenshot 2022-09-20 035410.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d923689f-e997-4dd2-b440-1e9b09646f72/Screenshot_2022-09-20_035410.png)
    
            5. 다운로드가 완료되면, `android-studio`를 클릭해서 실행해주세요.
    
                ![Screenshot 2022-09-20 035510.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b28f7995-98a2-4fa6-b100-d9ab0fa47387/Screenshot_2022-09-20_035510.png)
    
            6. 아래 이미지들을 모두 `Next` 버튼을 눌러 설치를 진행해 주세요.
    
                ![Screenshot 2022-09-20 035732.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5779e09c-c112-48f2-bbb0-952057a29503/Screenshot_2022-09-20_035732.png)
    
                ![Screenshot 2022-09-20 035744.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0b3b3939-942a-4861-bdce-ae3ffdbed13f/Screenshot_2022-09-20_035744.png)
    
                ![Screenshot 2022-09-20 035803.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6762f22f-60db-4c64-939d-0df372f4867a/Screenshot_2022-09-20_035803.png)
    
            7. `Install` 버튼을 눌러 주세요.
    
                ![Screenshot 2022-09-20 035818.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f8eca212-ff82-4d19-bd05-6cc7799c41a5/Screenshot_2022-09-20_035818.png)
    
            8. `Next` 버튼을 눌러주세요.
    
                ![Screenshot 2022-09-20 035928.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/025bbed0-ae37-4c57-8fb2-a34844a07596/Screenshot_2022-09-20_035928.png)
    
            9. 설치가 완료되었고, `Finish` 버튼을 눌러 Android Studio를 실행해 주세요.
    
                ![Screenshot 2022-09-20 035954.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c06fc74-6322-46f0-a89e-251455d29c79/Screenshot_2022-09-20_035954.png)
    
            10. 아래와 같은 창이 뜨면 `OK`를 눌러주세요.
    
                ![Screenshot 2022-09-20 040050.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fe9509eb-7bb4-4cbe-8bf9-a3b54db635b1/Screenshot_2022-09-20_040050.png)
    
            11. Android Studio 사용성 개선을 위해 Google에 데이터를 공유하고 싶으시다면, `Send usage statistics to Google` 버튼을 누르시고, 그렇지 않으시면 `Don't send` 버튼을 눌러주세요.
    
                ![Screenshot 2022-09-20 040150.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/014b2fd1-a3b1-4610-aa6c-a0a52b65caa5/Screenshot_2022-09-20_040150.png)
    
            12. Setup Wizard가 실행되면, `Next` 버튼을 눌러주세요.
    
                ![Screenshot 2022-09-20 040252.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1c2cdfad-1ca2-4ec5-9c9b-99b4ee05f9dc/Screenshot_2022-09-20_040252.png)
    
            13. `Next` 버튼을 눌러주세요.
    
                ![Screenshot 2022-09-20 040236.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/382ea58f-be44-4640-8347-8e6d21f13e54/Screenshot_2022-09-20_040236.png)
    
            14. 원하시는 테마를 선택하신 뒤 `Next`를 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f7e2a5fc-217d-4e63-badc-265e7439904e/Untitled.png)
    
            15. 만약 계정 이름이 한글이거나 띄어쓰기가 들어가 있다면`Your Android SDK location contains non-ASCII characters` 혹은 `your path contains white space etc.` 라도 뜨며 진행이 안됩니다.
    
                <aside>
                💡 에러가 없으신 분들은 `Next` 버튼을 누르고 26 순서로 넘어가 주세요
    
                </aside>
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/efaf14eb-3915-4a67-9509-a86dbad1991e/Untitled.png)
    
                Android SDK Location 경로를 직접 만들어주도록 하겠습니다.
    
            16. 이런 경우, 아래 그림과 같이 Android SDK Location에 `C:\Users\내 계정이름\Local`까지만 선택해 복사해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/453fce97-843d-4451-9fb7-a3dc81198db4/Untitled.png)
    
            17. `탐색기`를 열고 주소창에 붙여 넣은 뒤 엔터를 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0f83a23d-a1ac-4975-ac74-d12b2fd756d9/Untitled.png)
    
            18. 현재 Local이라는 폴더에 들어와 있고, 여기에 `Android`라는 폴더를 만들어 주세요. (대문자로 시작합니다)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/69dd03b0-8637-46ff-95f8-989545a5f507/Untitled.png)
    
            19. 생성한 `Android` 폴더에 들어간 뒤 `Sdk` 라는 폴더를 만들어주세요. (대문자로 시작합니다)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/36f523e9-78f0-4bf8-ac4a-e088101e3c89/Untitled.png)
    
            20. 생성한 `Sdk` 폴더 안으로 들어가 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/983e5efe-61c7-453f-a6f2-b794d91e14c7/Untitled.png)
    
            21. 윈도우 검색창에 `명령`이라고 검색한 뒤 `명령 프롬프트`를 우클릭하여 `관리자 권한으로 실행`해 주세요. 그냥 실행하면 안됩니다! 
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d164c4b7-9cc1-4e36-820e-423bcce82e1e/Untitled.png)
    
                실행할지 물어보는 팝업이 뜨면 예를 눌러서 실행해주세요.
    
            22. 아래 명령어를 복사한 뒤 명령 프롬프트에 붙여 넣어주세요. 아직 실행하시면 안됩니다.
    
                ```bash
                mklink /D C:\android-sdk 
                ```
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aa888d25-2d68-41e1-a200-4ee498e8b403/Untitled.png)
    
                다음 탐색기에서 주소창을 클릭해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c09bbf0b-9031-4dff-80ec-5995cc77b6ac/Untitled.png)
    
                주소를 복사해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8707cb46-2d75-497e-b4af-c0f8d13b4564/Untitled.png)
    
                복사한 경로를 명령 프롬프트 `android-sdk` 뒤에 붙여 넣어주세요. 참고로 아래 그림과 같이 사이에 띄어쓰기가 있어야합니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0b2b2e71-d645-4b4e-9912-8268a6cfcdc2/Untitled.png)
    
                명령어를 실행해 아래와 같이 기호화된 링크를 만들었다고 뜨면 성공입니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/447f5527-cee0-4a38-95ef-ec8e9ea839af/Untitled.png)
    
                <aside>
                💡 Android/Sdk 폴더에 바로가기를 C 드라이브 바로 밑에 생성하는 과정입니다.
    
                </aside>
    
            23. 다시 Android Studio Setup Wizard로 돌아와서, 아래 그림과 같이 Android SDK Location 밑에 폴더 아이콘을 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f15a708c-8f05-4d5b-9b4c-058734307ee2/Untitled.png)
    
                그러면 아래와 같이 경로를 선택하는 창이 뜹니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a7ea5d26-5815-42d4-a2c1-e05c8c1f0e48/Untitled.png)
    
            24. 아래 코드를 복사해서 아래 그림과 같이 경로 선택창의 주소에 붙여 넣고 `OK`를 눌러주세요.
    
                ```bash
                C:\android-sdk
                ```
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ca60021d-a9d3-4d03-a0d4-ea2e36a8b442/Untitled.png)
    
            25. 이제 경로 관련 빨간 에러가 사라지고 활성화된 `Next` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d970a8c9-2e3f-4e48-b03f-bd534f3b942d/Untitled.png)
    
            26. `Next` 를 눌러서 세팅을 진행해 주세요.
    
                ![Screenshot 2022-09-20 040522.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f03635e8-f1ff-4c6a-b91a-85506c8249eb/Screenshot_2022-09-20_040522.png)
    
            27. **License Agreement** 화면이 나옵니다. 왼쪽 밑줄 친 부분을 클릭해 모두 `Accept` 를 눌러주세요
    
                ![Screenshot 2022-09-20 040605.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c88f04d6-c9be-4227-a04d-864194411d00/Screenshot_2022-09-20_040605.png)
    
            28. `Accept` 를 모두 누르고 나면 `Finish` 버튼이 활성화됩니다. 눌러주세요.
    
                ![Screenshot 2022-09-20 041010.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/99167e06-dc28-4302-8b1a-4e7728c397eb/Screenshot_2022-09-20_041010.png)
    
            29. 설치가 진행됩니다. 시간이 다소 소요되니 천천히 기다려주세요!
    
                ![Screenshot 2022-09-20 041021.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0d41bef0-1e89-4670-9de5-2dc25755c5f7/Screenshot_2022-09-20_041021.png)
    
            30. 중간에 `이 앱이 디바이스를 변경할 수 있도록 허락하시겠어요?` 라는 팝업이 뜨면 `예를 선택`해주세요. 
            31. 모든 세팅이 완료되면 `Finish`를 클릭해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/59ef93c2-cb59-4883-9297-7d78b73b3e77/Untitled.png)
    
        - 2) Android Command-line Tools 설치
    
            <aside>
            💡 `Android Command-line Tools`는 Flutter에서 Android에 명령을 내리기 위해 필요합니다.
    
            </aside>
    
            1. 아래와 같이 Android Studio가 실행되면, `More Actions`를 클릭한 뒤 `SDK Manager`를 클릭해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8375a11c-71c1-40ba-ba2a-6d5929d738b6/Untitled.png)
    
            2. `SDK Tools`를 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/31ad9747-b889-4dc8-8b6e-efce041de413/Untitled.png)
    
            3. `Android SDK Command-line Tools (latest)`를 선택한 뒤 `OK`를 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6faa542-270d-495f-bb77-3a7cd2b94cbf/Untitled.png)
    
            4. Dialog가 뜨면 `OK` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5def3ca8-3748-4ae3-bec2-bb118c427cd9/Untitled.png)
    
            5. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/32b5f758-804f-48c1-85a2-6c710144e705/Untitled.png)
    
        - 3) Android Virtual Devices 설치
    
            <aside>
            💡 앱을 개발시 실제 휴대폰을 연결하여 개발을 진행할 때도 있지만, 대부분의 경우 Virtual Device(컴퓨터에 가상의 휴대폰을 띄우는 소프트웨어)를 이용하여 개발합니다.
    
            </aside>
    
            1. `More Actions` → `Virtual Device Manager` (또는 `AVD Manager`)를 선택해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/69051de0-5876-460e-8f11-d77cd168cd75/Untitled.png)
    
            2. 이미 Device 가 있는 분들은 아래 절차를 진행하지 않으셔도 됩니다.
    
                ![Screenshot 2022-09-20 042622.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8527ebd0-b48f-4daf-ba4b-f470669a0c6b/Screenshot_2022-09-20_042622.png)
    
            3. (Device 가 없는 경우) `Create Virtual Device`를 선택해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6e4b33d-a56d-46c0-a041-70ef779cbb25/Untitled.png)
    
            4. 기본적으로 선택되어 있는, `Phone` → `Pixel 2` 디바이스를 `Next` 버튼을 눌러 생성합니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dbb2fe70-9970-491e-a29b-204668dfc7e1/Untitled.png)
    
            5. Release Name `Q`의 `Download`를 클릭하여 가상 기기에 설치할 OS를 다운로드 합니다.
    
                <aside>
                💡 R 버전은 Virtual Device에서 문제가 있다고 해요. 그래서 **Q 버전**으로 진행할게요!
    
                </aside>
    
                ![Screenshot 2022-09-20 042844.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf446b06-b759-4156-af88-5e7de8b1f696/Screenshot_2022-09-20_042844.png)
    
            6. 설치가 완료되면 `Finish` 버튼을 눌러 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a8304bbc-b8d8-42f4-a5f1-0253ab75dd3a/Untitled.png)
    
            7. `Q` 버전의 OS를 선택한 뒤 `Next` 버튼을 눌러 주세요.
            **API Level 29**인지 확인해주세요 :)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0442c914-01e1-42bb-afd2-c02bde2ffb9d/Untitled.png)
    
            8. `Finish` 버튼을 눌러 Virtual Device 설치를 완료 해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c733f125-59d4-423b-93ae-d2434ba46a55/Untitled.png)
    
            9. 성공적으로 추가된 Virtual Device를 확인하시고, 이제 Android Studio는 종료해주세요!
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3215221e-ec74-4c41-ae1c-fa738500babb/Untitled.png)
    
        - 4) Android Licenses
            1. `cmd`를 검색해서 `명령 프롬프트`를 실행해줍니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/383052bb-12ac-468a-8044-7f663e4eaac2/Untitled.png)
    
            2. `flutter doctor`라고 입력한 뒤 엔터를 누릅니다.
            아래와 같이 `Android toolchain`의 좌측에  `[!]` 표시가 되어있습니다.
    
                ![Screenshot 2022-09-20 043142.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/504d7c2f-2725-479d-93b6-889615c328d4/Screenshot_2022-09-20_043142.png)
    
            3. 문제를 해결하기 위해 `flutter doctor --android-licenses`를 복사해서 실행해 줍니다.
            4. 실행하면 라이센스에 대한 동의를 여러번 구하는데, `y`를 입력하고 엔터를 눌러 진행해줍니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/299f3349-5434-4687-9a37-c0b94b9ce408/Untitled.png)
    
            5. `All SDK package licenses accepted` 라는 메시지가 뜨면 완료된 것입니다.
    
                ![Screenshot 2022-09-20 043215.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/915ccacc-e383-48bb-a183-b121b27b5096/Screenshot_2022-09-20_043215.png)
    
            6. `flutter doctor` 를 입력했을 때 아래와 같이 `Android toolchain`의 좌측에 체크(`[v]`) 되었다면 완료!
    
                ![Screenshot 2022-09-20 043631.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64df1c29-20fa-409a-bcfe-fabaa84af36a/Screenshot_2022-09-20_043631.png)
    
                <aside>
                💡 Visual Studio 는 windows 용 앱을 개발할 때 필요한 툴입니다. 지금은 Android, iOS 앱 개발만 진행하므로 신경쓰지 않으셔도 됩니다.
    
                </aside>
    
    - 4. Xcode → 애플 정책상 맥환경에서만 사용 가능합니다 😞
    
        <aside>
        💡 Xcode는 iOS 앱 개발시 필요한 툴로 macOS에서만 설치가 가능하므로 넘어가도록 하겠습니다.
    
        </aside>
    
        <aside>
        💡 [잠깐!] 그러면 window에서는 iOS 앱 개발을 할 수 없는 건가요?
    
        애플의 정책상 iOS 앱은 macOS에서만 개발이 가능합니다. 😂
    
        하지만 Flutter로 작성한 코드는 Android 뿐만 아니라 iOS에서도 이용할 수 있으니 향후 macOS가 생기신다면 기존 소스코드를 그대로 사용하여 iOS 앱도 출시할 수 있습니다!
    
        </aside>
    
    
    **설치를 완료하셨나요~? 잘 설치되었는지 확인해봅시다! ㅎㅎ**
    
    - 최종 설치 확인
        1. `cmd`를 검색해서 `명령 프롬프트`를 실행해줍니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/383052bb-12ac-468a-8044-7f663e4eaac2/Untitled.png)
    
        2. `flutter doctor` 를 입력했을 때 아래와 같이 모든 항목이 체크(`[v]`) 되었다면 설치 완료!
    
            ![Screenshot 2022-09-20 043631.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64df1c29-20fa-409a-bcfe-fabaa84af36a/Screenshot_2022-09-20_043631.png)
    
    
        <aside>
        💡 고생하셨습니다! 원래 개발 환경을 설정하는데 시간이 많이 들어갑니다 😂
        그럼 1주 차 수업 때 뵙도록 하겠습니다 🙂
    
        </aside>
    • MacOS ver 프로그램 설치 방법
    **[프레임워크 설치하기]**
    
    - **1. Flutter** - Android와 iOS 앱을 하나의 코드로 구현할 수 있도록 도와주는 프레임워크
    
        <aside>
        💡 Flutter 설치 과정은 일반적인 프로그램 설치와는 조금 다릅니다. 일반적으로는 `.pkg` 파일등을 실행하거나, `Applications` 폴더에 드래그 & 드롭해 설치를 진행하는데요.
    
        Flutter 설치는 아래와 같은 과정으로 진행됩니다.
    
        1. zip 파일을 다운로드 받아서 적절한 경로에 압축 해제
        2. 압축 해제한 flutter 폴더 경로를 환경변수에 등록 
        (flutter 라는 명령어가 이 경로에 있다는 것을 macOS에게 알리는 것과 같습니다)
        </aside>
    
        <aside>
        💡 시작하기에 앞서 **Apple Sillicon (M1, M2 등)** 을 사용하시는 분들은 **터미널**을 열고 아래 명령어를 입력해주세요 (인텔용 소프트웨어를 실행시킬 수 있는 **Rosetta** 라는 번역기를 설치하는 명령어입니다.)
    
        ```bash
        sudo softwareupdate --install-rosetta --agree-to-license
        ```
    
        ### 터미널이란?
    
        터미널은 마우스 클릭이 아닌 키보드로 컴퓨터에게 명령을 내리는 프로그램 입니다.
    
        ![terminal.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8def37ba-6141-4924-919d-9712a68a8981/terminal.png)
    
        ### 터미널 실행 방법
    
        1. macOS 우측 상단에 `돋보기 🔍` 아이콘을 선택해 주세요. 그러면 아래와 같이 `Spotlight 검색` 창이 뜹니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1d6cc87e-0a8a-4477-bf51-eb6f302c89b5/Untitled.png)
    
        2. `Spotlight`에 `terminal`이라고 검색한 뒤, 아래와 같이 하단에 `터미널` 프로그램이 보이면 엔터를 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6f1ffd0-6481-4c04-b40c-9a530a51fcb2/Untitled.png)
    
        3. 터미널 프로그램이 실행됩니다.
    
            ![스크린샷 2021-12-08 오후 10.11.21.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b52a3237-b714-410f-baed-2372d0c844da/스크린샷_2021-12-08_오후_10.11.21.png)
    
        </aside>
    
        - 1) 다운로드
            1. 먼저 flutter 를 설치할 폴더를 만들겠습니다. Downloads 폴더를 먼저 열어줍니다
    
                ![Screen Shot 2022-09-20 at 12.20.17 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/15210334-edd2-43ce-bf65-9e3004dd5942/Screen_Shot_2022-09-20_at_12.20.17_PM.png)
    
            2. 여기서 `Cmd + ↑(화살표 위 버튼)` 을 눌러주세요. 상위 폴더로 이동하는 단축키입니다. 아래와 같이 유저명과 같은 이름의 폴더로 이동하면 됩니다.
    
                ![Screen Shot 2022-09-20 at 12.22.11 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ae831c6-cec3-4b99-8034-2e3e4fb6cd0a/Screen_Shot_2022-09-20_at_12.22.11_PM.png)
    
            3. 이제 여기에 development 라는 이름의 폴더를 생성하겠습니다. 우측 상단의 `Actions` (점 3개 아이콘) 를 클릭하고 `New Folder` 를 선택해주세요.
    
                ![Screen Shot 2022-09-20 at 12.26.11 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/27a47ddd-37dc-431c-bfd9-04e6809be11a/Screen_Shot_2022-09-20_at_12.26.11_PM.png)
    
                `**development**` 라는 이름으로 생성해줍니다.
    
                ![Screen Shot 2022-09-20 at 12.28.07 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/08feceb1-3e60-41b4-a885-ed44ba1bfa46/Screen_Shot_2022-09-20_at_12.28.07_PM.png)
    
            4. 이제 생성한 폴더에 flutter 압축 파일을 다운로드하겠습니다.
    
                Chrome 브라우저에서 [링크](https://docs.flutter.dev/get-started/install/macos)를 열어주세요.
    
            5. 밑으로 조금 스크롤한 뒤, 파란색 버튼을 클릭해 다운로드를 진행해 주세요.
    
                ![Screen Shot 2022-09-20 at 12.01.03 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0bc321ce-513c-4200-baa8-251d187fbdbc/Screen_Shot_2022-09-20_at_12.01.03_PM.png)
    
                Intel 칩을 사용하는 맥북은 왼쪽을 Apple 칩을 사용하는 맥북은 오른쪽을 선택해 주세요.
    
                <aside>
                💡 **내 mac 이 어떤 프로세서를 사용하는지 알고 싶다면**
    
                좌측 상단 `Apple 로고` 클릭 → `이 Mac에 관하여`를 클릭하여 프로세서가 Intel 칩인지 Apple 칩인지 확인할 수 있습니다.
    
                - Intel chip
    
                    ![processor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e9a82a1-aed3-4ccf-9f4f-d7db45b782a0/processor.png)
    
                - Apple chip
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01d99ad9-718d-4a10-8e52-9e0ea731ec38/Untitled.png)
    
                </aside>
    
                우선 `다운로드` 폴더에 저장하고, 다운로드가 끝나면 파일을 옮기겠습니다. 저장 버튼을 눌러주세요
    
                ![Screen Shot 2022-09-20 at 12.31.23 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3cb97280-feb8-4658-bbc3-ac556a116674/Screen_Shot_2022-09-20_at_12.31.23_PM.png)
    
            6. 바탕화면에서 휴지통 우측에 있는 `다운로드` 폴더를 선택한 뒤 방금 다운로드한 flutter zip 파일을 우리가 만들어준 `development` 폴더로 드래그 앤 드롭합니다.
    
                ![Screen Shot 2022-09-20 at 12.42.27 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4539ac39-e0fb-47e9-b57a-3e23d9b06b83/Screen_Shot_2022-09-20_at_12.42.27_PM.png)
    
                아래와 같이 zip 파일이 해당 폴더로 이동했는지 확인합니다.
    
                ![Screen Shot 2022-09-20 at 12.42.40 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/db5d3cc0-d64e-49ee-9bc9-155c9d96d6ed/Screen_Shot_2022-09-20_at_12.42.40_PM.png)
    
            7. 다운받은 flutter 압축 파일을 실행해 주세요.
    
                ![Screen Shot 2022-09-20 at 12.45.08 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d4cc81c9-dfba-4829-bc01-b6286fb98a0d/Screen_Shot_2022-09-20_at_12.45.08_PM.png)
    
            8. 아래 사진과 같이 압축이 해제되고 `flutter`라는 폴더가 생성 됩니다.
    
                <aside>
                💡 만약 압축이 해제된 폴더 이름이 아래 사진과 다른 경우 `flutter`로 변경해 주시기 바랍니다.
    
                </aside>
    
                ![Screen Shot 2022-09-20 at 12.45.20 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/67baef48-1295-479d-96e8-096fd893ff70/Screen_Shot_2022-09-20_at_12.45.20_PM.png)
    
            9. `flutter` → `bin` 폴더에 들어가볼까요? 
            `dart`, `dart.bat`, `flutter`, `flutter.bat` 등의 파일이 있는 것을 볼 수 있습니다. 이들이 flutter 실행 파일입니다.
    
                ![Screen Shot 2022-09-20 at 12.58.43 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c93cea50-d1db-4fdc-bf47-268ba856efeb/Screen_Shot_2022-09-20_at_12.58.43_PM.png)
    
    
            <aside>
            💡 우리는 이 flutter 실행 파일을 **컴퓨터 어디에서도 모두 접근 가능하게끔**, 즉 어디에서도 `flutter` 라는 명령어를 사용할 수 있게끔 해야 합니다.
    
            환경 변수에 flutter 가 위치한 **폴더 경로를 추가**하면, 시스템의 모든 경로에서 이 flutter 파일에 접근하고, 실행할 수 있습니다.
    
            </aside>
    
        - 2) 환경변수 설정 및 설치
            1. 바탕화면에서 `사과 아이콘` → `이 Mac에 관하여`를 클릭하여 macOS 버전을 확인해 주세요.
    
                ![os version.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/311d4d76-7e17-4987-beee-a4690d4db783/os_version.png)
    
            2. macOS 버전을 확인한 뒤, 해당하는 명령어를 복사해 주세요.
    
                <aside>
                💡 mac OS 버전 순서
    
                ![Untitled.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f864e554-d9b0-4eb3-8c3e-70f0f952ace6/Untitled.png)
    
                **macOS Mojave** 이하 버전을 사용하는 경우 **설정하는 파일이 다릅**니다.
    
                </aside>
    
                - macOS 카타리나(Catalina) `**이상**`버전 명령어 (최신 버전은 여기에요!)
    
                    ```bash
                    echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.zshrc && source ~/.zshrc
                    ```
    
                - macOS 모하비(Mojave) **`이하`** 버전 명령어
    
                    ```bash
                    echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.bash_profile && source ~/.bash_profile
                    ```
    
    
                <aside>
                💡 어떤 명령어인지 궁금하신 분들은 아래 설명을 참고해 주세요.
    
                1. `~/development/flutter/bin` 폴더에 있는 flutter 파일을 어디서든 실행할 수 있도록 등록(환경변수에 등록)
    
                    (~ 는 유저명과 같은 이름의 Home 폴더를 의미합니다)
    
                    > macOS 모하비 버전에서는 `.bash_profile`이라는 파일에 등록하고 이후 버전에선 `.zshrc` 파일에 등록하기 때문에 명령어가 다릅니다.
                    > 
    
                    ```bash
                    echo 'export PATH="$PATH:$HOME/Developments/flutter/bin"' >> ~/.zshrc
                    ```
    
                2. 설정 반영
    
                    ```bash
                    source ~/.zsh
                    ```
    
                </aside>
    
            3. 터미널에 단축키 `Cmd + v` 또는 마우스 우클릭하여 `붙여넣기`를 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8a6e79fa-b9a7-4591-8701-d31a13c59735/Untitled.png)
    
                아래와 같이 붙여넣으면 아래와 같이 나오고, 엔터(enter)를 눌러 실행해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.17.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9324f3b5-9935-4f32-9cd0-7620d8caac47/Screen_Shot_2022-09-20_at_2.17.02_PM.png)
    
            4. 다음 명령어는 flutter의 버전을 확인하는 명령어입니다. 아래 명령어를 복사해 주세요.
    
                ```bash
                flutter --version
                ```
    
                터미널에 붙여넣고 실행해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.19.36 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bf87e55f-450b-41b2-a62f-da725622a58f/Screen_Shot_2022-09-20_at_2.19.36_PM.png)
    
            - [잠깐] 혹시 다음 팝업이 뜨면 `설치` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5e62eddb-6ee7-4664-a88b-51476ae199c2/Untitled.png)
    
                1. 사용권 계약 팝업이 뜨면 `동의` 버튼을 눌러주세요.
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d8f0617d-4b21-45ec-b5e2-ba1aea1b67f8/Untitled.png)
    
                2. 아래와 같이 설치가 완료되면 `완료` 버튼을 눌러주세요.
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/122d8290-ec39-4221-b23b-b25a0bfc93ef/Untitled.png)
    
        - 3) 설치 확인
            1. 터미널 창에 Flutter 버전을 확인하는 아래 명령어를 붙여넣고 실행해 주세요.
    
                ```bash
                flutter --version
                ```
    
                실행하면 `Building flutter tool...` 이라고 출력되고 잠시 후 아래와 같이 Flutter 버전이 출력되면 Flutter 설치 완료!
    
                ![Screen Shot 2022-09-20 at 2.19.15 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/97a87ad4-67c9-4a28-ab0c-6428707eb0de/Screen_Shot_2022-09-20_at_2.19.15_PM.png)
    
                <aside>
                💡 위 이미지에선 Flutter 3.3.2 버전이 출력되는데 시간이 지나면 최신 버전으로 업데이트 되어 버전이 다를 수 있습니다. 전혀 문제 없으니 그대로 진행해주세요.
    
                </aside>
    
            2. 다음 명령어를 복사해 터미널에 붙여넣고 실행해 주세요.
    
                <aside>
                💡 Flutter 개발하는데 필요한 항목들의 상태를 확인하는 명령어 입니다.
    
                </aside>
    
                ```bash
                flutter doctor
                ```
    
                ![_flutter doctor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2cae54a1-1763-4e82-880f-37c3f0cfccdb/_flutter_doctor.png)
    
                <aside>
                💡 Android 앱을 만드는데 필요
    
                - Android Studio
                - Android SDK
    
                iOS 앱을 만드는데 필요
    
                - Xcode
                - CocoaPods
                </aside>
    
                위 프로그램들을 하나씩 설치해 보도록 하겠습니다. 
    
    
    **[에디터 설치하기]**
    
    - 2. Visual Studio Code
    
        <aside>
        💡 Visual Studio Code (줄여서 VSCode) 는 앞으로 실제 코드를 작성할 편집 툴입니다.
    
        Flutter 개발은 Android Studio와 VSCode 둘 중 원하는 툴을 사용하여 진행할 수 있습니다. VSCode가 더 가볍기 때문에 앞으로 수업은 VSCode를 활용해 진행하도록 하겠습니다.
    
        </aside>
    
        - 1) VSCode 설치
            1. [링크](https://code.visualstudio.com/download)에 접속해 주세요.
            2. 애플 아이콘 하단에 있는 Mac 버튼을 클릭해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/732ff958-a930-4446-9b3a-2eae43005ef4/Untitled.png)
    
            3. `다운로드` 폴더에 저장해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fc103a3e-b9a5-400a-bcb2-fffcbbb63e28/Untitled.png)
    
            4. 바탕화면에 다운로드 폴더를 클릭한 뒤 `Finder에서 열기` 버튼을 클릭해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e0efbc3d-75c5-4299-b0ad-94bbcfbfd511/Untitled.png)
    
            5. 다운받은 `VSCode-darwin-universal.zip` 파일을 실행해 압축을 풀어주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/292fa7d8-ecb7-426b-913e-78e335908468/Untitled.png)
    
            6. 압축이 풀리고 생성된 `Visual Studio Code` 파일을 드래그해서 왼쪽 `응용 프로그램`에 떨어뜨려 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d6388951-bc4a-4d04-8937-2a0783830a6a/Untitled.png)
    
            7. 화면 우측 상단 `돋보기 🔍` 아이콘을 클릭한 뒤 `visual` 이라고 검색해 주세요. 그리고 하단에 `Visual Studio Code`가 보이면 엔터를 눌러 실행해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/72e585cc-a9b0-4a46-932d-c725469e69cd/Untitled.png)
    
            8. 아래와 같은 팝업이 뜨면 `열기` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1b1713a2-0f9a-4bbb-848b-38f76eef5292/Untitled.png)
    
            9. 그러면 아래와 같이 VSCode가 실행되면 설치 완료!
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64675e1d-55d2-41b9-8a4e-027d93b08eda/Untitled.png)
    
                <aside>
                💡 우측 하단에 한국어로 변경하라는 팝업이 뜹니다.
                하지만 VSCode 사용법이나 대부분의 개발 자료는 영어로 되어 있기 때문에, 가급적 적용하지 않기를 권장 드립니다. (수업 자료도 영어 버전으로 되어있어요!)
    
                해당 알람을 다시 보지 않으려면 `우측 톱니바퀴 ⚙` 아이콘을 누른 뒤 `Don't Show Again`을 선택해 주세요. (만약 사라져서 버튼을 누르지 못했다면 다음번에 눌러주세요!)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1bbcc25a-bac1-412e-a0ee-a8302ce700e2/Untitled.png)
    
                </aside>
    
        - 2) Extension 설치
    
            <aside>
            💡 VSCode는 Flutter 뿐만 아니라 다양한 개발을 모두 할 수 있는 통합 에디터입니다. VSCode에서 Flutter 앱 개발을 하려면 VSCode에 Extension 탭에서 아래 목록의 Extension 들을 설치해야 합니다.
    
            **Flutter** : VSCode에서 Flutter 개발 환경 지원
            **Dart** : Flutter 개발 시 사용되는 Dart 개발 환경 지원
    
            </aside>
    
            1. 좌측에 extension 아이콘(동그라미)을 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fea993b-6a6f-4d4f-9354-fa2a33248122/Untitled.png)
    
            2. `flutter` 라고 검색한 뒤, 해당 익스텐션을 선택하고 `install` 버튼을 눌러 설치해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c51b36d-aa33-4ad7-abde-6a334e7bb2cb/Untitled.png)
    
            3. 위 `flutter` 익스텐션을 설치하면서 `dart` 익스텐션도 일반적으로 함께 설치가 됩니다.
    
                `dart` 라고 검색하신 뒤 혹시 설치가 안되었다면 해당 익스텐션도 같이 설치해주세요. `uninstall`이라고 뜨신다면 이미 설치가 된 것이니 넘어가시면 됩니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1ea5cf2-200a-451f-83e9-330a5d4c533e/Untitled.png)
    
    
    **[IDE 설치하기]** ** IDE : 소프트웨어 애플리케이션*
    
    <aside>
    💡 **MacOS 에서는 안드로이드 환경, iOS 환경 모두에서 코드를 돌려볼 수 있습니다!**
    
    아래의 상황에 따라 원하는 원하시는 애뮬레이터를 골라 설치해보세요! ㅎㅎ
    
    **애뮬레이터 : 컴퓨터에서 가상으로 스마트폰 OS를 돌리는 프로그램*
    
    1. **안드로이드와 ios 모두 확인하고 싶은 경우**
    
        → Android Studio 와 Xcode 모두 설치합니다.
    
    2. **안드로이드만 확인하고 싶은 경우**
    
        → Android Studio만 설치합니다.
    
    3. **ios만 확인하고 싶은 경우**
    
        → Xcode만 설치합니다.
    
    
    **[잠깐!] Xcode 는 용량을 크게 차지하니 주의해주세요! :)**
    
    - 용량이 부족하다면 Android Studio 만 설치하셔도 괜찮습니다. 
    이러면 iOS Simulator 는 사용할 수 없지만, 개발에는 큰 지장이 없습니다! ㅎㅎ
    </aside>
    
    - 3. Android Studio
        - 1) Android Studio 설치
            1. [링크](https://developer.android.com/studio)에 접속한 뒤, `Download Android Studio` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a9598ce0-fa0e-4c98-acc0-3222c2a34adb/Untitled.png)
    
            2. 약관이 뜨면 아래로 쭉 스크롤 해주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/95e19502-1158-45c4-9d84-9afdf2df50c7/Untitled.png)
    
            3. Intel 칩을 사용하는 맥북은 왼쪽 `Mac with Intel chip`을 Apple 칩을 사용하는 맥북은 오른쪽 `Mac with Apple chip`을 선택해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.29.56 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/39c5a846-f4a1-4a38-b5c4-7ce788a51996/Screen_Shot_2022-09-20_at_2.29.56_PM.png)
    
                <aside>
                💡 좌측 상단 `Apple 로고` 클릭 → `이 Mac에 관하여`를 클릭하여 Intel 칩인지 Apple 칩인지 확인할 수 있습니다.
    
                - Intel chip
    
                    ![processor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e9a82a1-aed3-4ccf-9f4f-d7db45b782a0/processor.png)
    
                - Apple chip
    
                    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01d99ad9-718d-4a10-8e52-9e0ea731ec38/Untitled.png)
    
                </aside>
    
            4. 다운로드 팝업이 뜨면 `저장` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c495cdb-daa3-4529-a9c0-66c42c002404/Untitled.png)
    
            5. 바탕화면 하단에 휴지통 좌측에 있는 `다운로드` 폴더를 클릭한 뒤 `Finder`에서 열기 버튼을 클릭해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b129d044-3975-4ff7-ad8b-2dc69078b22f/Untitled.png)
    
            6. 다운로드가 완료된 `android-studio~~.dmg` 파일을 실행해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/021331d6-475a-4ddf-ae6a-c53aa3078490/Untitled.png)
    
            7. 아래와 같은 창이 뜨면 왼쪽에 `Android Studio`를 드래그해서 `Applications`에 떨어뜨려주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c72763c1-1eec-4770-a954-bfebbeb7ada1/Untitled.png)
    
            8. 설치가 완료되면 좌측 상단에 빨간 X를 눌러서 아래 화면을 종료해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4a1637d-6518-4aa8-b93e-e2cd52e064cd/Untitled.png)
    
                <aside>
                💡 바탕화면에 아래 사진과 같은 파일이 생겼다면 휴지통으로 드래그해 삭제하셔도 됩니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfa41421-9575-429e-af13-3d999315cf89/Untitled.png)
    
                </aside>
    
            9. 우측 상단에 `돋보기🔍` 아이콘을 클릭하고, 팝업창이 뜨면 `android`라고 입력해 주세요. 그리고 아래와 같이 `Android Studio`가 자동완성으로 뜨면 엔터를 눌러 실행해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a6bad6de-8aa9-4896-a9d8-5372870f604e/Untitled.png)
    
            10. 아래와 같이 팝업이 뜨면 `열기` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/328ff0d3-085c-4bfb-ad59-f82c67659e6d/Untitled.png)
    
            11. 아래와 같은 창이 뜬다면 `OK` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd9a712e-8000-48fd-9764-9bdec2a8aa7f/Untitled.png)
    
            12. `Import Android Studio Settings` 팝업이 뜨면 `Do not import settings`를 선택하신 뒤 `OK` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/00c3d439-86fd-4334-b944-4b93b2633ea0/Untitled.png)
    
            13. 안드로이드 스튜디오 사용 데이터를 구글에 전달하여 사용성 개선에 참여하고 싶다면 `Send usage statistics to Google`을 선택해주시고, 그렇지 않은 경우 `Don't send`를 선택해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.35.27 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7910e8fc-9433-4cfb-a277-0b2c2dbed8e0/Screen_Shot_2022-09-20_at_2.35.27_PM.png)
    
            14. 다음과 같은 안드로이드 스튜디오 설정 화면이 나오면 `Next` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/748967d6-689f-4aaf-af7c-f8c7da147582/Untitled.png)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/90290328-7729-429e-bd5f-1b6fd2b48311/Untitled.png)
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/43a8a218-0bc9-4ddb-a5d4-658650c7cbb9/Untitled.png)
    
            15. `Finish` 버튼을 눌러 Android SDK 설치를 진행해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f8728adf-ae30-4237-aac8-da26ed3af750/Untitled.png)
    
            16. **License Agreement** 화면이 나온다면 왼쪽 밑줄 친 부분을 클릭해 모두 `Accept` 를 눌러주세요
    
                ![Screenshot 2022-09-20 040605.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c88f04d6-c9be-4227-a04d-864194411d00/Screenshot_2022-09-20_040605.png)
    
            17. `Accept` 를 모두 누르고 나면 `Finish` 버튼이 활성화됩니다. 눌러주세요.
    
                ![Screenshot 2022-09-20 041010.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/99167e06-dc28-4302-8b1a-4e7728c397eb/Screenshot_2022-09-20_041010.png)
    
            18. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/caef348a-0104-4904-81ac-1f3d62a6e53a/Untitled.png)
    
            19. 아래와 같은 화면이 뜨면 Android Studio 설치 완료!
    
                ![Screen Shot 2022-09-20 at 2.43.20 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c9e3e9a-a781-482d-91a5-92deb90993d8/Screen_Shot_2022-09-20_at_2.43.20_PM.png)
    
        - 2) Android Command-line Tools 설치
    
            <aside>
            💡 `Android Command-line Tools`는 Flutter에서 Android에 명령을 내리기 위해 필요합니다.
    
            </aside>
    
            1. Android Studio에서 `More Actions`를 선택한 뒤 `SDK Manager`를 선택해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.43.43 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/765f159a-d30c-452b-82db-56925a758f31/Screen_Shot_2022-09-20_at_2.43.43_PM.png)
    
            2. 그러면 아래와 같이 `Preferences for New Projects` 팝업이 뜨면 `SDK Tools` 탭을 선택 → `Android SDK Command-line Tools (latest)` 선택 → `Apply` 를 선택해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.44.40 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/86cdd3d2-0c6d-4bf0-9044-3b1640723ad3/Screen_Shot_2022-09-20_at_2.44.40_PM.png)
    
            3. 팝업이 뜨면 `OK` 버튼을 클릭해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.45.33 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5b5466b0-3eda-4a24-b326-5e8f361b0aac/Screen_Shot_2022-09-20_at_2.45.33_PM.png)
    
            4. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/179110f8-4fef-49e6-a781-8672450baef3/Untitled.png)
    
        - 3) Android Virtual Devices 설치
    
            <aside>
            💡 앱을 개발시 실제 휴대폰을 연결하여 개발을 진행할 때도 있지만, 대부분의 경우 Virtual Device(컴퓨터에 가상의 휴대폰을 띄우는 소프트웨어)를 이용하여 개발합니다.
    
            </aside>
    
            1. `More Actions` → `Virtual Device Manager` (또는 `AVD Manager`)를 선택해 주세요.
    
                ![Screen Shot 2022-09-20 at 2.46.47 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e72abe9e-f703-405d-ada1-08eafbdb80e1/Screen_Shot_2022-09-20_at_2.46.47_PM.png)
    
            2. 이미 Device 가 있는 분들은 아래 절차를 진행하지 않으셔도 됩니다.
    
                ![Screen Shot 2022-09-20 at 2.51.09 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8d5b8d36-7e78-4ef2-911d-d5a5dc7ef4a4/Screen_Shot_2022-09-20_at_2.51.09_PM.png)
    
            3. (Device 가 없는 경우) `Create Virtual Device...`를 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/35588fa8-02ce-4a17-82b2-e4ebd4cc1924/Untitled.png)
    
            4. 하드웨어를 선택하는 화면이 나오면 `Next`를 눌러서 기본으로 설정된 `Pixel 2` 휴대폰을 설치하도록 하겠습니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fe1e55c6-ea12-4d75-b619-e41587b9e51b/Untitled.png)
    
            5. 휴대폰에 설치할 Android OS를 선택하는 화면입니다. `Q` 옆에 있는 `Download` 버튼을 클릭하여 OS를 다운로드해 주세요.
    
                <aside>
                💡 R 버전은 Virtual Device에서 문제가 있다고 해요. 그래서 **Q 버전**으로 진행할게요!
    
                </aside>
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4ca338ba-a53c-448a-84fa-be1735599608/Untitled.png)
    
            6. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/58629f13-a9e6-4ddd-ac93-acc9441fbfab/Untitled.png)
    
            7. `Q` 옆에 `Download` 버튼이 사라졌습니다. 우측에 현재 선택된 OS 버전이 29인지 확인한 뒤 `Next` 버튼을 눌러주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6d8fade8-aa03-46b7-b417-c55b3b7b2342/Untitled.png)
    
            8. `Finish` 버튼을 눌러 Virtual Device 설치를 완료해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23afd0e5-79c6-426e-9ee3-690f6998ba43/Untitled.png)
    
            9. 아래와 같이 `Pixel 2 API 29` 라는 Virtual Device가 추가되었습니다.
    
                이제 좌측 상단에 빨간 버튼을 눌러 창을 종료해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1134aa4e-c890-4437-9820-264c40413b8a/Untitled.png)
    
            10. 하단에 Android Studio 아이콘을 우클릭한 뒤 `종료` 버튼을 눌러를 Android Studio를 종료해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ffcab44-bef5-43d2-b10a-7d0c1d1b1a86/Untitled.png)
    
                <aside>
                💡 Android Studio를 클릭한 상태에서 단축키(`Cmd + Q`)를 누르셔도 됩니다.
    
                </aside>
    
        - 4) Android Licenses
            1. 터미널에서 `flutter doctor`라고 입력한 뒤 엔터를 누릅니다.
            아래와 같이 `Android toolchain`의 좌측에  `[!]` 표시가 되어있습니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cfa0d817-282d-4edd-b7e6-0178ad077990/Untitled.png)
    
            2. 문제를 해결하기 위해 `flutter doctor --android-licenses`를 복사해서 터미널에 붙여 넣고 실행해 주세요. 실행시 라이센스에 대한 동의를 여러번 구하는데, `y`를 입력하고 엔터를 눌러 진행해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d0492492-b2e0-4205-8875-71fd7c3bc5ce/Untitled.png)
    
            3. 모든 동의가 완료되면 `All SDK package licenses accepted` 라고 뜹니다.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ef18b6df-8d01-40f7-9657-8b7cfbda449e/Untitled.png)
    
            4. 마지막으로 터미널에 `flutter doctor` 를 입력했을 때 아래와 같이 `Android toolchain`, `Android Studio` 가 체크 완료되면 완료!
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bfb47eed-12cb-418d-86f0-9044be87cfba/Untitled.png)
    
    - 4. Xcode
    
        <aside>
        💡 iOS 앱을 개발하는데 필요한 Xcode를 설치해 보도록 하겠습니다.
    
        </aside>
    
        1. [링크](https://apps.apple.com/us/app/xcode/id497799835)를 클릭해 열어주세요.
        2. 아래와 같은 팝업이 띄면 `App Store 열기` 버튼을 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dd013e1b-8bc8-415c-8826-7daf6173016b/Untitled.png)
    
        3. `App Store`가 실행되고 `Xcode`가 아래와 같이 뜨면 `받기` 버튼을 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63f8b6d9-4a9d-4c7d-918c-c50d2bf3299c/Untitled.png)
    
        4. `설치` 버튼으로 변하면 한 번 더 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12804375-a9da-4223-9e13-730cfc461d7c/Untitled.png)
    
        5. 만약 App Store에 Apple ID로 로그인이 되어있지 않아 아래와 같이 창이 뜨는 경우, 로그인을 진행해 주세요.
    
            <aside>
            💡 계정이 없다면 `Apple ID 생성`을 눌러 가입을 진행해주신 뒤, 로그인을 진행해 주세요.
    
            </aside>
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/001c2d44-028a-4b49-962a-86773edc12c1/Untitled.png)
    
        6. 로그인을 완료하면 설치가 진행 됩니다.
    
            <aside>
            💡 Xcode 설치 시간은 인터넷 상황에 따라 다르지만 보통 1시간 30분 ~ 2시간 정도 소요 됩니다. 😂
    
            </aside>
    
        7. 설치가 완료되면 `열기` 버튼을 눌러주세요!
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/adcee5ed-0d4a-4782-8879-c9945d7d9e49/Untitled.png)
    
        8. 라이센스 동의 팝업이 뜨면 `Agree`를 선택해 주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2338d57a-712a-482b-a577-27921cb7f9f1/Untitled.png)
    
        9. 아래와 같이 암호를 입력하는 창이 뜨면 컴퓨터 시작 비밀번호를 입력한 뒤 `확인` 버튼을 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f3cf1771-5843-48fb-96e5-f6d1b30877bd/Untitled.png)
    
        10. 만약 Xcode가 실행이 안되면, 다시 `열기` 버튼을 눌러주세요.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/adcee5ed-0d4a-4782-8879-c9945d7d9e49/Untitled.png)
    
    
        12. 아래와 같은 창이 뜨면 Xcode 설치완료!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c7c37404-84c3-4289-8133-7ead2d049c85/Untitled.png)
    
        1. 설치를 완료했으니`AppStore`와 `Xcode`를 종료해 주세요.
            - 하단에 App Store를 우클릭하여 `종료`를 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e4b2242-4ea0-404d-9da3-4ad29444212e/Untitled.png)
    
            - 하단에 Xcode를 우클릭하여 `종료`를 선택해 주세요.
    
                ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ac0c0aeb-75bf-420a-8b63-591333ea792b/Untitled.png)
    
        2. 이제 Homebrew 를 설치하도록 하겠습니다.
    
            <aside>
            💡 Homebrew 는 맥에서 소프트웨어를 설치 삭제할 수 있는 패키지 관리자입니다.
            아래에서 brew install 명령어를 쓰기 위해 미리 설치해줍니다.
    
            </aside>
    
            아래 명령어를 복사해 터미널에 붙여넣고 엔터를 눌러주세요.
    
            ```bash
            /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
            ```
    
            ![Screen Shot 2022-09-20 at 3.11.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3828ed3-a3e0-4880-aa76-d312f986ecd1/Screen_Shot_2022-09-20_at_3.11.02_PM.png)
    
        3. 다음으로 CocoaPods을 설치해 보도록 하겠습니다.
    
            <aside>
            💡 CocoaPods은 다른 사람이 만든 코드를 가져올 때 필요한 프로그램으로 Xcode와 함께 iOS 앱 개발시 필요합니다.
    
            </aside>
    
            아래 명령어를 복사해 터미널에서 붙여넣고 엔터를 눌러주세요.
    
            ```bash
            brew install cocoapods
            sudo gem install cocoapods
            ```
    
        4. 아래와 같이 비밀번호를 입력하는 창이 나오면, 컴퓨터 시작시 입력하는 비밀번호를 입력한 뒤 엔터를 눌러주세요.
    
            <aside>
            💡 참고로 키보드를 눌러도 화면에 입력되는 모습은 보이지 않으니, 입력한 뒤 엔터를 누르면 됩니다.
    
            비밀번호를 틀린 경우, `Ctrl + C`를 누르면 명령이 종료되고, 다시 명령어를 붙여넣고 실행해주세요.
    
            </aside>
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e74269ff-349a-4ae5-addf-2b5d3344dd54/Untitled.png)
    
        5. 명령이 정상적으로 실행되면 아래와 같이 뜹니다.
    
            ![Screen Shot 2022-09-20 at 3.12.24 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/06f113d2-7915-4d06-ba45-5a2f9c3b521c/Screen_Shot_2022-09-20_at_3.12.24_PM.png)
    
        6. 아래 명령어를 복사해 터미널에서 실행해 주세요.
    
            ```bash
            flutter doctor
            ```
    
            그러면 아래와 같이 `Xcode` 설치가 완료된 것을 보실 수 있습니다.
    
            ![Screen Shot 2022-09-20 at 3.13.31 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dbcf4554-1f37-4303-a74e-f828f447b5ef/Screen_Shot_2022-09-20_at_3.13.31_PM.png)
    
    
    **설치를 완료하셨나요~? 잘 설치되었는지 확인해봅시다! ㅎㅎ**
    
    - 최종 설치 확인
    
        터미널에서 `flutter doctor`라고 검색한 뒤 아래와 같이 화면이 나온다면 모든 설치가 완료하신 것입니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/610c4f60-5d66-4495-baf7-7a4b82fa0d6d/Untitled.png)
    
        <aside>
        💡 다운로드 폴더에 있는 파일들은 모두 삭제하셔도 됩니다.
    
        </aside>
    
        <aside>
        💡 고생하셨습니다! 원래 개발 환경을 설정하는데 시간이 많이 들어갑니다 😂
        그럼 1주 차 수업 때 뵙도록 하겠습니다 🙂
    
        </aside>

    03. Flutter 이해하기

    • 1) 레고 같이 조립할 수 있는 위젯(Widget)

      출처 : [pixabay](https://pixabay.com/vectors/lego-toys-blocks-puzzle-6390233/)

      출처 : pixabay

    • 2) Android Material & iOS Cupertino

      코드스니펫을 복사해서 새 탭에서 열면 Flutter에서 위젯을 소개하는 공식 문서가 보입니다.

    ![Widget Catalog.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d3775be8-a1eb-49e0-b5a1-84e49f827086/Widget_Catalog.png)
    
    <aside>
    💡 **머터리얼 위젯(Material Widget)**은 **Android**에서 사용되는 기본 화면 구성 요소를 Flutter에서 재현한 위젯입니다.
    
    **쿠퍼티노 위젯(Cupertino Widget)**은 **iOS**에서 사용되는 화면 구성 요소를 Flutter에서 재현한 위젯입니다.
    
    Flutter는 특정 플랫폼에 종속되지 않은 고유의 디자인을 입힌 **커스텀 위젯(Custom Widget)**도 쉽게 만들 수 있습니다.
    
    </aside>
    
    <aside>
    💡 **Material**, **Cupertino** 그리고 **Custom 위젯** 중 어떤 방법을 사용하든 **사용성만 해치지 않는다면** 앱을 출시하실 수 있습니다.
    
    </aside>
    
    <aside>
    💡 Flutter에서 위젯이 어떻게 사용되는지 로그인 페이지를 만들며 배워봅시다.
    
    </aside>

    05. 프로젝트 준비

    • 1) Flutter 프로젝트 생성하기

      1. 바탕화면에 flutter 폴더를 만들어주세요.

        (이 때, 플러터 프로젝트 경로에 한글이 오지 않도록 주의해주세요!)

        Screen Shot 2022-08-28 at 7.40.46 PM.png

      2. Visual Studio Code(VSCode)를 실행해 주세요.

        Untitled

      3. ViewCommand Palette 버튼을 클릭해주세요.

        Untitled

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

        Untitled

      5. Application을 선택해주세요.

        Untitled

      6. 프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔 flutter 폴더를 선택한 뒤 Select a folder to create the project in 버튼을 눌러 주세요.

        Untitled

      7. 프로젝트 이름을 hello_flutter로 입력해주세요.

        Untitled

        macOS의 경우 아래와 같은 팝업이 뜨면 확인 버튼을 눌러주세요.

        Untitled

        중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요.

        Untitled

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

        Untitled

        왼쪽 폴더 구조를 살짝 보고 가도록 하겠습니다.

        Directory.png

        lib : 주로 코딩하는 폴더

        pubspec.yaml : 라이브러리 및 설정을 하는 폴더


        android : Android 프로젝트 폴더

        ios : iOS 프로젝트 폴더

        web : Web 프로젝트 폴더

      9. 아래 main.dart 코드스니펫을 복사해서 기존 코드를 모두 지운 뒤, main.dart 파일에 붙여 넣고 저장해주세요.

        • [코드스니펫] main.dart

            import 'package:flutter/material.dart';
          
            void main() {
              runApp(MyApp());
            }
          
            class MyApp extends StatelessWidget {
              const MyApp({Key? key}) : super(key: key);
          
              @override
              Widget build(BuildContext context) {
                return MaterialApp(
                  debugShowCheckedModeBanner: false,
                  home: Scaffold(
                    appBar: AppBar(),
                  ),
                );
              }
            }
      10. 다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.

        analysis_options.yaml 파일을 열고, 아래 코드스니펫을 복사해서 24번째 라인 뒤(rules 뒤)에 붙여 넣고 저장해 주세요. (들여쓰기를 꼭 맞춰주세요)

        • [코드스니펫] analysis_options.yaml

          
                prefer_const_constructors: false
                prefer_const_literals_to_create_immutables: false
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
    
        <aside>
        💡 상세 내용은 아래를 참고해 주세요.
    
        - 어떤 의미인지 궁금하신 분들을 위해
    
            `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
    
            파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
    
            12번째 라인에 마우스를 올리면 아래와 같이 설명이 보입니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
    
            위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수(변하지 않는 수)로 선언하라는 힌트입니다. 화면을 새로고침 할 때(화면 내 값이 변할 때마다 화면은 새로고침 됩니다) 상수로 선언된 위젯들은 새로고침을 할 필요가 없어서 성능상 이점이 있습니다.
    
            아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
    
            지금은 학습 단계이니 눈에 띄지 않도록 `analysis_options.yaml` 파일에 아래와 같이 설정하여 힌트를 숨기도록 하겠습니다.
    
            ```dart
                prefer_const_constructors: false
                prefer_const_literals_to_create_immutables: false
            ```
    
        </aside>
    • 2) VSCode Dart 세팅

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

        Untitled

      2. 아래와 같이 dart recommend라고 검색한 뒤 Dart: Use Recommended Settings를 선택해 주세요. 그러면 저장 시 자동 줄 정렬 기능과 같이 편의 기능 설정이 적용됩니다.

        Untitled

  • 3) Emulator 실행하기

    1. ViewCommand Palatte를 선택해주세요.

      Untitled

    2. launch를 입력한 뒤 Flutter: Launch Emulator를 선택해 주세요.

      Untitled

    3. android 에뮬레이터를 선택해주세요.

      Untitled

      macOS의 경우, Start iOS Simulator를 선택하실 수도 있습니다.

      Untitled

      macOS의 경우 아래와 같은 팝업이 뜬다면 확인 버튼을 눌러주세요.

      Untitled

    4. 잠시 기다리시면, 에뮬레이터가 실행 됩니다. 아래와 같이 완전히 부팅이 끝날 때 까지 기다려주세요!

      emulator.png

      Untitled

  • 4) 실행하기

    1. VSCode 우측 상단에 버튼을 누르고 Run Without Debugging 버튼을 클릭해주세요.

      Untitled

    2. 첫 번째 실행은 시간이 다소 소요되니 잠시만 기다려주세요!

      드디어 첫 번째 앱 화면을 띄웠습니다! 축하드립니다👏👏

      Untitled

      console.png

      • 다시 켜는 방법이 궁금하다면?

        1. VS Code 의 좌측 아래 설정 버튼을 누르고, Settings 를 눌러주세요

          Screen Shot 2022-08-28 at 9.22.58 PM.png

        2. 검색창에 widget inspect 라고 검색하시고, Dart: Open Dev Tools 옵션을 never 에서 flutter 로 변경해주세요.

          Screen Shot 2022-08-28 at 9.24.45 PM.png

        3. 그러면 앱을 실행할 때 아래와 같이 오른쪽에 Widget Inspector 탭이 열립니다.

          Screen Shot 2022-08-29 at 4.03.00 AM.png

  • 05. 로그인 페이지 만들기

    • 최종 완성 이미지

      Screen Shot 2022-08-29 at 6.28.42 AM.png

    • 1) Scaffold & Text

      lib 폴더 밑에 있는 main.dart 파일에서 코딩을 해보도록 하겠습니다.

      Group 90.png

      14번째 줄을 보면 home이라고 되어있습니다. 이곳에 첫 화면에 보여줄 위젯을 넣어주는데, 여기에 Scaffold라는 위젯이 사용되고 있습니다.

      출처 - [https://pixabay.com/photos/construction-work-framework-670278/](https://pixabay.com/photos/construction-work-framework-670278/)

      출처 - https://pixabay.com/photos/construction-work-framework-670278/

      15번째 줄에 Scaffold의 appBar 영역에 AppBar() 위젯이 들어가 에뮬레이터에 상단 영역을 차지하고 있습니다.

      Untitled

      1. Scaffold에 body 영역에 Text 위젯을 추가해보겠습니다. 16번째 줄에 body: Text("Hello Flutter"), 라고 입력한 뒤 저장(Ctrl/Cmd + S)하면 에뮬레이터에 Hello Flutter가 나타납니다.

        Untitled

      2. 16번째 줄에 텍스트 크기를 키워보도록 하겠습니다.

        먼저 16번째 줄 “Hello Flutter" 뒤에 콤마(,)를 추가한 뒤 저장(Ctrl/Cmd + S)해 주세요.

        Group 91.png

        그리고 17번째 줄 맨 뒤에서 아래 자동완성 단축키를 눌러주세요.

        Untitled

        그러면 Text 위젯이 가진 속성 목록이 나옵니다. 이 중에서 style을 이용하면 뭔가 꾸밀 수 있을 것 같다는 느낌이 옵니다. style을 선택해 주세요.

        Untitled

        그러면 위와 같이 style 속성이 추가되는데 여기에 무엇을 넣어야하는지 궁금하실 겁니다.

        이런 경우, style 속성에 마우스를 올리면 여기에 넣는 값의 타입을 볼 수 있습니다. 아래 이미지를 보면 style의 경우TextStyle을 받는다고 적혀있습니다.

        Untitled

        TextStyle을 조금만 타이핑해 보면 자동완성으로 아래와 같이 TextStyle을 추천해 줍니다. 아래 이미지에서 첫 번째 항목을 선택하면 TextStyle까지만 완성해주고, 네 번째 항목을 선택하면 TextStyle()까지 완성해 주므로 네 번째 항목을 선택해 줍니다.

        Untitled

        그리고 저장해주면 아래와 같이 18번째 줄에 style: TextStyle(),이 추가 됩니다.

        Untitled

        TextStyle에 어떤 값을 넣을 수 있는지 자동완성 기능으로 보도록 하겠습니다. 18번째 줄 TextStyle의 소괄호 사이에서 자동완성 단축키(Ctrl + Space / Option + Esc)를 눌러주세요.

        Untitled

        위와 같이 추천된 속성 중에 fontSize를 이용하면 텍스트 크기를 변경할 수 있을 것 같습니다. fontSize를 선택해 주세요.

        Untitled

        fontSize에는 숫자를 넣으시면 됩니다(double은 실수를 의미하는 자료형입니다). 28을 입력한 뒤 저장해보면 에뮬레이터에서 폰트 크기가 커진 것을 볼 수 있습니다.

        Untitled

    • 2) Column & TextField

      Text 위젯 밑에 ID를 입력받는 입력창을 추가해 보도록 하겠습니다. 세로 방향으로 위젯들을 나열하려면 Column 위젯을 사용해야합니다.

      1. 16번째 줄에 Text 위젯을 클릭한 뒤, 왼쪽에 있는 전구(💡) 아이콘을 클릭해 주세요.

        Untitled

        그러면 아래와 같이 Text 위젯을 손쉽게 다른 위젯을 감싸거나 추출할 수 있도록 도와주는 리펙터(Refactor) 기능이 나타납니다.

        Untitled

      2. Wrap with Column을 선택하여 Text 위젯을 Column 위젯으로 감싸주겠습니다.

        Untitled

        그러면 Text 위젯이 Column 위젯의 children 속성으로 들어간 것을 볼 수 있습니다.

        Untitled

      3. 21번째 줄 맨 뒤에, TextField(),를 추가해주세요.

        Untitled

        그리고 저장(Ctrl/Cmd + S)해주면 아래와 같이 에뮬레이터에 텍스트 입력란이 생성됩니다.

        Untitled

        • iOS 에뮬레이터에서 가상 키보드 보이도록 설정하는 법

          실제 모바일 환경에서는 가상 키보드가 화면에 나타나는 부분을 고려하여 화면을 만들어야 합니다. 이럴 때 아래와 같은 방법을 이용해주세요.

          iOS 에뮬레이터에서 처음에 텍스트 입력이 안되는 경우, 에뮬레이터를 선택한 뒤 상단에 I/OKeyboardConnect Hardware Keyboard를 선택해 주세요.

          Untitled

          그리고 Hello Flutter 텍스트 밑에 파란 줄을 클릭하고 키보드를 입력하면 입력이 잘 됩니다.

          Untitled

          키보드를 보이게 하고 싶은 경우, 상단에 I/OKeyboardConnect Hardware Keyboard를 다시 선택하여 체크를 해제하면 키보드를 눌러도 입력되지 않고, 에뮬레이터에 나타난 가상 키보드를 입력할 수 있습니다. 키보드를 내리려면 완료 버튼을 눌러주세요.

          Untitled

          mac OS 의 경우 Cmd + K 를 눌러 가상 키보드를 올리고 내릴 수 있습니다

      4. TextField에 이메일을 입력하도록 이름표를 달아 봅시다. 아래 이미지와 같이 TextFielddecoration속성에 InputDecoration()을 넣고, labelText: "이메일",를 추가해주세요.
        (쉼표를 잘 찍어서 코드가 정렬되도록 합니다)

        Untitled

        에뮬레이터 상에 “이메일”이라는 label이 생성 되었습니다.

        Untitled

      5. 22 ~ 26번째 줄에 TextField를 그대로 복사해서 비밀번호 입력란을 만들어 주세요.

        Untitled

        Untitled

        값을 입력해보면 비밀번호가 그대로 보입니다. 입력된 비밀번호를 보이지 않도록 만들어봅시다.

        Untitled

      6. 28번째 줄에 obscureText: true, 라는 속성을 주면 비밀번호가 안보이게 됩니다.
        TextField 위젯이 이미 속성들을 가지고 있기 때문에 우리는 해당 속성에 적절한 값만 넣어주면 됩니다!

        Untitled

    • 3) Button

      로그인 버튼을 만들어 봅시다.

      1. 33번째 줄(Column 내부)에 elev라고 입력하면 자동완성으로 ElevatedButton()이 추천되고, 해당 항목을 선택해 주세요.

        Untitled

        아래와 같이 완성이 되고, 버튼에 필수로 입력해야하는 속성 두 가지가 뜹니다.

        Group 92.png

        33번째 줄 앞쪽 onPressed에 마우스를 올려보면, 필수로 전달하라는 의미인 required가 보이고 전달해야하는 타입은 함수를 의미하는 void Function()이라고 적혀 있습니다.

        Untitled

      2. 아직 문법을 배우지 않았으므로, 아래와 같이 onPressed() {}를 입력하고, child에 Text("로그인"), 이라고 입력해주세요.

        Untitled

        그리고 저장(Ctrl/Cmd + S)를 누르면 아래와 같이 정렬됩니다.

        Untitled

        에뮬레이터 상에 버튼이 추가된 것을 확인할 수 있습니다.

        Untitled

    • 4) AppBar

      15번째 줄에 AppBar() 위젯에 18 ~ 21번째 줄의 Text를 넣어보도록 하겠습니다.

      Untitled

      1. 15번째 줄 AppBar()의 소괄호 사이 title:이라고 입력해 주세요.

        Untitled

      2. 18 ~ 21번째 줄의 Text 위젯을 15번째 줄에 title: 뒤로 이동해 주세요.

        Untitled

        그리고 저장(Ctrl/Cmd + S)을 눌러주시면 앱바에 title 영역으로 Hello Flutter가 이동됩니다.

        Untitled

      3. AppBarcenterTitle: true, 라고 넣어 두 플랫폼에서 모두 중앙 정렬이 되도록 만들어줍시다.

        Untitled

        이제 Android에서도 중앙 정렬이 됩니다.

        Untitled

    • 5) Padding

      에뮬레이터 상에서 보면 TextField가 기기 외곽에 너무 붙어 있는 것 같아 여백을 추가해 보도록 하겠습니다.

      Untitled

      Scaffoldbody 속성에 위젯들은 아래와 같이 배치되어 있습니다.

      Untitled

      Column 위젯을 Padding이라는 위젯으로 감싸면 여백을 추가할 수 있습니다.

      Untitled

      1. 22번째 줄에 Column 위젯을 클릭한 뒤, 왼쪽 전구(💡) 아이콘을 선택해 주세요. 그리고 Wrap with Padding을 선택해 주세요.

        Untitled

      2. Column 위젯을 Padding 위젯으로 감싸졌고, 어느정도 여백을 줄지 설정하는 23번 째 줄에 padding 속성이 추가되었습니다.

        Untitled

      3. 23번 째 줄에 8.0을 16으로 변경한 뒤 저장해보면 에뮬레이터에 내부에 여백이 추가된 것을 볼 수 있습니다.

        Untitled

    • 6) Container

      로그인 버튼을 화면 가로로 가득 채우도록 키워보겠습니다.

      Untitled

      1. ElevatedButton 자체에는 width 속성이 없고, 부모 위젯에 크기를 이용해 조절 할 수 있습니다.

        37번째 줄에 ElevatedButton을 클릭한 뒤 왼쪽 전구(💡) 아이콘을 클릭하고 Wrap with Container를 선택해 주세요.

        Untitled

        그러면 아래와 같이 Container 위젯이 ElevatedButton 위젯을 감싸게 됩니다.

        Untitled

      2. 다음 Container 위젯에 width: double.infinity,라고 추가해주세요. 그러면 Container의 폭이 부모를 가득 채우게 되고, 버튼도 함께 최대 크기로 늘어납니다.

        Untitled

      3. 비밀번호를 입력하는 TextField와 로그인 버튼 사이에 여백을 추가해 보도록 하겠습니다.

        아래 이미지와 같이 Container에 margin: EdgeInsets.only(top: 24),라고 추가해 주세요.

        Untitled

    • 7) Image & SingleChildScrollView

      이메일 입력란 상단에 이미지를 넣어보겠습니다.

      1. 26번째 줄에 아래와 같이 Image.network 위젯을 추가해주세요.

         Image.network("https://i.ibb.co/nngK6j3/startup.png"),

        Screen Shot 2022-08-29 at 4.54.23 AM.png

        이미지에 크기를 지정하지 않았으므로, 원본 사진의 크기로 들어가게 되고, 이 상태에서 이메일 입력란을 클릭하여 가상 키보드를 올려보면 아래와 같이 하단 화면이 짤린다고 에뮬레이터에 표시됩니다.

        Screen Shot 2022-08-29 at 4.55.48 AM.png

      2. 키보드가 올라와 화면이 가려지는 경우, 스크롤을 할 수 있도록 만들어봅시다. 22번째 줄에 Padding 위젯을 클릭한 뒤 왼쪽에 전구(💡) 아이콘을 선택해 주세요. Wrap with widget...을 선택해 Padding 위젯을 다른 위젯으로 감싸주겠습니다.

        Screen Shot 2022-08-29 at 4.59.11 AM.png

        아래와 같이 Padding 위젯이 widget이라는 익명의 위젯으로 감싸집니다.

        Screen Shot 2022-08-29 at 5.00.13 AM.png

      3. 22번째 줄에 widgetSingleChildScrollView로 변경한 뒤 저장해 주세요.

        Screen Shot 2022-08-29 at 5.00.35 AM.png

        이제 에뮬레이터에서 화면을 이메일을 클릭해도 화면이 넘치지 않고, 스크롤을 할 수 있습니다.

        Screen Shot 2022-08-29 at 5.01.14 AM.png

      4. 이미지를 적절한 크기로 조절해보도록 하겠습니다. width: 81,Image.network 위젯에 추가해 주세요.

        Screen Shot 2022-08-29 at 5.02.30 AM.png

      5. 이미지를 Padding으로 감싸 다른 위젯과 간격을 추가해 보겠습니다. 27번째 줄의 Image를 선택한 뒤 오른쪽 전구(💡) 아이콘을 누르고 Wrap with Padding을 선택해 주세요.

        Screen Shot 2022-08-29 at 5.04.59 AM.png

        아래와 같이 Padding 위젯으로 Image.network 위젯이 감싸집니다.

        Screen Shot 2022-08-29 at 5.06.16 AM.png

        28번째 줄에 EdgeInsets8.032로 변경한 뒤 저장해주면 완성!

        Screen Shot 2022-08-29 at 5.06.45 AM.png

    • 최종 완성 코드

        import 'package:flutter/material.dart';
      
        void main() {
          runApp(MyApp());
        }
      
        class MyApp extends StatelessWidget {
          const MyApp({Key? key}) : super(key: key);
      
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: Scaffold(
                appBar: AppBar(
                  centerTitle: true,
                  title: Text(
                    "Hello Flutter",
                    style: TextStyle(fontSize: 28),
                  ),
                ),
                body: SingleChildScrollView(
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      children: [
                        Padding(
                          padding: const EdgeInsets.all(32),
                          child: Image.network(
                            "https://i.ibb.co/nngK6j3/startup.png",
                            width: 81,
                          ),
                        ),
                        TextField(
                          decoration: InputDecoration(
                            labelText: '이메일',
                          ),
                        ),
                        TextField(
                          obscureText: true,
                          decoration: InputDecoration(
                            labelText: '비밀번호',
                          ),
                        ),
                        Container(
                          width: double.infinity,
                          margin: EdgeInsets.only(top: 24),
                          child: ElevatedButton(
                            onPressed: () {},
                            child: Text("로그인"),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            );
          }
        }

    06. Flutter 공부 방법

    • Flutter 위젯(Widget) 살펴보기

  • 에러 메시지 잘 읽고 구글링 잘하기

    Screen Shot 2022-08-29 at 5.31.19 AM.png

  • 커뮤니티 가입하기

    1. 질문하기 전에 스스로 Google에 검색해서 해결하려고 노력해보기!
      (검색 키워드 자체를 모른다면 빠르게 물어보기)
    2. 문제 발생시 에러 로그도 함께 올리기!
      Flutter는 에러 발생시 Debug Console에 에러 메세지를 보여줍니다. 먼저 에러 메세지를 먼저 구글에 검색해보고, 해결이 잘 안된다면 에러 로그와 함께 질문해주세요.
      (에러 메세지가 없으면 다른 개발자들이 도와주기가 힘들어요!)
  • 프로그래밍 언어 다트(Dart) 배우기

  • 07. Dart 문법

    • DartPad

        ```dart
        void main() {
          for (int i = 0; i < 5; i++) {
            print('hello ${i + 1}');
          }
        }
        ```
    
    - 실행 순서
    
        <aside>
        💡 `main`은 Dart에서 처음 시작 시 호출하는 약속된 ****함수입니다. 
        앞의 void 자리는 함수가 반환하는 값의 자료형을 표시하는 것입니다. 비워둬도 괜찮습니다.
    
        </aside>
    
        ```dart
        void main() {}
    
        main2 () {}
    
        String main3 () {
            return "Hello";
        }
        ```
    
        ![Screen Shot 2022-08-29 at 5.45.09 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/da41bae5-2351-4cea-9f8b-654647bc7de5/Screen_Shot_2022-08-29_at_5.45.09_AM.png)
    
    - `//` : 주석으로 컴퓨터가 읽지 않습니다. 주로 개발하면서 메모할 때 사용합니다.
    - `print()` : print의 소괄호 안쪽에 값을 넣으면 오른쪽 `Console`에 값이 출력 됩니다. 정상적으로 잘 작동하는지 확인(디버깅) 할 때 사용합니다.
    - `;` : Dart에서는 마지막에 세미콜론(semicolon)을 찍어줍니다.
    - 에러 로그 읽는 법
    
        <aside>
        💡 아래 이미지를 보면 3번 째 줄에 마지막 세미콜론이 빠졌습니다. 이 상태로 `Run`을 하게되면 우측에 에러 메세지가 나옵니다.
    
        3:27 (3번째 줄 27번째 칸)에 에러가 발생했군요!
    
        **에러 메세지를 보면 문제가 발생하는 위치와 해결 방법을 알 수 있습니다!** 앞으로 에러가 발생한다면 에러 메세지 부터 확인해 주세요 🙂
    
        </aside>
    
        ![Screen Shot 2022-08-29 at 5.46.28 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4575b348-6506-4f62-b826-4de805032f07/Screen_Shot_2022-08-29_at_5.46.28_AM.png)
    
    
    <aside>
    💡 DartPad에서 `Reset` 버튼을 누르면, 변경한 내용이 초기화 됩니다.
    
    ![Screen Shot 2022-08-29 at 5.48.19 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/96a5d3ae-eaa8-4847-b75b-e1c11d0ac9af/Screen_Shot_2022-08-29_at_5.48.19_AM.png)
    
    </aside>
    • 1) 변수

      1. 변수 만들기

        Untitled

        1) 자료형 (= 바구니에 담을 수 있는 값의 종류)

        var : 처음 담긴 값으로 타입이 지정됩니다.

        String : 문자만 담을 수 있습니다.

        String? : 문자 또는 비어있는(null) 상태일 수 있습니다.

        const: 처음에 변수를 선언하며 담은 값을 변경할 수 없습니다.

        final : 선언하고 나중에 값을 담을 수 있으나, 한 번 담으면 변경할 수 없습니다.

         void main() {
           var a = 1;
           String b = "hello world";
           String? c = null;
           const d = 1;
           final e;
           e = 2;
             print(a);
         }

        2) 변수명 (= 바구니 이름)

    • 2) 조건문

        if (bool1) {
            // bool1이 **true**면 실행
        } else {
            // bool1이 **false**면 실행
        }
        if (bool1) {
            // bool1이 **true**면 실행
        } else if (bool2) {
            // bool1이 **false**이고, bool2가 **true**이면 실행
        } else if (bool3) {
            // bool1과 bool2가 **false**이고, bool3가 **true**이면 실행
        } else {
            // bool1, bool2, bool3가 모두 **false**이면 실행
        }
    • 3) 반복문

      _for.png

      • 반복문 구성

        1 : int i = 0i라는 변수가 0으로 시작합니다. (한 번만 실행됩니다)

        2 : i < 5i의 값이 5보다 작은지 조건을 확인합니다. (false → 반복문 종료 / true → 3번)

        3 : 중괄호 안쪽 영역 → 반복해 실행하는 코드들이 들어있습니다.

        4 : i++i값을 1만큼 증가 시키고 2번으로 흐름이 다시 넘어갑니다.

    • 4) 함수(function)

        1. 함수의 호출 & 실행 순서

          아래 코드 스니펫을 복사해서 DartPad에서 실행해보세요!

          • [코드스니펫] Dart 함수

             void main() {
               print("1. 시작");
            
               say();
            
               print("4. 종료!");
             }
            
             void say() {
               print("2. 안녕");
               print("3. Hello");
             }
          • 함수의 생김새

            Untitled

            • say라고 적혀있는 부분이 함수의 이름입니다.
            • 중괄호({ }) 안쪽 영역이 함수가 가진 실행 코드들 입니다.
          • 함수 호출 방법

            Untitled

            Untitled

        1. 함수의 표현 방법

    • 5) 클래스(Class)

        1. 클래스 생김새

          class 클래스이름 {
          
          }
        1. 클래스의 구성 요소

          class Bread {
             // 생성자 함수 (클래스명과 똑같음. 클래스의 객체가 생성될 때 호출되는 함수)
             Bread(String core) {
                 content = core; // 전달 받은 core를 content에 넣어줍니다.
             }
          
             // Bread 클래스가 가진 content 속성 (클래스 내의 변수)
             String? content;
          
             // Bread 클래스가 가진 getDescription 메소드 (클래스 내의 함수)
             String getDescription() {
             return "맛있는 $content빵입니다."; // 맛있는 팥빵입니다.
           }
          }
        1. 인스턴스(Instance)

        1. 상속(extends)

          Untitled

        1. Dart의 모든 것은 Class

          _docs.png

    08. 숙제 - Movie Reviews 만들기

    • 최종 완성 모습
    [Simulator Screen Recording - iPhone 13 - 2022-08-29 at 06.19.22.mp4](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a674a8d1-f48c-4b42-86fd-58d07abbf343/Simulator_Screen_Recording_-_iPhone_13_-_2022-08-29_at_06.19.22.mp4)
    
    ## 사용한 위젯
    
    Scaffold
    
    AppBar
    
    Text
    
    IconButton
    
    Column
    
    Padding
    
    TextField
    
    Icon
    
    Divider
    
    Expanded
    
    ListView.builder
    
    Card
    
    Stack
    
    Image.network
    
    Container
    
    Text
    • 1) 실습 준비

      • 1) Flutter 프로젝트 생성하기

        1. ViewCommand Palette 버튼을 클릭해주세요.

          Untitled

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

          Untitled

        3. Application을 선택해주세요.

          Untitled

        4. 프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔 flutter 폴더를 선택한 뒤 Select a folder to create the project in 버튼을 눌러 주세요.

        5. 프로젝트 이름을 movie_reviews 로 입력해주세요.

          Screen Shot 2022-06-20 at 5.17.14 AM.png

        6. 프로젝트가 생성되면 아래 main.dart 코드스니펫을 복사해서 기존 코드를 모두 지운 뒤, main.dart 파일에 붙여 넣고 저장해주세요.

          • [코드스니펫] main.dart

              import 'package:flutter/material.dart';
            
              void main() {
                runApp(MyApp());
              }
            
              class MyApp extends StatelessWidget {
                const MyApp({Key? key}) : super(key: key);
            
                @override
                Widget build(BuildContext context) {
                  return MaterialApp(
                    debugShowCheckedModeBanner: false,
                    home: HomePage(), // 홈페이지 보여주기
                  );
                }
              }
            
              class HomePage extends StatelessWidget {
                const HomePage({Key? key}) : super(key: key);
            
                @override
                Widget build(BuildContext context) {
                  // 음식 사진 데이터
                  List<Map<String, dynamic>> dataList = [
                    {
                      "category": "탑건: 매버릭",
                      "imgUrl": "https://i.ibb.co/sR32PN3/topgun.jpg",
                    },
                    {
                      "category": "마녀2",
                      "imgUrl": "https://i.ibb.co/CKMrv91/The-Witch.jpg",
                    },
                    {
                      "category": "범죄도시2",
                      "imgUrl": "https://i.ibb.co/2czdVdm/The-Outlaws.jpg",
                    },
                    {
                      "category": "헤어질 결심",
                      "imgUrl": "https://i.ibb.co/gM394CV/Decision-to-Leave.jpg",
                    },
                    {
                      "category": "브로커",
                      "imgUrl": "https://i.ibb.co/MSy1XNB/broker.jpg",
                    },
                    {
                      "category": "문폴",
                      "imgUrl": "https://i.ibb.co/4JYHHtc/Moonfall.jpg",
                    },
                  ];
            
                  // 화면에 보이는 영역
                  return Scaffold();
                }
              }
        7. 다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.

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

          • [코드스니펫] analysis_options.yaml

            
                  prefer_const_constructors: false
                  prefer_const_literals_to_create_immutables: false
      • 2) 에뮬레이터 실행하기

        1. VSCode 우측 하단에 Chrome (web-javascript)를 클릭해주세요.

          Untitled

        2. Start Pixel 2 API 29 mobile emulator를 선택해주세요.

          Untitled

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

          Untitled

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

          Untitled

          에뮬레이터에 아래와 같이 흰 화면이 나오면 정상적으로 실행이 완료된 것입니다!

          Untitled

    • 2) AppBar 만들기

    ![simulator_screenshot_429E6845-74A6-4FFB-9C5B-31AC91DCAA3E.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed279ba5-e234-405a-a87c-16dca9a0de65/simulator_screenshot_429E6845-74A6-4FFB-9C5B-31AC91DCAA3E.png)
    
    ## 사용한 위젯
    
    Scaffold
    
    AppBar
    
    IconButton
    
    Text
    
    Icon
    • 3) Body 만들기
    ![simulator_screenshot_52382C76-EC68-46A1-8F79-EF5D14DD85BE.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb77a00c-232a-483d-83ff-b377c084364b/simulator_screenshot_52382C76-EC68-46A1-8F79-EF5D14DD85BE.png)
    
    ## 사용한 위젯
    
    Column
    
    Padding
    
    TextField
    
    Icon
    
    Divider
    
    Expanded
    
    ListView.builder
    
    Card
    
    Stack
    
    Image.network
    
    Container
    
    Text

    • 숙제 답안 (main.dart)

        import 'package:flutter/material.dart';
      
        void main() {
          runApp(MyApp());
        }
      
        class MyApp extends StatelessWidget {
          const MyApp({Key? key}) : super(key: key);
      
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              home: HomePage(), // 홈페이지 보여주기
            );
          }
        }
      
        class HomePage extends StatelessWidget {
          const HomePage({Key? key}) : super(key: key);
      
          @override
          Widget build(BuildContext context) {
            // 영화 제목, 사진 데이터
            List<Map<String, dynamic>> dataList = [
              {
                "category": "탑건: 매버릭",
                "imgUrl": "https://i.ibb.co/sR32PN3/topgun.jpg",
              },
              {
                "category": "마녀2",
                "imgUrl": "https://i.ibb.co/CKMrv91/The-Witch.jpg",
              },
              {
                "category": "범죄도시2",
                "imgUrl": "https://i.ibb.co/2czdVdm/The-Outlaws.jpg",
              },
              {
                "category": "헤어질 결심",
                "imgUrl": "https://i.ibb.co/gM394CV/Decision-to-Leave.jpg",
              },
              {
                "category": "브로커",
                "imgUrl": "https://i.ibb.co/MSy1XNB/broker.jpg",
              },
              {
                "category": "문폴",
                "imgUrl": "https://i.ibb.co/4JYHHtc/Moonfall.jpg",
              },
            ];
      
            // 화면에 보이는 영역
            return Scaffold(
              appBar: AppBar(
                elevation: 0,
                backgroundColor: Colors.white,
                centerTitle: false,
                iconTheme: IconThemeData(color: Colors.black),
                title: Text(
                  "Movie Reviews",
                  style: TextStyle(
                    color: Colors.black,
                    fontSize: 28,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                actions: [
                  IconButton(
                    onPressed: () {},
                    icon: Icon(Icons.person_outline),
                  )
                ],
              ),
              body: Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: TextField(
                      decoration: InputDecoration(
                        hintText: "영화 제목을 검색해주세요.",
                        border: OutlineInputBorder(
                          borderSide: BorderSide(color: Colors.black),
                        ),
                        suffixIcon: IconButton(
                          icon: Icon(Icons.search),
                          onPressed: () {},
                        ),
                      ),
                    ),
                  ),
                  Divider(height: 1),
                  Expanded(
                    child: ListView.builder(
                      itemCount: dataList.length,
                      itemBuilder: (context, index) {
                        String category = dataList[index]['category'];
                        String imgUrl = dataList[index]['imgUrl'];
      
                        return Card(
                          child: Stack(
                            alignment: Alignment.center,
                            children: [
                              Image.network(
                                imgUrl,
                                width: double.infinity,
                                height: 200,
                                fit: BoxFit.cover,
                              ),
                              Container(
                                width: double.infinity,
                                height: 200,
                                color: Colors.black.withOpacity(0.5),
                              ),
                              Text(
                                category,
                                style: TextStyle(
                                  color: Colors.white,
                                  fontSize: 36,
                                ),
                              ),
                            ],
                          ),
                        );
                      },
                    ),
                  ),
                ],
              ),
            );
          }
        }

    전체 목록 바로가기

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

    다음 강의 바로가기

    2주차 - 다양한 위젯을 활용해 화면 그리기


    Copyright ⓒ TeamSparta All rights reserved.

     

    경축! 아무것도 안하여 에스천사게임즈가 
    새로운 모습으로 재오픈 하였습니다.

    어린이용이며, 설치가 필요없는 브라우저 게임입니다.

    http://s1004games.com

     

    s1004games.com (에스천사게임즈닷컴) | 천사같은 천사개의 게임을 개발합니다.

    s1004games.com (에스천사게임즈닷컴) © 2023. All rights reserved. V-1.5.6

    s1004games.com

     

    [스파르타코딩클럽] 게임개발 종합반 - 5주차

    [수업 목표]

    1. 런칭을 위해 필요한 주변 기술 학습
    2. 소리넣기, 광고, 출시까지 직접 경험하기
    3. 만들 수 있는 게임의 범위 알기

    [목차]


    01. 오늘 배울 것

    • 1) 5주차 수업의 목표와 범위

      • 이번주는 편하게 따라하시면 되는 것이랍니다.

      • 런칭을 하려면 준비해야 할 몇 가지를 더 다뤄볼게요!

        → 게임에 음악 입히기

        → 스플래시 화면 붙이기

        → 광고 붙이기

        → 무료 에셋 구경하기

      • 오늘은 머리 아픈 것 없습니다! 찬찬히 따라하면 끝~! 😎

    • 2) 오늘 만들 것

    • 3) 4주차 르탄이 카드 뒤집기 게임 작동 확인하기

      → 잘 작동하네요! 이제, 시작합니다!

      Untitled

    02. 시작화면 만들기

    • 1) 시작씬을 만들어봅니다.

      1. Scenes 폴더 → StartScene 만들기

        Untitled

      2. 화면 색 바꾸기

        1. 카메라 색 : rgb ⇒ 90, 90, 225으로 설정 해줍니다.

          Untitled

      3. 이미지 넣기

        1. 오른쪽 클릭 - > UI → Image 클릭 → 이름은 rtans라고 지정해 줍니다.

        2. 크기는 width: 400, height: 400으로 설정 합니다.

          Untitled

      4. 타이틀 만들기

        • [코드스니펫] 폰트 다운로드

            [https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/game_new/week04/BMHANNA_11yrs_ttf.ttf](https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/game_new/week04/BMHANNA_11yrs_ttf.ttf)

          BMHANNA_11yrs_ttf.ttf

        1. Assets → fonts 폴더 생성 → 폰트 파일 넣어줍니다.

        2. font-size: 70, y: 350으로 설정 해줍니다.

        3. width: 760, height: 200으로 설정 해줍니다.

        4. text에 르탄이를 찾아라 라고 적어 줍니다.

        5. 폰트를 적용 해주고, 폰트 사이즈는 70으로 적용시켜 줍니다.

        6. 가운데 정렬로 맞춰줍니다.

        7. color: (255, 255, 255, 255)로, 컬러는 흰색을 맞춰 줍니다.

          Untitled

      5. 버튼 만들기

        1. Canvas → 오른쪽 클릭 → UI → Image 만들어서

        2. 이름은 startBtn로 변경

        3. 사이즈와 위치는 width: 360, height: 120, PosY: -400 로 적용해 줍니다.

        4. add component → shadow 컴포넌트 추가

        5. distance는 x: 10, y: -10, 색 (color)은 250, 250, 0 , 200로 설정 해줍니다.

        6. Title 복사( ctrl+d )해서 startBtn 아래에 두기.

        7. PosY: 0, width: 360, height: 120, color: (0, 0, 0, 255)로 설정 해줍니다.

          Untitled

    • 2) 르탄이 이미지 애니메이션 넣기

      1. Animations 폴더 → rtans 만들기 → rtans 이미지에 붙이기

        Untitled

      2. 애니메이션 만들기

        → 애니메이션 더블클릭하고, 르탄이 이미지 전체(0~7)를 끌어다놓기

        → 오른쪽 바를 끌어서 40에 맞추기. 르탄이가 빠르게 회전하는 것을 확인!

        Untitled

    • 3) startBtn 에 기능 만들기

      1. startBtn.cs 만들기

         using UnityEngine.SceneManagement;
        
         public void startGame()
         {
             SceneManager.LoadScene("MainScene");
         }
      2. 버튼 컴포넌트 만들고 클릭 붙이기

        Untitled

      3. 잘 되는지 확인하기

        → 완료! 이제 시작해볼까요?

        Untitled

    03. 스플래시 이미지 만들기

    • 1) 스플래시 이미지란?

      • 예) 카카오톡 시작할 때 뜨는 이미지 같은 것이랍니다.

        Untitled

      • 이 씬은 우리가 만드는 게 아니라, 유니티에서 몇 가지 세팅만으로 만들 수가 있어요.

        → 즉, Scene 만들어 SceneManager.Loadscene.. 하는 게 아닙니다. 😉

      • 다만! 무료버전에서는 유니티 로고가 함께 노출된답니다.

        → 아쉽게도 로고를 지우려면 Pro 버전을 구매해야..

        → 한편, made with Unity 라고 쓰여진 게임들은 모두 무료버전의 유니티로 제작했음을 우리도 알 수 있겠죠! 자, 그럼 직접 세팅해볼까요?

    • 2) 이미지 세팅하기

      1. Edit → Project settings → Player → Splash Image로 접근하기

        Untitled

      2. Preview를 눌러 확인하기

        → Splash Style : 배경 / 로고 색

        → Animation : Dolly - 잠깐 커짐 / Static - 일정 크기

        Untitled

      3. Images 폴더에 로고 준비하기

        → Mesh Type : Full Rect ⇒ Apply 클릭
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e8396268-b03d-47a3-946e-d62074c3fc89/Untitled.png)
    
    4. 스플래시 화면 세팅하기
    
        → Animation : Static 으로 맞추기
    
        → Draw Mode : All Sequential 로 맞추기
    
        → 이미지 : select → spartaMsg 클릭
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b10603a3-0039-4afd-8dcf-ce3c35c2c45a/Untitled.png)
    
    5. preview 눌러서 확인하기
    
        → 잘 되네요!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6bd7db93-b51d-4669-817e-f171f56fd666/Untitled.png)

    04. 소리 & 배경음악 넣기

    • 1) 사운드 구상하기

      1. 배경음악 : 게임이 시작하면 배경음악이 나오면 좋겠어요!
      2. 뒤집을 때 : 카드 뒤집을 때 뒤집는 소리가 나면 좋겠죠!
      3. 맞췄을 때 : 카드 두 장이 같을 때 소리가 나면 좋겠죠!
    • 2) 음원 준비하기

      1. 다운받아서 적당한 폴더에 풀고 → 파일을 들어봅니다.

      2. Unity에 Sounds 폴더를 만들고 가져다놓기

        Untitled

    • 3) 소리 재생하기: 카드 뒤집기

      1. cardAudioSource 컴포넌트 달기

        Untitled

      2. card.cs 준비하기

        → AudioClip, AudioSource 받기

         public AudioClip flip;
         public AudioSource audioSource;

        Untitled

      3. 적절한 순간에 재생되게 하기

        → flip을 한번만 재생되게 하기

         public void openCard()
         {
             audioSource.PlayOneShot(flip);
        
             anim.SetBool("isOpen", true);
             transform.Find("front").gameObject.SetActive(true);
             transform.Find("back").gameObject.SetActive(false);
        
             if (gameManager.I.firstCard == null)
             {
                 gameManager.I.firstCard = gameObject;
             }
             else
             {
                 gameManager.I.secondCard = gameObject;
                 gameManager.I.isMatched();
             }
         }
    • 4) 소리 재생하기: 카드 맞췄을 때

      1. gameManager.cs 에서 같게 세팅하기

        → AudioSource 컴포넌트 붙이고

        → AudioSource, AudioClip 받고

         public AudioSource audioSource;
         public AudioClip match;

        Untitled

      2. 재생하기

         public void isMatched()
         {
             string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
             string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        
             if (firstCardImage == secondCardImage)
             {
                 audioSource.PlayOneShot(match);
        
                 firstCard.GetComponent<card>().destroyCard();
                 secondCard.GetComponent<card>().destroyCard();
        
                 int cardsLeft = GameObject.Find("cards").transform.childCount;
                 if (cardsLeft == 2)
                 {
                     endTxt.SetActive(true);
                     Time.timeScale = 0.0f;
                 }
             }
             else
             {
                 firstCard.GetComponent<card>().closeCard();
                 secondCard.GetComponent<card>().closeCard();
             }
        
             firstCard = null;
             secondCard = null;
         }
    • 5) 조금 특별하게 - audioManager 만들기

      1. gameManager처럼, audioManager를 만들어 줍니다.

        → 스크립트도 audioManager.cs 를 만들어 붙여주세요!

        Untitled

      2. AudioSource 컴포넌트를 붙이고, 준비합니다.

         public AudioSource audioSource;
         public AudioClip bgmusic;

        Untitled

      3. Start() 에서 실행해줍니다.

        → 이번엔 계속 실행될 예정이니, 아래 두 줄을 넣어주세요!

         audioSource.clip = bgmusic;
         audioSource.Play();

    05. 빌드하기

    • 1) 마켓에 올리기 전 확인해야 하는 설정들

      • EditPreferenceExternal Tools 체크

        • Android 빌드를 위해서는 JDK, NDK, SDK 설정이 필수

          → Unity Hub에서 1주차에 설치시 함께 완료했습니다!

          • 만약 설치하지 않았다면?

            → Unity Hub 를 통해서 추가 설치가 가능합니다.

            https://s3-us-west-2.amazonaws.com/secure.notion-static.com/44ea4f0c-df2f-4cb6-aca5-16e844e2162f/Untitled.png

            https://s3-us-west-2.amazonaws.com/secure.notion-static.com/55c2489d-9403-4255-a2f0-2d55ef4887d1/Untitled.png

        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/040f86ea-7f52-4eb0-8246-abac147403e2/Untitled.png)
    
    - `Edit` → `Project Settings` → `Player`
        - Company Name 과 Product Name, 그리고 Version 을 적절히 입력해주세요
    
            → Company Name : `SpartaCodingClub`
    
            → Product Name : `findRtan`
    
            → Version : `1.0`
    
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a869b62f-360f-4d94-81a7-9dc081c8d1fd/Untitled.png)
    
    - Icon
        - Select아이콘을 누르고 spartaMsg를 선택.
    - Resolution and Presentation
        - 안드로이드로 바꾸고, Landscape Right, Left를 꺼줍니다. (우리는 세로형 게임!)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a464db26-3bfb-4f7e-b47d-339f98e05d62/Untitled.png)
    
    - Other Settings
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/94bb555c-663b-4a1f-93ff-04dc2173df80/Untitled.png)
    
        - 안드로이드 마켓에 배포하려면 64 bit 지원이 필수가 되었기 때문에 Scripting Backend 를 IL2CPP 로 변경합니다
        - Target Architectures 에서 ARM64 를 체크하도록 합니다.
    - Publishing Settings
    
        <aside>
        💡 Keystore란? 안드로이드에서 이 앱을 배포할 수 있는 권리!
    
        </aside>
    
        1. `Keystore Manager` 눌러서 만들기
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f2d9ddf3-c84b-43a2-9a0d-83b737d301f6/Untitled.png)
    
        2. Keystore → Create New → Anywhere 클릭
    
            → `spartakey` 로 바탕화면에 저장해주세요
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1309b92a-3604-44b0-a683-f86d890844aa/Untitled.png)
    
        3. 그 외 입력하기
    
            → Alias : `spartakey`
    
            → Password : `123456` (간단하게 설정해주세요)
    
            → Add Key 클릭!
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89834ffa-9f34-4d09-8c6b-1527a294d72e/Untitled.png)
    
        4. `Yes` 를 클릭
    
            ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2d783adf-1ab1-41a5-933d-45b4e9e6347a/Untitled.png)
    • 2) 원하는 OS 대상으로 Switch Platform 하고 빌드하기

      1. Build Settings 항목 클릭

        Untitled

      2. Scenes in Build에 씬 추가하기

        Untitled

      3. Switch Platform 눌러주기 (한참 시간이 걸립니다)

        Untitled

      4. 다시 화면 사이즈를 정해주고

        Untitled

      5. 다시, 우측 하단 Build를 눌러서 빌드해보기 (한참 시간이 걸립니다!)

        → apk 파일 이름은 myFirstGame 으로 할게요!

        Untitled

        → 아래와 같이 파일이 생성되면 완료!

        Untitled

      • [참고] 빌드시 Can not sign the application 오류가 뜨는 경우!

        Untitled

        keystore의 비밀번호를 틀린 경우이니 keystore의 비밀번호를 다시 확인해주세요!

    • 3) 배포하려면

      • (1) 안드로이드 폰이 있다면 → 바로 볼 수 있어요!

        • 컴퓨터에 폰을 usb로 연결하고 → 개발자 옵션 → USB디버깅을 켜주기!

          Untitled

        • target device 를 설정하고 → build and run 을 누르면 끝!

          Untitled

      • (2) 방금 만든 .apk 파일을 "구글플레이스토어"나, "애플 앱스토어"에 올리면 되는 것!

        • 구글플레이에는 → 누구나 올릴 수 있습니다!

          → 참고: $25 (1회)의 개발자 등록 비를 내야 한답니다.

        • 애플 앱스토어에 올리려면 → Mac PC가 있어야 해요!

          → 참고: 1년 129,000원의 개발자 등록비를 내야 한답니다.

          • 참고자료

            • 1) 애플 개발자 등록

              • 애플 개발자 사이트에 들어가서 계정을 생성 및 라이센스를 구매합니다

                • 애플 개발자 센터

                  https://s3-us-west-2.amazonaws.com/secure.notion-static.com/56b9b3cf-5c2c-4f78-9dd6-58cb81110f13/Untitled.png

                • 하단의 Join the Apple Developer Program을 눌러 개발자 라이센스 구매절차를 따릅니다

                  https://s3-us-west-2.amazonaws.com/secure.notion-static.com/78b18d54-253e-45aa-aaef-8e02bb07a5bc/Untitled.png

                  https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0322a804-dea0-4297-92a0-0733b61e3dc6/Untitled.png

                • 개인을 눌러주세요( 혹시 회사나 단체면 그에 맞는 것을 눌러주세요

                  https://s3-us-west-2.amazonaws.com/secure.notion-static.com/361a0cbb-b1a2-4d03-be70-8a32fc63ab05/Untitled.png

                  https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb9acf80-b0f1-455a-9294-3b10724f3408/Untitled.png

            • 2) iOS 빌드

                expo build:ios
              • 우린 배포를 할거니 아카이브를 선택합니다. 그리고 애플 계정을 만들고, 개발자 라이센스도 구매하였다면! 여기서 Y를 누릅니다

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e58bac47-0433-4a09-ae9f-2e0b6ad31d1c/Untitled.png

              • 다음과 같이 계정을 입력하면, 추가 인증을 할 수도 있습니다. 자동으로 맥에서 추가인증 코드가 뜨거나 문자로 받아 6자리 숫자를 알려주니, 이걸 그대로 터미널에 입력 후 엔터!

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd88cc8b-c31d-4b05-89c9-9a998c46d91f/Untitled.png

              • 그러면 다음과 같이 인증키(안드로이드에서의 싸인키 정도의 의미)를 여러분들이 직접 추가할지, 엑스포가 알아서 하게 할지 결정하라고 합니다. 엑스포에게 맡겨줍니다.

              • 혹시 기존에 앱을 만들었던 적이 있으면 다음과 같이 기존 키를 사용할것인가? 라고 선택하는 문구를 볼수도 있는데, 이땐 새로 추가를 하시면 됩니다. 그럼 방금 전의 화면처럼 새로 만들 키를 엑스포가 알아서 만들어 추가하게 할지 말지를 결정하는 화면을 보게됩니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0da73def-bb73-4374-874b-736f7d8d53b0/Untitled.png

              • 키생성을 엑스포에 위임했다면 엑스포가 추후에 앱에서 사용할 수도 있는 노티피케이션(푸시 알람) 기능을 위한 푸시 키까지 생성을 도와줍니다. 이것 또한 엑스포가 알아서 생성하고 처리할 수 있게 위임합니다. 이것 또한 기존에 만들었던게 있으면 다음과 같이 n을 누르고 엑스포에 위임한다!를 선택하시면 됩니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6860ec1f-efab-495a-8a86-45246c08e703/Untitled.png

              • 프로비져닝도 엑스포에 위임

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8a51641e-74f4-473c-b5d8-d2a4f4594237/Untitled.png

              • 이 모든 과정이 끝나면 빌드 단계에 들어갑니다. ipa 파일을 생성하고 있는 겁니다. 그럼 끝다길 기다립니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d80f1615-191a-48ec-9fc4-f63adae8ff91/Untitled.png

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c30a71dd-7db5-4190-ad86-70c67fd55eaf/Untitled.png

              • 빌드가 완료됐다!라는 터미널 화면을 보게되면 엑스포 대시보드에 로그인하여 artifact 파일을 내려봤습니다. 쉽게 말해 앱 파일입니다. 그럼 드디어 우린 ipa 앱 파일을 다운 받게됩니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/39ce4437-73a0-421d-8cd4-d80163f77a54/Untitled.png

            • 3) 앱스토어에서 개발중인 앱 선택 및 작업 공간 활성화

              https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6b4fb6ee-d2d5-4546-8e1e-f276e652a711/Untitled.png

              https://s3-us-west-2.amazonaws.com/secure.notion-static.com/27c01740-7e61-413b-9c97-434253e3da71/Untitled.png

              https://s3-us-west-2.amazonaws.com/secure.notion-static.com/760afeaa-46d0-47af-b0c5-8ea8518c93be/Untitled.png

              • 플랫폼은 iOS

              • 이름은 여러분이 원하는데로!

              • 기본언어는 한국어!

              • 번들 ID는 여러분들이 방금 엑스포에서 빌드(배포)단계를 거쳤다면 표시가 나옵니다. 그걸 누르세요!

              • SKU는 유니크한 앱 아이디를 쓰면 되는데, app.json 에 썼던 "bundleIdentifier": "com.sparta.psytest", 의 값을 넣어주세요. com.sparta.psytest 이거요!

              • 그리고 전체 엑세스!

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6a68eb77-f188-42e9-846a-eaff49df14bb/Untitled.png

            • 4) 트랜스포터로 앱 파일을 앱스토어로 전송

              https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8f331f9b-966a-4490-b5f8-61bbfaf6fb24/Untitled.png

              https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9e09a4ea-cec8-44b1-9d51-a1ddf1e370f8/Untitled.png

              • 앱 스토어에서 해당 프로그램을 다운받고 실행합니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/618fdaee-7671-4149-ae8c-8285982bae5f/Untitled.png

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3d1dbfc1-079f-4361-88a7-f75161d61d7d/Untitled.png

              • Expo 대시보드에서 내려받은 ipa 파일(대시보드 상에서 보았던 artifact 파일)을 선택하고 전송을 누릅니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3193ca0f-0447-4b29-b51d-efcca65eee15/Untitled.png

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/50b5de6a-7c63-4e05-a899-1188f1f794a0/Untitled.png

              • 전송이 완료되면, 앱 스토어 관리자 페이지 활동 내역에서 처리중인것을 확인이 가능합니다. 좀 기다리면 처리중에서 사용 가능한 단계로 바뀝니다.

              • 우리가 안드로이드 배포때 만든 앱로고도 보이죠?

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/777ceb44-4691-4e24-a3d9-749b5f5cfda7/Untitled.png

            • 5) 이미지 및 최종 정보 등록 후 승인 요청

              • 그럼 실제 사용 단계 전까지 기다리는 동안 나머지 이미지들과 정보를 기입합니다.

              • 이미지는 권고사항을 그대로 따라야 합니다. (권고 사항 보기)

              • 이 또한 온라인 포토샵 또는 디자인 툴로 준비를 해서 차근차근 업로드를 합니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8e7761cf-31c2-48c4-977f-74d112207a50/Untitled.png

              • 프로모션, 설명, 키워드는 앱 관련해서 적고 싶은 정보를 기입하고, 지원 URL, 마케팅 URL은 일단! 스파르타코딩 클럽 홈페이지 주소로 넣습니다. 혹시 심사가 이것때문에 통과되지 않는다면 관련 홈페이지를 간단하게 준비하셔야해요!

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f7e7c4f2-ab1e-4122-97e0-56f4573d446e/Untitled.png

              • 앱클립, iMessage, Apple Watch 모두 넘어갑니다. 우립 앱을 만들고 있으니까요. 그리고 빌드 파일 선택은 위에서 우리가 트랜스포터로 전송 시킨 앱이 처리중에서 사용가능 단계가 되면 선택을 할 수 있게 됩니다. 일단 넘어갑니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c91341a4-b972-4103-8cfb-4617dbe13508/Untitled.png

              • 다음 정보에선 등급만 설정합니다.

              • 저희 앱은 전부 아니오를 체크하면 됩니다. 해당사항이 없습니다 😂

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4c6fc260-3d12-4955-a05a-e1a91ffa7136/Untitled.png

              • 앱 심사 정보는 앱이 제대로 통과가 안되면, 안된 사유를 알려주기 위해 혹은 커뮤니케이션 할 사람의 정보를 기입하면됩니다.

              • 유의할점은 전화번호 형식은+8210~과 같이 국가 번호를 입력해야합니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b5f69d0a-c443-4696-908b-15394ac9c3b2/Untitled.png

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ab8d92cf-f743-4ab4-b51e-4edbdf48736d/Untitled.png

              • 이번 절차 또는 추후(앱 재배포)에 앱 심사를 거친다음 앱을 본인이 직접 버튼을 눌러 앱 스토어에 제출할건지 아니면 자동으로 제출하게 할건지 결정합니다.

              • 광고 식별자는 우리에게 해당사항이 없으므로 아니요!

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e409446a-884a-4191-aa14-48274c9994ce/Untitled.png

              • 그리고 저장을 누릅니다

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/82239bb9-7a70-4af4-8284-a8c0a2fba6d0/Untitled.png

              • 저장을 눌렀으면 왼쪽에 앱 정보를 누릅니다.

              • 개인정보 처리방침 URL은 스파르타코딩 클럽 홈페이지 주소를 넣습니다.
                ( 사실 이렇게하면 결과적으로 앱 심사를 통과할 수 없습니다. iOS 앱 심사는 정말 까다롭기 때문에, 실체적이고 유효한 값들을 넣어야 하는데, 일단 그 과정만 같이 하는 것에 의의를 두고 추후에 앱을 정말 배포 하실땐 관련 정보를 제대로 준비하셔야 합니다.)

                깃허브 리파짓 토리를 파고 다음 사례처럼 README.md 에 작성하여 URL로 사용할 수 있습니다.
                (사례)

              • 부재도 적당히 기입해줍니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/917f9831-1dcc-4539-ba3b-8ca120f65bc7/Untitled.png

              • 카테고리도 앱 성격에 맞게 선택해주시고, 콘텐츠 권한도 심리테스트 앱에서 타사 콘텐츠에 엑세스 안함을 선택해줍니다.

              • 그리고 저장!

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d8205872-a42a-4bc8-a080-2545d79cb599/Untitled.png

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b2f35112-4f1f-4ca5-a762-91061e886700/Untitled.png

              • 왼쪽의 가격 및 사용 가능 여부를 선택한다음 무료를 선택해주세요

              • 그리고 저장!

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/625c0b94-742f-4f2f-b393-3a7e763cb54d/Untitled.png

              • 자 이제 상단의 App Store 탭을 누르면 우리가 초반에 작성했던 페이지를 보게됩니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9e6aeb60-85a8-42fc-a30a-7c9d7c52502a/Untitled.png

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/33e7b0d0-cd6e-4bab-b338-736fa635bc9b/Untitled.png

              • 암호화를 사용하고 있지 않음을 눌러줍니다

              • 그 다음 마지막으로 우측 상단에 심사를 위해 제출을 누릅니다

              • 그러면 왼쪽에 심사 대기중을 보게 됩니다.

                https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c9917bfe-9fe6-4fad-badc-077158b7069a/Untitled.png

    06. 광고 붙이기

    [준비하기]

    • 1) Unity Ads 란?

      • 기존에도 광고를 붙일 수 있는 방법은 많았으나, Unity 에서 직접 지원하는 이 방법을 통해 코딩을 잘 몰라도 손쉽게 광고를 붙이고 관리할 수 있게 되었습니다!
      • 이 밖에 google ads 등이 있지만, 게임을 만들 때에는 Unity Ads가 무척 편하답니다.
    • 2) Unity 에디터 내에서 Unity Ads 추가하기

      1. WindowsGeneralServices 탭을 클릭하여 Service 메뉴 보기

        → General Settings 클릭

        Untitled

      2. organizations 드롭다운 해서 선택 → Create Project ID 클릭

        Untitled

      3. Ads의 off 클릭

        Untitled

      4. 패키지 매니저 사용하기

        • [참고] 추가 설치를 왜 진행 하나요?

          유니티 업데이트로 기본 Advertisement 설치시 4.4.1 버전이 설치됩니다.

          이 버전으로 설치하시고 진행하시면 오류가 발생하니

          강의 영상과 동일한 3.7.5 버전의 패키지 설치를 위해 패키지 매니저를 사용합니다!

        1) 14세 이하 ... **No**
    
        2) Window → Package Manager로 패키지 매니저 열고 Packages: Unity Registry 클릭합니다.
    
        ![unity registry.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8f21317e-2e20-474c-986d-bc69be5f3c3b/unity_registry.jpg)
    
        3) Advertisement install 클릭합니다 ( 3.7.5 버전인지 꼭 확인! )
    
        ![advertisement1.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b8f9f85-1350-47b4-9f6a-b22cbd945de9/advertisement1.jpg)
    
        4) Service로 돌아와서 **off** 를 **on** 으로 변경 합니다.
    
        5) **on** 이라고 뜨면 일단 준비 끝납니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dacd38c9-8bc1-4224-8569-70b4d76eeff3/Untitled.png)
    
        → 강의 영상과 달리 **Install Lastest Version** 은 클릭하실 필요 없습니다!
    
        6) Test mode 체크하기!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0d642823-e5ee-484f-893e-282a6b436997/Untitled.png)
    
        7) Ads Package 클릭 → **Install Lastest Version** 해주기

    [게임 끝나면 붙이기]

    • 1) adsManager 준비하기

      1. adsManager 만들기

        • 단, 이번엔 꽤 작업할 것이 많아서, 붙여놓고 살펴볼게요!

        • adsManager를 만들고, adsManager.cs 를 붙여줍니다.

          → 아래를 먼저 추가하고, 코드스니펫 추가!

            using UnityEngine.Advertisements;
          • [코드스니펫] adManager.cs 코드

              public static adsManager I;
            
              string adType;
              string gameId;
              void Awake()
              {
                  I = this;
            
                  if (Application.platform == RuntimePlatform.IPhonePlayer)
                  {
                      adType = "Rewarded_iOS";
                      gameId = "iOS 아이디";
                  }
                  else
                  {
                      adType = "Rewarded_Android";
                      gameId = "Android 아이디";
                  }
            
                  Advertisement.Initialize(gameId, true);
              }
            
              public void ShowRewardAd()
              {
                  if (Advertisement.IsReady())
                  {
                      ShowOptions options = new ShowOptions { resultCallback = ResultAds };
                      Advertisement.Show(adType, options);
                  }
              }
            
              void ResultAds(ShowResult result)
              {
                  switch (result)
                  {
                      case ShowResult.Failed:
                          Debug.LogError("광고 보기에 실패했습니다.");
                          break;
                      case ShowResult.Skipped:
                          Debug.Log("광고를 스킵했습니다.");
                          break;
                      case ShowResult.Finished:
                          // 광고 보기 보상 기능 
                          Debug.Log("광고 보기를 완료했습니다.");
                          break;
                  }
              }
      2. 몇가지 세팅하기

        • Start()Update() 는 지워주세요.

        • iOS 아이디, Android 아이디 부분에 내 아이디 (숫자)를 적으세요

          → 어디있냐고요?

          → Window → General → Services → Ads 클릭 → 아랫쪽에!

          Untitled

      3. 광고 활성화 시켜주기

        1. Dashboard를 누르고 로그인 합니다.

          Untitled

          b. Get Started 를 누르고 활성화를 시켜줍니다.

          unity monetization_1.jpg

          c. Project Setup 으로 진행 합니다.

          ads.jpg

          ads1_.jpg

          ads2_.jpg

          d. 완료하면 아래 화면에서 ID를 확인할 수 있습니다 😎

          ad fin.jpg

          e. Finish setup 이후에는 Ad Units에서 확인하실 수 있습니다.

          ad_unit.jpg

    • 2) 텍스트를 눌렀을 때 광고 뜨게 하기

      1. gameManager.cs 에서 30초 → 3초에 끝나게 하기 😁

         if (time > 3.0f)
         {
             endTxt.SetActive(true);
             Time.timeScale = 0.0f;
         }
      2. endTxt.cs 에서 넘어가던 씬을 gameManager.cs 의 함수로 만들기

        → 우선, 따라해볼게요!

         using UnityEngine.SceneManagement;
        
         public void retryGame()
         {
             SceneManager.LoadScene("MainScene");
         }
      3. adsManager.cs 에서 보상을 적어두기

         void ResultAds(ShowResult result)
         {
             switch (result)
             {
                 case ShowResult.Failed:
                     Debug.LogError("광고 보기에 실패했습니다.");
                     break;
                 case ShowResult.Skipped:
                     Debug.Log("광고를 스킵했습니다.");
                     break;
                 case ShowResult.Finished:
                     // 광고 보기 보상 기능 
                     gameManager.I.retryGame();
                     break;
             }
         }
      4. endTxt.cs 에서 마지막으로 텍스트가 눌렸을 때 광고를 보게 하기

         public void retryGame()
         {
             adsManager.I.ShowRewardAd();
         }
      5. 텍스트를 눌렀을 때 아래와 같이 나오면 완성!

        Untitled

    • 3) 주의사항

      1. Advertisement.Initialize(gameId, true);

        → 런칭할 때는 여기 true 를 false 로만 바꿔줘야 합니다. (true는 테스트를 하겠다는 뜻)

      2. 여기도 체크 해제해줘야겠죠!

        Untitled

      3. 그리고 다 했으면!

        → 게임 종료 조건 3.0f 를 다시 → 30.0f 로 바꿔둬야겠죠! 😎

    07. 게임제작 꿀팁 _ 에셋스토어, next step

    [무료 에셋스토어 구경하기]

    • 1) 무료 에셋스토어란?

      • 음악, 배경, 캐릭터, 애니메이션.. 모든 것을 할 수 없겠죠!
      • 그럴 때 이용하는 것이 "무료 에셋스토어"들이랍니다.
      • 사실은 "유료"라고 하더라도 비싼 가격은 아니기에, 실제 1인 개발자들은 틈 날 때마다 이 곳을 들여다보고 괜찮은 것들을 사두기도 한답니다. 할인 이벤트도 자주 해요!
    • 2) 유명한 곳 둘러보기 (1)

      • [코드스니펫] OpenGameArt.org

          https://opengameart.org/
      • UI가 조금 올드해보여도 이만한 데가 없답니다!

      • Browse → 2D Art → CCO 에 클릭하고 다시 검색을 눌러 둘러볼까요?

        [우선 이 정도로만 알아둘게요]

        → CC-BY , GPL , ... ⇒ 사용에 뭔가 조건이 있음

        → CC0 ⇒ 사용에 아무런 조건이 없음

        [사용법]

        → 눌러서 다운로드 받아 사용하면 된답니다.

        Untitled

    • 3) 유명한 곳 둘러보기 (2)

      • [코드스니펫] 유니티에셋스토어

          https://assetstore.unity.com/2d?category=2d&free=true&orderBy=1&rows=264
      • [코드스니펫] 예제 import 해보기

          https://assetstore.unity.com/packages/audio/sound-fx/free-casual-game-sfx-pack-54116
    → `Add to My Assets` 클릭 → Accept 클릭 → Open in Unity 클릭
    
    → `Download` 클릭 → `Import` 클릭
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b87411f-9557-466b-8262-5890203dfae5/Untitled.png)
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7943cbea-eb76-42aa-8145-04e1d90eda08/Untitled.png)
    
    → Import 를 클릭하면 폴더가 생긴답니다!
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/117be35f-6d63-4f1c-907f-c3d87b79b776/Untitled.png)
    
    → 여기서 쓰고 싶은 것만 남기고 나머지를 삭제하셔도 무방합니다. (또는 다른 곳에 보관!)
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfe81a5d-1619-444e-83ed-f562464a0906/Untitled.png)
    • 4) 팁 - 사용 방법

      • 배경이미지, 효과음, 배경음악 을 가져다 쓰는 게 좀 더 적당하답니다.
      • 사실 여러분이 오늘 붙인 효과음도 OpenGameArt.org 에서 가져온 것!

    [우리가 만들만한 게임의 범위]

    • 1) 되짚어보기 + 더 공부한다면?

      • 1주차: 유니티의 기본 사용법 - 빗물받는르탄이

      • 2주차: 유니티 사용법 복습1 + 데이터저장 - 풍선을 지켜라

      • 3주차: 유니티 사용법 복습2 + 레벨 구현 - 고양이 밥주기

      • 4주차: 퍼즐게임 만들기 & 로직체험 - 르탄이 맞추기

      • 5주차: 소리, 광고

      • 서버에 데이터 저장하기( firebase ), 3D로 카메라를 활용하기


      ⇒ 이것으로 딱 만들만한 것은, 하이퍼캐주얼 게임 !

    • 2) 하이퍼캐주얼 게임이란?

      • 대부분 1) 세로로 플레이, 2) 원 버튼(터치, 슬라이드), 3) 30~60초에 한 판

      • 아래와 같은 게임들 많이 보셨죠!

        Untitled

      • 막간상식) 하이퍼캐주얼 게임으로 굉장히 유명해진 회사들도 많아요!

        → 한 달에 3~4개씩 게임을 만들어, 몇 개가 초 대박을 터뜨리면서 큰! 회사가 됐답니다.

        Voodoo , Ketchapp , Lion studio와 같은 회사들이 있어요!

    • 3) 내가 게임을 기획해본다면?

      • 쫄깃 류 : 아주 간단한데, 콤보가 핵심이라, 10 → 11 → 12콤보 갈 때에 심장이 쫄깃!

        Untitled

        Untitled

      • 통쾌 류 : 다다다다다 없앨 때 느끼는 쾌감

        Untitled

      • 경쟁 류 : 내가 죽나 네가 죽나 해보자! (지금 수준에선 만들기 어려워요!)

        Untitled

      • 콘텐츠 류 : 더 궁금해. 궁금해!

        Untitled

      • 퍼즐 류 : 으으으. 깨고 싶다 깨고 싶어!

        Untitled

    08. 숙제 - 게임 둘러보기

    • [코드스니펫] 구글플레이 스토어

        https://play.google.com/store/search?q=ketchapp&c=apps
    • 힌트요정 - 👻

      → 우선 Ketchapp 이라는 회사로 맞춰뒀어요.

      → 여기서부터 출발해서, 유사한 콘텐츠를 타고 돌아다녀보세요!

      Untitled

      예) 제출 링크 예시

        https://play.google.com/store/apps/details?id=com.ketchapp.dunkshot

    HW. 5주차 숙제 해설

    마음에 드는 게임의 URL을 제출하면 끝!

    [스파르타코딩클럽] 게임개발 종합반 - 4주차

    [수업 목표]

    1. 보드 게임을 만들어보기
    2. 카드 뒤집기 게임을 만들면서, 총복습하기
    3. 게임에 필요한 "로직"을 경험하기

    [목차]


    01. 오늘 배울 것

    • 1) 4주차 수업의 목표와 범위

      • 여태까지 배웠던 것들로 만들거예요!

      • 카드 뒤집기 게임을 만들면서 보드게임 만들기의 기초를 배워봅니다.

        → 보드게임은 우리가 만들었던 게임과 살~짝 다르게, "로직"이 중요하답니다.

        → 지난 주차에 했던 것들이 새록새록 기억 날 거예요!

    • 2) 오늘 만들 것 : 르탄이 카드 뒤집기 게임

    • 3) 만들 순서

      1. 기본 씬 구성하기 : 배경, 타이머, 리소스 받아두기
      2. 시간 보내기
      3. 카드 깔기
      4. 카드 뒤집기 애니메이션 만들기
      5. 같은 카드을 뒤집었을 때 없애기

    [기본 씬 구성하기]

    • 1) 기본 세팅하기

      • windows → 2x3 layout 클릭! free aspect → phone 클릭!

        rgb ⇒ 90, 90, 225 로 맞출게요! (MainCamera)

        Untitled

    • 2) 타이머 만들어두기

      • UI → Text 클릭 → timeTxt 로 만들어두기

        font size: 70 , y: 400, width: 200, height: 200, Color: (255, 255, 255, 255)

        Untitled

    02. 카드 만들기 - 한 장

    • 1) 르탄이 이미지 받아두기

      1. Images 폴더 아래에 rtan 이미지 풀어두기

        Untitled

    • 2) 카드 한 장만 만들어두기

      1. Cards (Create Empty!) 아래에 → card (Create Empty) 한 장만 만들어둘게요

        → 앞면(front)은 르탄이 이미지가 들어가고, 뒷면(back)은 ? 물음표가 들어갑니다.

        → card 아래에 front/back 로 sprite를 만들어둘게요. 아래처럼!

        Untitled

      2. front 스프라이트에 rtan0 이미지를 끌어다 놓습니다.

        → 앗, 너무 크네요! 이미지를 클릭해서 pixels per unit 을 450으로 맞춥니다.

        → 다른 르탄이들도 모두 450으로 맞춰주세요! (shift 눌러서 한번에 잡아 바꾸기)

        Untitled

      3. 우선 front 는 꺼두겠습니다.

        Untitled

      4. back 아래에 Canvas를 만들어 Text로 ? 표시를 넣어주세요

        1. Canvas UI를 만듭니다.

        2. Render-Mode를 World Space 로 변경 해줍니다.

        3. Transform Reset 버튼을 통해 Rect Transform을 초기화합니다.

          a5.jpg

        4. Order in layer를 1로 바꾸어 줍니다.

        5. Text에서 Font Size는 50으로 설정 합니다.

        6. Scale 부분을 (0.01 , 0.01)로 맞추어 줍니다.

          Untitled

      5. 마지막으로 Card의 Scale을 1.3으로 바꾸기

        Untitled

    03. 시간 가게 하기

    • 1) gameManager 세팅하기 (쉬운 것부터!)

      1. gameManager 만들기

        Untitled

      2. 시간이 가게 하기

         public Text timeTxt;
         float time = 0.0f;
        
         void Update()
         {
             time += Time.deltaTime;
             timeTxt.text = time.ToString("N2");
         }
      3. Play 해서 확인해보기!

        Untitled

    04. 카드(1)_배치하기

    • 1) 배치 전략

      • 카드를 16장 만들어서 직접 배치하는 방법은 → 100장이면 너무 힘들잖아요!

      • 지금 카드 사이즈가 x:1.3, y:1.3 이니까, 1.4 만큼씩 띄워서 붙여주려고요!

        Untitled

    • 2) 자동으로 카드 생성하기

      1. card를 프리팹으로 만들기

        → 기존 것은 과감히 삭제하기!

        Untitled

      2. 카드 생성하기 전에 반복문 을 구경해보기 (튜터만 할게요!)

         void Start()
         {
             for (int i = 0; i < 16; i++)
             {
                 Debug.Log(i);
             }
         }
      3. card를 새로 만들어서 cards 아래에 붙이기

        → 실행해서 확인해볼까요?

         public GameObject card;
        
         void Start()
         {
             for (int i = 0; i < 16; i++)
             {
                 GameObject newCard = Instantiate(card);
                 newCard.transform.parent = GameObject.Find("cards").transform;
             }
         }

        Untitled

    • 3) 카드 위치 잡아주기

      1. 전략을 생각하기

        [예를 들어 1번째 라고 하면]

        → x : 1을 4로 나눈 몫 = 0

        → y : 1을 4로 나눈 나머지 = 1

        ⇒ (0,1) 위치 ⇒ (1.4씩 곱해주면) ⇒ (0, 1.4) 위치

        [예를 들어 7번째 라고 하면]

        → x : 7을 4로 나눈 몫 = 1

        → y : 7을 4로 나눈 나머지 = 3

        ⇒ (1,3) 위치 ⇒ (1.4씩 곱해주면) ⇒ (1.4, 4.2) 위치

      2. 카드 위치 잡아주기

         void Start()
         {
             for (int i = 0; i < 16; i++)
             {
                 GameObject newCard = Instantiate(card);
                 newCard.transform.parent = GameObject.Find("cards").transform;
        
                 float x = (i / 4) * 1.4f;
                 float y = (i % 4) * 1.4f;
                 newCard.transform.position = new Vector3(x, y, 0);
             }
         }

        Untitled

      3. 카드를 전체적으로 옮겨주기

        → x, y를 적당히 빼줘서 전체를 가운데에 위치하게 하기

        → 여기서는 x: -2.1f, y: -3.0f

         void Start()
         {
             for (int i = 0; i < 16; i++)
             {
                 GameObject newCard = Instantiate(card);
                 newCard.transform.parent = GameObject.Find("cards").transform;
        
                 float x = (i / 4) * 1.4f - 2.1f;
                 float y = (i % 4) * 1.4f - 3.0f;
                 newCard.transform.position = new Vector3(x, y, 0);
             }
         }
      4. 완성된 배치 구경하기

        Untitled

    05. 카드(2)_르탄이 넣기, 애니메이션

    [르탄이 넣기]

    • 1) 랜덤으로 섞기 전략

      1. 일단 [0, 0, 1, 1, 2, 2, ..., 7, 7]까지 쓰인 리스트를 만들고
      2. 이걸 섞어서 [2, 3, 4, 1, 2, 0, 1, ..., 7]로 만들고
      3. 카드가 만들어 질 때 하나씩 꺼내서 르탄이 이미지를 붙여주기!
    • 2) 리스트를 랜덤으로 섞기

      1. 우선 리스트 만들고 출력하기

        → 카드가 만들어지면서 Debug.Log 해보면 되겠죠?

         void Start()
         {
                 int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
        
             for (int i = 0; i < 16; i++)
             {
                 GameObject newCard = Instantiate(card);
                 newCard.transform.parent = GameObject.Find("cards").transform;
        
                 float x = (i / 4) * 1.4f - 2.1f;
                 float y = (i % 4) * 1.4f - 3.0f;
                 newCard.transform.position = new Vector3(x, y, 0);
        
                 Debug.Log(rtans[i]);
             }
         }
      2. 리스트를 섞기

        → 아래를 맨 위에 추가

         using System.Linq;
        • [코드스니펫] 랜덤하게 섞기

            rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
        ```csharp
        void Start()
        {
                int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
    
            rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
    
            for (int i = 0; i < 16; i++)
            {
                GameObject newCard = Instantiate(card);
                newCard.transform.parent = GameObject.Find("cards").transform;
    
                float x = (i / 4) * 1.4f - 2.1f;
                float y = (i % 4) * 1.4f - 3.0f;
                newCard.transform.position = new Vector3(x, y, 0);
    
                Debug.Log(rtans[i]);
            }
        }
        ```
    
    3. `Play` 해서 확인해보기
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7c6a4f13-1954-4a0f-8d85-7026bee3c74c/Untitled.png)
    • 3) 르탄이 붙여주기

      1. 이미지를 꺼내오려면? → Resources 폴더에 옮겨두기

        Untitled

      2. 르탄이 붙이기

        string rtanName = "rtan" + rtans[i].ToString();

        → 르탄이 이름(이미지 이름)을 만들어두기

        newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite =

        → 새 카드 아래에 front 를 찾아서, sprite를 변경

        Resources.Load<Sprite>(rtanName);

        Resources 폴더에 있는 rtanName 이미지를 가져오자

         void Start()
         {
             int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
        
             rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
        
             for (int i = 0; i < 16; i++)
             {
                 GameObject newCard = Instantiate(card);
                 newCard.transform.parent = GameObject.Find("cards").transform;
        
                 float x = (i / 4) * 1.4f - 2.1f;
                 float y = (i % 4) * 1.4f - 3.0f;
                 newCard.transform.position = new Vector3(x, y, 0);
        
                 string rtanName = "rtan" + rtans[i].ToString();
                 newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
             }
         }
      3. 확인해보기

        → 프리팹에서 front를 켜고, back을 끈 다음 확인합니다.

        → 잘 나오네요! 😎

        Untitled

    [카드 애니메이션]

    • 1) 기본 애니메이션 만들기 card_idle

      1. Animations 폴더 생성 후 card_idle 애니매이션 만들기

        → 프리팹 card를 열어서 붙여놓기, Loop Time 체크하는 것 잊지 말기!

        Untitled

      2. 카드를 꺼내놓기

        → 안보이면 만들기 어려우니까!

        Untitled

      3. 애니메이션 레코딩하기

        0:20 에만 rotation z:3 을 만들어놓기

        Untitled

    • 2) 뒤집기 애니메이션 만들기 card_flip

      1. Animations 폴더 생성 후 card_flip 애니메이션 만들기

        → 프리팹을 열어 card에 붙여놓기

        → 뒤집기는 한번 이므로 loop time 체크할 필요 없음!

        Untitled

      2. 애니메이션 만들기

        0:10 부분만 Scale 을 x:1.2, y:1.2 로 만들어두기

        → 살짝 눌린 것처럼!

        Untitled

    • 3) 애니메이션 조건 만들기

      1. Animator 를 열고, transition 만들어 줍니다.

        1. 카드의 오른쪽 클릭하고 make transition 을 클릭합니다.

        2. 오는 것과 가는 것 두 개의 transition을 생성합니다.

        3. 각 transition에 대해서, has exit time 에 체크 해제하고, transition duration을 0으로 변경합니다.

          Untitled

      2. 파라미터 만들기

        → bool 형식(true / false)의 isOpen

        Untitled

      3. transition에 파라미터 조건 붙이기

        idle ⇒ flip : bool이 true 가 되면 발동

        flip ⇒ idle : bool이 false 가 되면 발동

        Untitled

    06. 카드(3)_뒤집기, 매칭하기

    [뒤집기]

    • 1) 카드 클릭하면 뒤집어지기 (=쉬어가기 🙂)

      1. card c# 스크립트에서 button 속성 붙이기

        1. card.cs 만들어서, 클릭하면 front가 보이게 해줍니다.

          public void openCard()
          {
              transform.Find("front").gameObject.SetActive(true);
              transform.Find("back").gameObject.SetActive(false);
          }
        b.온클릭 함수에 card를 붙여 넣은 후, card에, openCard button 함수를 붙여 줍니다.
    
        ![opencard.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/22262d1e-0413-4224-898c-55db44be50fc/opencard.jpg)
    
    
    1. 카드에 animtion 적용하기
    
        ```csharp
        public Animator anim;
    
        public void openCard()
        {
            anim.SetBool("isOpen", true);
            transform.Find("front").gameObject.SetActive(true);
            transform.Find("back").gameObject.SetActive(false);
        }
        ```
    
    2. `Play` 해서 잘 뒤집어지는지 실행해보기
    
        → 잘 되네요!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e93f57c8-9ad7-4f2d-af7d-cbf43ef78a90/Untitled.png)

    [카드 매칭하기]

    • 1) 카드 매칭하기 - 전략

      • 일치하면? ⇒ (1초 후에) 카드를 둘 다 없애주기
      • 안 일치하면? ⇒ (1초 후에) 두 카드를 다시 뒤집어주기
    • 2) 카드 매칭하기

      1. 우선, gameManager 싱글톤화

        → 이제 다른 곳에서 막 부를 것이니까!

         public static gameManager I;
        
         void Awake()
         {
             I = this;
         }
      2. gameManager에서 카드 이름을 저장해둘 수 있게 하기 + 매칭 로직 함수 만들어두기

         public GameObject firstCard;
         public GameObject secondCard;
        
         public void isMatched()
         {
             Debug.Log("판단하자");
         }
      3. card.cs 에서 openCard하면 firstCard 또는 secondCard에 나를 넣기

         public void openCard()
         {
             anim.SetBool("isOpen", true);
             transform.Find("front").gameObject.SetActive(true);
             transform.Find("back").gameObject.SetActive(false);
        
             if (gameManager.I.firstCard == null)
             {
                 gameManager.I.firstCard = gameObject;
             }
             else
             {
                 gameManager.I.secondCard = gameObject;
                 gameManager.I.isMatched();
             }
         }
      4. gameManager.cs 에서 isMatched() 만들기

        front 를 찾아서 카드 이미지 이름을 받아오기

        → 다 끝났으면 다시 비워주기

         public void isMatched()
         {
             string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
             string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        
             if (firstCardImage == secondCardImage)
             {
                 Debug.Log("같다!");
             }
             else
             {
                 Debug.Log("같지 않다!");
             }
        
             firstCard = null;
             secondCard = null;
         }
      5. card 행동 준비하기

        card.cs 안에 없애기, 뒤집어주기 함수 만들어두기

        → 같으면 → 1초 후에 둘 다 없애기 / 다르면 → 1초 후에 둘 다 뒤집어주기

        → 1초 후에 실행해야하니까, invoke를 따로 만들어줘야 하겠죠?

         public void destroyCard()
         {
             Invoke("destroyCardInvoke", 1.0f);
         }
        
         void destroyCardInvoke()
         {
             Destroy(gameObject);
         }
        
         public void closeCard()
         {
             Invoke("closeCardInvoke", 1.0f);
         }
        
         void closeCardInvoke()
         {
             anim.SetBool("isOpen", false);
             transform.Find("back").gameObject.SetActive(true);
             transform.Find("front").gameObject.SetActive(false);
         }
      6. gameManager.cs 에서 같을 때 / 같지 않을 때 적절한 함수 불러주기

        firstCard.GetComponent<card>().destroyCard()

        → firstCard에 붙어있는 card.cs 에서 destoryCard 를 불러라!

         public void checkMatched()
         {
             string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
             string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        
             if (firstCardImage == secondCardImage)
             {
                 firstCard.GetComponent<card>().destroyCard();
                 secondCard.GetComponent<card>().destroyCard();
             }
             else
             {
                 firstCard.GetComponent<card>().closeCard();
                 secondCard.GetComponent<card>().closeCard();
             }
        
             firstCard = null;
             secondCard = null;
         }
      7. 확인해보기 ⇒ 잘 되네요! 😎

        Untitled

    07. 게임 끝내기

    • 1) 카드가 모두 없어지면 게임 끝내기

      1. 끝! 텍스트를 미리 만들어두기

        1. fontsize 20, timeTxt 를 [윈도우] ctrl+d [맥] command + D 해서 만들면 편하겠죠!

        2. width: 300, height: 300, posY: 0 으로 설정 해주세요.

        3. color : (220, 255, 0, 255)도 설정해줍니다.

        4. 안보이게 세팅 하여 감춰두세요!

          Untitled

      2. gameManager.cs 에서, 매칭 됐을 때 남은 카드를 체크하기

        → cards를 찾아서, 아래에 몇 개의 자식이 있는지를 판단하면 끝!

        → 실행해보기!

         public void checkMatched()
         {
             string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
             string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        
             if (firstCardImage == secondCardImage)
             {
                 firstCard.GetComponent<card>().destroyCard();
                 secondCard.GetComponent<card>().destroyCard();
        
                 int cardsLeft = GameObject.Find("cards").transform.childCount;
                 Debug.Log(cardsLeft);
             }
             else
             {
                 firstCard.GetComponent<card>().closeCard();
                 secondCard.GetComponent<card>().closeCard();
             }
        
             firstCard = null;
             secondCard = null;
         }
      3. 실행해보면, 지금 맞춘 두 장을 포함해서 나오는 것을 알 수 있어요

        → 즉, 2 가 나오면 마지막이라는 뜻!

        Untitled

      4. 게임 끝내기!

         public GameObject endTxt;
        
         public void isMatched()
         {
             string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
             string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
        
             if (firstCardImage == secondCardImage)
             {
                 firstCard.GetComponent<card>().destroyCard();
                 secondCard.GetComponent<card>().destroyCard();
        
                 int cardsLeft = GameObject.Find("cards").transform.childCount;
                 if (cardsLeft == 2)
                 {
                     endTxt.SetActive(true);
                     Time.timeScale = 0.0f;
                 }
             }
             else
             {
                 firstCard.GetComponent<card>().closeCard();
                 secondCard.GetComponent<card>().closeCard();
             }
        
             firstCard = null;
             secondCard = null;
         }
      5. 버튼에 다시시작하기 버튼 붙이기!

        → (1) 버튼 컴포넌트 추가하기

        → (2) endTxt.cs 만들고, endTxt에 붙이기

         using UnityEngine.SceneManagement;
        
         public void retryGame()
         {
             SceneManager.LoadScene("MainScene");
         }

        → (3) gameManager.cs start에서 TimeScale을 다시 되돌려놓는 것도 잊지말기

         Time.timeScale = 1.0f;
      6. 확인하기 ⇒ 잘 되네요!

        Untitled

    • 2) 미세 조정하기

      • 이렇게, 다 만들고 변수들을 조정해서 마지막으로 게임을 손 본답니다!

      • card.cs 에서 없어지거나 / 다시 뒤집히는 시간을 0.5f 로 바꿔둘게요!

        public void destroyCard()
        {
          Invoke("destroyCardInvoke", 0.5f);
        }
        
        void destroyCardInvoke()
        {
          Destroy(gameObject);
        }
        
        public void closeCard()
        {
          Invoke("closeCardInvoke", 0.5f);
        }
        
        void closeCardInvoke()
        {
          anim.SetBool("isOpen", false);
          transform.Find("back").gameObject.SetActive(true);
          transform.Find("front").gameObject.SetActive(false);
        }

    08. 숙제 - 30초가 지나면 게임 끝내기

    • 이렇게 되면 완성!

      르탄이 맞추기_숙제완성.mp4

    • 힌트요정 - 👻

      [수정해야할 부분]

      gameManager.csUpdate() 부분만 수정하면 된답니다!

      → 어떤 조건이 되면 아래 코드가 실행되면 되겠죠!

        endTxt.SetActive(true);
        Time.timeScale = 0.0f;

    HW. 4주차 숙제 해설

    • gameManager.cs 부분

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
        using UnityEngine.UI;
      
        using System.Linq;
      
        public class gameManager : MonoBehaviour
        {
            public Text timeTxt;
            float time = 0.0f;
      
            public GameObject endTxt;
      
            public GameObject card;
      
            public static gameManager I;
      
            public GameObject firstCard;
            public GameObject secondCard;
      
            void Awake()
            {
                I = this;
            }
      
            // Start is called before the first frame update
            void Start()
            {
                Time.timeScale = 1.0f;
      
                int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
      
                rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
      
                for (int i = 0; i < 16; i++)
                {
                    GameObject newCard = Instantiate(card);
                    newCard.transform.parent = GameObject.Find("cards").transform;
      
                    float x = (i / 4) * 1.4f - 2.1f;
                    float y = (i % 4) * 1.4f - 3.0f;
                    newCard.transform.position = new Vector3(x, y, 0);
      
                    string rtanName = "rtan" + rtans[i].ToString();
                    newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
                }
            }
      
            // Update is called once per frame
            void Update()
            {
                time += Time.deltaTime;
                timeTxt.text = time.ToString("N2");
      
                if (time > 30.0f)
                {
                    endTxt.SetActive(true);
                    Time.timeScale = 0.0f;
                }
            }
      
            public void isMatched()
            {
                string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
                string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
      
                if (firstCardImage == secondCardImage)
                {
                    firstCard.GetComponent<card>().destroyCard();
                    secondCard.GetComponent<card>().destroyCard();
      
                    int cardsLeft = GameObject.Find("cards").transform.childCount;
                    if (cardsLeft == 2)
                    {
                        endTxt.SetActive(true);
                        Time.timeScale = 0.0f;
                    }
                }
                else
                {
                    firstCard.GetComponent<card>().closeCard();
                    secondCard.GetComponent<card>().closeCard();
                }
      
                firstCard = null;
                secondCard = null;
            }
        }

    [스파르타코딩클럽] 게임개발 종합반 - 3주차


    Copyright ⓒ TeamSparta All rights reserved.

    [스파르타코딩클럽] 게임개발 종합반 - 3주차

    [수업 목표]

    1. 그럴싸한 게임을 완성해보기
    2. hp바 만들기
    3. 레벨 시스템을 구상해보기

    [목차]


    01. 3주차 오늘 배울 것

    • 1) 3주차 수업의 목표와 범위

      • 스파르타가 만들어둔 이미지를 가지고 게임을 만들어봅니다.

        → 이미지들을 진짜 디자이너 에게 받았다고 생각하고 만들어보세요!

      • 3주차 내용도 1, 2주차의 것과 80% 같고, 20%만 새롭답니다.

        → 한번 더 복습해볼까요? 이번주까지 하고나면, 꽤 익숙해질거예요!

    • 2) 오늘 만들 것 : 고양이 밥주기 게임

      • 배고픈 고양이들에게 밥을 주고, 생선가게를 지켜보아요!

        → 새롭게 배우는 것: 여러 Scene 만들기!, 슬라이더 바, 레벨시스템

        고양이 밥주기 게임.mp4

    • 3) 만들 순서

      1. 기본 씬 구성하기 (UI, 강아지, 고양이)
      2. 강아지 움직임 더하기 + 밥 쏘기
      3. 고양이 내려오게 하기
      4. 고양이 밥 먹기 + 옆으로 가게 하기
      5. 새로운 고양이 나오게 하기
      6. 레벨업하기

    [기본 씬 구성하기]

    • 1) 기본 세팅하기 & 배경 만들기

      1. windows → 2x3 layout 클릭! free aspect → phone 클릭!

        MainScene으로 변경하는 것도 잊지마세요!

        Untitled

      2. 메인카메라의 size를 5 → 25로 바꿀게요!

        → 약간 더 멀리서 보겠다는 이야기! 이미지들이 이 사이즈에 맞춰 작업되어 있습니다. 😎

        Untitled

      3. 카메라 색은 hex ⇒ FFF0B2 으로 할게요! 그러면, 배경과 같은 효과!

        → 이렇게도 할 수 있다는 사실! 알고갈게요!

        Untitled

    • 2) 이미지 받아두기

      1. Assets 폴더 아래에 Images(대, 소문자 구분) 폴더 생성합니다.
      2. Images 폴더 아래에 파일 전체를 저장 해줍니다.
    • 3) 오브젝트 배치하기

      • 생선가게 : y: -22

      • 강아지 : y: -16.1

      • 위치를 지정하신 후, Inspector의 Sprite에 이미지를 드래그해주세요!

        Untitled

    02. 시작 씬 만들기

    • 1) 시작씬 구성하기

      1. Scenes 폴더 안에 create → Scene 이름은 StartScene 그리고 더블클릭!

      2. 마찬가지로 Camera size ⇒ 25

      3. sprite 이미지 만들어서 ⇒ intro 이미지 끌어다넣기

        Untitled

      4. UI → Image를 만들어서 startBtn 해두기

        → startBtn 이미지 끌어다넣기

        → posY: -300

        width: 300, height: 100

        Untitled

    • 2) 씬 넘어가기

      1. startBtn 에 버튼 컴포넌트 달기

        → 참고) 버튼 컴포넌트는 "sprite"가 아니라, 반드시 "UI Image"에 붙여야 작동한다는 사실!

      2. 스크립트 만들기

        → Scripts 폴더 만들고, startBtn.cs 만들기 → startBtn에 붙여두기

         using UnityEngine.SceneManagement;
        
         public void GameStart()
         {
             SceneManager.LoadScene("MainScene");
         }
      3. onclick에 붙이기

        Untitled

    • 3) Play 해서 확인해보기!

      → 잘 되네요! 이제 MainScene으로 이동해서 작업할게요! (더블클릭!)

    03. 밥 쏘기

    • 1) 밥 만들어두기

      → sprite → circle 클릭, 이름 : food

      → scale: x: 6, y: 6

      → Sprite : Knob 으로 설정하고, 색은 컬러추출기 를 이용해서 강아지 색으로 설정하기

      Untitled

    • 2) 밥 복사해서 쏘기

      1. food.cs 만들어서 붙여두기. 생성하면 무조건 위로 직진!

         void Update()
         {
             transform.position += new Vector3(0.0f, 0.5f, 0.0f);
         }
      2. 프리팹으로 만들어두기

        → Prefabs 폴더 만들고 끌어다놓기! 과감하기 원래 있던 food는 삭제!

        Untitled

      3. GameManager 만들기

        → 0.2초 마다 밥을 쏘기

        → 강아지 위치에서 y 좌표만 2.0f 높아진 곳에서 쏘기

        Quaternion.identity 는 회전 없다는 뜻! (no rotation)

         public GameObject food;
         public GameObject dog;
        
         void Start()
         {
             InvokeRepeating("makeFood", 0.0f, 0.2f);
         }
        
         void makeFood()
         {
             float x = dog.transform.position.x;
             float y = dog.transform.position.y + 2.0f;
             Instantiate(food, new Vector3(x,y,0), Quaternion.identity);
         }
      4. 앗, 그런데 food가 안 없어진다!

        food.cs 에서 y 좌표가 26.0f 가 넘으면 없어지게 하기

         void Update()
         {
             transform.position += new Vector3(0.0f, 0.5f, 0.0f);
             if (transform.position.y > 26.0f)
             {
                 Destroy(gameObject);
             }
         }
    • 3) 강아지 움직이기

      1. 강아지 마우스 따라 움직입니다.

        1. dog.cs 만들어주세요.

        2. 마우스 좌표 중 X 좌표만 가져옵니다. (2주차에서 썼던 코드 입니다.)

        3. y 좌표는 transform.position.y 내 좌표를 그대로 씁니다.

        4. 여기서 잠깐! 외워서 쓰는 게 아니라, 검색해보고 쓴다!라고 생각하세요.

          void Update()
          {
           Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
           transform.position = new Vector3(mousePos.x, transform.position.y, 0);
          }
      2. fishShop을 벗어나지 않게 합니다.

        1. x 좌표 -8.5f ~ 8.5f 로 고정합니다.

          void Update()
          {
          Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
          float x = mousePos.x;
          if (x > 8.5f)
          {
              x = 8.5f;
          }
          if (x < -8.5f)
          {
              x = -8.5f;
          }
          transform.position = new Vector3(x, transform.position.y, 0);
          }

    04. 고양이 나타내기(1) - 만들기

    • 1) normalCat 만들기

      1. create empty → normalCat 만들기

      2. normalCat 안에 full / hungry로 sprite를 만들어 붙이기

      3. hungry 안에 canvas → image를 만들고 back을 만들기!

        → Canvas를 만들 때, RenderMode를 World Space로 변경해주기!

        width: 4, height: 0.5

        → canvas의 position을 y:-4 로 고정하기

        Untitled

      4. front는 back을 (ctrl+d)해서 복사한 뒤 작업시작!

        → 색 : 추출기로 fish-shop 색 추출

        → pivot의 x 값을 0 으로 만들고, scale의 x 값을 0.4로 조정해보기!

        → 이제 x scale만 조정해주면 게이지를 만들 수 있음. 이것이 바로 hp 바 만들기!

        Untitled

    • 2) normalCat에 애니메이션 붙이기

      1. Animations 폴더 만들고 normalCat 만들기

        → Loop time 체크 잊지 말기!

        Untitled

      2. normalCat에 붙여넣고, 더블클릭해서 녹화시작하기

        → 0:10에 normalCat_2, 0:20에 다시 normalCat_1

        Untitled

    • 3) fatCat 만들기

      → ctrl+d 를 이용해서 만들거예요! 두 번째는 처음부터 만들 필요가 없겠죠 😎

      → (1) hungry, full 의 이미지를 교체해주기

      → (2) Canvas의 x 좌표를 -0.5 해두기

      → (3) animation 을 새로 만들어서 넣어두기 (기존에 있던 animator를 먼저 삭제하기)

      Untitled

    • 4) 두 개를 모두 프리팹 화 해두기

      → fatCat은 game view에서는 삭제해두기

      → 참고) 프리팹을 고칠 때는 프리팹을 더블클릭해서 작업합니다!

      Untitled

    05. 고양이 나타내기(2) - 내려오기/충돌/등장하기 / 종료

    • 1) 고양이 내려오게 하기

      1. cat.cs 만들기

      2. 임의의 위치에서 내려오게 하기

        → y 좌표는 30.0f 화면 밖에서 내려오게 하기

         void Start()
         {
             float x = Random.Range(-8.5f, 8.5f);
             float y = 30.0f;
             transform.position = new Vector3(x, y, 0);
         }
        
         // Update is called once per frame
         void Update()
         {
             transform.position += new Vector3(0.0f, -0.05f, 0.0f);
         }
      • [추가 팁!] 고양이가 너무 빨라요!

        • 왜 이런 현상이 발생 하나요?

          • 1강과 동일하게 Update 함수의 기본 기능에 의해 발생하는 문제입니다.
        • 어떻게 해결 하나요?

          rrr.jpg

          • VSync 옵션을 켠다면 → Update의 프레임은 사용자 모니터의 주파수에 맞게 조절되어 속도가 느려지게 됩니다.
          • 단, VSync 옵션은 유니티 에디터 내부에서만 적용되는 옵션입니다.
          • 그리하여, 추후 실제 게임으로 만들어 플레이시에는 적용이 되지 않을 수 있습니다.
    • 2) 고양이와 밥 충돌하게 하기

      1. 밥에 tag 주기

        Untitled

      2. 밥에 rigidbody 2D, circle collider 2D 달아주기

        → 참고) 충돌 = 한쪽에 rigidbody + 양쪽에 collider

        → 단! Body Type을 Kinematic 으로 잡아주기 = 중력의 영향을 안 받겠다는 뜻!

        → 그리고 isTrigger 에 체크! 중력의 영향을 안 받을 때에는 이것을 체크해주세요!

        (옵션을 체크하지 않으면 충돌감지가 안됩니다!)

        Untitled

        Untitled

      3. 고양이에 box collider 2D 달아주기 (나중에 fatCat에도 달아야겠죠?)

        → collider size를 조정해줘야 해요! x:4, y:7 로 맞춰볼까요?

        Untitled

      4. cat.cs 에 충돌 구현하기

         void OnTriggerEnter2D(Collider2D coll)
         {
             if (coll.gameObject.tag == "food")
             {
                 Debug.Log("맞았다!");
             }
         }

        Untitled

    • 3) 충돌하면 에너지를 채워주기

      1. 기본 에너지 세팅

         float full = 5.0f;
         float energy = 0.0f;
      2. 밥 충돌하면 에너지 올라가게 하기 + 밥은 없애기

         void OnTriggerEnter2D(Collider2D coll)
         {
             if (coll.gameObject.tag == "food")
             {
                 energy += 1.0f;
                 Destroy(coll.gameObject);
             }
         }
      3. energy 와 full이 같아지면 배불렀다!

         void OnTriggerEnter2D(Collider2D coll)
         {
             if (coll.gameObject.tag == "food")
             {
                 if (energy < full)
                         {
                                 energy += 1.0f;
                         Destroy(coll.gameObject);
                         }
                 else
                 {
                     Debug.Log("배가 다 찼어요");
                 }
             }
         }

        Untitled

      4. 게이지 채우기

        → 시작 게이지를 0으로 해둘게요! (front의 x scale을 0으로)

        → hungry 밑에, Canvas 밑에, front를 찾아서, x scale을 enery/full 값으로 조절

         void OnTriggerEnter2D(Collider2D coll)
         {
             if (coll.gameObject.tag == "food")
             {
                 if (energy < full)
                         {
                                 energy += 1.0f;
                         Destroy(coll.gameObject);
                                 gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
                         }
                 else
                 {
                     Debug.Log("배가 다 찼어요");
                 }
             }
         }
      5. 게이지가 모두 찼으면, hungry는 안보이고, full이 보이게 하기!

         void OnTriggerEnter2D(Collider2D coll)
         {
             if (coll.gameObject.tag == "food")
             {
                 if (energy < full)
                         {
                                 energy += 1.0f;
                         Destroy(coll.gameObject);
                                 gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
                         }
                 else
                 {
                                 gameObject.transform.Find("hungry").gameObject.SetActive(false);
                     gameObject.transform.Find("full").gameObject.SetActive(true);
                 }
             }
         }

        Untitled

      6. 에너지 꽉 차면 옆으로 비키게 하기

        → 단, 화면 오른쪽에 있었으면(x>0) 오른쪽으로, 왼쪽이면 왼쪽으로 비키게 하기

         void Update()
         {
             if (energy < full)
             {
                 transform.position += new Vector3(0.0f, -0.05f, 0.0f);
             }
             else
             {
                 if (transform.position.x > 0)
                 {
                     transform.position += new Vector3(0.05f, 0.0f, 0.0f);
                 }
                 else
                 {
                     transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
                 }
             }
         }
        • [참고] 고양이 이미지 변경 시점이 다른 이유는?

          게임 설계상 OnColliderEnter2DUpdate에서 ‘따로’ energy를 체크 하기 때문에 고양이의 이미지가 변경되는 시점과 옆으로 비키는 시점이 조금 다릅니다.

          어떻게 다를지 로직을 체크해봅니다.

      7. 3초 뒤에 없애기

         void Update()
         {
             if (energy < full)
             {
                 transform.position += new Vector3(0.0f, -0.05f, 0.0f);
             }
             else
             {
                 if (transform.position.x > 0)
                 {
                     transform.position += new Vector3(0.05f, 0.0f, 0.0f);
                 }
                 else
                 {
                     transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
                 }
                 Destroy(gameObject, 3.0f);
             }
         }
    • 4) gameManager에서 고양이 부르기

      1. 등장시키기

        → 1.0초에 한 마리씩 등장. 이제 normalCat 을 Hierachy에서 없애도 됩니다!

         public GameObject normalCat;
        
         void Start()
         {
             InvokeRepeating("makeFood", 0.0f, 0.2f);
             InvokeRepeating("makeCat", 0.0f, 1.0f);
         }
        
         void makeCat()
         {
             Instantiate(normalCat);
         }
      2. order-in-layer 조정하기

        → 실행시켜보면 고양이의 에너지바가 fish-shop보다 위에 온답니다.

        → dog와 fish-shop의 order-in-layer를 1 로 조정하기!

        Untitled

    • 5) 특정 y 좌표 밑으로 내려오면 게임 종료

      1. retryBtn 만들고, 숨겨두기

        → Image로 만들기

        width: 300, height: 100

        → button 컴포넌트 달기

        retryBtn.cs 만들어서 넣고, onclick에 연결하기

         using UnityEngine.SceneManagement;
        
         public void ReGame()
         {
             SceneManager.LoadScene("MainScene");
         }

        Untitled

      2. gameManager 싱글톤 화

        → 어딘가에서 나를 부르기 전에는 반드시!

         public static gameManager I;
        
         void Awake()
         {
             I = this;
         }
      3. gameManager 내에 gameOver() 함수 만들기

         public GameObject retryBtn;
        
         public void gameOver()
         {
             retryBtn.SetActive(true);
             Time.timeScale = 0.0f;
         }
      4. cat.cs 에서 특정 y 좌표 밑으로 내려가면 gameOver() 부르기

         void Update()
         {
             if (energy < full)
             {
                 transform.position += new Vector3(0.0f, -0.05f, 0.0f);
        
                 if (transform.position.y < -16.0f)
                 {
                     gameManager.I.gameOver();
                 }
             }
             else
             {
                 if (transform.position.x > 0)
                 {
                     transform.position += new Vector3(0.05f, 0.0f, 0.0f);
                 }
                 else
                 {
                     transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
                 }
                 Destroy(gameObject, 3.0f);
             }
         }

        Untitled

    06. 레벨 구성하기

    [레벨업 표기하기]

    • 1) 레벨 UI 만들기

      1. 오른쪽 위에 레벨 표기하기

        1. level 폴더를 Canvas 게임오브젝트로 묶어 배치합니다.

        2. Rect Transform을 먼저 리셋해주시고 위치는 x:250, y:500 으로 설정 해줍니다.

        3. back / front는 width: 100, height: 15, y: -70 으로 설정 해줍니다.

        4. Text는 width: 20, height: 100, font size: 40, font style: Bold, x: -50 으로 설정 합니다.

          Untitled

    • 2) 5마리 당 레벨 1씩 올리기

      1. gameManager에 레벨업 함수 만들기

         int level = 0;
         int cat = 0;
        
         public void addCat()
         {
             cat += 1;
             level = cat / 5;
         }
      2. 고양이가 배부를 때 addCat() 함수 부르기

        → 다만, 이렇게 하면 안됩니다!

        → 이유는, 이렇게하면 옆으로 빠질 때 계속 점수가 올라가겠죠!

         void OnTriggerEnter2D(Collider2D coll)
         {
             if (coll.gameObject.tag == "food")
             {
                 if (energy < full)
                 {
                     energy += 1.0f;
                     Destroy(coll.gameObject);
        
                     gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
                 }
                 else
                 {
                     gameManager.I.addCat();
                     gameObject.transform.Find("hungry").gameObject.SetActive(false);
                     gameObject.transform.Find("full").gameObject.SetActive(true);
                 }
             }
         }
      3. 그래서! 약간 수정을 합니다.

        상태 를 isFull 로 만들고, 제어합니다.

         bool isFull = false;
        
         if (isFull == false)
         {
             gameManager.I.addCat();
             gameObject.transform.Find("hungry").gameObject.SetActive(false);
             gameObject.transform.Find("full").gameObject.SetActive(true);
        
             isFull = true;
         }
    • 3) 레벨업 표기해주기

      1. 레벨 업 라벨 초기화해주기

        → 레벨업 텍스트 0으로

        → 레벨업 front 바 x scale 0으로

      2. 레벨 업 라벨 바꿔주기

        → 레벨업 텍스트, front 바

         using 
        
         public Text levelText;
         public GameObject levelFront;
        
         public void addCat()
         {
             cat += 1;
             level = cat / 5;
        
             levelText.text = level.ToString();
             levelFront.transform.localScale = new Vector3((cat - level * 5) / 5.0f, 1.0f, 1.0f);
         }

        Untitled

    [레벨 반영하기]

    • 1) Lv.1, Lv.2 : 더 많은 고양이 출현시키기

      1. gameManager.cs 에서 확률에 따른 추가 고양이 출현

        → 단, 빠른 진행을 위해 makeFood를 0.2f0.1f 로 바꿔둘게요!

        → 이미 생각보다 쉽지 않을걸요!

         void makeCat()
         {
             Instantiate(normalCat);
        
             if (level == 1)
             {
                 float p = Random.Range(0, 10);
                 if (p < 2) Instantiate(normalCat);
             }
             else if (level >= 2)
             {
                 float p = Random.Range(0, 10);
                 if (p < 5) Instantiate(normalCat);
             }
         }
    • 2) Lv.3 : fatCat 출현시키기

      1. cat.cs 에 고양이 타입을 만들기!

        type: 0 은 normalCat, type: 1 은 fatCat !!

        → 아래와 같이 써두면, 오브젝트에서 타입을 조절할 수 있습니다.

        → 다시 normalCat 프리팹에 가서 type: 0 으로 만들어보기

         public int type;

        Untitled

      2. fatCat 프리팹 준비하기

        → front의 x scale: 0으로 만들기

        → 스크립트 붙이고 type: 1로 만들기

        → box collider 2D를 붙이는 것도 잊지 말기!

        Untitled

        Untitled

      3. 고양이 타입을 고려한 cat.cs 만들기

        type:1 (fatCat)인 경우 full: 10 이고, 내려오는 속도가 늦음

         void Start()
         {
             float x = Random.Range(-8.5f, 8.5f);
             float y = 30.0f;
             transform.position = new Vector3(x, y, 0);
        
             if (type == 1)
             {
                 full = 10.0f;
             }
         }
         // 업데이트 구문 안의 if문
         if (energy < full)
         {
             if (type == 0)
             {
                 transform.position += new Vector3(0.0f, -0.05f, 0.0f);
             }
             else if (type == 1)
             {
                 transform.position += new Vector3(0.0f, -0.03f, 0.0f);
             }
        
             if (transform.position.y < -16.0f)
             {
                 gameManager.I.gameOver();
             }
         }
      4. fatCat 을 등장시키기

         void makeCat()
         {
             Instantiate(normalCat);
        
             if (level == 1)
             {
                 float p = Random.Range(0, 10);
                 if (p < 2) Instantiate(normalCat);
             }
             else if (level == 2)
             {
                 float p = Random.Range(0, 10);
                 if (p < 5) Instantiate(normalCat);
             }
             else if (level >= 3)
             {
                 float p = Random.Range(0, 10);
                 if (p < 5) Instantiate(normalCat);
        
                 Instantiate(fatCat);
             }
         }

        Untitled

    07. 마무리 - 게임 즐겨보기 & 버그잡기

    • 1) startScene으로 가서 게임을 즐겨봅니다!

      → 앗, 그런데 Replay 후에 총알 발사가 안되네요!

      Untitled

    • 2) gameManager 시작할 때 timeScale = 1.0f 로 바꿔주기

        void Start()
        {
            Time.timeScale = 1.0f;
            InvokeRepeating("makeFood", 0.0f, 0.1f);
            InvokeRepeating("makeCat", 0.0f, 1.0f);
        }

    08. 숙제 - 해적 고양이 만들기!

    • 해적고양이의 속성

      • normalCat 보다 사이즈가 작음 scale: x:0.8, y:0.8
      • normalCat 보다 빠르게 내려옴 -0.1f
    • 해적고양이 준비하기 (튜터와 함께)

      1. normalCat 프리팹을 가져와서, 오른쪽 키 → unpack 합니다.

        Untitled

      2. 이미지 교체해주고, pirateCat의 scale을 작게하기

        Untitled

      3. pirateCat의 animator를 떼어내고, 새로 붙여주기

        Untitled

      4. cat.cs 스크립트 붙이고 type: 2 로 바꿔두기

        Untitled

      5. 프리팹 화 해두고 과감히 삭제하기

        Untitled

    • 이렇게 되면 완성!

      → 제법 흥미진진하죠?

      고양이 밥주기 게임(숙제).mp4

    • 힌트요정 - 👻

      [수정해야할 부분]

      1. cat.cs 의 Update 부분 : type 나오는 곳에 아래를 추가해주기!

         transform.position += new Vector3(0.0f, -0.1f, 0.0f);
      2. gameManager.cs 의 makeCat 부분 : level >= 4 만들어주기!

        level >= 3 부분은 level == 3 으로 바꿔야겠죠?

         else if (level >= 4)
         {
             float p = Random.Range(0, 10);
             if (p < 5) Instantiate(normalCat);
        
             Instantiate(fatCat);
             Instantiate(pirateCat);
         }

    HW. 3주차 숙제 해설

    • gameManager.cs 부분

      makeCat() 부분을 보세요

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
        using UnityEngine.UI;
      
        public class gameManager : MonoBehaviour
        {
            public GameObject food;
            public GameObject dog;
      
            public GameObject normalCat;
            public GameObject fatCat;
            public GameObject pirateCat;
      
            public GameObject retryBtn;
      
            int level = 0;
            int cat = 0;
      
            public Text levelText;
            public GameObject levelFront;
      
            public static gameManager I;
            void Awake()
            {
                I = this;
            }
      
            // Start is called before the first frame update
            void Start()
            {
                Time.timeScale = 1.0f;
                InvokeRepeating("makeFood", 0.0f, 0.1f);
                InvokeRepeating("makeCat", 0.0f, 1.0f);
            }
      
            void makeFood()
            {
                float x = dog.transform.position.x;
                float y = dog.transform.position.y + 2.0f;
                Instantiate(food, new Vector3(x,y,0), Quaternion.identity);
            }
      
            void makeCat()
            {
                Instantiate(normalCat);
      
                if (level == 1)
                {
                    float p = Random.Range(0, 10);
                    if (p < 2) Instantiate(normalCat);
                }
                else if (level == 2)
                {
                    float p = Random.Range(0, 10);
                    if (p < 5) Instantiate(normalCat);
                }
                else if (level == 3)
                {
                    float p = Random.Range(0, 10);
                    if (p < 5) Instantiate(normalCat);
      
                    Instantiate(fatCat);
                }
                else if (level >= 4)
                {
                    float p = Random.Range(0, 10);
                    if (p < 5) Instantiate(normalCat);
      
                    Instantiate(fatCat);
                    Instantiate(pirateCat);
                }
            }
      
            // Update is called once per frame
            void Update()
            {
      
            }
      
            public void gameOver()
            {
                retryBtn.SetActive(true);
                Time.timeScale = 0.0f;
            }
      
            public void addCat()
            {
                cat += 1;
                level = cat / 5;
      
                levelText.text = level.ToString();
                levelFront.transform.localScale = new Vector3((cat - level * 5) / 5.0f, 1.0f, 1.0f);
            }
        }
    • cat.cs 부분

      Update() 부분을 보세요

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
      
        public class cat : MonoBehaviour
        {
            float full = 5.0f;
            float energy = 0.0f;
      
            bool isFull = false;
      
            public int type;
      
            // Start is called before the first frame update
            void Start()
            {
                float x = Random.Range(-8.5f, 8.5f);
                float y = 30.0f;
                transform.position = new Vector3(x, y, 0);
      
                if (type == 1)
                {
                    full = 10.0f;
                }
            }
      
            // Update is called once per frame
            void Update()
            {
                if (energy < full)
                {
                    if (type == 0)
                    {
                        transform.position += new Vector3(0.0f, -0.05f, 0.0f);
                    }
                    else if (type == 1)
                    {
                        transform.position += new Vector3(0.0f, -0.03f, 0.0f);
                    }
                    else if (type == 2)
                    {
                        transform.position += new Vector3(0.0f, -0.1f, 0.0f);
                    }
      
                    if (transform.position.y < -16.0f)
                    {
                        gameManager.I.gameOver();
                    }
                }
                else
                {
                    if (transform.position.x > 0)
                    {
                        transform.position += new Vector3(0.05f, 0.0f, 0.0f);
                    }
                    else
                    {
                        transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
                    }
                    Destroy(gameObject, 3.0f);
                }
            }
      
            void OnTriggerEnter2D(Collider2D coll)
            {
                if (coll.gameObject.tag == "food")
                {
                    if (energy < full)
                    {
                        energy += 1.0f;
                        Destroy(coll.gameObject);
      
                        gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
                    }
                    else
                    {
                        if (isFull == false)
                        {
                            gameManager.I.addCat();
                            gameObject.transform.Find("hungry").gameObject.SetActive(false);
                            gameObject.transform.Find("full").gameObject.SetActive(true);
      
                            isFull = true;
                        }
                    }
                }
            }
        }

    [스파르타코딩클럽] 게임개발 종합반 - 2주차

    [수업 목표]

    1. 유니티 기본 사용법 복습해보기
    2. 유명 게임을 완성해보기
    3. 베스트 스코어 기록해보기

    [목차]


    01. 2주차 오늘 배울 것

    • 1) 2주차 수업의 목표와 범위

      • 1주차에 배웠던 것을 복습 → 복습 → 복습 하는 게 전체적인 수업의 구성이에요!

        → 이번 주는 조금 더 익숙해진 자신을 볼 수 있을 것이랍니다.

        → 참고로 오늘, 쉽습니다! (복습이 많기 때문에! 😎)

      • Rise Up! 이란 게임을 유사하게 만들어보면서, 유니티의 기초를 다시 다집니다.

      • 실제 게임 모습 보기: 전세계 1억 다운로드 이상의 히트작이랍니다.

        Untitled

        Untitled

    • 2) 오늘 만들 것 : 풍선을 지켜라

      • 네모가 풍선에 닿으면 끝! 오래 살아남는 게 목표랍니다.

        → 새롭게 배우는 것: 마우스로 제어하기, 베스트스코어 기록, 애니메이션 전환

        Untitled

    • 3) 만들 순서

      1. 기본 씬 구성하기 - 배경, 풍선, 마우스, 네모, 시간
      2. 풍선 애니메이션 더하기
      3. 마우스 움직임 더하기
      4. 시간 가게 하기
      5. 네모 내려오게 하기 + 충돌 구현
      6. 게임 끝내기(1): 판넬 만들기
      7. 게임 끝내기(2): 베스트 스코어 기록하기
      8. 풍선 애니메이션 전환하기

    [기본 씬 구성하기]

    • 1) 기본 세팅하기 & 배경 만들기

      1. windows → 2x3 layout 클릭! free aspect → phone 클릭!

        Untitled

      2. 배경 색은 rgb ⇒ 20, 20, 80 으로 할게요! 사이즈는 x: 6, y: 10

        Untitled

    • 2) 풍선, 마우스 만들기

      1. 간단한 풍선을 만들어둡니다. (balloon)

        → position x: 0, y: -3.2 에 맞춰두기!

        Untitled

      2. 마우스를 만들어둡니다. (shield)

        → scale x: 0.5, y: 0.5 로 세팅해두기! position은 그대로 둘게요!

        rgb ⇒ 0, 0, 255 로 가겠습니다!

        Untitled

    • 3) 타이머 만들기

      • 시간 라벨 만들기 (timeTxt)

        → UI → Text를 활용!

        font size 70 , position x: 0, y: 450 으로 맞춰주세요!

        → width: 200, height :200

        → posY: 450으로 맞춰주세요!

        → color: 255, 0, 0 (빨강)

        Untitled

    02. 풍선 & 마우스 만들기

    • 1) 애니메이션 더하기

      1. Animations 폴더 → 애니메이션 만들기 (balloon_idle)

        → 이따가 풍선이 "터지는" 애니메이션도 만들어야 하니, 이것은 idle(기본 상태)로 둘게요!

        → Loop Time에 체크하는 것 잊지 말기!

        Untitled

      2. 풍선에 끌어다 놓고 애니메이션 만들기

        →레코드 (빨간색 동그라미!)를 눌러서 같이 세팅합시다!

        → 0:00, 0:40은 처음 모습 그대로

        → 0:20은 rgb ⇒ 200, 200, 255 로 세팅하기

        Untitled

      3. Play 버튼을 눌러서 확인하기

        Untitled

    • 2) 마우스에 움직임 더하기

      1. Scripts → shield.cs 만들기 + shield에 붙이기

        Untitled

      2. 마우스 포인터를 따라 움직이게 하기

        → 외우지 말고, 나중에도 보고 쓰는 코드랍니다. 튜터도 외우고 있지 않아요!

        → mouse 의 좌표계를 카메라 좌표계로 바꾸고, shield의 위치에 넣어주기

         void Update()
         {
             Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
             transform.position = new Vector3(mousePos.x, mousePos.y, 0);
         }
      3. Play 해서 움직임 확인하기

        Untitled

    03. 네모 만들기

    [내려오기]

    • 1) 네모 만들기

      1. sprite → square 로 만들고, 이름 바꿔두기 (square)

        → position x:0, y:3 에 맞추기

        Untitled

      2. order in layer 맞추기

        → 네모, 마우스, 풍선 모두 order in layer를 1로 바꿔줍니다! (참고로 배경은 0 !)

        Untitled

    • 2) 네모 떨어지게 하기/충돌효과 주기

      → rigidbody 2D 와, box collider 2D 를 줍니다.

      Untitled

    • 3) 풍선, 마우스에도 충돌효과 주기

      1. 풍선, 마우스에 circle collider 2D 달기

        Untitled

      2. Play 버튼을 눌러 마우스와 네모가 부딪히는지 보기

        Untitled

    [나타나기]

    • 1) gameManager 만들기

      → gamaManager gameObject 와 script 를 만들고 서로 붙여줍니다!

      → 이후 이 gameManager에서 네모를 만들어 줄 예정!

      Untitled

    • 2) 네모 랜덤으로 나타내기

      1. square.cs 만들고, 네모에 붙이기

        Untitled

      2. 랜덤 위치에서 생성하기

        x: -3.0f ~ 3.0f, y: 3.0f ~ 5.0f

        → 저장 후 Play 해서 확인하기

         void Start()
         {
             float x = Random.Range(-3.0f, 3.0f);
             float y = Random.Range(3.0f, 5.0f);
        
             transform.position = new Vector3(x, y, 0);
         }
      3. 랜덤 사이즈로 생성하기

        size: 0.5f ~ 1.5f

        → 저장 후 Play 해서 확인하기

         void Start()
         {
             float x = Random.Range(-3.0f, 3.0f);
             float y = Random.Range(3.0f, 5.0f);
        
             transform.position = new Vector3(x, y, 0);
        
             float size = Random.Range(0.5f, 1.5f);
             transform.localScale = new Vector3(size, size, 1);
         }
    • 3) 네모를 prefab으로 만들기

      1. Prefabs 폴더를 만들고 → 끌어다넣기

      2. 기존의 square 오브젝트는 과감하게 삭제!

        Untitled

    • 4) gameManager.cs 에서 네모를 만들기

      1. 반복 실행하게 하기

        → 0.5f 마다 makeSquare 함수를 실행!

         void Start()
         {
             InvokeRepeating("makeSquare", 0.0f, 0.5f);
         }
        
         void makeSquare()
         {
             Debug.Log("반복한다!");
         }

        Untitled

      2. 네모 만들기

        → square 프리팹을 받아서, 복제하기

         public GameObject square;
        
         void makeSquare()
         {
             Instantiate(square);
         }

        Untitled

    04. 시간 올라가게 하기

    • 1) 시간 올리기

      1. UI text 받기

         using UnityEngine.UI;
        
         public Text timeTxt;
      2. 시간 올리기

         float alive = 0f;
        
         void Update()
         {
             alive += Time.deltaTime;
             timeTxt.text = alive.ToString("N2");
         }

    05. 게임 끝내기

    [판넬 만들기]

    • 1) 게임 종료 판넬 만들기

      1. Image 만들기

        → 사이즈 x: 450, y: 600

        → shadow 효과주기 : rgba ⇒ 255, 255, 0, 150 (Add Component로 추가!)

        → 그림자 위치는 x: 15, y: -15 로 맞추기

        Untitled

      2. 폰트 가져오기

        • [코드스니펫] 배달의민족 주아체

            http://pop.baemin.com/fonts/jua/BMJUA_ttf.ttf
        → Fonts 폴더 만들고 끌어다넣기
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf440841-5349-4ef3-b30a-6a4e3cd9a845/Untitled.png)
    
    3. 끝 메시지, 현재 스코어, 최고 스코어 만들기
    
        → 폰트 사이즈는 메시지는 50, 라벨은 40으로 해주세요!
    
        → position (-100, 100), (150, 100), (-100, 0), (150, 0)으로 맞춰주세요!
    
        → ctrl+d (복제) 를 이용하면 무척 편하답니다.
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e3a20e24-4dfc-4150-8cb4-1ffb415b5aaa/Untitled.png)
    
    4. retry 버튼 만들기
    
        → retryBtn 오브젝트 안에 만들게요!
    
        → 이미지 색은 `rgb ⇒ 80, 80, 200` 으로 할게요!
    
        → width: 300, height: 100
    
        → posY : -200
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4d01b527-7cf4-4328-a0b9-fa70ead600d2/Untitled.png)
    
    5. 버튼에 `button` 속성 달고 Image 끌어다놓기
    
        → 그래야 클릭 할 때 color 틴트가 일어납니다!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/40e041f9-18bc-4402-b2e5-530955df6e1e/Untitled.png)
    
    6. 우선, 판넬 전체를 숨겨둡니다.
    
        → `SetActive(true)` 로 나중에 켤 것이랍니다!
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/44088764-b8e5-4a8d-b560-030ec83fc768/Untitled.png)

    [판넬 나타내기]

    • 1) gameManager 싱글톤 처리하기

        public static gameManager I;
      
        void Awake()
        {
            I = this;
        }
    • 2) 게임 종료하기

      1. gameManager.cs 에 종료 함수 만들어두기

         public GameObject endPanel;
        
         public void gameOver()
         {
             Time.timeScale = 0.0f;
             endPanel.SetActive(true);
         }
      2. square.cs 네모가 풍선과 부딪히면 게임 종료하기

        → 우선, 풍선에 "balloon"이라는 tag를 주기

         void OnCollisionEnter2D(Collision2D coll)
         {
             if (coll.gameObject.tag == "balloon")
             {
                 gameManager.I.gameOver();
             }
         }
      3. Play 해서 확인하기

        Untitled

    • 3) 현재 점수 보여주기

      1. thisScoreText 가져오기

         public Text thisScoreTxt;
      2. gameOver() 수정하기

         public void gameOver()
         {
             Time.timeScale = 0.0f;
                 thisScoreTxt.text = alive.ToString("N2");
             endPanel.SetActive(true);
         }
      3. 한걸음 더 : Update() 함수를 멈추게 하기

        → Update()와 gameOver() 간의 약간의 시간차가 있기 때문에, 이것을 제어해보겠습니다.

        → 그래야 timeTxtthisScoreTxt 가 같은 값으로 나온답니다!

        Untitled

         bool isRunning = true;
        
         void Update()
         {
             if (isRunning)
             {
                 alive += Time.deltaTime;
                 timeTxt.text = alive.ToString("N2");
             }
         }
        
         public void gameOver()
         {
             isRunning = false;
             Time.timeScale = 0.0f;
             thisScoreTxt.text = alive.ToString("N2");
             endPanel.SetActive(true);
         }
    • 4) 다시하기 만들기

      1. gameManager.cs - 다시하기 함수 만들기

        using UnityEngine.SceneManagement;
        
        public void retry()
        {
            SceneManager.LoadScene("MainScene");
        }
      2. 시간을 다시 켜주기

        → 이렇게 다시 할 때에는 반드시 "시간"도 되돌려 놓아야 합니다!

         void Start()
         {
             Time.timeScale = 1.0f;
             InvokeRepeating("makeSquare", 0.0f, 0.5f);
         }
      3. 다시하기 버튼에 retry() 함수 붙이기

        Untitled

    6. 최고 점수 나타내기

    • 1) 데이터를 보관하는 방법: PlayerPrefs

      • 데이터 저장하기

          PlayerPrefs.SetFloat("bestScore", 어떤숫자값);
          PlayerPrefs.SetString("bestScore", 어떤문자열);
      • 데이터 불러오기

          어떤숫자값 = PlayerPrefs.getFloat("bestScore");
          어떤문자열 = PlayerPrefs.getString("bestScore");
      • 데이터를 저장했었는지 확인

        → 있으면 true 없으면 false

          PlayerPrefs.HasKey("bestScore")
      • 데이터를 모두 지우기

          PlayerPrefs.DeleteAll();
    • 2) 최고 점수 보여주기

      1. 로직 생각하기

         if (최고 점수가 없으면)
         {
             최고점수 = 지금점수
         }
         else
         {
             if (최고점수 < 지금점수)
             {
                 최고점수 = 지금점수
             }
         }
      2. 구현하기

         public void gameOver()
         {
             isRunning = false;
             Time.timeScale = 0.0f;
             thisScoreTxt.text = alive.ToString("N2");
             endPanel.SetActive(true);
        
             if (PlayerPrefs.HasKey("bestScore") == false)
             {
                 PlayerPrefs.SetFloat("bestScore", alive);
             }
             else
             {
                 if (PlayerPrefs.GetFloat("bestScore") < alive)
                 {
                     PlayerPrefs.SetFloat("bestScore", alive);
                 }
             }
         }
      3. 최고 점수 띄워주기

         public Text bestScoreTxt;
        
         bestScoreTxt.text = PlayerPrefs.GetFloat("bestScore").ToString("N2");

        Untitled

    07. 풍선 애니메이션 전환하기

    • 1) 풍선이 터지면서 끝나게 하기

      1. 터지는 애니메이션(balloon_die) 만들고 balloon에 끌어다 놓고 add New Clip 해주기

        Untitled

      2. 0:20 에 기록하기

        x:2, y:2 그리고 rgba ⇒ 255, 0, 0, 125 으로, 터지는 것처럼!

        Untitled

      3. balloon animator를 열어서, idle → die로 transition 만들기

        → 마우스 오른쪽 클릭후 make transition 하면 됩니다!

        Untitled

      4. Parameters에, bool 형식의 isDie 를 만들기

        Untitled

      5. transition을 클릭하고 아래와 같이 세팅하기

        → has exit time 을 체크 해제해야 : 즉시 전환됩니다!

        Untitled

    • 2) 풍선 애니메이션 전환하기

      1. gameManager.cs 에서 - animator를 받기

         public Animator anim;

        Untitled

      2. gameOver() 할 때 isDie 값을 바꿔주기

         public void gameOver()
         {
             anim.SetBool("isDie", true);
        
             isRunning = false;
             Time.timeScale = 0.0f;
             thisScoreTxt.text = alive.ToString("N2");
             endPanel.SetActive(true);
        
             if (PlayerPrefs.HasKey("bestScore") == false)
             {
                 PlayerPrefs.SetFloat("bestScore", alive);
             }
             else
             {
                 if (PlayerPrefs.GetFloat("bestScore") < alive)
                 {
                     PlayerPrefs.SetFloat("bestScore", alive);
                 }
             }
             bestScoreTxt.text = PlayerPrefs.GetFloat("bestScore").ToString("N2");
         }
      3. 확인하기 : 앗, 안된다!

        → 그 이유는, 애니메이션이 나올 틈이 없이 시간을 멈추기 때문

        → 0.5초 후에 시간을 멈추도록 Invoke 로 처리하기!

         public void gameOver()
         {
             anim.SetBool("isDie", true);
        
             isRunning = false;
             Invoke("timeStop", 0.5f);
             thisScoreTxt.text = alive.ToString("N2");
             endPanel.SetActive(true);
        
             if (PlayerPrefs.HasKey("bestScore") == false)
             {
                 PlayerPrefs.SetFloat("bestScore", alive);
             }
             else
             {
                 if (PlayerPrefs.GetFloat("bestScore") < alive)
                 {
                     PlayerPrefs.SetFloat("bestScore", alive);
                 }
             }
             bestScoreTxt.text = PlayerPrefs.GetFloat("bestScore").ToString("N2");
         }
        
         void timeStop()
         {
             Time.timeScale = 0.0f;
         }

    08. 숙제 - 떨어지는 네모를 없애기!

    • 현재 상황

      Untitled

      → "뜨악" 시간이 지나면 네모가 계속 쌓이고 있었네요..!

      → 화면을 넘어가면 square를 Destroy 해줄 수 있을까요?

    • 이렇게 되면 완성!

      → 화면에 보여지는 네모와 square(clone) 수가 일치하면 완성!

      Untitled

    • 힌트요정 - 👻

      square.cs 만 수정하면 된답니다!

      → Update() 안에 딱 세 줄만 넣으면 됩니다! 딱 5분만 더 해보면 될 거예요!

      → y좌표 구하기 ⇒ transform.position.y 기억나시죠!

      → 없애라 ⇒ Destroy(gameObject) 기억나시죠!

        Update()
        {
            if (만약에 y좌표가 -5.0f 보다 작다면)
            {
                없애라;
            }
        }

    HW. 2주차 숙제 해설

    • square.cs 코드

        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
      
        public class square : MonoBehaviour
        {
            // Start is called before the first frame update
            void Start()
            {
                float x = Random.Range(-3.0f, 3.0f);
                float y = Random.Range(3.0f, 5.0f);
      
                transform.position = new Vector3(x, y, 0);
      
                float size = Random.Range(0.5f, 1.5f);
                transform.localScale = new Vector3(size, size, 1);
            }
      
            // Update is called once per frame
            void Update()
            {
                if (transform.position.y < -5.0f)
                {
                    Destroy(gameObject);
                }
            }
      
            void OnCollisionEnter2D(Collision2D coll)
            {
                if (coll.gameObject.tag == "balloon")
                {
                    gameManager.I.gameOver();
                }
            }
        }

    [스파르타코딩클럽] 게임개발 종합반 - 1주차

    [수업 목표]

    1. 유니티 다뤄보기
    2. C# 기본 문법 익히기
    3. 유니티 기본 사용법 익히기

    [목차]


    01. 1주차 오늘 배울 것

    • 1) 게임개발종합반 수업의 목표와 범위

        "어차피 게임 개발자들도 모든 유니티 코드를 외우고 있지 않습니다.
      
        결국, 어떻게 동작하는지 대략적인 기능을 이해하고,
        내가 필요한 부분을 찾아서 만들 수 있는 단계로 오르는 것이 중요합니다."
      • 핵심: 유니티는 안 어렵습니다! 그런데, 처음에 사용법을 깔끔하게 설명해둔 곳이 없습니다!
      • 4~5개를 만들어보게 되면, 결국 코드는 돌고 돈다-는 것을 알게 되실 거예요.
      • C# 이라는 프로그래밍 언어를 사용하는데요, 이것은 하면서! 알려드리겠습니다. 😄
    • 2) raindrop - 친환경 게임: 빗물 받는 르탄이

      빗물 받는 르탄이.mp4

    • 3) 5주 강의 구성

      • 1주차 - 빗물 받는 르탄이 : 유니티 세팅, 기초 문법 연습
      • 2주차 - 풍선을 구해라! 백만 다운로드 게임 따라만들기 : 유니티 기초 복습
      • 3주차 - 고양이 밥주기 게임 : hp바, 레벨 연습하기
      • 4주차 - 르탄이 카드 뒤집기 게임 : 보드 게임 기초 구현하기
      • 5주차 - 주변 기능 학습 : 스플래시 화면 구성, 광고붙이기, 배포하기, 무료 에셋 구경하기
    • 4) 오늘 만들 순서

      (1) 유니티 - 기본 세팅, 씬 구성하기

      (2) 캐릭터 왔다 갔다 하게 하기 + 클릭 시 방향 전환 구현

      (3) 비 내리기 구현

      (4) 비 충돌 구현

      (5) UX (남은 시간 / 숫자합) 구현

      (6) 게임 오버(팝업) 구현

    02. 유니티 설치하기

    • 1) OS별 각각의 다운로드 링크(Window / Mac)를 클릭해서 Unity-hub를 다운받습니다.

      • [코드스니펫] (Window) 유니티허브 다운로드

          https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe?_ga=2.197600431.1066071928.1631537679-831002153.1627910894
      • [코드스니펫] (Mac) 유니티허브 다운로드

          https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.dmg
    • 2) 로그인 진행

      1. 로그인하기 클릭 후 회원가입 진행 → 구글로 로그인

        Untitled

    • 3) 라이선스 발급받기

      1. 라이선스 관리 클릭

        Untitled

      2. 새 라이선스 활성화 클릭 → Personal → 완료

        Untitled

        Untitled

      3. 아래와 같은 화면이 나오면 발급 완료!

        Untitled

    • 4) 유니티 설치하기

      1. 뒤로가기 눌러서 메인으로 돌아온 뒤 → 설치 → '추가' 클릭

        Untitled

      2. 아래 화면에서 '다음' 클릭

        Untitled

      3. VisualStudio 클릭 + Android build supprt 클릭

        • 맥의 경우 'Mac build support' / 윈도우의 경우 'Windows build support' 클릭

          (모듈은 추후에도 추가할 수 있으니 잘못 체크했을까봐 너무 걱정 마세요!)

          Untitled

          Untitled

      4. 동의 → 동의 → 완료 클릭. 꽤 오랜 시간 (최장 20~30분까지) 기다리면 설치 완료!

        Untitled

      5. 마지막으로, 한국어 세팅하면 끝!

        Untitled

    03. 기본 씬 구성 및 애니메이션 맛보기

    • 1) 유니티에서 개발하기

      • 게임 개발에 최적화된 개발 환경. 특히 2D 게임은 거의 100% 유니티로 개발한 것으로 생각하면 됨. 최근엔 대놓고 unity 로고를 보이는 게임들도 많음. 그림판 vs 포토샵.

      • 프로젝트 생성 후 window → 아래와 같이 뷰 환경을 세팅!

        Untitled

        project → 오른쪽 클릭 → one column layout 클릭

        Untitled

    • 2) 각각 뭐 하는 뷰일까?

      • Scene : 실제 게임의 구성요소를 보는 곳. 실질적인 게임 개발 씬
      • Game : 게임이 실제로 보여지는 곳. play 버튼 클릭 후 볼 수 있음
      • Hierachy : 게임 내 구성요소를 볼 수 있는 곳. 개발 시 자주 필요
      • Project : 이 프로젝트에 포함된 파일들을 모아볼 수 있는 곳
      • Inspector : 클릭한 요소의 속성과 정보를 보여주는 곳(차차 보면 알게 됨!)
    • 3) 배경 세팅하기

      1. 메인 씬 이름 바꾸기

        → project에서 오른쪽 클릭 후 MainScene으로 변경

        Untitled

      2. Game 씬 사이즈 바꾸기

        + 버튼을 클릭하고 760 x 1280 Phone을 입력 → Phone 으로 변경

        Untitled

      3. 배경 입히기

        → 2D Object → Sprite → Square 클릭 → background로 이름 바꾸기

        → 색을 255,255,220,255로 맞추기

        → Scale을 X:6, Y:10으로 맞추기

        Untitled

    • 4) UI박스(점수) 세팅하기

      1. 검은색 박스 만들기

        → 2D Object → Sprite → Square 클릭 → ground로 이름 바꾸기

        → 색을 50, 50, 50, 255로 맞추기

        → Scale을 X:6, Y:1.5으로 맞추기 + Position은 Y:-4.3으로 맞추기

        → order in layer를 1로 맞추기

        Untitled

      2. '에셋'에 캐릭터 넣어두기

        → Assets 에서 Images 폴더 생성 → 르탄이 이미지 압축 풀고 끌어다놓기
    
    3. 르탄이 캐릭터 만들기
    
        → 2D Object → Sprite → Square 클릭 → rtan으로 이름 바꾸기
    
        → Sprite 부분에 르탄이1 이미지 끌어다놓기
    
        → Order in Layer를 1로 바꾸기
    
        → position `Y:-2.9`
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/93acc45d-b094-43e3-ad72-bb05e3781ee6/Untitled.png)
    • 5) 간단한 애니메이션을 입혀보기

      1. 애니메이션 폴더 만들기 (Asset → Animation)

      2. 애니메이션 파일을 만들고, Loop Time에 체크

        Untitled

      3. 이것을 만들어둔 르탄 캐릭터에 sprite에 끌어다놓기

      4. Controller가 생긴 것을 확인!

        → Controller는 : Animation을 컨트롤 하는 것 (예 - 보통 상태 / 맞을 때 / 뛸 때 어떤 애니메이션을 써라)

        → Animation은 : 동작 파일

        Untitled

    • 6) 기본 Animation 만들어보기

      1. rtan_run.anim 더블 클릭 후 → 르탄이 캐릭터 클릭

      2. 르탄이1, 2파일을 적당한 시간 간격으로 끌어다두기

        Untitled

      3. Animator 간단 설명

        → 시작하면 무조건 rtan_run을 실행하게 되어있고

        → rtan_run은 끝이 없는 애니메이션임

        Untitled

      4. 실행해보면, 움직인다!

    04. 캐릭터 움직이기

    • 1) 먼저 세팅하기 : Visual Studio

      • 윈) Edit → Preferences → External Tools → Visual Studio Community 2019로 맞추기

      • 맥) Unity → Preferences → External Tools → Visual Studio for mac

        Untitled

    • 2) 캐릭터에 코딩을 더하는 법

      → 유니티에서는, 캐릭터가 코드를 갖고 있을 수 있음

      → 즉, 캐릭터에 코드를 입히는 것. "너는 태어날 때 여기서 태어나고, 매 순간 이렇게 작동해라"

      → 가장 중요한 두 가지 함수가 있음. start (너는 태어날 때) / update (매 순간 이렇게 해라)

    • 3) Script 만들기

      → Assets 우클릭 → Create → Folder (이름 Scripts) → Create → C# script (이름 rtan)

      → C#은? Microsoft가 개발한 코딩 개발 언어. 희한하게 유니티에서만 주류로 쓰이고 있다.

      Untitled

    • 4) 캐릭터 좌우 움직임 코딩하기

      • 1) 캐릭터 오른쪽으로 이동하기

        → 아래와 같이 입력하고 캐릭터에 스크립트를 끌여다 놓기

        • [코드스니펫] 캐릭터 오른쪽으로 이동하기

            void Update()
            {
                transform.position += new Vector3(0.05f, 0, 0);
            }
        ```csharp
        void Update()
        {
            transform.position += new Vector3(0.05f, 0, 0);
        }
        ```
    
        → Play 버튼을 눌러보기 (캐릭터가 오른쪽으로 이동한다!)
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/40d4b96e-31cf-42d1-9270-5fe26045e685/Untitled.png)
    
        → transform의 의미: 캐릭터의 위치와 중, position을 계속 바꿔달라는 것
    
        → `transform.position += new Vector3(0.05f, 0, 0);`
    
        → 트랜스폼 안의 포지션을, Vector3 방향으로 계속 더해주세요
    
        → float 란? 소수점을 나타내는 자료형. 즉, 소수를 쓰고 싶으면 뒤에 f 를 붙여줘야 함
    
        → 위에 변수를 선언해서 이렇게 쓸 수도 있음!
    
        ```csharp
        float direction = 0.05f;
    
        // Start is called before the first frame update
        void Start()
        {
    
        }
    
        // Update is called once per frame
        void Update()
        {
            transform.position += new Vector3(direction, 0, 0);        
        }
        ```
    
    - 2) 캐릭터가 벽에 닿으면 다른 방향을 보게 하기
    
        → 2-0) Debug.Log 로 위치 보기
    
        - **[코드스니펫] Debug.log**
    
            ```csharp
            Debug.Log(transform.position.x);
            ```
    
    
        ```csharp
        Debug.Log(transform.position.x);
        ```
    
        C#
    
        → 2-1) 760보다 클 때 다른 방향 보게 하기
    
        - **[코드스니펫] 760보다 클 때 다른 방향 보게 하기**
    
            ```csharp
            float direction = 0.05f;
    
            // Start is called before the first frame update
            void Start()
            {
    
            }
    
            // Update is called once per frame
            void Update()
            {
                if (transform.position.x > 2.8f)
                {
                    direction = -0.05f;
                }
                transform.position += new Vector3(direction, 0, 0);
            }
            ```
    
    
        ```csharp
        float direction = 0.05f;
    
        // Start is called before the first frame update
        void Start()
        {
    
        }
    
        // Update is called once per frame
        void Update()
        {
            if (transform.position.x > 2.8f)
            {
                direction = -0.05f;
            }
            transform.position += new Vector3(direction, 0, 0);
        }
        ```
    
        → 2-2) 0보다 작을 때 다른 방향 보게 하기
    
        ```csharp
        float direction = 0.05f;
        // Start is called before the first frame update
        void Start()
        {
    
        }
    
        // Update is called once per frame
        void Update()
        {
            if (transform.position.x > 2.8f)
            {
                direction = -0.05f;
            }
            if (transform.position.x < -2.8f)
            {
                direction = 0.05f;
            }
            transform.position += new Vector3(direction, 0, 0);
        }
        ```
    
        → 📝(직접 해보기) 2-3) 방향 전환하기
    
        (힌트1: "유니티 2d 좌우반전하기"로 검색)
    
        (힌트2: 이런 코드를 만나면 굿! `transform.localScale = new Vector3(-1, 1, 1);` )
    
        - **[코드스니펫] 방향 좌우반전하기**
    
            ```python
            transform.localScale = new Vector3(-1, 1, 1);
            ```
    
        - 펼치면 답!
    
            ```csharp
            float direction = 0.05f;
            // Start is called before the first frame update
            void Start()
            {
    
            }
    
            // Update is called once per frame
            void Update()
            {
                if (transform.position.x > 2.8f)
                {
                    direction = -0.05f;
                    transform.localScale = new Vector3(-1, 1, 1);
                }
                if (transform.position.x < -2.8f)
                {
                    direction = 0.05f;
                    transform.localScale = new Vector3(1, 1, 1);
                }
                transform.position += new Vector3(direction, 0, 0);
            }
            ```
    • 5) 클릭 시 움직임 바꾸기

      • 위의 코드를 조금만 예쁘게 다듬고,

          float direction = 0.05f;
          float toward = 1.0f;
          // Start is called before the first frame update
          void Start()
          {
        
          }
        
          // Update is called once per frame
          void Update()
          {
              if (transform.position.x > 2.8f)
              {
                  direction = -0.05f;
                  toward = -1.0f;
              }
              if (transform.position.x < -2.8f)
              {
                  direction = 0.05f;
                  toward = 1.0f;
              }
              transform.localScale = new Vector3(toward, 1, 1);
              transform.position += new Vector3(direction, 0, 0);
          }
      • 마우스 클릭하면 → 움직이는 방향/이미지 방향 바꾸기

        • [코드스니펫] 마우스 클릭시 방향 바꾸기

            if (Input.GetMouseButtonDown(0))
            {
                toward *= -1;
                direction *= -1;
            }
        ```csharp
        if (Input.GetMouseButtonDown(0))
        {
            toward *= -1;
            direction *= -1;
        }
        ```

    05. 빗방울 코딩하기

    [빗방울 내리게 하기]

    • 1) 빗방울 특징

      • 빗방울은 "하늘 랜덤한 위치에서 내림"
      • 큰 / 중간 / 작은 빗방울 존재 (3-2-1점)
      • 캐릭터와 부딪히면 점수 더하기
    • 2) 빗방울 그리기

      • Sprite → Circle 클릭 → rain 으로 이름 바꾸기

        → 색을 150,150,255,255으로 맞추기

        → Position Y:4 세팅하기

        Untitled

    • 3) 빗방울 떨어지게 하기

      • rigidbody 2D를 달아 중력의 영향을 받게 하기

        Untitled

    • 4) 땅에 닿으면 없어지게 하기(충돌 세팅)

      1. circle collider 2d를 달고, 반경 조정. 자세히 보면 초록색 선이 보임!

        Untitled

      2. 바닥에도 box collider 2d를 달아주기

      3. 게임을 실행하면 땅과 충돌을 합니다.

        Untitled

    • 5) 땅에 닿으면 없어지게 하기(충돌 조작)

      1. "땅"인지 알 수 있게, ground 라고 tag를 주기

        Untitled

        Untitled

      2. 땅에 닿았는지 확인하기

        rain 스크립트 만들고, 빗방울에 붙이기

        OnCollisionEnter2D 함수는 다른 콜라이더에 부딪혔을 때 실행되는 내장함수

        → coll (부딪힌 것의) tag 가 ground 이면!

        • [코드스니펫] 땅에 닿았는지 확인하기

            void OnCollisionEnter2D(Collision2D coll)
            {
                if (coll.gameObject.tag == "ground")
                {
                    Debug.Log("땅이다!");
                }
            }
        ```csharp
        void OnCollisionEnter2D(Collision2D coll)
        {
            if (coll.gameObject.tag == "ground")
            {
                Debug.Log("땅이다!");
            }
        }
        ```
    
    3. 비가 없어지게 하기
    
        → Debug.Log 대신, `Destroy(gameObject)`
    
        → gameObject는 나 자신
    
        - **[코드스니펫] 비가 없어지게 하기**
    
            ```python
            Destroy(gameObject);
            ```
    
    
        ```csharp
        void OnCollisionEnter2D(Collision2D coll)
        {
            if (coll.gameObject.tag == "ground")
            {
                Destroy(gameObject);
            }
        }
        ```

    [빗방울 랜덤하게 나타나게 하기]

    • 1) 랜덤하게 위치 잡아주기

      • start() 함수에 랜덤 position 세팅하기

          void Start()
          {
              float x = Random.Range(-2.7f, 2.7f);
              float y = Random.Range(3.0f, 5.0f);
              transform.position = new Vector3(x, y, 0);
          }
    • 2) 랜덤하게 사이즈(큰/중간/작은) 잡아주기

      • 어떤 사이즈로 나올지 생각하고 →

        사이즈 변경: transform.localScale = new Vector3(size, size, 0);

        색 변경: GetComponent<SpriteRenderer>().color = new Color(100 / 255f, 100 / 255f, 255 / 255f, 255 / 255f); (참고 : 255.0f 로 나눠주는 게 핵심!)

          int type;
          float size;
          int score;
        
          // Start is called before the first frame update
          void Start()
          {
              float x = Random.Range(-2.7f, 2.7f);
              float y = Random.Range(3.0f, 5.0f);
              transform.position = new Vector3(x, y, 0);
        
              type = Random.Range(1, 4);
        
              if (type == 1)
              {
                  size = 1.2f;
                  score = 3;
                  GetComponent<SpriteRenderer>().color = new Color(100 / 255f, 100 / 255f, 255 / 255f, 255 / 255f);
              }
              else if (type == 2)
              {
                  size = 1.0f;
                  score = 2;
                  GetComponent<SpriteRenderer>().color = new Color(130 / 255f, 130 / 255f, 255 / 255f, 255 / 255f);
              }
              else
              {
                  size = 0.8f;
                  score = 1;
                  GetComponent<SpriteRenderer>().color = new Color(150 / 255f, 150 / 255f, 255 / 255f, 255 / 255f);
              }
        
              transform.localScale = new Vector3(size, size, 0);
          }
        • [코드스니펫] type이 1일 때

            size = 1.2f;
            score = 3;
            GetComponent<SpriteRenderer>().color = new Color(100 / 255f, 100 / 255f, 255 / 255f, 255 / 255f);
        • [코드스니펫] type이 2일 때

            size = 1.0f;
            score = 2;
            GetComponent<SpriteRenderer>().color = new Color(130 / 255f, 130 / 255f, 255 / 255f, 255 / 255f);
        • [코드스니펫] type이 3일 때

            size = 0.8f;
            score = 1;
            GetComponent<SpriteRenderer>().color = new Color(150 / 255f, 150 / 255f, 255 / 255f, 255 / 255f);

    [빗방울 계속 나오게 하기]

    • 1) GameManager 만들기

      • 예) 점수 / 다시 시작 / 3번 째 다시 시작에 부스터 / 광고보기 등

      • 빈 곳에 object를 만들고 "gameManager"로 만들어둡니다.

      • 마찬가지로 스크립트도 만들어 붙입니다. (어떻게 알았는지 아이콘 모양이 다르네요!)

        Untitled

    • 2) 빗방울 복제하기 - Prefabs

      • 프리팹 구현하기 (폴더 만들고 끌어다 놓기 & 오브젝트는 삭제)

        Untitled

    • 3) 빗방울 복제하기 - Instantiate

      1. gameManager에서 : 빗방울을 받기 + 프리팹 끌어다놓기

         public GameObject rain;

        Untitled

      2. 0.5초마다 한번씩 실행되는 코드

        • [코드스니펫] InvokeRepeating 함수

            InvokeRepeating("makeRain", 0, 0.5f);
        • [코드스니펫] makeRain 함수

            void makeRain()
            {
                Debug.Log("비를 내려라!");
            }
        ```csharp
        void Start()
        {
            InvokeRepeating("makeRain", 0, 0.5f);
        }
    
        void makeRain()
        {
            Debug.Log("비를 내려라!");
        }
        ```
    
    3. 빗방울 프리팹을 복제하기
        - **[코드스니펫] Instantiate 함수**
    
            ```csharp
            Instantiate(rain);
            ```
    
    
        ```csharp
        void makeRain()
        {
            Instantiate(rain);
        }
        ```

    06. 점수 올라가게 하기

    • 1) 점수 보드 만들기

      1. 폰트 적용하기

        → Assets에 fonts 폴더 만들고 옮겨두기

        • [코드스니펫] 배민-한나체

            http://pop.baemin.com/fonts/hanna11yrs/BMHANNA_11yrs_ttf.ttf
      2. Sprite vs UI 그리고 Canvas

        UI → Text 클릭 → 아래 설정을 따라하기 (폰트사이즈, 위치 등)

        Untitled

        Untitled

      3. text를 네 번 복사→붙여넣기 해서 아래와 같이 맞추기

        Untitled

    • 2) gameManager - 싱글톤 화

      • [코드스니펫] 싱글톤 화

          public static gameManager I;
        
          void Awake()
          {
              I = this;
          }
    ```csharp
    public static gameManager I;
    
    void Awake()
    {
        I = this;
    }
    ```
    • 3) gameManager - 점수 올라가는 함수 만들기

        int totalScore = 0;
      
        public void addScore(int score)
        {
            totalScore += score;
        }
    • 4) 빗방울 - 캐릭터에 맞으면 점수 올라가게 하기

      1. 캐릭터에 tag 주기 + collider 주기

        Untitled

        Untitled

      2. 빗방울 - 캐릭터에 맞으면 점수 올라가고 + 사라지기

         void OnCollisionEnter2D(Collision2D coll)
         {
             if (coll.gameObject.tag == "ground")
             {
                 Destroy(gameObject);
             }
        
             if (coll.gameObject.tag == "rtan")
             {
                 gameManager.I.addScore(score);
                 Destroy(gameObject);
             }
         }
      3. gameManager - addScore 함수에 Debug.Log를 걸어서 확인 → 잘된다!

        Untitled

    • 5) gameManager - 올라가는 점수 표기하기

      1. UI Text 받기

         using UnityEngine.UI;
         public Text scoreText;
      2. Text 바꿔주기

         public void addScore(int score)
         {
             totalScore += score;
             scoreText.text = totalScore.ToString();
         }
      3. 처음 스코어는 0으로 만들어주기

        Untitled

    07. 게임 끝내기

    • 1) Retry 판넬 만들기

      • 정리하기 : 아래와 같이 세팅하기

        → image 사이즈: 400 / 250

        → txt 사이즈: 80

        → 글자 색상 (255, 255, 255, 255)

        → 배경 색상 (232, 52, 78, 255)

        → Inactive로 만들어두기

        Untitled

    • 2) gameManager - 시간이 가게 하기

      1. 시간이 흐르게 하기

         void Update()
         {
             limit -= Time.deltaTime;
             timeTxt.text = timeLimit.ToString("N2");
         }
      2. 멈추게 하기

         void Update()
         {
             limit -= Time.deltaTime;
             if (limit < 0)
             {
                 Time.timeScale = 0.0f;
                 limit = 0.0f;
             }
             timeTxt.text = timeLimit.ToString("N2");
         }
    • 3) 0초에 Retry 판넬 나오게하기

      1. Panel 받기

         public GameObject panel;
      2. Panel 나오게 하기

         void Update()
         {
             limit -= Time.deltaTime;
        
             if (limit < 0)
             {
                 limit = 0.0f;
                 panel.SetActive(true);
                 Time.timeScale = 0.0f;
             }
        
             timeText.text = limit.ToString("N2");
         }
    • 4) 판넬 클릭하면 다시 시작하게 하기

      1. 판넬에 button 달기

        Untitled

      2. 씬 불러오는 것은 중앙에서 해야할 일! (gameManager.cs)

         using UnityEngine.SceneManagement;
        
         public void retry()
         {
             SceneManager.LoadScene("MainScene");
         }
      3. 클릭하면 작동 할 함수 만들기 (panel.cs)

        • [코드스니펫] panel.cs

            public void retry()
            {
                gameManager.I.retry();
            }
        ```csharp
        public void retry()
        {
            gameManager.I.retry();
        }
        ```
    
    4. onclick 연결하기
    
        ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8f3c7d45-7553-4b37-a00f-766acd4c1427/Untitled.png)
    • 5) gameManager - 초기화 함수를 만들기

      • 초기화 해야 할 요소들

        → timeScale, timeLimit, totalScore

          void Start()
          {
              InvokeRepeating("makeRain", 0, 0.5f);
              initGame();
          }
          void initGame()
          {
              Time.timeScale = 1.0f;
              totalScore = 0;
                limit = 30.0f;
          }
    • 6) 수업 전체 코드

    08. 숙제 - 빨강 빗방울 만들기

    • 사이즈는 0.8로 해주세요!

    • 색은 new Color(255 / 255.0f, 100.0f / 255.0f, 100.0f / 255.0f, 255.0f / 255.0f); 이렇게!

    • 이렇게 되면 완성!

      빗물받는르탄이_숙제완성.mp4

    • 힌트요정 - 👻

      rain.cs 만 수정하면 된답니다!

      type = Random.range( .. 여기부터, if else 까지!

    HW. 1주차 숙제 해설

    • new Color

        new Color(255 / 255.0f, 100.0f / 255.0f, 100.0f / 255.0f, 255.0f / 255.0f);
    • rain.cs 코드

    [유머] 만우절 장난치는 여친

     

     

    + Recent posts