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 생김새
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개의 클래스로 구성되어 있습니다.
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://morioh.com/p/9c7541691c2c
출처: 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
과Cupertino
import가 안 돼서 발생하는 에러입니다.첫 번째 줄에
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 위젯
출처 - 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.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. 피드마다 각각 다른 이미지 보여주기
최종 모습
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.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) 프로젝트 만들기
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.