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.

    + Recent posts