2주차 - 다양한 위젯을 활용해 화면 그리기
PDF 파일
단축키 모음
- 새로고침
F5 - 저장
- Windows:
Ctrl+S - macOS:
command+S
- Windows:
- 전체선택
- Windows:
Ctrl+A - macOS:
command+A
- Windows:
- 잘라내기
- Windows:
Ctrl+X - macOS:
command+X
- Windows:
- 콘솔창 줄바꿈
shift+enter
- 코드정렬
- Windows:
Ctrl+Alt+L - macOS:
option+command+L
- Windows:
- 들여쓰기
Tab- 들여쓰기 취소 :
Shift+Tab
- 주석
- Windows:
Ctrl+/ - macOS:
command+/
- Windows:
- 새로고침
[수업 목표]
- Flutter의 Widget 이해하기
- 화면 그리는 위젯 이해하기
- 당근마켓 만들기
[목차]
01. Flutter Widget 이해하기
1) StatelessWidget
StatelessWidget 생김새
.png)
extends StatelessWidget: StatelessWidget의 기능을 물려받습니다.생성자: 클래스 이름과 동일한 함수build 함수: 화면에 보여줄 자식 위젯을 반환
코드스니펫을 복사한 뒤 주소창에 붙여 넣어, DartPad로 접속하여 StatelessWidget을 배워봅시다.
[코드스니펫] DartPad StatelessWidget 학습
https://dartpad.dev/?id=f8a7bf195ec729c400634d97b10f0f84
<aside>
💡 **StatelessWidget**의 **생김새** 및 **실행 순서**만 집중해주세요!
</aside>
`Run` 버튼을 누르면 우측에 `hello Stateless Widget` 이라는 문구가 표시됩니다.

9번째 줄에 `MyApp` 클래스가 직접 만든 **StatelessWidget** 위젯입니다.

참고: [Key 란 무엇인가](https://nsinc.tistory.com/214)
3. 실행 순서
좌측 하단에 `Console` 버튼을 클릭해주세요.

1. 처음에 3번째 줄 `main()` 함수가 호출되어 Console에 `시작`이 출력됩니다.
2. MyApp 위젯이 첫 번째 위젯으로 등록되고 `build` 함수가 호출 되면서 Console에 `build 호출`이 출력됩니다.

<aside>
💡 화면에 보이는 첫 번째 위젯은 일반적으로 **MaterialApp** 또는 **CupertinoApp** 위젯으로 시작합니다.
</aside>2) StatefulWidget
StatefulWidget 생김새
기본적으로 2개의 클래스로 구성되어 있습니다.
.png)
MyApp: StatefulWidget의 기능을 물려받은 클래스_MyAppState:MyApp상태를 가진 클래스(실질적인 내용은 여기에 들어가요!)
화면을 그리는
build 함수는 상태 클래스 (_MyAppState) 에 있습니다.
코드스니펫을 복사한 뒤 주소창에 붙여 넣어, DartPad로 접속하여 StatefulWidget을 배워봅시다.
[코드스니펫] DartPad StatefulWidget 학습
https://dartpad.dev/?id=513188d8ad8004aaf524ce52d668d84c
<aside>
💡 **StatefulWidget**의 **생김새** 및 **실행 순서**만 집중해주세요!
</aside>
`Run` 버튼을 누르면 우측에 `1`이 표시됩니다.

3. 실행 순서
좌측 하단에 Console 버튼을 클릭해 주세요.

4번째 줄의 `main 함수`와 23번째 줄의 `build 함수`가 차례대로 실행되어 아래와 같이 출력됩니다.

우측에 파란 버튼을 누르면 화면에는 숫자가 2로 변경된 것을 볼 수 있습니다.

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로 접속합니다.
-
https://dartpad.dev/?id=185dbad13d31ea8b99d7f83fe1f8ec3a
-

<aside>
💡 화면이 많아지는 경우, [Named Route](https://docs.flutter.dev/cookbook/navigation/named-routes) 방식을 사용하기도 합니다.
</aside>02. 프로젝트 준비
1) Flutter 프로젝트 생성하기
Visual Studio Code(VSCode)를 실행해 주세요.
View→Command Palette버튼을 클릭해주세요.
명령어를 검색하는 팝업창이 뜨면,
flutter라고 입력한 뒤Flutter: New Project를 선택해주세요.
Application을 선택해주세요.
프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔
flutter폴더를 선택한 뒤Select a folder to create the project in버튼을 눌러 주세요.프로젝트 이름을
daangn으로 입력해주세요.
만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요.

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

아래
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 세팅
View→Command Palette를 선택해 주세요.
아래와 같이
dart recommend라고 검색한 뒤Dart: Use Recommended Settings를 선택해 주세요. 그러면 자동으로 저장 시 자동 줄 정렬해 주는 기능과 같이 편의 기능 설정이 적용됩니다.
다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.
analysis_options.yaml 파일을 열고, 아래 코드스니펫을 복사해서 24번째 라인 뒤에 붙여 넣고 저장해 주세요.
[코드스니펫] analysis_options.yaml
prefer_const_constructors: false prefer_const_literals_to_create_immutables: false

<aside>
💡 상세 내용은 아래를 참고해 주세요.
- 어떤 의미인지 궁금하신 분들을 위해
`main.dart` 파일을 열어보시면 파란 실선이 있습니다.

파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
12번째 라인에 마우스를 올리면 아래와 같이 설명이 보입니다.

위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수(변하지 않는 수)로 선언하라는 힌트입니다. 상수로 만들면 화면을 새로고침 할 때, 상수로 선언된 위젯들은 새로고침을 할 필요가 없어서 성능상 이점이 있습니다.
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.

지금은 학습 단계이니 눈에 띄지 않도록 `analysis_options.yaml` 파일에 아래와 같이 설정하여 힌트를 숨기도록 하겠습니다.
```dart
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
```
</aside>3) 에뮬레이터 실행하기
VSCode 우측 하단에
Chrome (web-javascript)를 클릭해주세요.
Start Pixel 2 API 29 mobile emulator를 선택해주세요.
잠시 기다리면 에뮬레이터가 실행됩니다.

VSCode 우측 상단에 아래 화살표를 눌러
Run Without Debugging을 눌러주세요.
에뮬레이터에 아래와 같이 흰 화면이 나오면 정상적으로 실행이 완료된 것입니다!
[Android]

[iOS]


03. 당근마켓 화면 만들기
- 최종 완성 이미지

1) HomePage 만들기
main.dart파일 19번째 라인에st라고 입력한 뒤, 자동 완성으로 추천되는Flutter Stateless Widget을 선택해주세요.
그러면 아래와 같이 StatelessWidget 클래스가 완성됩니다.

HomePage라고 이름을 적어주세요. 그러면 클래스의 이름과 생성자(constructor)에 아래와 같이 작성이 됩니다.
아래 코드스니펫을 복사해서 24번째 라인을 지우고 붙여 넣어 저장해주세요.
(window :ctrl + s/ macOS :cmd + s)[코드스니펫] HomePage Scaffold
return Scaffold( body: Center(child: Text("home page")), );

5. 앱 실행시 14번째 라인에 `home: Scaffold()`를 `home: HomePage()`로 변경한 뒤 저장해주세요.

6. 이제 에뮬레이터에 `HomePage`가 뜨는 것을 볼 수 있습니다.

<aside>
💡 내용 요약
앱을 시작할 때 `MaterialApp`으로 앱을 시작하고, `home`이라는 **이름지정 매개변수(named parameter)**에 첫 번째 페이지 위젯을 만들어 전달합니다.

</aside>2) appBar 만들기
AppBar의 영역에 대한 명칭은 아래와 같습니다.

우리는 아래와 같이
leading과actions에 각각 아이콘과 이미지를 넣어주면 되겠군요!
leading&actions아이콘 버튼 만들기아래 코드스니펫을 복사해서 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), ), ], ),

2. 47번째 라인에 `CupertinoIcons`가 빨간 줄로 그어져 있습니다. 마우스를 올려보면 아래와 같이 `CupertinoIcons` 위젯이 인식이 안된다고 나옵니다.
<aside>
💡 **빨간 줄**은 문제가 있다는 표시입니다. 이 상태에서는 저장을 해도 앱에 반영이 되지 않으니 항상 해결을 하고 넘어가야 합니다.
</aside>

위에 `Quick Fix`를 클릭해주세요.
<aside>
💡 에러가 있는 곳을 클릭하신 뒤 단축키를 눌러 Quick Fix를 바로 실행하실 수도 있어요.
window : `ctrl + .`
macOS : `cmd + .`
- window 단축키가 안되는 경우
`ctrl + .`을 눌렀을 때 `·` 가 입력되고 단축키가 먹히지 않는 경우가 있습니다.

`win + space` 를 눌서 `한컴 입력기`가 아닌 `Microsoft 입력기`를 선택해주세요.

한컴 입력기를 삭제하는 방법은 아래 링크를 참고해 주세요.
- **[[코드스니펫] 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';`를 선택해주세요

그러면 맨 위에 이런 구문이 추가된 것을 보실 수 있습니다.
`import 'package:flutter/cupertino.dart';`
<aside>
💡 `cupertino.dart` 에는 아이콘이나 위젯들이 미리 정의되어 있습니다.
우리는 이를 가져다 쓰기 위해 import 해준 것입니다.
</aside>

3. 저장하면 에뮬레이터에 AppBar와 아이콘이 출력됩니다.

4. AppBar 의 색상과 그림자를 변경해보겠습니다.
26번째 줄 맨 뒤에 엔터를 눌러 빈 라인을 추가한 뒤, 코드스니펫을 복사해서 붙여 넣어 주세요.
- **[코드스니펫] AppBar backgroundColor, elevation**
```dart
backgroundColor: Colors.white,
elevation: 0.5,
```
각각 `AppBar`의 속성으로 들어가는 값들 이기 때문에 쉼표로 구분해줍니다.

<aside>
💡 AppBar를 완성했습니다!
</aside>3)
body만들기 - 레이아웃레이아웃 나누기

62번째 줄에
Center를 클릭한 뒤마우스 우클릭→Refactor를 선택합니다.
Wrap with Row을 선택합니다.
Center위젯이 아래와 같이Row위젯으로 감싸집니다.
64번째 라인을 삭제하고 아래 코드 스니펫을 붙여넣어주세요.
[코드스니펫] 레이아웃
// 이미지 들어갈 자리 Column( children: [ // 'M1 아이패드 프로 11형(3세대) 와이파이 128G 팝니다.' // '봉천동 · 6분 전' // '100만원' Row( children: [ // 빈 칸 // 하트 아이콘 // '1' ], ), ], ),

- 먼저 `Row` 안에 가로 방향으로 이미지와 나머지 요소들의 `Column` 을 배치해줍니다.
- `Column` 안에 각각의 텍스트 요소들을 넣어줍니다.
- 마지막 줄에 나오는 하트 아이콘과 숫자는 다시 `Row` 로 묶어줍니다.

<aside>
💡 디자인을 보고 큰 단위에서부터 차근차근 Row 와 Column 을 사용해 요소들을 배치하는 연습이 필요합니다.
레이아웃을 잡았으니, `children`에 위젯들을 하나씩 넣어봅시다.
</aside>
<aside>
💡 이 둘만으로도 많은 레이아웃을 만들어낼 수 있으나, 여러 요소들을 겹치게 표현하기 위해서는 Stack 이라는 클래스를 사용해야 합니다.

자세한 사용법과 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,
),
),
```

<aside>
💡 우리는 정사각형의 위젯에 이미지를 채워서 보여주려 합니다.
`fit: BoxFit.cover`라고 넣어주면 이미지의 비율을 유지하면서 고정된 폭과 높이에 맞추어 이미지를 적절히 잘라서 보여줍니다.

폭과 높이를 넘어가는 이미지 부분은 모두 잘립니다
[BoxFit 이 더 궁금하다면?](https://api.flutter.dev/flutter/painting/BoxFit.html)
</aside>
- 텍스트 만들기
<aside>
💡 아래와 같이 텍스트를 세로로 배치해보겠습니다. `Column` 위젯 속에 `Text` 위젯들을 배치하면 되겠죠!

</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,
),
),
```

위와 같이 overflow가 발생할 겁니다! 이는 지정된 사이즈를 위젯이 넘어갔다는 뜻인데요.
이는 `Column` 위젯의 폭이 설정되지 않았기 때문에 발생하는 문제입니다. `Expanded` 위젯을 통해 이 문제를 해결해봅시다.
75번째 줄의 `Column` 위젯에 마우스 커서를 가져다두면 왼쪽에 전구 모양 아이콘이 생길텐데요. 이를 눌러봅시다. (마우스 우클릭해서 나오는 `Refactor` 와 같은 옵션입니다!)

`Wrap with widget` 을 눌러주시면 아래와 같이 `widget` 으로 `Column` 이 쌓이게 됩니다. widget 자리에 우리가 원하는 위젯 이름을 넣어주면 됩니다.

`Expanded` 라고 입력해줍니다.

`Expanded` 위젯으로 `Column` 을 감싸면 해당 위젯이 차지할 수 있는 공간을 최대한 차지하게 됩니다. 남는 공간 만큼을 위젯의 폭으로 사용할 수 있는 것이죠. 위젯의 폭이 설정되니 overflow 도 해결됩니다. 저장하고 확인해볼까요?

오른쪽에 뜨던 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설정하기이제 이미지와 텍스트의 정렬을 맞춰주면 되겠군요.
Row와Column은 주축, 부축을 기준으로 정렬할 수 있는데요. 주축을 main axis, 부축을 cross axis 라고 합니다. 아래 사진을 참고해주세요!

내부 요소들의 정렬은
MainAxisAlignment.center와 같은 식으로 설정해줍니다.Alignment 방법들을 시각화하면 아래와 같습니다.
](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f6d88258-4017-42bd-836a-16454dc772bd/Untitled.png)
출처: https://morioh.com/p/9c7541691c2c
](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/247e696a-b938-4043-b6a4-61088f85cf01/Untitled.png)
출처: https://morioh.com/p/9c7541691c2c

위와 같이 배치되어 있는 요소들을 같은 선상에 맞추려면,
Row의CrossAxisAlignment를 설정해주면 되겠죠!코드스니펫을 복사해서 62번째 라인 뒤에 붙여 넣어 요소들을 위로 정렬해 보도록 하겠습니다.
[코드스니펫] Row CrossAxisAlignment
crossAxisAlignment: CrossAxisAlignment.start,

아래 사진처럼 요소들이 위쪽 시작점에 붙는 것을 볼 수 있습니다.

마찬가지로 코드스니펫을 복사해서 77번째 라인 뒤에 붙여 넣어 요소들을 왼쪽으로 정렬해 보도록 하겠습니다.
- **[코드스니펫] Column CrossAxisAlignment**
```dart
crossAxisAlignment: CrossAxisAlignment.start,
```

텍스트 요소들이 왼쪽 시작점에 붙는 것을 볼 수 있습니다.
<aside>
💡 **MainAxisAlignment, CrossAxisAlignment** 를 설정해서 `Row`, `Column` 내 요소들을 정렬할 수 있습니다.
</aside>
- `SizedBox` 를 이용해 위젯간 간격 설정하기
<aside>
💡 아래 사진처럼 이미지와 텍스트가 너무 붙어있을 때는 어떻게 하는 게 좋을까요?

이미지와 텍스트 사이에 빈 `SizedBox` 위젯을 넣어 간격을 조정할 수 있습니다.
</aside>
코드스니펫을 복사해서 75번째 라인 뒤에 붙여 넣어 `SizedBox` 를 추가해주겠습니다.
- **[코드스니펫] SizedBox**
```dart
SizedBox(width: 12),
```

저장하면 아래와 같이 요소 사이에 간격이 추가된 것을 확인할 수 있습니다.

- 좋아요 버튼 추가하기
<aside>
💡 오른쪽 아래에 좋아요 버튼을 추가하기 위해 `Row` 위젯과 `Spacer` 위젯을 활용해봅시다.

</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),
),
],
),
)
```

좋아요 아이콘과 숫자가 오른쪽 끝에 배치된 것을 볼 수 있습니다.
(`GestureDetector` 는 추후에 좋아요 버튼 클릭을 구현하기 위해 우선 추가했습니다.)

<aside>
💡 `Spacer` 위젯은 빈 공간을 차지하는 위젯입니다.
</aside>
피드 1개를 완성했습니다!5)
floatingActionButton만들기아래 코드스니펫을 복사해 132번째 라인 뒤에 붙여 넣습니다.
[코드스니펫] FloatingActionButton
floatingActionButton: FloatingActionButton( onPressed: () {}, backgroundColor: Color(0xFFFF7E36), elevation: 1, child: Icon( Icons.add_rounded, size: 36, ), ),

저장하면 아래와 같이 `FloatingActionButton` 이 생성된 것을 볼 수 있습니다.

<aside>
💡 코드가 너무 길 때는 body 요소가 어디에서 끝나는지 닫는 괄호를 찾기가 힘들 수 있습니다.
이럴 때는 아래와 같은 VS Code 의 접어두기 기능이 유용합니다.

해당 버튼을 누르면 아래와 같이 해당 요소가 접힙니다. 새로운 요소를 추가하고 싶다면, 이 아래에 추가해주면 되겠죠.

</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, ),

저장하면 아래와 같이 `BottomNavigationBar` 이 생성된 것을 볼 수 있습니다.
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. 파일 분리
최종 파일 구조

main.dart: 앱 시작home_page.dart: 첫 번째 페이지 레이아웃feed.dart:HomePage에서body
1)
home_page.dart파일 분리lib폴더를 선택한 뒤 마우스 우클릭으로New File을 선택해주세요.
이름을
home_page.dart로 지어주세요.
main.dart파일에 20번째 라인부터 마지막 줄 까지HomePage위젯이므로 잘라서home_page.dart파일로 옮겨주세요!처음 옮기고 나면 아래와 같이 빨간줄 투성이로 에러가 발생하는데,
Material과Cupertinoimport가 안 돼서 발생하는 에러입니다.
첫 번째 줄에
StatelessWidget을 클릭하신 뒤 Quick Fix(window :Ctrl + ./ macOS :Cmd + .)를 누르면 아래와 같이 나옵니다. 여기서import 'package:flutter/cupertino.dart';를 선택해주세요.
Cupertino 패키지가 import 되고도 에러가 발생하는데, 빨간 줄이 있는 곳에서 Quick Fix(window :
Ctrl + ./ macOS :Cmd + .)를 눌러주신 뒤import 'package:flutter/material.dart';를 선택해주세요.
그러면 빨간 줄이 모두 없어졌습니다.
Ctrl + S또는Cmd + S를 눌러 저장해주세요.
다음
main.dart파일을 보면 15번째 라인에 에러가 있습니다.HomePage위젯을 별도로 옮겼기 때문에 발생하는 에러로 Quick Fix(window :Ctrl + ./ macOS :Cmd + .) 눌러서import library 'home_page.dart'를 선택해줍니다.
4번째 라인에
home_page위젯을 불러오는 코드가 추가 되며 문제가 해결되었습니다.
저장을 하시면 에뮬레이터에서 정상적으로 결과가 나오는 것을 보실 수 있습니다.
2)
feed.dart파일 분리home_page.dart에 46번째 라인Row위젯을 우클릭하여Refactor메뉴를 선택해주세요.
Extract Widget을 선택 해주세요. 그러면 해당 위젯을 별도의StatelessWidget클래스로 분리할 수 있습니다.
위젯 이름을
Feed라고 입력해주세요.
그러면 아래 사진과 같이 46번째 라인에
Feed()인스턴스가 입력되고, 95번째 라인에Feed클래스가 생성됩니다.
lib폴더를 우클릭한 뒤New File을 선택해주세요.
파일 이름은
feed.dart라고 입력해주세요.
home_page.dart에 95번째 라인에 있는Feed클래스를 잘라내어feed.dart파일로 옮겨주세요.
1. `feed.dart` 파일에 1번째 `StatelessWidget`을 클릭한 뒤 Quick Fix(window : `Ctrl + .` / macOS : `Cmd + .`)을 눌러 `cupertino`를 `import`해 주세요.

33번째 줄에 빨간 줄이 있는 `Colors`을 선택한 뒤 Quick Fix(window : `Ctrl + .` / macOS : `Cmd + .`)을 눌러 `material`을 `import`하고 저장해주세요.

그럼 `feed.dart` 파일의 모든 에러가 해결됩니다.
2. `home_page.dart`에서 46번째 라인에 `Feed`도 Quick Fix(window : `Ctrl + .` / macOS : `Cmd + .`) 눌러서를 눌러서 `import` 한 뒤 저장해주세요.

3. VSCode 우측 상단에 `Restart` 버튼을 눌러보면 정상적으로 작동하는 것을 볼 수 있습니다.

<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. 좋아요 구현하기 & 피드 리스트 만들기
최종 모습

1) 좋아요 표시하기
Feed를StatefulWidget로 변환하기feed.dart파일을 열고, 4번째 줄에StatelessWidget을 클릭한 뒤 Refactor(window :ctrl + ./ macOS :Cmd + .)를 누르고 아래와 같은 팝업이 뜨면Convert to StatefulWidget을 선택한 뒤 저장해 주세요.
StatefulWidget이 되면서 상태를 관리하는_FeedState클래스가 추가됩니다
isFavorite상태 추가하기코드스니펫을 복사해서 아래 이미지와 같이 13번째 줄에 맨 뒤에 추가하고 저장해주세요.
[코드스니펫] isFavorite 선언
// 좋아요 여부 bool isFavorite = false;
<aside>
💡 `isFavorite` 변수는 좋아요 여부를 나타내는 상태 변수입니다.
</aside>

- 3. 하트 버튼 클릭시 상태 변경하기
<aside>
💡 버튼 클릭 이벤트를 코드의 어느 부분에서 받을 수 있는지 흐름을 잘 알고 있어야합니다.
</aside>
하트 버튼을 클릭하는 경우 68번째 줄의 `onTap` 함수가 실행되므로, 코드스니펫을 복사해 68번째 라인에 `onTap` 함수의 중괄호 안에 붙여넣고 저장해 주세요.
- **[코드스니펫] heart 버튼 클릭**
```dart
// 화면 갱신
setState(() {
isFavorite = !isFavorite; // 좋아요 토글
});
```

아래와 같이 추가해주면, 클릭시 `isFavorite` 변수의 값이 반전되면서 화면이 갱신 됩니다.

<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,
```

<aside>
💡 위젯에 전달하는 값을 조건에 따라 다르게 보여줄 때 아래와 같이 한 줄 if문을 사용합니다.
```dart
조건 ? 반환값1 : 반환값2
```
- `조건`이 `true`인 경우 `반환값1`이 할당되고, `false`인 경우에는 `반환값2`가 할당됩니다.
</aside>
에뮬레이터에서 `좋아요 버튼`을 누르면 버튼 색상이 바뀌는 것을 볼 수 있습니다.
2) 피드 리스트 만들기
ListView 위젯
](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7f1ec699-ad66-4f71-a2ae-e6ddfab58d32/list_view.png)
출처 - dribbble
DartPad에서 ListView 위젯을 사용해 보도록 하겠습니다. 코드스니펫을 복사한 뒤 주소창에 붙여 넣어주세요.
-
https://dartpad.dev/?id=5aa6067a9ea6f34a6beca110cb882117
-

```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
```

- 2. `ListView.builder` 적용하기
<aside>
💡 `ListView.builder` 를 활용해 원하는 개수만큼의 Feed 를 보여주도록 하겠습니다.
</aside>
`home_page.dart` 의 48번째 줄에 있는 `Feed` 를 여러개 반복해서 보여주겠습니다.

`Feed`를 마우스 우클릭하고 Refactor(window : `ctrl + .` / macOS : `Cmd + .`) 를 눌러줍니다. (왼쪽의 전구 아이콘을 눌러도 됩니다)
`Wrap with Builder` 옵션을 선택해주세요.

48 번째 줄의 Builder 를 `ListView.builder` 로 바꿔줍니다.

50번째 줄 중괄호와 소괄호 사이에 쉼표를 추가해 정렬을 맞춰줍니다.
`builder` 를 `itemBuilder` 로 바꾸고, context 뒤에 `index` 를 써줍니다.

저장하고 앱을 스크롤 해보면 피드가 여러 개로 늘어난 것을 보실 수 있습니다. (무한대로 늘어나 있습니다!)

`ListView.builder` 에서는 itemCount 라는 속성을 조정해 자식 위젯이 그려지는 개수를 조절할 수 있습니다. 48번째 줄 맨 뒤에 아래 코드 스니펫을 추가해줍니다.
- **[코드스니펫] itemCount**
```dart
itemCount: 10,
```
<aside>
💡 이제 딱 10개의 피드만 나타나는 것을 확인할 수 있습니다.
</aside>
- 3. `ListView.separated` 적용하기
<aside>
💡 `ListView.separated` 를 활용해 각각의 Feed 사이에 `Divider` 를 추가해주겠습니다.
</aside>
48 번째 줄의 builder 를 separated 로 바꿔줍니다.

52번째 줄 맨 뒤에 아래 코드스니펫을 복사한 후 붙여넣어줍니다.
- **[코드스니펫] separatorBuilder**
```dart
separatorBuilder: (context, index) {
return Divider();
},
```

<aside>
💡 각각의 요소 사이에 `Divider` (구분선) 이 추가된 것을 볼 수 있습니다.
</aside>
- 4. Padding 추가하기
<aside>
💡 `Padding` 을 조절해 Feed 를 보기 좋게 수정해봅시다.
</aside>
48번째 줄의 ListView 를 마우스 우클릭하고 Refactor (window : `ctrl + .` / macOS : `Cmd + .`) 를 눌러줍니다.

`Wrap with Padding` 을 선택해주세요.

49번째 줄을 지우고 아래 코드스니펫을 붙여넣어주세요
- **[코드스니펫] ListView horizontal padding**
```dart
padding: const EdgeInsets.symmetric(horizontal: 16),
```

<aside>
💡 가로 방향 패딩이 추가된 것을 볼 수 있습니다.
</aside>
53번째 `Feed()`를 마우스 우클릭하고 Refactor (window : `ctrl + .` / macOS : `Cmd + .`) 를 눌러줍니다.

`Wrap with Padding` 을 선택합니다.

54번째 줄을 지우고 아래 코드스니펫을 붙여넣어주세요
- **[코드스니펫] Feed vertical padding**
```dart
padding: const EdgeInsets.symmetric(vertical: 12),
```

<aside>
💡 이제 각 피드 사이의 간격도 보기 좋게 설정되었습니다.
</aside>최종 코드
main.dartimport '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.dartimport '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.dartimport '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. 피드마다 각각 다른 이미지 보여주기
최종 모습

1) Feed 위젯 수정하기
feed.dart파일에 8번째 줄에 아래 코드를 붙여 넣어 이미지 URL을 담을 변수를 만들어 줍니다.[코드스니펫] feed.dart / imageUrl 변수 선언
final String imageUrl; // 이미지를 담을 변수
<aside>
💡 `imageUrl`은 한 번 전달받은 뒤 변경되지 않기 때문에 앞에 `final` 키워드를 붙여줍니다.
</aside>

2. Feed 위젯의 생성자를 호출 할 때, `imageUrl`을 전달받아 방금 생성한 변수에 할당하도록 만들어주겠습니다. 아래 코드스니펫을 복사해서 6번째 줄에 붙여 넣어 주세요.
- **[코드스니펫] feed.dart / imageUrl 변수 주입**
```dart
required this.imageUrl,
```
<aside>
💡 `required` : 필수 전달 매개 변수로 만들어줍니다.
`this.imageUrl` : 많은 Feed 인스턴스 중 현재 인스턴스의 `imageUrl`
</aside>

3. 전달 받은 `imageUrl`을 화면에 보여줍시다.
아래 코드 스니펫을 복사해서 30번째 줄을 지우고, 붙여 넣은 뒤 저장해주세요.
- **[코드스니펫] feed.dart / imageUrl 사용하기**
```dart
widget.imageUrl, // 10번째 줄의 imageUrl 가져오기
```

<aside>
💡 **StatefulWidget**의 상태 클래스에서 `widget.변수명`으로 전달받은 변수에 접근할 수 있습니다.

</aside>
<aside>
💡 Feed 위젯 이미지 url를 전달 받을 준비가 완료되었습니다.
</aside>2) 데이터 전달하기
화면에 보여줄 데이터를 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", ];

2. `images`의 개수만큼 Feed를 보여주도록 수정해봅시다. 63번째 줄의 `itemCount : 10,` 을 삭제하고 아래 코드스니펫을 붙여 넣어주세요.
- **[코드스니펫] home_page.dart / itemCount**
```dart
itemCount: images.length, // 이미지 개수만큼 보여주기
```

3. `Feed` 위젯에 `index`에 해당하는 image를 전달해 봅시다.
먼저 이미지 url 1개를 `image` 라는 이름의 변수에 저장합니다. 64번째 줄 맨 뒤에 아래 코드스니펫을 붙여 넣어주세요.
- **[코드스니펫] home_page.dart / image 변수 선언**
```dart
final image = images[index]; // index에 해당하는 이미지
```

`Feed`의 `imageUrl`에 image 변수에 저장된 url 을 넘겨줍니다. 68번째 줄을 지우고 아래 코드스니페을 붙여 넣어주세요.
- **[코드스니펫] home_page.dart / Feed 에 imageUrl 전달**
```dart
child: Feed(imageUrl: image), // imageUrl 전달
```

4. 그리고 저장해주면 완성!

<aside>
💡 개발자들은 데이터를 보여주는 껍데기를 뷰(View)라고 부릅니다.
View와 데이터를 분리하면 View를 재활용 할 수 있습니다.
</aside>최종 코드
main.dartimport '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.dartimport '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.dartimport '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) 프로젝트 만들기
View→Command Palette버튼을 클릭해주세요.
명령어를 검색하는 팝업창이 뜨면,
flutter라고 입력한 뒤Flutter: New Project를 선택해주세요.
Application을 선택해주세요.
프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔
flutter폴더를 선택한 뒤Select a folder to create the project in버튼을 눌러 주세요.프로젝트 이름을
shazam으로 입력해주세요.
만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요.

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

아래
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('세번째 페이지')); } }
다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.
analysis_options.yaml파일을 열고, 아래 코드스니펫을 복사해서 24번째 라인 뒤에 붙여 넣고 저장해 주세요.[코드스니펫] analysis_options.yaml
prefer_const_constructors: false prefer_const_literals_to_create_immutables: false

<aside>
💡 상세 내용은 아래를 참고해 주세요.
- 어떤 의미인지 궁금하신 분들을 위해
`main.dart` 파일을 열어보시면 파란 실선이 있습니다.

파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
12번째 라인에 마우스를 올리면 아래와 같이 설명이 보입니다.

위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수(변하지 않는 수)로 선언하라는 힌트입니다. 상수로 만들면 화면을 새로고침 할 때, 상수로 선언된 위젯들은 새로고침을 할 필요가 없어서 성능상 이점이 있습니다.
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.

지금은 학습 단계이니 눈에 띄지 않도록 `analysis_options.yaml` 파일에 아래와 같이 설정하여 힌트를 숨기도록 하겠습니다.
```dart
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
```
</aside>

<aside>
💡 만들고 싶은 페이지를 선택해서 정답을 참고하며 구현해 보세요.
</aside>
| | 첫 번째 페이지 | 두 번째 페이지 | 세 번째 페이지 |
| --- | --- | --- | --- |
| 난이도 | ⭐️⭐️⭐️ | ⭐️ | ⭐️⭐️ |숙제 답안
이전 강의 바로가기
1주차 - Flutter 앱 개발 맛보기 & Dart 문법 익히기
다음 강의 바로가기
3주차 - 패키지 사용법 익히기 & 앱의 기능 만들기 (마이메모)
Copyright ⓒ TeamSparta All rights reserved.