한림대 경제학과 개쌕기 아닌 씹ㅅㄲ 김진영 영화감독 그것이 알고싶다. 영화 "B컷"개봉
한림대 경제학과 개쌕기 아닌 씹ㅅㄲ 김진영 영화감독 그것이 알고싶다. 영화 "B컷"개봉
PDF 파일
단축키 모음
F5
Ctrl
+ S
command
+ S
Ctrl
+ A
command
+ A
Ctrl
+ X
command
+ X
shift
+ enter
Ctrl
+ Alt
+ L
option
+ command
+ L
Tab
Shift
+ Tab
Ctrl
+ /
command
+ /
[수업 목표]
[목차]
1) API는 무엇인가요?
코드스니펫을 복사해 새 탭에서 열어주세요.
https://www.data.go.kr/data/15084084/openapi.do
<aside>
💡 아래 이미지와 같이 사용 설명서를 작성해 두었는데 이를 **API 문서**라고 부릅니다. API를 이해하는데 필요한 배경 지식을 배워보도록 하겠습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/775c9eae-15a7-4ec8-8460-872d1be881c9/Untitled.png)
<aside>
💡 그 외에도 `인스타그램 API`, `Youtube API`, `Kakao API`와 같이 검색해보시면 많은 서비스들이 API를 제공하고 있는 것을 보실 수 있습니다. (API는 유료도 있고, 무료도 있습니다)
- **[[코드스니펫] Public APIs](https://github.com/public-apis/public-apis)**
```dart
https://github.com/public-apis/public-apis
```
</aside>
2) API를 이해하기 위한 배경 지식
클라이언트와 서버
프로토콜(Protocol)
HTTP
요청(Request)
URL : 목적지
메소드(method) : 원하는 액션
GET : 조회
POST : 생성 / 수정 / 삭제
https://developer.mozilla.org/ko/docs/Web/HTTP/Methods
파라미터(Parameter)
실제 예제를 보도록 하겠습니다. 코드스니펫을 복사해서 새 탭에서 열어주세요.
https://en.dict.naver.com/#/search?query=hello&range=all
<aside>
💡 아래와 같이 `hello`가 검색된 영어 사전 페이지가 뜹니다.
주소창을 자세히 보면 `query=hello`라고 검색어가 동일하게 있는 것을 확인하실 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5cd59c04-73e0-4835-a87f-5d2ed76b52de/Untitled.png)
<aside>
💡 주소창에 `hello`를 `good`으로 변경한 뒤 엔터를 눌러보면 검색창에 `good`이 작성되어 있는 것을 보실 수 있습니다.
이와 같이 네이버 서버에 내가 알고 싶은 단어를 `query`라는 키에 값으로 전달하여 결과를 받아올 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4eccae2b-8d7c-4853-9727-6aa11471031e/Untitled.png)
- 응답(Response)
- 웹페이지
<aside>
💡 웹 브라우저 주소창에 `naver.com`이라고 입력하면 네이버 웹페이지를 응답해줍니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd350dfc-c579-47d0-84ba-f17fc10c0152/Untitled.png)
- 데이터
<aside>
💡 개발자가 아닌 이상 평소에 볼 일이 없지만 웹페이지가 아닌 데이터만 응답해주기도 합니다.
</aside>
코드스니펫을 복사해 새 탭에서 접속해주세요.
- [**[코드스니펫] json 데이터 응답 URL**](https://jsonplaceholder.typicode.com/posts)
```dart
https://jsonplaceholder.typicode.com/posts
```
<aside>
💡 데이터를 보내줄 때에도 정해진 형식에 따라 주는데, 보통 `JSON`이라는 형식을 많이 사용합니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cfe18aff-da00-43a9-a7be-cff6184a29a8/Untitled.png)
- 크롬 JSON Viewer 확장 프로그램 설치
<aside>
💡 JSON 구조를 조금 더 쉽게 파악할 수 있도록 도와주는 크롬 확장 프로그램을 설치해 보도록 하겠습니다.
</aside>
코드스니펫을 복사해서 새 탭에서 열어주세요.
- **[[코드스니펫] JSON Viewer 확장 프로그램 URL](https://chrome.google.com/webstore/detail/jsonview/gmegofmjomhknnokphhckolhcffdaihd?hl=ko)**
```dart
https://chrome.google.com/webstore/detail/jsonview/gmegofmjomhknnokphhckolhcffdaihd?hl=ko
```
1. `Chrome에 추가` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/276bbe51-2df3-4a77-862d-2835f367844d/Untitled.png)
2. `확장 프로그램 추가` 버튼을 눌러서 설치를 진행해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/775d4ffb-9a25-4f1f-aa8c-2df5eca4719a/Untitled.png)
3. JSON 샘플 웹페이지를 새로고침 해주시면 아래와 같이 좀 더 가독성 있게 JSON 구조를 볼 수 있습니다.
![Screen Shot 2022-09-18 at 6.46.32 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a00eaad3-598a-48ac-8d11-945349a45da6/Screen_Shot_2022-09-18_at_6.46.32_PM.png)
<aside>
💡 JSON은 Dart 문법 시간에 배운 **문자열, 숫자, 배열, 맵(딕셔너리)** 형태 구조로 이루어져 있어 쉽게 익힐 수 있습니다.
자세한 사용 방법은, 실습하면서 배워보도록 하겠습니다.
</aside>
- 상태 코드
<aside>
💡 상태 코드란 HTTP 응답시 요청이 성공했는지 실패했는지 한 번에 알 수 있는 약속된 숫자입니다.
</aside>
<aside>
💡 자주 사용하는 상태코드
200 : 성공
4xx : 잘못된 요청
5xx : 서버 문제로 실패
좀 더 자세한 상태코드 종류는 아래 링크를 참고해 주세요.
- **[[코드스니펫] HTTP 상태코드](https://developer.mozilla.org/ko/docs/Web/HTTP/Status)**
```dart
https://developer.mozilla.org/ko/docs/Web/HTTP/Status
```
</aside>
![_http (1).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c13ec2d-379c-4fc3-a46c-50cb451dc2a7/_http_(1).png)
3) Google Book API 문서 읽어보기
그럼 직접 API 문서를 읽어볼까요? 아래 코드스니펫을 복사해서 새 탭에서 열면 구글 책 검색 API 문서가 열립니다.
https://developers.google.com/books/docs/v1/reference/volumes/list
<aside>
💡 **요청(Request)**
- URL : `https://www.googleapis.com/books/v1/volumes`
- method : GET
- Parameter : `q=검색어`
![bookApi (1).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c691e45b-6c91-421c-9d0c-0934dca56b7a/bookApi_(1).png)
</aside>
<aside>
💡 **응답(Response)**
- JSON
![bookApiResponse.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8a170df0-5846-422f-8537-30f84495a4ac/bookApiResponse.png)
</aside>
4) Google Book API 사용해보기
q
라는 파라미터에 고양이
라는 단어를 검색하여 책 데이터를 가져왔습니다.
[코드스니펫] DartPad google 책 검색 API
https://dartpad.dev/?id=1519f6b1a7ac26cf42e4e302750650e0
Run
버튼을 눌러 보면 Console에 JSON 응답이 출력되는 것을 볼 수 있습니다.
1) 동기 & 비동기
비동기 방식이 소요시간은 더 짧습니다.
2) async & await
코드스니펫을 복사해 새 탭에서 붙여넣어 DartPad를 열어주세요.
https://dartpad.dev/?id=1984d6cae83118a401f59f8f0034c8e4
<aside>
💡 console을 통해 실행 순서를 보면, `print("2");`의 경우 1초 뒤 실행되기 때문에 **비동기** 방식으로 실행되어 1 → 3 → 2 순서대로 실행된 것을 볼 수 있습니다.
</aside>
![Screen Shot 2022-09-18 at 7.25.47 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/809b2f90-9be0-4de6-b914-9dd36351a865/Screen_Shot_2022-09-18_at_7.25.47_PM.png)
<aside>
💡 위 코드를 **동기** 방식으로 실행해 1 → 2 → 3 순서대로 출력되도록 만들어 보겠습니다. 코드스니펫을 복사해서 새 탭에 붙여넣어 DartPad를 열어주세요.
- **[[코드스니펫] DartPad async & await](https://dartpad.dev/?id=83d6c5a955cc9230d3f44ea5106236a7)**
```dart
https://dartpad.dev/?id=83d6c5a955cc9230d3f44ea5106236a7
```
![Screen Shot 2022-09-18 at 7.26.18 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1c50e0ca-cc6f-490d-bca8-7be11c43ce32/Screen_Shot_2022-09-18_at_7.26.18_PM.png)
</aside>
<aside>
💡 **비동기** 코드인 7번째 줄 앞에 `await`을 붙이고, 해당 코드가 속해있는 `main`함수의 소괄호와 중괄호 사이에 `async`라고 적어주면 **비동기** 방식으로 실행되는 코드를 **동기** 방식으로 실행할 수 있습니다.
![async & await.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b94b2f90-89c9-46b8-b7fd-dd65473a2c70/async__await.png)
</aside>
3) Future
코드스니펫을 복사해 새 탭에서 붙여넣어 DartPad를 열어주세요.
https://dartpad.dev/?id=a6734196a437a06985388c183dc78c44
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/abee377c-58b1-456b-b175-1c45e2f2f4fd/Untitled.png)
<aside>
💡 `async`가 붙은 함수는 내부에 코드 실행이 완료되기를 기다리는 `await` 코드가 있을 수 있기 때문에, 미래에 언젠간 값을 반환한다는 의미로 반환 값의 타입을 `Future<반환타입>` 형태로 작성해 줍니다.
![future (1).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/30ef794c-2efd-4d92-b66a-4976bc6c1fd6/future_(1).png)
실행 순서를 보면 9번째 줄에 `await`이 되어있으므로, 해당 코드가 끝날 때까지 기다립니다. 따라서 13번째 반환 값은 9번 째 줄이 끝난 미래에 반환이 되므로 8번째 줄에 반환 타입을 `Future<String>` 이라고 표시한다고 이해하시면 됩니다.
</aside>
<aside>
💡 해당 함수를 호출하는 쪽에서도 `await`을 함수 앞에 붙여주면 **동기** 방식으로 결과가 응답될 때까지 기다리기 때문에 `Future`를 벗겨낸 타입으로 반환 값을 받을 수 있습니다.
![future (2).png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9d1e291a-87c2-448a-92e7-2a80bb2b730f/future_(2).png)
</aside>
4) 요약
배운 이론들을 활용해 프로젝트에서 API를 사용해 보도록 하겠습니다.
1) Flutter 프로젝트 생성
VSCode를 실행해주세요.
View
→ Command Palette
를 선택해주세요.
명령어를 검색하는 팝업창이 뜨면, flutter
라고 입력한 뒤 Flutter: New Project
를 선택해주세요.
Application
을 선택해주세요.
프로젝트를 시작할 폴더를 선택하는 화면이 나오면 flutter
폴더를 선택한 뒤 Select a folder to create the project in
버튼을 눌러 주세요.
프로젝트 이름을 watcha_pedia
으로 입력해주세요.
만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요. (팝업이 안보이시면 넘어가주세요!)
다음과 같이 프로젝트가 생성되고, 다음으로 불필요한 힌트를 숨기도록 하겠습니다.
코드스니펫을 복사해서 analysis_options.yaml
파일에 24번째 라인 뒤에 붙여 넣어주세요.
[코드스니펫] analysis_options.yaml
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
- 어떤 의미인지 궁금하신 분들을 위해
- `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
- 파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
12번째 라인에 마우스를 올리면 아래와 같이 설명이 뜹니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다.
<aside>
💡 상수로 만들면 어떤 이점이 있나요?
상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다.
</aside>
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
- 지금은 학습 단계이니 눈에 띄지 않도록 해주도록 하겠습니다.
8. Provider 패키지를 시작하는 코드에서 사용하고 있으므로 패키지 설치부터 진행하도록 하겠습니다. `View` → `Terminal`을 선택해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/db3f319b-cb6e-4e78-96ce-52685882adbe/Untitled.png)
아래 코드 스니펫을 복사해서 터미널에 붙여넣고 실행해 주세요.
- **[코드스니펫] provider 패키지 설치**
```dart
flutter pub add provider
```
![Screen Shot 2022-09-18 at 9.52.23 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/28d0f310-52b4-4da2-b179-4890f65467eb/Screen_Shot_2022-09-18_at_9.52.23_PM.png)
`pubspec.yaml`을 열어서 39번째 라인에 `provider`가 있으면 설치가 잘 되신 겁니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89172053-32a7-489d-837d-e51351df92bf/Untitled.png)
9. 아래 `lib/main.dart` 코드스니펫을 복사해서 기존 내용을 모두 지우고, `main.dart` 파일에 붙여 넣고 저장해 주세요.
- **[코드스니펫] main.dart**
```dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'book_service.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => BookService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var bottomNavIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
SearchPage(),
LikedBookPage(),
].elementAt(bottomNavIndex),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Colors.black,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedFontSize: 12,
unselectedFontSize: 12,
iconSize: 28,
type: BottomNavigationBarType.fixed,
onTap: (value) {
setState(() {
bottomNavIndex = value;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '검색',
),
BottomNavigationBarItem(
icon: Icon(Icons.star),
label: '좋아요',
),
],
currentIndex: bottomNavIndex,
),
);
}
}
class SearchPage extends StatelessWidget {
SearchPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
toolbarHeight: 80,
title: TextField(
onSubmitted: (value) {},
cursorColor: Colors.grey,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
body: Center(
child: Text("검색"),
),
);
}
}
class LikedBookPage extends StatelessWidget {
const LikedBookPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("좋아요"),
),
);
}
}
```
10. 그리고 `lib` 폴더 밑에 `book.dart`와 `book_service.dart` 파일도 만들어주신 뒤 아래 코드스니펫을 붙여 넣어주세요.
<aside>
💡 `Book` 클래스는 책에 대한 정보를 담을 클래스 입니다.
`BookService`는 `Book`에 대한 CRUD를 담당하는 클래스 입니다.
</aside>
- **[코드스니펫] book.dart**
```dart
class Book {}
```
- **[코드스니펫] book_service.dart**
```dart
import 'package:flutter/material.dart';
import 'book.dart';
class BookService extends ChangeNotifier {
List<Book> bookList = []; // 책 목록
}
```
최종적으로 `lib` 폴더 밑에 아래와 같이 파일들이 있으면 됩니다.
![Screen Shot 2022-09-18 at 9.38.21 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b4bf8f96-ef8e-4164-9d2b-d25cd968b318/Screen_Shot_2022-09-18_at_9.38.21_PM.png)
2) 에뮬레이터 실행하기
VSCode 우측 하단에 Chrome (web-javascript)
를 클릭해주세요.
(에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)
Start Pixel 2 API 29 mobile emulator
를 선택해주세요.
잠시 기다리면 Android 에뮬레이터가 실행됩니다.
VSCode 우측 상단에 아래 화살표를 눌러 Run Without Debugging
을 눌러주세요.
에뮬레이터에 아래와 같이 탭으로 전환되는 화면이 나오면 준비 완료!
구현 목표
1) 통신 패키지 dio 설치하기
코드스니펫을 복사해 새 탭에서 열어주세요.
[코드스니펫] Pub.dev / dio / Installing
https://pub.dev/packages/dio/install
dio 패키지의 Installing 페이지가 열리면 flutter pub add dio
우측 아이콘을 눌러서 복사해주세요.
VSCode View
→ Terminal
을 선택해주세요.
Terminal
창에 복사한 flutter pub add dio
를 붙여넣고 엔터를 눌러 실행해 주세요.
아래와 같이 나오면 정상적으로 작동한 것입니다.
pubspec.yaml
파일을 열어서 아래와 같이 40번째 라인에 dio
가 있으면 설치가 완료된 것입니다.
2) dio 사용법
아래 코드는 GET
메소드로 URL
로 요청을 보내는 코드입니다. dio 패키지는 매우 직관적이고 쉽게 사용할 수 있습니다.
main() {
Dio().get("URL");
}
HTTP 요청은 응답까지 시간이 걸리기 때문에 비동기 코드입니다. 따라서 동기 방식으로 작동하게 하려면 아래와 같이 async
& await
을 추가해야 합니다.
main() async {
Response result = await Dio().get("URL");
print(result.data); // data 안에 응답 내용이 들어 있습니다.
}
3) Google Book API 응답값 자세히 보기
https://www.googleapis.com/books/v1/volumes?q=%EA%B3%A0%EC%96%91%EC%9D%B4
![Screen Shot 2022-09-18 at 11.49.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f722ad7b-42b5-4cae-948d-b4d0112d1de0/Screen_Shot_2022-09-18_at_11.49.35_PM.png)
<aside>
💡 위와 같이 GET방식으로 해당 URL로 요청시 JSON 응답이 옵니다.
`thumbnail` 이라고 적힌 부분에 파란 링크를 클릭해보시면 책 표지 사진이 나옵니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1171cf1d-9e93-46aa-bd43-7605fd043ef4/Untitled.png)
</aside>
<aside>
💡 Google Book API에 대한 자세한 사항은 아래 코드스니펫 링크를 참고해주세요.
- **[[코드스니펫] Google Book API 문서](https://developers.google.com/books/docs/v1/reference/volumes/list)**
```dart
https://developers.google.com/books/docs/v1/reference/volumes/list
```
</aside>
1. 주소창에 `q=고양이` 라고 되어있는 부분을 `q=dog`로 변경해 보면 아래와 같은 결과가 나옵니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8ad130cb-92db-4864-b2cf-c56eba35b8de/Untitled.png)
`totalItems` 라는 요소 안에 조회된 책의 전채 개수가 나오는 것을 확인할 수 있습니다.
`items` 안에 있는 토글 버튼(화살표 표시) 를 눌러 요소를 두개 접어봅시다. `items` 안에 각각의 책에 대한 정보가 마치 `Map` 과 같은 형태로 담겨 있습니다.
![Screen Shot 2022-09-19 at 12.01.06 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ad41601b-38c4-434b-80fb-8abd97b0bcc0/Screen_Shot_2022-09-19_at_12.01.06_AM.png)
<aside>
💡 위 JSON 데이터의 구조는 Dart 문법에서 나오는 `List`와 `Map`의 조합과 같다고 보셔도 좋습니다.
| 자료형 | 설명 | 예시 |
| --- | --- | --- |
| List | 배열 | [1, 2, 3] |
| Map | {key : value} 형태의 자료형 | {
'name': '철수',
'age': 20
} |
</aside>
<aside>
💡 예를 들어 3번째 책의 `title` 을 가져오고 싶다면 아래와 같은 문법으로 데이터에 접근하면 됩니다.
```dart
// data 에 위 json 데이터가 담겨온다고 가정
String title = data['items'][2]['volumeInfo']['title']
print(title) // "Everyday Dog"
```
</aside>
4) Book 클래스 수정하기
json 데이터의 값들을 활용해 Book
객체를 만들고 이를 화면에 띄워주는 과정이 필요하겠죠.
먼저 Book 클래스가 id
, title
, subtitle
, thumbnail
, previewLink
요소를 가질 수 있게끔 book.dart
파일을 수정해줍니다. 코드스니펫을 복사해 book.dart
파일 내의 내용을 모두 지우고 붙여넣어주세요.
[코드스니펫] book.dart
class Book {
String id;
String title;
String subtitle;
String thumbnail; // 썸네일 이미지 링크
String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
Book({
required this.id,
required this.title,
required this.subtitle,
required this.thumbnail,
required this.previewLink,
});
}
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8c770840-ef46-4dbf-9606-bc3e5a3f735c/Untitled.png)
5) Google Book API 호출하기
키보드에서 완료 버튼을 누르면, TextField
에 입력된 문자열을 인자로 받는 onSubmitted
함수가 실행됩니다.
BookService
내에 검색 로직을 추가한 뒤, 이를 onSubmitted
에서 호출하도록 하겠습니다.
아래 코드스니펫을 복사해 book_service.dart
6번째 줄 맨 뒤에 붙여넣어주세요.
[코드스니펫] 책 목록 search 함수
void search(String q) async {
if (q.isNotEmpty) {
Response res = await Dio().get(
"https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
);
List items = res.data["items"];
print(items);
}
}
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6883dea5-fbac-4b66-b500-decdb75b40e7/Untitled.png)
3. 에러를 해결하겠습니다. 10번째 줄의 `Dio()` 을 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 눌러 `Import library ‘package:dio/dio.dart’` 를 선택해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/725da2df-eb4e-4a8e-aa89-2bbb683c6f97/Untitled.png)
4. 첫번째 줄에 Import 구문이 추가되며 에러가 해결되었습니다. 아까 설치해준 `Dio` 패키지를 이제 사용할 수 있습니다.
![Screen Shot 2022-09-19 at 1.49.06 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/55679400-1d20-455b-b18a-d1d0689e6b47/Screen_Shot_2022-09-19_at_1.49.06_AM.png)
5. `SearchPage` 로 돌아와 `BookService` 의 `search` 함수를 사용할 수 있도록 해봅시다.
저번 시간에 배운 `Provider` 내용을 복기해봅시다. Service 내의 변수나 함수에 접근하기 위해서는 아래 두가지 방법을 사용합니다.
<aside>
💡 Provider 에서 Service 를 사용하는 방법
1. `Consumer<클래스명>` : 클래스 정보 갱신시 화면을 새로고침 해야 할 때 사용
2. `context.read<클래스명>` : 1회성으로 클래스 접근할 때 사용 (화면 새로고침이 필요 없을 때)
</aside>
우리는 검색이 일어날 때마다 화면에 책 목록을 다시 그려줄 것이므로, `Consumer` 를 사용해야 합니다.
`main.dart` 의 79번째 줄의 `Scaffold` 를 누르고, 왼쪽의 전구를 클릭해 `Wrap with Builder` 를 선택합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d76c4285-7faf-48bf-bfc7-77155363ea95/Untitled.png)
아래와 같이 `Scaffold` 위젯을 `Builder` 위젯이 감쌉니다. `Consumer` 위젯과 형식이 비슷한 `Builder` 를 사용해 모양을 잡아줬습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/945394a9-d0d2-4e15-a04b-799356ca346e/Untitled.png)
6. 아래 이미지와 같이 79번째 라인의 `Builder`를 `Consumer<BookService>`로 변경해주세요.
`context` 뒤에 `bookService` 와 `child` 도 추가해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e172af43-21dc-4931-badb-859a84bfa6c8/Untitled.png)
그리고 줄 정렬을 예쁘게 해주기 위해 105번째 라인의 중괄호와 소괄호 사이에 콤마(`,`)를 찍은 뒤 저장해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/505edd08-2861-4d71-86d6-c0263d2c040b/Untitled.png)
저장하면 코드가 정렬됩니다.
<aside>
💡 **복습:** `Consumer<BookService>`는 위젯 트리를 타고 올라가 **Provider**로 등록된 **BookService**를 찾습니다. 찾은 **BookService**를 80번째 라인의 두 번째 파라미터인 `bookService` 에 담아줍니다.
우리는 `bookService` 변수를 통해 **BookService** 안에 있는 변수에 접근해 수정하고, 화면을 새로고침할 수 있습니다.
</aside>
<aside>
💡 `BookService`에서 값을 변경하고 `notifyListeners();`를 호출하면, 해당 서비스를 `Consumer`로 등록한 모든 위젯의 `builder` 함수가 재호출 되면서 화면이 갱신 됩니다.
</aside>
7. 86번째 줄 `onSubmitted` 의 중괄호 사이에 아래 코드스니펫을 붙여넣습니다.
- **[코드스니펫] TextField 의 onSubmitted**
```dart
bookService.search(value);
```
저장하면 아래 이미지와 같이 코드가 정렬됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1260462-b5e2-4366-9dd5-9afb0d152096/Untitled.png)
8. 이제 키보드에 값을 입력하고 Debug Console 을 확인해볼까요? VS Code 에서 `View → Debug Console` 를 눌러 콘솔을 켜줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/25bb78ef-a5f0-4bde-b411-db7bc6775fd1/Untitled.png)
아래와 같이 dog 라는 키워드를 입력하고 검색을 해보면 콘솔에 리스트가 출력되는 것을 확인할 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3ff8ec6-b13c-4c28-b29d-deb8d2dd6eb6/Untitled.png)
이 리스트의 정체는 무엇일까요? 아까 **BookService** 에 만들어둔 `search` 함수를 찬찬히 뜯어봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64154232-0b2d-445f-8f9c-2f0c809f573f/Untitled.png)
응답으로 온 데이터 (`res.data`) 에 `items` 라는 Key 로 Value 를 가져오는 코드네요!
`res.data` 에 담겨오는 데이터는 바로…
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e4040b39-0799-4785-9d80-0d7b3cece952/Untitled.png)
아까 보았던 **json 데이터**입니다. `totalItems` 아래에 `items` 에 있는 데이터가 우리가 만들어둔 `items` (`book_service.dart` 14번째 줄) 변수에 List 형식으로 담긴 것입니다.
6) JSON 파싱(parsing)
콘솔에서는 JSON 구조가 잘 안보이니, 샘플을 보면서 원하는 데이터만 뽑아보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.
https://www.googleapis.com/books/v1/volumes?q=dog&startIndex=0&maxResults=40
![items 에서 맨 위 두개의 요소는 토글 버튼을 눌러 접어뒀습니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e4040b39-0799-4785-9d80-0d7b3cece952/Untitled.png)
items 에서 맨 위 두개의 요소는 토글 버튼을 눌러 접어뒀습니다.
보시는 바와 같이 큰 `Map(맵)` 내에 items 라는 `key` 의 `value` 로 `List(배열)` 가 있고, 이 안에 다시 `Map(맵)`이 들어 있는 형태입니다.
<aside>
💡 List 안에 여러개의 `Map`이 들어있습니다. 따라서 **반복문**을 통해 해당 `List`에 있는 모든 `Map`을 각각 `Book` 클래스의 객체로 만들어 `bookList` 에 추가해주면 되겠군요!
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fb6daa22-f1c6-444d-b43b-25ab9c6ab172/Untitled.png)
2. 데이터를 잘 가져오는 데 성공했으니, 이제는 이를 사용하기 편하게 `Book` 클래스의 객체로 만들겠습니다. 위에서 `book.dart` 에 아래와 같이 `Book` 클래스를 만들었습니다. 코드를 다시 볼까요?
![Screen Shot 2022-09-19 at 2.49.25 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01adf851-c3fb-47c5-80f2-c89b62727396/Screen_Shot_2022-09-19_at_2.49.25_AM.png)
Book 클래스의 생성자(8번째 줄)를 보면 어떤 값들을 넣어줘야 하는지 알 수 있습니다. `id`, `title`, `subtitle`, `thumbnail`, `previewLink` 에 값을 넣어 Book 클래스의 객체를 만들어줍시다.
아래 코드스니펫을 복사해 `book_service.dart` 의 15번째 줄을 지우고 붙여넣어주세요.
- **[코드스니펫] for 문 / Book 객체를 만들고 bookList 에 추가하는 함수**
```dart
for (Map<String, dynamic> item in items) {
Book book = Book(
id: item['id'],
title: item['volumeInfo']['title'] ?? "",
subtitle: item['volumeInfo']['subtitle'] ?? "",
thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
"https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
previewLink: item['volumeInfo']['previewLink'] ?? "",
);
bookList.add(book);
}
```
![?? 는 해당 변수에 담긴 값이 null일 경우, 즉 값이 없을 경우 뒤에 오는 값을 사용하겠다는 뜻입니다.](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e2a12f29-d6a8-4ba3-9d23-f32e7ad59a0f/Untitled.png)
?? 는 해당 변수에 담긴 값이 null일 경우, 즉 값이 없을 경우 뒤에 오는 값을 사용하겠다는 뜻입니다.
`item` 이라는 변수에 담긴 `Map` 에서는 key 값으로 데이터들을 가져올 수 있습니다.
아래 json 데이터의 `items` 리스트 안에 포함된 각각의 요소가 위 코드의 `item` 변수에 들어갔다고 생각하시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/92325eba-d64c-475c-837e-8f00d3d89d7d/Untitled.png)
만들어진 Book 객체는 25번째 줄의 `bookList.add` 를 통해 `bookList` 에 추가됩니다.
3. 매번 검색을 할 때마다 `bookList` 에 담긴 값을 모두 비워줘야 해당 검색어에 대한 결과만을 볼 수 있겠죠. 아래 코드스니펫을 복사해 `book_service.dart` 9번째 줄 맨 뒤에 붙여넣습니다.
- **[코드스니펫] bookList 비우기**
```dart
bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
```
![Screen Shot 2022-09-19 at 3.34.27 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3b9d33a1-ded3-45f4-bc71-e77d6494239b/Screen_Shot_2022-09-19_at_3.34.27_AM.png)
<aside>
💡 `BookService`에서 Google Book API 데이터를 가져와 `bookList`에 저장하는 것까지 완성했습니다.
</aside>
7) 검색 결과 보여주기
ListView.builder
를 사용해 bookList 내의 요소들을 화면에 보여주도록 하겠습니다.
아래 코드스니펫을 복사해 main.dart
파일의 104-106 번째 줄을 지우고 붙여넣어주세요.
[코드스니펫] SearchPage / ListView.builder
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.bookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
Book book = bookService.bookList.elementAt(index);
return ListTile();
},
),
),
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/44ac70cb-05dc-4a2c-8a7f-c5b51d604413/Untitled.png)
2. 112번째 줄에 있는 Book 을 클릭해 아래 사진처럼 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library ‘book.dart’` 를 선택합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/586a27ce-98b9-466f-8d6e-0ee18394bcaf/Untitled.png)
4번째 줄에 Import 문이 생기며 에러가 사라졌습니다.
![Screen Shot 2022-09-19 at 10.04.08 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4ea57bdd-bd69-4a49-bb92-930f88bc23ca/Screen_Shot_2022-09-19_at_10.04.08_AM.png)
3. 이제 ListTile 내에 Book의 내용을 보여주도록 하겠습니다. ListTile 의 소괄호 사이에 `title: Text(book.title)` 라고 입력해보세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e16c593-35ef-43cf-b2c7-7d9ee7f36a4d/Untitled.png)
위와 같이 검색 결과가 에뮬레이터에 잘 표시되는 것을 확인할 수 있습니다.
4. 이제 나머지 요소들도 화면에 보여주도록 하겠습니다. 아래 코드스니펫을 복사해 114번째 줄을 **지우고** 붙여넣어주세요.
- **[코드스니펫] ListTile**
```dart
return ListTile(
onTap: () {},
leading: Image.network(
book.thumbnail,
fit: BoxFit.fitHeight,
),
title: Text(
book.title,
style: TextStyle(fontSize: 16),
),
subtitle: Text(
book.subtitle,
style: TextStyle(color: Colors.grey),
),
trailing: IconButton(
onPressed: () {},
icon: Icon(Icons.star_border),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a8a676df-6cd0-465b-adfe-2aa4a85c46e8/Untitled.png)
`ListTile` 의 각 요소는 아래와 같이 배치됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7639eb36-5b37-43b8-87d0-7cf97013552e/Untitled.png)
다른 검색어를 입력해보세요. 검색이 잘 구현되었나요?
화면에 검색 결과가 뜨지 않는군요. 이는 검색을 통해 `bookList` 는 수정되었지만, 이후 화면을 **새로고침하지 않아서** 생기는 문제입니다.
`notifyListeners();` 를 `book_service.dart` 의 29번째 줄 아래에 추가해줍니다.
(위치에 주의해주세요!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4a23b383-94e7-4805-a5f7-bea5a93d6d36/Untitled.png)
<aside>
💡 `notifyListeners();` 는 해당 `Service` 의 `Consumer` 로 등록된 모든 위젯의 `builder` 함수를 재호출해 화면을 새로고침합니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/036862cd-218a-401b-93e4-afe0c590ee1c/Untitled.png)
5. 이제 검색 결과는 잘 나오지만, `Debug Console` 에 아래와 같은 에러가 발생하고 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0af5da55-36c1-4648-8497-80b1ca2ed385/Untitled.png)
이는 `BookService` 의 `search` 함수에서 이전 데이터를 지워줄 때 순간적으로 `bookList` 가 비어서 생기는 문제입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/de463a28-727c-43f5-ae14-a37fd7fb1641/Untitled.png)
아래 코드스니펫을 복사해 `main.dart` 112번째 줄 뒤에 붙여넣어주세요.
- **[코드스니펫] bookList 가 비어있을 때 처리**
```dart
if (bookService.bookList.isEmpty) return SizedBox();
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/19dad2a7-7193-41d0-97da-874a0efaffde/Untitled.png)
`bookList` 가 비어있을 때는 `elementAt` 함수를 호출하지 않고 바로 return 하도록 해 `bookList` 의 `range` 를 벗어나는 인덱스로 접근이 일어나지 않도록 합니다.
이제 에러 없이 잘 동작합니다!
구현 목표
1) 파일 분리
main.dart 의 115번째 줄에 있는 ListTile 을 우클릭해 Refactor(Ctrl + Shift + R)
을 선택하고,
Extract Widget
을 선택해주세요.
위젯의 이름을 입력하는 칸이 나오면 아래와 같이 BookTile
이라고 적은 후 엔터를 눌러줍니다.
ListTile
내에 있던 코드가 BookTile
이라는 이름의 별도의 위젯으로 분리된 것을 확인할 수 있습니다.
2) BookService
에 좋아요 기능 추가
먼저 좋아요 한 책들을 담는 List 를 만들겠습니다.
아래 코드스니펫을 복사해 book_service.dart
7번째 줄 맨 뒤에 붙여넣어주세요.
[코드스니펫] likedBookList
List<Book> likedBookList = [];
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/08c2094c-88ff-45c7-a30f-d36353445eba/Untitled.png)
2. 이제 **좋아요 아이콘을 누르면 호출될 함수**를 추가하겠습니다.
<aside>
💡 해당 함수에서 구현되어야 할 기능은 아래와 같습니다.
- **좋아요가 눌러져 있지 않은 경우 (`likedBookList` 에 없는 경우)
⇒** 좋아요 추가 (`likedBookList` 에 추가)
- **좋아요가 이미 눌러져있다면 (`likedBookList` 에 있는 경우)
⇒** 좋아요 취소 (`likedBookList` 에서 제거)
</aside>
아래 코드스니펫을 복사해 `book_service.dart` 8번째 줄 맨 뒤에 붙여넣어주세요.
- **[코드스니펫] toggleLikeBook**
```dart
void toggleLikeBook({required Book book}) {
String bookId = book.id;
if (likedBookList.map((book) => book.id).contains(bookId)) {
likedBookList.removeWhere((book) => book.id == bookId);
} else {
likedBookList.add(book);
}
notifyListeners();
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/730739ac-1558-4e4b-9567-ce6efc17d47b/Untitled.png)
<aside>
💡 위 코드들에 대한 설명입니다.
`**likedBookList.map((book) => book.id)**` : likedBookList 의 요소들을 순회하며 id 들만 뽑아 새로 iterable(List와 비슷) 을 만듭니다.
`**likedBookList.removeWhere((book) => book.id == bookId)`** : toggleLikeBook 함수가 인자로 받는 book 과 id 가 같은 Book 이 likedBookList 내에 있다면 제거합니다.
`**notifyListeners`** : 화면을 새로고침합니다.
</aside>
<aside>
💡 왜 `**likedBookList.contains(book)`** 과 같이 작성하지 않을까요?
클래스를 통해 생성된 두개의 객체는 처음에 대입해주는 값들(Book 에서는 title, subtitle 등)이 같더라도, 서로 다른 존재입니다.
코드로 작성해보면 아래와 같습니다.
```dart
var book1 = Book(id: "1", title: "톰 소여의 모험");
var book2 = Book(id: "1", title: "톰 소여의 모험");
print(book1 == book2) // false
```
우리가 작성한 코드에서도 똑같은 일이 일어납니다. 서로 다른 객체로 인식하기 때문에 contains 는 항상 false 를 반환할 수밖에 없습니다.
```dart
var likedBookList = [Book(id: "1", title: "톰 소여의 모험")];
var book2 = Book(id: "1", title: "톰 소여의 모험");
print(likedBookList.contains(book2)) // false
```
이를 해결하기 위해서 `==` 연산자를 덮어씌워 비교 로직을 새로 작성하거나, `Equatable` 등의 플러그인 등을 사용하기도 합니다. 핵심은 두개의 객체가 같은 데이터를 가리키고 있는지를 어떤 식으로 알아낼 것이냐는 점입니다.
우리는 Google Book API 가 제공하는 id (고유한 값입니다) 를 이용해서 비교 로직을 작성했습니다.
`**likedBookList.map((book) => book.id)**` 와 같이 id 들이 담긴 iterable(리스트와 유사합니다) 를 만들고, 이것이 우리가 좋아요 버튼을 누른 Book 의 id 를 포함하는지 비교해, 이미 좋아요가 눌린 책인지 아닌지를 구분할 수 있겠죠!
</aside>
3. `ListTile` 의 trailing 에 있는 좋아요 버튼을 누르면 `toggleLikeBook` 함수를 호출하고, 좋아요 여부에 따라 아이콘의 색깔을 바꿔주도록 하겠습니다.
먼저 위에서 분리한 `BookTile` 위젯에서 `BookService` 를 사용할 수 있도록 해야합니다. 다시 Provider 사용법을 복기해볼까요?
<aside>
💡 Provider 에서 Service 를 사용하는 방법
1. `Consumer<클래스명>` : 클래스 정보 갱신시 화면을 새로고침 해야 할 때 사용
2. `context.read<클래스명>` : 1회성으로 클래스 접근할 때 사용 (화면 새로고침이 필요 없을 때)
</aside>
우리가 만든 `BookTile` 은 좋아요를 누를 때마다 trailing 에 있는 아이콘의 색깔이 바뀌어야 합니다. 즉 새로고침이 일어나는 위젯입니다.
하지만, `BookTile` 을 포함하고 있는 **SearchPage** 가 `Consumer`로 감싸져 있기 때문에, `BookTile` 은 별개의 `Consumer` 로 감싸지 않더라도 함께 새로고침이 일어나게 됩니다.
그러므로, `BookTile` 에서는 `context.read` 만 사용해서 BookService 에 접근해주겠습니다.
아래 코드스니펫을 복사해 main.dart 134번째 줄 맨 뒤에 붙여넣어주세요
- **[코드스니펫] context.read<BookService>**
```dart
BookService bookService = context.read<BookService>();
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9673abd3-e54a-4e1f-8ce8-76fa61a8665a/Untitled.png)
이제 `bookService` 라는 이름으로 `BookService` 클래스에 있는 변수와 함수를 사용할 수 있습니다.
아래 코드스니펫을 복사해 `main.dart` 의 152, 153번째 줄을 지우고 붙여넣어주세요.
- **[코드스니펫] ListTile trailing 아이콘**
```dart
onPressed: () {
bookService.toggleLikeBook(book: book);
},
icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
? Icon(
Icons.star,
color: Colors.amber,
)
: Icon(Icons.star_border),
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fe835df4-045e-4fdc-b019-acd55bed32e4/Untitled.png)
<aside>
💡 위 코드들에 대한 설명입니다.
`**bookService.toggleLikeBook(book: book)**` : BookService 에 생성해준 toggleLikeBook 에 해당 book 데이터를 인자로 넘겨 호출해줍니다.
`**bookService.likedBookList.map((book) => book.id).contains(book.id)`** : bookService 에 있는 likedBookList(좋아요 한 책들 담겨있는 리스트) 가 해당 book 을 담고있는지, 즉 이미 좋아요를 누른 상태인지 확인합니다.
아래 삼항연산자 (조건 ? a : b) 를 통해 좋아요가 눌러진 상태라면 노란색 별 아이콘을 보여줍니다.
</aside>
아래와 같이 좋아요를 누를 때 화면이 잘 갱신되는 것을 볼 수 있습니다.
![Screen Shot 2022-09-19 at 12.06.51 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12e8db01-a01f-449c-8174-436c43da5c97/Screen_Shot_2022-09-19_at_12.06.51_PM.png)
3) LikedBookPage
에서 좋아요 한 책들 모아보기
먼저 LikedBookPage 에서도 likedBookList 의 변화에 따라 화면이 갱신되도록 해야합니다.
Consumer
를 사용해 Scaffold
를 감싸주겠습니다. 171번째 줄의 Scaffold 를 클릭한 뒤 Quick Fix(Ctrl/Cmd + .
)를 눌러 Wrap with Builder
를 선택해주세요.
아래와 같이 Scaffold
위젯을 Builder
위젯이 감쌉니다. Consumer
위젯과 형식이 비슷한 Builder
를 사용해 모양을 잡아줬습니다.
아래 이미지와 같이 79번째 라인의 Builder
를 Consumer<BookService>
로 변경해주세요.
context
뒤에 bookService
와 child
도 추가해줍니다.
그리고 줄 정렬을 예쁘게 해주기 위해 177번째 라인의 중괄호와 소괄호 사이(위 화살표)에 콤마(,
)를 찍은 뒤 저장해주세요. 저장하면 코드가 정렬됩니다.
아래 코드스니펫을 복사해 main.dart
의 174-176 번째 줄을 지우고 붙여넣어주세요. 위 SearchPage 에 작성한 코드를 그대로 가져왔습니다.
[코드스니펫] LikedBookPage / BookTile
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.bookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.bookList.isEmpty) return SizedBox();
Book book = bookService.bookList.elementAt(index);
return BookTile(book: book);
},
),
),
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/548eb9ab-6c0c-449f-b847-943299ca4d7b/Untitled.png)
좋아요 한 책만 모아보기 위해서는 위에 밑줄친 `bookList` 대신에 `likedBookList` 를 사용하면 되겠죠! 모두 아래와 같이 수정해봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3d1e194e-4014-45c6-9e51-76f63128f334/Untitled.png)
이제 좋아요 탭에서 아래와 같이 좋아요 누른 책들을 모아볼 수 있습니다.
![Screen Shot 2022-09-19 at 12.49.08 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1f969888-e359-4b43-8895-506eaad7bb27/Screen_Shot_2022-09-19_at_12.49.08_PM.png)
구현 목표
1) WebView 란?
대표적인 예시가 네이버 앱입니다. 앱에 들어가보시면 네이버 모바일 웹사이트와 거의 동일한 UI 를 볼 수 있습니다. 껍데기 부분만 앱으로 만들고 내부 위젯에 WebView 를 사용해 웹사이트를 그대로 띄우는 방식으로 구현되어 있다는 것을 알 수 있겠죠!
이외에도 당근마켓, 토스와 같은 앱들도 내부 기능 일부를 WebView 를 활용해 구현했습니다.
2) webview_flutter 패키지 설치하기
해당 패키지 사이트는 아래와 같습니다.
[코드스니펫] Pub.dev / webview_flutter
https://pub.dev/packages/webview_flutter
강의 영상과 조금 다른 부분이니 주의해서 봐주세요! webview_flutter 패키지가 업데이트 되면서 아래 강의 내용대로 진행하면 에러가 뜹니다. 이전 버전 패키지를 설치함으로서 이 에러를 해결해주도록 하겠습니다
아래 코드스니펫을 복사해주세요.
[코드스니펫] webview_flutter 3.0.4 버전 설치 스크립트
flutter pub add webview_flutter:^3.0.4
VSCode View
→ Terminal
을 선택해주세요.
Terminal
창에 복사한 flutter pub add webview_flutter:^3.0.4
를 붙여넣고 엔터를 눌러 실행해 주세요.
아래와 같이 나오면 정상적으로 작동한 것입니다.
pubspec.yaml
파일을 열어서 아래와 같이 41번째 라인에 webview_flutter: ^3.0.4
가 있으면 설치가 완료된 것입니다.
이제 아래부터는 강의 내용과 같습니다!
android/app/build.gradle
파일을 열고(경로에 유의하세요!)
50 번째 줄의 flutter.minSdkVersion
을 19
로 수정해줍니다.
android {
defaultConfig {
minSdkVersion 19
}
}
아래와 같이 작성해주면 됩니다.
webview_flutter 를 앱에서 사용하기 위해서는 앱을 재설치해야합니다.
먼저 앱을 정지하고
main.dart
를 열고 우측 상단에서 Run Without Debugging
을 클릭해 다시 앱을 설치 / 실행해줍니다.
<aside>
💡 WebView 를 앱에서 사용할 준비가 완료되었습니다.
</aside>
3) WebViewPage 위젯 만들기
webview_flutter 의 설치가 끝났으니, 이제 각 Book 의 previewLink
로 이동하는 기능을 구현해보겠습니다.
먼저 WebViewPage 라는 StatelessWidget 을 하나 만들어주겠습니다. 아래 코드스니펫을 복사해 192번째 줄 맨 뒤에 붙여넣어주세요.
[코드스니펫] WebViewPage
class WebViewPage extends StatelessWidget {
WebViewPage({super.key, required this.url});
String url;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.grey,
title: Text(url),
),
body: WebView(initialUrl: url),
);
}
}
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f549f171-9a60-42a5-868b-b94c08e27e62/Untitled.png)
2. 206번째 줄에 에러가 발생했군요. 에러가 발생한 `WebView` 를 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 누르고, `Import library ‘package:webview_flutter/webview_flutter.dart` 를 선택해주세요.
![Screen Shot 2022-09-19 at 3.17.21 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e59b7f90-e05c-4a4d-8193-bc4267b127e1/Screen_Shot_2022-09-19_at_3.17.21_PM.png)
3번째 줄에 Import 문이 추가되면서 에러가 해결됩니다.
![Screen Shot 2022-09-19 at 3.17.51 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f3026c0d-bd4d-49d8-a5c1-62d8c1f18457/Screen_Shot_2022-09-19_at_3.17.51_PM.png)
3. 코드를 자세히 뜯어볼까요?
![Screen Shot 2022-09-19 at 3.18.28 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/be60e694-dae6-46c9-ac11-0cbe5470d9d9/Screen_Shot_2022-09-19_at_3.18.28_PM.png)
**WebViewPage** 위젯으로 이동하면서 url 이라는 인자를 넘겨받고, 이를 `WebView` 위젯에 다시 인자로 넘겨 웹페이지를 `Scaffold` 의 body 내에 띄워주는 코드입니다.
4) 웹 URL 연결하기
아래 코드스니펫을 복사해 ListTile
의 onTap
함수 중괄호 사이 (139번째 줄) 에 붙여넣어줍니다.
[코드스니펫] BookTile / ListTile / onTap
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewPage(
url: book.previewLink.replaceFirst("http", "https"),
),
),
);
![Screen Shot 2022-09-19 at 3.20.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/54280695-d3b7-4957-b6c6-06e86cb57f87/Screen_Shot_2022-09-19_at_3.20.05_PM.png)
`ListTile` 을 누르면 방금 만든 **WebViewPage** 로 이동하도록 했습니다. 이 때 해당 book의 `previewLink` 을 인자로 전달해, 해당 링크를 웹뷰로 열도록 합니다.
<aside>
💡 Book 의 `previewLink` 의 경우 http 로 시작하기 때문에 보안 정책상 웹뷰로 열리지 않습니다. 이 때문에 `replaceFirst`를 사용해 해당 url 의 http 를 https 로 바꿔주었습니다.
http, https 에 대한 보다 자세한 내용이 궁금하시다면 아래 링크를 참고해주세요.
[**HTTP와 HTTPS 차이점**](https://brunch.co.kr/@hyoi0303/10)
</aside>
이제 에뮬레이터에서 검색 후에 각각의 BookTile 을 클릭해봅시다.
아래와 같이 앱 내에서 해당 웹페이지를 잘 보여주는 것을 확인할 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ab54991c-1b5a-4ae9-9855-91967b4aadb4/Untitled.gif)
최종 코드
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'book.dart';
import 'book_service.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => BookService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var bottomNavIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
SearchPage(),
LikedBookPage(),
].elementAt(bottomNavIndex),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Colors.black,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedFontSize: 12,
unselectedFontSize: 12,
iconSize: 28,
type: BottomNavigationBarType.fixed,
onTap: (value) {
setState(() {
bottomNavIndex = value;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '검색',
),
BottomNavigationBarItem(
icon: Icon(Icons.star),
label: '좋아요',
),
],
currentIndex: bottomNavIndex,
),
);
}
}
class SearchPage extends StatelessWidget {
SearchPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(
builder: (context, bookService, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
toolbarHeight: 80,
title: TextField(
onSubmitted: (value) {
bookService.search(value);
},
cursorColor: Colors.grey,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.bookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.bookList.isEmpty) return SizedBox();
Book book = bookService.bookList.elementAt(index);
return BookTile(book: book);
},
),
),
);
},
);
}
}
class BookTile extends StatelessWidget {
const BookTile({
Key? key,
required this.book,
}) : super(key: key);
final Book book;
@override
Widget build(BuildContext context) {
BookService bookService = context.read<BookService>();
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewPage(
url: book.previewLink.replaceFirst("http", "https"),
),
),
);
},
leading: Image.network(
book.thumbnail,
fit: BoxFit.fitHeight,
),
title: Text(
book.title,
style: TextStyle(fontSize: 16),
),
subtitle: Text(
book.subtitle,
style: TextStyle(color: Colors.grey),
),
trailing: IconButton(
onPressed: () {
bookService.toggleLikeBook(book: book);
},
icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
? Icon(
Icons.star,
color: Colors.amber,
)
: Icon(Icons.star_border),
),
);
}
}
class LikedBookPage extends StatelessWidget {
const LikedBookPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(
builder: (context, bookService, child) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.likedBookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.likedBookList.isEmpty) return SizedBox();
Book book = bookService.likedBookList.elementAt(index);
return BookTile(book: book);
},
),
),
);
},
);
}
}
class WebViewPage extends StatelessWidget {
WebViewPage({super.key, required this.url});
String url;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.grey,
title: Text(url),
),
body: WebView(initialUrl: url),
);
}
}
book.dart
class Book {
String id;
String title;
String subtitle;
String thumbnail; // 썸네일 이미지 링크
String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
Book({
required this.id,
required this.title,
required this.subtitle,
required this.thumbnail,
required this.previewLink,
});
}
book_service.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'book.dart';
class BookService extends ChangeNotifier {
List<Book> bookList = []; // 책 목록
List<Book> likedBookList = [];
void toggleLikeBook({required Book book}) {
String bookId = book.id;
if (likedBookList.map((book) => book.id).contains(bookId)) {
likedBookList.removeWhere((book) => book.id == bookId);
} else {
likedBookList.add(book);
}
notifyListeners();
}
void search(String q) async {
bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
if (q.isNotEmpty) {
Response res = await Dio().get(
"https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
);
List items = res.data["items"];
for (Map<String, dynamic> item in items) {
Book book = Book(
id: item['id'],
title: item['volumeInfo']['title'] ?? "",
subtitle: item['volumeInfo']['subtitle'] ?? "",
thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
"https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
previewLink: item['volumeInfo']['previewLink'] ?? "",
);
bookList.add(book);
}
}
notifyListeners();
}
}
1) Google Book API 에서 제공하는 다른 정보 보여주기
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'book.dart';
import 'book_service.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => BookService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var bottomNavIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
SearchPage(),
LikedBookPage(),
].elementAt(bottomNavIndex),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Colors.black,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedFontSize: 12,
unselectedFontSize: 12,
iconSize: 28,
type: BottomNavigationBarType.fixed,
onTap: (value) {
setState(() {
bottomNavIndex = value;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '검색',
),
BottomNavigationBarItem(
icon: Icon(Icons.star),
label: '좋아요',
),
],
currentIndex: bottomNavIndex,
),
);
}
}
class SearchPage extends StatelessWidget {
SearchPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(
builder: (context, bookService, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
toolbarHeight: 80,
title: TextField(
onSubmitted: (value) {
bookService.search(value);
},
cursorColor: Colors.grey,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.bookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.bookList.isEmpty) return SizedBox();
Book book = bookService.bookList.elementAt(index);
return BookTile(book: book);
},
),
),
);
},
);
}
}
class BookTile extends StatelessWidget {
const BookTile({
Key? key,
required this.book,
}) : super(key: key);
final Book book;
@override
Widget build(BuildContext context) {
BookService bookService = context.read<BookService>();
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewPage(
url: book.previewLink.replaceFirst("http", "https"),
),
),
);
},
leading: Image.network(
book.thumbnail,
fit: BoxFit.fitHeight,
),
title: Text(
book.title,
style: TextStyle(fontSize: 16),
),
subtitle: Text(
"${book.authors.join(", ")}\n${book.publishedDate}",
style: TextStyle(color: Colors.grey),
),
trailing: IconButton(
onPressed: () {
bookService.toggleLikeBook(book: book);
},
icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
? Icon(
Icons.star,
color: Colors.amber,
)
: Icon(Icons.star_border),
),
);
}
}
class LikedBookPage extends StatelessWidget {
const LikedBookPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(
builder: (context, bookService, child) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.likedBookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.likedBookList.isEmpty) return SizedBox();
Book book = bookService.likedBookList.elementAt(index);
return BookTile(book: book);
},
),
),
);
},
);
}
}
class WebViewPage extends StatelessWidget {
WebViewPage({super.key, required this.url});
String url;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.grey,
title: Text(url),
),
body: WebView(initialUrl: url),
);
}
}
book.dart
class Book {
String id;
String title;
String subtitle;
List authors;
String publishedDate;
String thumbnail; // 썸네일 이미지 링크
String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
Book({
required this.id,
required this.title,
required this.subtitle,
required this.authors,
required this.publishedDate,
required this.thumbnail,
required this.previewLink,
});
}
book_service.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'book.dart';
class BookService extends ChangeNotifier {
List<Book> bookList = []; // 책 목록
List<Book> likedBookList = [];
void toggleLikeBook({required Book book}) {
String bookId = book.id;
if (likedBookList.map((book) => book.id).contains(bookId)) {
likedBookList.removeWhere((book) => book.id == bookId);
} else {
likedBookList.add(book);
}
notifyListeners();
}
void search(String q) async {
bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
if (q.isNotEmpty) {
Response res = await Dio().get(
"https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
);
List items = res.data["items"];
for (Map<String, dynamic> item in items) {
Book book = Book(
id: item['id'],
title: item['volumeInfo']['title'] ?? "",
subtitle: item['volumeInfo']['subtitle'] ?? "",
authors: item['volumeInfo']['authors'] ?? [],
publishedDate: item['volumeInfo']['publishedDate'] ?? "",
thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
"https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
previewLink: item['volumeInfo']['previewLink'] ?? "",
);
bookList.add(book);
}
}
notifyListeners();
}
}
2) 좋아요 누른 책 목록 기기에 저장하기
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'book.dart';
import 'book_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => BookService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var bottomNavIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
SearchPage(),
LikedBookPage(),
].elementAt(bottomNavIndex),
bottomNavigationBar: BottomNavigationBar(
selectedItemColor: Colors.black,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedFontSize: 12,
unselectedFontSize: 12,
iconSize: 28,
type: BottomNavigationBarType.fixed,
onTap: (value) {
setState(() {
bottomNavIndex = value;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '검색',
),
BottomNavigationBarItem(
icon: Icon(Icons.star),
label: '좋아요',
),
],
currentIndex: bottomNavIndex,
),
);
}
}
class SearchPage extends StatelessWidget {
SearchPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(
builder: (context, bookService, child) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
toolbarHeight: 80,
title: TextField(
onSubmitted: (value) {
bookService.search(value);
},
cursorColor: Colors.grey,
decoration: InputDecoration(
prefixIcon: Icon(Icons.search, color: Colors.grey),
hintText: "작품, 감독, 배우, 컬렉션, 유저 등",
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
borderRadius: BorderRadius.all(Radius.circular(10)),
),
),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.bookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.bookList.isEmpty) return SizedBox();
Book book = bookService.bookList.elementAt(index);
return BookTile(book: book);
},
),
),
);
},
);
}
}
class BookTile extends StatelessWidget {
const BookTile({
Key? key,
required this.book,
}) : super(key: key);
final Book book;
@override
Widget build(BuildContext context) {
BookService bookService = context.read<BookService>();
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewPage(
url: book.previewLink.replaceFirst("http", "https"),
),
),
);
},
leading: Image.network(
book.thumbnail,
fit: BoxFit.fitHeight,
),
title: Text(
book.title,
style: TextStyle(fontSize: 16),
),
subtitle: Text(
"${book.authors.join(", ")}\n${book.publishedDate}",
style: TextStyle(color: Colors.grey),
),
trailing: IconButton(
onPressed: () {
bookService.toggleLikeBook(book: book);
},
icon: bookService.likedBookList.map((book) => book.id).contains(book.id)
? Icon(
Icons.star,
color: Colors.amber,
)
: Icon(Icons.star_border),
),
);
}
}
class LikedBookPage extends StatelessWidget {
const LikedBookPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<BookService>(
builder: (context, bookService, child) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ListView.separated(
itemCount: bookService.likedBookList.length,
separatorBuilder: (context, index) {
return Divider();
},
itemBuilder: (context, index) {
if (bookService.likedBookList.isEmpty) return SizedBox();
Book book = bookService.likedBookList.elementAt(index);
return BookTile(book: book);
},
),
),
);
},
);
}
}
class WebViewPage extends StatelessWidget {
WebViewPage({super.key, required this.url});
String url;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.grey,
title: Text(url),
),
body: WebView(initialUrl: url),
);
}
}
book.dart
class Book {
String id;
String title;
String subtitle;
List authors;
String publishedDate;
String thumbnail; // 썸네일 이미지 링크
String previewLink; // ListTile 을 눌렀을 때 이동하는 링크
Book({
required this.id,
required this.title,
required this.subtitle,
required this.authors,
required this.publishedDate,
required this.thumbnail,
required this.previewLink,
});
Map toJson() {
return {
"id": id,
"title": title,
"subtitle": subtitle,
"authors": authors,
"publishedDate": publishedDate,
"thumbnail": thumbnail,
"previewLink": previewLink,
};
}
factory Book.fromJson(json) {
return Book(
id: json['id'],
title: json['title'],
subtitle: json['subtitle'],
authors: json['authors'],
publishedDate: json['publishedDate'],
thumbnail: json['thumbnail'],
previewLink: json['previewLink'],
);
}
}
book_service.dart
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'book.dart';
import 'main.dart';
class BookService extends ChangeNotifier {
BookService() {
loadLikedBookList();
}
List<Book> bookList = []; // 책 목록
List<Book> likedBookList = [];
void toggleLikeBook({required Book book}) {
String bookId = book.id;
if (likedBookList.map((book) => book.id).contains(bookId)) {
likedBookList.removeWhere((book) => book.id == bookId);
} else {
likedBookList.add(book);
}
notifyListeners();
saveLikedBookList();
}
void search(String q) async {
bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기
if (q.isNotEmpty) {
Response res = await Dio().get(
"https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
);
List items = res.data["items"];
for (Map<String, dynamic> item in items) {
Book book = Book(
id: item['id'],
title: item['volumeInfo']['title'] ?? "",
subtitle: item['volumeInfo']['subtitle'] ?? "",
authors: item['volumeInfo']['authors'] ?? [],
publishedDate: item['volumeInfo']['publishedDate'] ?? "",
thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
"https://thumbs.dreamstime.com/b/no-image-available-icon-flat-vector-no-image-available-icon-flat-vector-illustration-132482953.jpg",
previewLink: item['volumeInfo']['previewLink'] ?? "",
);
bookList.add(book);
}
}
notifyListeners();
}
saveLikedBookList() {
List likedBookJsonList =
likedBookList.map((book) => book.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(likedBookJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('likedBookList', jsonString);
}
loadLikedBookList() {
String? jsonString = prefs.getString('likedBookList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List likedBookJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
likedBookList =
likedBookJsonList.map((json) => Book.fromJson(json)).toList();
}
}
PDF 파일
단축키 모음
F5
Ctrl
+ S
command
+ S
Ctrl
+ A
command
+ A
Ctrl
+ X
command
+ X
shift
+ enter
Ctrl
+ Alt
+ L
option
+ command
+ L
Tab
Shift
+ Tab
Ctrl
+ /
command
+ /
[수업 목표]
[목차]
1) 패키지란?
Flutter는 pub.dev
라는 사이트에서 패키지들을 검색하실 수 있습니다. 코드스니펫을 복사해서 새 탭에서 띄워주세요.
https://pub.dev/
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9e74b84f-b51e-4789-9a74-c38325a6f3c8/Untitled.png)
- 밑으로 조금 내려와 보면 Flutter Favorites라고 인증된 신뢰할 만한 패키지 목록이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/68298554-45dc-4a21-8796-49647462169c/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7a7eb553-ac5a-45dd-ab1e-5bf60e361959/Untitled.png)
2) 패키지 문서 보는 방법
코드스니펫을 복사해 새 탭에서 열어주세요.
https://pub.dev/packages/google_fonts
<aside>
💡 처음에는 패키지 설치 방법 및 사용 설명서를 읽는데 집중해주세요!
모든 문서가 영어로 되어 있지만, 계속 보다 보면 나오는 용어들이 계속 나오는 것을 보실 수 있습니다.
</aside>
![Pub.dev.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6eaff257-24ce-4f9f-957f-249dd7030249/Pub.dev.png)
<aside>
💡 Readme와 Example 탭을 보면 해당 패키지의 사용법을 익힐 수 있습니다.
</aside>
3) 패키지 추천 사이트
Awesome Flutter : Flutter에 관련된 모든 자료를 모아둔 Github 문서입니다.
Flutter Gems : 카테고리별 Flutter 패키지 모음
<aside>
💡 직접 프로젝트를 진행하면서 패키지 사용법을 배워보도록 하겠습니다.
</aside>
완성본
CRUD는 가장 기본이 되는 데이터 처리 기능입니다.
게시판 기능을 만든다면 아래 CRUD 기능이 필수적으로 제공되어야 합니다.
유저 정보를 다루는 과정도 CRUD로 표현하면 다음과 같습니다.
회원 가입(Create)
프로필 보여주기(Read)
회원 정보 수정(Update)
회원 탈퇴(Delete)
이와 같이 CRUD는 다루는 데이터의 종류만 바뀌고 항상 기본적으로 구현하는 데이터 처리 기능입니다.
1) 프로젝트 준비
Flutter 프로젝트 생성
VSCode에서 아래와 같이 네모 모양의 Stop
버튼을 눌러 기존에 실행한 앱을 종료해주세요.
View
→ Command Palette
를 선택해주세요.
명령어를 검색하는 팝업창이 뜨면, flutter
라고 입력한 뒤 Flutter: New Project
를 선택해주세요.
Application
을 선택해주세요.
프로젝트를 저장할 폴더를 선택하는 화면이 나오면 flutter
폴더를 선택한 뒤 Select a folder to create the project in
버튼을 눌러 주세요.
프로젝트 이름을 mymemo
로 입력해주세요.
만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요. (팝업이 안보이시면 넘어가주세요!)
다음과 같이 프로젝트가 생성됩니다.
불필요한 힌트 숨기기
코드스니펫을 복사해서 analysis_options.yaml
파일의 24번째 라인 뒤에 붙여 넣고 저장해주세요.
[코드스니펫] analysis_options.yaml
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
- 어떤 의미인지 궁금하신 분들을 위해
- `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
- 파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
12번째 라인에 마우스를 올리면 아래와 같이 설명이 뜹니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다.
<aside>
💡 상수로 만들면 어떤 이점이 있나요?
상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다.
</aside>
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
- 지금은 학습 단계이니 눈에 띄지 않도록 해주도록 하겠습니다.
9. 아래 코드스니펫을 복사해서, `main.dart`의 기존 코드를 모두 지우고 붙여 넣은 뒤 저장해 주세요.
- **[코드스니펫] main.dart**
```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: Center(child: Text("메모를 작성해주세요")),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
),
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key});
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
},
),
),
);
}
}
```
- 에뮬레이터 실행하기
1. VSCode 우측 하단에 `Chrome (web-javascript)`를 클릭해주세요.
(에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5d0db764-7e21-4261-8c19-94162c4d0ba8/Untitled.png)
2. `Start Pixel 2 API 29 mobile emulator`를 선택해주세요. macOS의 경우 iOS 에뮬레이터로 진행하셔도 무방합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5e0e7243-7d95-4b57-99df-08fd28e1b4d7/Untitled.png)
잠시 기다리면 에뮬레이터가 실행됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf5640d3-c9a9-4105-bd40-9dcb309412f5/Untitled.png)
3. VSCode 우측 상단에 **아래 화살표**를 눌러 `Run Without Debugging`을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fba923c5-b4b0-4ddb-b381-ae8686c0315c/Untitled.png)
<aside>
💡 `Start Debugging`으로 실행해도 무방합니다. 디버깅 모드는 특정 라인에서 앱 실행을 멈추고 해당 변수에 어떤 값이 들어있는지 볼 수 있지만 `Run without debugging`이 실행 속도가 더 빨라 안내를 위와 같이 드렸습니다 🙂
</aside>
에뮬레이터에 아래와 같이 **메모장** 화면이 나오면 완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1092421d-a876-4748-aa68-7370d5a1cb3b/Untitled.png)
<aside>
💡 위와 같이 메모를 보여주는 `HomePage`와 메모를 작성하는 `DetailPage` 두 페이지로 구성된 앱입니다.
기본적인 레이아웃과 페이지 이동 기능은 구현되어 있고, CRUD 기능을 함께 구현해 보도록 하겠습니다.
</aside>
2) 메모 목록 조회(Read)
HomePage
는 메모의 유무, 메모의 개수라는 상태에 따라 다른 화면을 갱신해야 하므로 StatefulWidget
으로 변경해줍니다.
21번째 라인에 StatelessWidget
을 클릭한 뒤 Quick Fix(Ctrl/Cmd + .
)를 누른 뒤 Convert to StatefulWidget
을 선택해주세요.
아래와 같이 _HomePageState
라는 상태 클래스가 추가되었습니다.
메모 목록을 가지고 있을 상태 변수 memoList
를 추가해 줍니다.
아래 코드스니펫을 복사해서 28번째 라인 맨 뒤에 붙여 넣어 주세요.
[코드스니펫] HomePage / memoList 상태 변수
List<String> memoList = ['장보기 목록: 사과, 양파']; // 전체 메모 목록
<aside>
💡 `memoList`는 `String`만 받는 배열로 선언하였습니다.
미리 `memoList`에 `'장보기 목록: 사과, 양파'` 라는 항목을 하나 넣어 두었습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/46fc74b3-4b74-48d5-8eb8-f4530e9f546d/Untitled.png)
3. `memoList`에 항목이 있을 때 `body`에 해당 항목이 보이도록 해봅시다.
코드스니펫을 복사해서 37번째 라인을 교체해 주세요.
- **[코드스니펫] HomePage / memoList 조건문**
```dart
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: Center(child: Text('메모가 존재합니다!')),
```
<aside>
💡 `조건 ? true : false` 형태의 조건문을 사용하여 조건에 따라 위젯을 다르게 보여줄 수 있습니다. (이를 “삼항 연산자” 라고 합니다.)
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a52ec0e1-e3de-4ccb-affe-afc086f9a0c3/Untitled.png)
미리 `memoList`에 `'장보기 목록: 사과, 양파’`라는 항목을 하나 넣어 두었기 때문에 `memoList.isEmpty`는 `false`가 되고, `메모가 존재합니다!` 라는 문구가 에뮬레이터에 나오는 것을 보실 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/204c4470-92f2-4ebf-821b-6b5601a53f13/Untitled.png)
4. `memoList`가 존재하는 경우, `ListView`를 이용해 보여주도록 코드스니펫을 복사해서 39번째 라인을 교체해 주세요.
- **[코드스니펫] HomePage / ListView**
```dart
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
String memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
print('$memo : 클릭 됨');
},
);
},
),
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aa6d810c-cacf-42a1-8ec9-e3f71fd0c928/Untitled.png)
<aside>
💡 `ListTile` 위젯을 활용하면 박스 안에 여러 영역에 다른 위젯을 손쉽게 배치할 수 있습니다.
![ListTile.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2a86c971-bcf5-4882-a07f-37a674357946/ListTile.png)
좀 더 상세한 설명은 아래 공식 문서를 참고해 주세요.
- **[[코드스니펫] ListTile 공식문서 URL](https://api.flutter.dev/flutter/material/ListTile-class.html)**
```dart
https://api.flutter.dev/flutter/material/ListTile-class.html
```
</aside>
저장해주시면 에뮬레이터에 메모가 추가된 것을 볼 수 있습니다.
![simulator_screenshot_5FAD2CD3-E9AC-4DCD-B347-58A12C2F36AB.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e67a7ca-f59f-4be8-b089-a5e4c0042c09/simulator_screenshot_5FAD2CD3-E9AC-4DCD-B347-58A12C2F36AB.png)
<aside>
💡 클릭시 print문은 `view` → `Debug Console`을 선택하셔서 보실 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d7d99dcc-642e-4282-80df-8b8331584a01/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23627feb-d8e8-43e1-89b1-e1c313210f5c/Untitled.png)
5. `memoList` 상태 변수에 `, '새 메모'` 를 추가해 봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e886bb08-2a5d-4f4d-b75d-0d6591ac1217/Untitled.png)
<aside>
💡 저장을 해도 에뮬레이터에 반영이 안되실 겁니다.
아직 우리는 변경된 상태에 따라 화면을 새로고침 해주는 코드를 작성하지 않았습니다.
따라서 `Restart` 를 해야 앱에 반영됩니다.
</aside>
우측 상단에 `Restart` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fed3dbef-6b58-4dfd-a519-419a3db55550/Untitled.png)
앱에 새로 추가된 메모가 보이는 것을 보실 수 있습니다.
![Screen Shot 2022-09-07 at 11.02.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/31a55655-af59-48d9-9dde-f5ca2027ead9/Screen_Shot_2022-09-07_at_11.02.35_PM.png)
여기까지 메모 목록 조회 기능 완성!
3) 메모 개별 조회 (Read)
메모 목록에서 ListTile
을 눌렀을 때 DetailPage
로 이동하도록 합니다.
아래 코드스니펫을 복사해서 59번째 라인을 지우고 붙여넣어주세요.
[코드스니펫] ListTile onTap / DetailPage 로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/61e61367-b85b-4169-8c80-811d65988e2f/Untitled.png)
<aside>
💡 메모 목록의 메모 하나를 누르면 DetailPage 로 잘 넘어가는 것을 확인할 수 있습니다.
</aside>
2. 이제 `DetailPage` 에서 해당 메모 내용을 볼 수 있도록 해야겠죠!
`memoList` 와 해당 메모의 `index` 를 `DetailPage` 로 넘겨주겠습니다.
(`memoList[index]` 와 같은 식으로 개별 메모의 내용을 불러오기 위함입니다)
우선 `DetailPage` 가 메모의 내용을 변수에 담아 받도록 해야합니다. 아래 코드스니펫을 복사해서 83번째 라인 뒤에 붙여넣어주세요.
- **[코드스니펫] ListTile onTap / DetailPage 로 이동**
```dart
final List<String> memoList;
final int index;
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89a8640a-f80b-4bd4-bdf8-4d77a5fa949a/Untitled.png)
83번째 라인에 빨간 밑줄이 그어졌네요! 이는 우리가 선언한 `memoList` 와 `index` 라는 변수에 아무것도 담지 않아 생긴 일입니다.
<aside>
💡 빨간 밑줄이 그어진 코드에 마우스를 올려보면 보다 자세한 에러 메시지를 확인할 수 있습니다
</aside>
![Screen Shot 2022-09-12 at 4.11.53 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0930cb3c-6dda-4117-ae2f-28964cb38360/Screen_Shot_2022-09-12_at_4.11.53_PM.png)
아래 사진처럼 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Add final field formal parameters` 를 클릭해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/62fc6605-6198-4b1c-a7a4-fe5892871acf/Untitled.png)
아래와 같이 생성자에 넘겨받는 변수를 추가해줌으로서 문제가 해결되었습니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e76e8c3-b1b1-448b-bc8a-8a68611f6026/Untitled.png)
그러자 이번엔 `HomePage` 에서 에러가 발생했네요! (61번째 줄, 73번째 줄)
<aside>
💡 아래 사진처럼 VS Code 우측의 빨간 부분을 클릭해서 문제가 발생한 코드 줄을 빠르게 확인할 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/03eae13b-dd60-4d91-9dea-74b5892b86e1/Untitled.png)
61번째 줄에서 마우스를 에러가 난 부분 위에 올려보면, `memoList` 와 `index` 변수를 받아야 하는데 넘겨주지 않아서 생긴 문제라는 것을 확인할 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/36c0f8f5-8e35-4196-880c-2a6cbfbbda46/Untitled.png)
`Quick Fix(Ctrl/Cmd + .)`를 누른 뒤 `Add required argument ‘index’` 와 `Add required argument ‘memoList’` 를 클릭해줍니다.
![Screen Shot 2022-09-12 at 4.16.56 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9d970a5c-3522-41e0-a626-9633f19c70dd/Screen_Shot_2022-09-12_at_4.16.56_PM.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fe8b75c-4a48-4f56-b68a-b37fbcb31588/Untitled.png)
index 뒤 `null` 이 들어간 자리에 `index` 라고 작성해 위에서 만든 `index` 변수를 대입해줍니다.
memoList 뒤에도 마찬가지로 `memoList` 변수를 그대로 대입해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fc0df8e-5ce3-495f-9a38-297be4098a8c/Untitled.png)
`DetailPage` 괄호가 닫히는 부분(위 체크 표시) 에 쉼표를 찍어서 코드 정렬도 맞춰줍시다.
![Screen Shot 2022-09-12 at 4.24.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1026ebcc-e93a-4bb0-a14c-1d9fce463ec5/Screen_Shot_2022-09-12_at_4.24.05_PM.png)
나머지 에러도 마저 해결해보겠습니다.
아래 코드스니펫을 복사해 76번째~79번째 줄을 지우고 붙여넣어줍니다.
- **[코드스니펫] 메모 생성 / floatingActionButton onPressed**
```dart
String memo = ''; // 빈 메모 내용 추가
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoList.indexOf(memo),
memoList: memoList,
),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b3199c26-e897-4f03-bd5f-1ad945c031a3/Untitled.png)
3. 넘겨받은 변수에 담긴 내용을 화면에 표시해줍니다.
`DetailPage` 에서 받은 `memoList` 의 `index` 번째에 있는 메모 내용을 화면에 띄워줘야겠죠!
아래 코드스니펫을 복사해 102번째 줄 맨 뒤에 붙여넣어줍니다.
- **[코드스니펫] commentController 의 text 값 설정**
```dart
contentController.text = memoList[index];
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7f66f71d-fa75-4aca-b5dd-3bd1a57c825e/Untitled.png)
이제 `DetailPage` 에 해당 메모의 내용이 보이는 것을 확인하실 수 있습니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1d60e22-5141-4f8c-8aeb-d15d8e459ebd/Untitled.png)
4) 메모 작성(Create)
우측 하단의 FloatingActionButton
을 클릭할 때마다 앞에서 만들어준 memoList 변수에 빈 메모를 추가 해주겠습니다.
지금은 해당 버튼을 누를 때 아래와 같은 에러가 발생할텐데요.
이는 우리가 새로운 메모를 memoList
에 추가하지 않았기 때문에 memoList.indexOf(memo)
가 -1
을 반환해서 생기는 문제입니다.
아래 코드스니펫을 복사해 76번째 줄 맨 뒤에 붙여넣어주세요.
[코드스니펫] memoList 에 memo 추가
memoList.add(memo);
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d4e62b4f-5996-415b-a2d5-42cd96a3b83b/Untitled.png)
이제 해당 버튼을 누를 때마다 `memoList` 에 빈 메모가 추가되고, `DetailPage` 로 이동하겠죠!
<aside>
💡 생성한 메모의 index 를 `indexOf` 를 통해 찾아줍니다.
`memoList.length - 1` 로 작성해도 같은 결과가 나옵니다
(add 는 배열의 맨 뒤에 요소를 추가하므로)
</aside>
2. 화면 상에 잘 나타나는지 확인해봅시다.
우측 하단의 `FloatingActionButton` 을 누르면 내용이 빈 메모의 `DetailPage`로 잘 넘어갑니다.
다시 `HomePage` 로 돌아와 보세요.
![Screen Shot 2022-09-08 at 2.39.59 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/77174237-2dff-4d4c-beb1-9537d703ac33/Screen_Shot_2022-09-08_at_2.39.59_AM.png)
메모가 새로 생성이 되지 않는군요.
`memoList` 에는 변화가 생겼지만, 화면을 새로 그려주지 않아서 생기는 문제입니다.
(`Hot Reload` 를 한 번 실행해주면 새로 생성된 메모가 보일거에요)
<aside>
💡 이럴 때는 `setState` 를 활용해 변화한 상태(변수)에 맞게 화면을 새로 그려주면 됩니다.
</aside>
아래 코드스니펫을 복사해 77번째 줄을 지우고 붙여넣어주세요. `memoList` 에 `memo` 를 추가하는 코드를 `setState` 로 감싸줬습니다.
- **[코드스니펫] setState**
```dart
setState(() {
memoList.add(memo);
});
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4dce72fc-434e-4d94-8bc1-23332bd18ddb/Untitled.png)
이제 정상적으로 추가된 빈 메모가 화면에 나타나는 것을 볼 수 있습니다.
5) 메모 수정 (Update)
DetailPage
에 있는 TextField
의 onChange
에 들어가는 함수를 수정하면, 텍스트가 변할 때마다 특정한 동작을 할 수 있습니다.
먼저, 우측 하단의 FloatingActionButton
을 클릭하여 DetailPage
로 이동해 주세요.
iOS 에뮬레이터에서 키보드를 보이게 하는 방법
133번째 줄에 `print(value)` 를 입력하고 `TextField` 의 값을 변경해보면
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c20d3969-7f49-452a-8b40-2e683b853962/Untitled.png)
`TextField` 에 내용을 적는대로 콘솔에 프린트 되는 것을 볼 수 있습니다. 이제 `memoList` 에 해당 내용을 추가해주면 되겠네요!
아래 코드스니펫을 복사해 133번째 줄을 지우고 붙여넣어줍니다.
- **[코드스니펫] TextField onChanged / memoList[index] 의 값 설정**
```dart
memoList[index] = value;
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7b32a817-4ae7-49c8-85f4-d0aa474152c5/Untitled.png)
자 그럼 메모를 작성하고 돌아와볼까요? `HomePage` 에 변화가 생겼는지 확인해봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12182fc3-bc81-49f3-a1c8-f8cf8e98b266/Untitled.png)
메모 내용이 변경되지 않는군요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cce61e92-1a2c-4056-b048-8b36d97e7d3c/Untitled.png)
`Hot Reload` 를 실행해야 정상적으로 변경된 메모 내용이 뜨는 것을 확인할 수 있습니다. 이는 HomePage 의 화면이 갱신되지 않았다는 것인데요.
<aside>
💡 기존에 우리는 `StatefulWidget` 의 `setState` 를 활용해 **해당 위젯의 화면**을 갱신할 수 있었습니다. 그러나 위 경우는 `DetailPage` 에서 발생한 변화에 따라 `HomePage` 의 화면을 갱신해줘야 합니다.
</aside>
<aside>
💡 `HomePage` 의 `setState` 함수를 통으로 `DetailPage` 로 넘겨주는 방법도 있지만, 보다 편리하게 코드를 작성하기 위해서는 **상태 관리(State Management)** 라는 것을 사용하게 됩니다.
우선 메모 삭제를 구현한 후, 아래에서 이에 대해 보다 자세히 다뤄보겠습니다.
</aside>
6) 메모 삭제 (Delete)
삭제 버튼 클릭 이벤트는 112번째 줄, DetailPage
의 우상단에 있는 IconButton
위젯의 onPressed
의 함수로 받을 수 있습니다.
먼저 삭제 버튼을 누르는 경우 Dialog
를 띄워보도록 하겠습니다.
코드스니펫을 복사해 113번째 라인 맨 뒤에 붙여넣어주세요.
[코드스니펫] DetailPage / showDialog
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
);
},
);
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f0333d17-7044-46c2-88c2-6db40c8e8496/Untitled.png)
삭제 버튼을 눌러서 `Dialog`를 확인해보세요
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f84a2a93-7d23-4b9e-80d0-6463fdf86384/Untitled.png)
<aside>
💡 Dialog 배경을 클릭하여 끌 수 있습니다.
</aside>
2. `AlertDialog`에 `취소`와 `확인` 버튼을 추가해 보도록 하겠습니다.
코드스니펫을 복사해 118번째 라인 뒤에 붙여 넣어주세요.
- **[코드스니펫] DetailPage / Dialog actions**
```dart
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
```
<aside>
💡 `AlertDialog`의 `actions`는 배열로 원하는 위젯을 넣을 수 있는 파라미터 입니다.
</aside>
<aside>
💡 버튼을 클릭하는 경우 `Navigator.pop(context);`를 호출하여 `Dialog`를 종료할 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/661d9525-c0d1-49f7-a577-a1d7a6d16029/Untitled.png)
![Screen Shot 2022-09-12 at 6.31.28 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/da8304ff-d203-404c-8f18-11931aec5805/Screen_Shot_2022-09-12_at_6.31.28_PM.png)
3. `확인` 버튼 클릭 시 `MemoList`에서 항목을 삭제해 보도록 하겠습니다.
코드스니펫을 복사해 130번째 줄을 지우고 붙여넣어주세요.
- **[코드스니펫] HomePage / delete memo**
```dart
memoList.removeAt(index); // index에 해당하는 항목 삭제
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
```
<aside>
💡 `List.removeAt(index)`를 이용하면 배열에서 index에 해당하는 항목을 삭제할 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ef4c648c-8295-44ae-9585-dd7787622b4f/Untitled.png)
4. 저장 후 다이얼로그를 다시 띄우고 `확인` 버튼을 누르면..
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9da3d5a0-cc5c-49bb-af18-ee65ee35478a/Untitled.png)
이번에도 “메모 수정” 때와 마찬가지로, Hot Reload 를 활용해 화면을 새로고침 해줘야 변경사항이 반영되는 것을 확인하실 수 있습니다.
<aside>
💡 위와 같은 상황에서는 `DetailPage` 에서 발생한 변화에 따라 `HomePage` 의 화면을 갱신해줘야 합니다.
서로 다른 페이지에서 변수를 공유하고, 변수의 값을 바꾸며 여러 페이지의 화면을 한꺼번에 갱신해주기 위해 **상태 관리(State Management)** 라는 것을 배우고 적용해보겠습니다.
</aside>
main.dart
완성 코드
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<String> memoList = ['장보기 목록: 사과, 양파', '새 메모']; // 전체 메모 목록
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
String memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
memoList: memoList,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
String memo = ''; // 빈 메모 내용 추가
setState(() {
memoList.add(memo);
});
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoList.indexOf(memo),
memoList: memoList,
),
),
);
},
),
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.memoList, required this.index});
final List<String> memoList;
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
contentController.text = memoList[index];
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoList.removeAt(index); // index에 해당하는 항목 삭제
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoList[index] = value;
},
),
),
);
}
}
상태 관리(State Management)의 필요성
1) Provider 패키지 추가
코드스니펫을 복사해서 새 탭에서 열어주세요.
https://pub.dev/packages/provider/install
Provider 패키지 Install 탭이 아래와 같이 나오면, flutter pub add provider
명령어 옆에 아이콘을 눌러 복사해 주세요.
View
→ Terminal
을 열어 주세요.
아래와 같이 복사한 코드를 붙여넣고 엔터를 눌러주세요.
그러면 아래와 같이 실행이 됩니다.
pubspec.yaml
파일에 39번째 라인에 아래와 같이 Provider가 있으면 정상 설치 완료입니다.
2) MemoService 만들기
lib
폴더에서 우클릭을 해주신 뒤 New File
을 선택해주세요.
파일 이름은 memo_service.dart
로 짓도록 하겠습니다.
코드스니펫을 복사해 memo_service.dart
파일에 붙여 넣어주세요.
[코드스니펫] MemoService / init
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
});
String content;
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
}
<aside>
💡 **MemoService**의 `memoList`가 변경되는 경우 해당 값을 보여주는 화면들을 갱신시켜 주는 기능을 구현하기 위해 `ChangeNotifier` 클래스의 기능을 물려 받았습니다.
`ChangeNotifier`를 상속 받은 경우 `notifylisteners();`를 호출하여 위젯들을 갱신하는 기능을 사용할 수 있습니다. 자세한 사용법은 뒤쪽에서 알아보도록 하겠습니다.
</aside>
<aside>
💡 앞으로 `Memo` 관련 데이터는 모두 **MemoService** 에서 담당할 예정이므로, 이전에 HomePage에서 구현했던 `memoList` 배열을 **MemoService** 에 추가했습니다.
17, 18번째 `Memo`는 바로 테스트를 진행할 수 있도록 넣은 가짜(dummy) 데이터입니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/831f2d05-82e5-4307-a4ff-095cf682151a/Untitled.png)
3) Provider 패키지 설정
코드스니펫을 복사해 main.dart
파일의 5번째 라인을 지우고 붙여넣어주세요.
[코드스니펫] Provider init
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
<aside>
💡 `MultiProvider`로 `MyApp`을 감싸서 모든 위젯들의 최상단에 `provider`들을 등록해줍니다. `MultiProvider`는 위젯트리 꼭대기에 여러 `Service` 들을 등록할 수 있도록 만들 때 사용합니다.
</aside>
<aside>
💡 **MemoService**는 `memoList` 값이 변하는 경우 `HomePage`에 변경사항을 알려주도록 구현해야 하므로 `ChangeNotifierProvider`로 **Provider**에 등록해줍니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/33619a81-8d46-402f-ac51-c5fe41edc020/Untitled.png)
2. **Provider**와 **MemoService**가 `Import` 되지 않아 에러가 발생합니다.
6번째 라인에 `MultiProvider`를 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 눌르고 `Import 'package:provider/provider.dart';`를 선택해주세요.
<aside>
💡 만약 `import 'package:provider/provider.dart';`가 안보이신다면 `pubspec.yaml` 파일을 열고 저장 단축키를 눌러주세요.
저장 단축키
window : `Ctrl + S`
macOS : `Cmd + S`
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8580c991-b81d-4001-90d3-7e21e45d081e/Untitled.png)
3. 마찬가지로 9번째 라인 **MemoService**를 클릭하신 뒤 `import 'memo_service.dart';`를 선택해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0135ea61-101f-4f4d-8de6-305f851db553/Untitled.png)
**Provider** 설정이 완료 되었습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b44f89cb-ce78-4833-b8f8-cb8ee4ef104f/Untitled.png)
4. `Restart`를 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c26d3381-5b05-433e-8429-a25e46733820/Untitled.png)
이제 `Provider` 를 사용할 준비가 되었습니다!
1) 메모 목록 조회(Read)
HomePage 전체 위젯을 Consumer
로 감싸주겠습니다. main.dart
파일에 43번째 라인에 Scaffold
를 우클릭하여 Refactor(Ctrl + Shift + R)
을 선택하고, Wrap with Builder
를 선택해주세요.
아래와 같이 Scaffold
위젯을 Builder
위젯이 감싸집니다.
Consumer
위젯과 형식이 비슷한 Builder
를 사용해 형식을 잡아줬습니다.
아래 이미지와 같이 43번째 라인에 Builder
를 Consumer<MemoService>
로 변경해주세요.
에러를 해결하겠습니다.
아래 이미지와 같이 ,memoService, child
를 43번째 라인의 context 뒤에 추가해주세요.
그리고 줄 정렬을 예쁘게 해주기 위해 103번째 라인의 중괄호와 소괄호 사이에 콤마(,
)를 찍은 뒤 저장해주세요.
저장하면 코드가 정렬됩니다.
코드스니펫을 복사해 44번째 라인 뒤에 붙여넣어 주세요.
[코드스니펫] MemoService에서 memoList 가져오기
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f52d5fa9-d3bf-4749-97c2-84112f9b6c87/Untitled.png)
5. `Provider` 를 사용해 모든 `Memo` 데이터는 `MemoService` 에서 관리하기로 했습니다.
39번째 줄에 있는 `memoList` 는 더이상 사용하지 않으므로 지워줍니다.
지우고 난 후 코드는 아래와 같습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a4375405-ca68-4ff3-95cd-40ff0df47381/Untitled.png)
6. 에러들을 해결하겠습니다.
<aside>
💡 에러를 모두 해결하고 나면 자연스럽게 **메모 목록 조회(Read)** 기능이 구현되어 있을 겁니다!
</aside>
이전에는 `String` 자료형의 메모 데이터를 사용했다면, 이번엔 `memo_service.dart` 에서 `Memo` 라는 클래스로 데이터의 틀을 잡고, 각각의 메모 데이터를 객체의 형태로 만들어 활용한다는 점을 유의해주세요.
이렇게 데이터의 형식(자료형)이 바뀌었기 때문에 코드상에 에러가 발생합니다. 55번째 줄의 에러 위에 마우스를 올려봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ff555c78-1f86-42c0-a36b-f27a37fc52ee/Untitled.png)
`String` 자료형의 변수인 `memo` 에 `Memo` 자료형의 값이 들어갈 수 없다는 뜻이군요!
Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `‘Change ‘String’ to ‘Memo’ type annotation` 을 눌러 변수의 타입(자료형)을 바꿔줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d556b38b-9f54-4db7-ae9f-7556f474eba9/Untitled.png)
코드가 아래와 같이 바뀌면서 에러가 사라지는 것을 볼 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5978d022-8b8e-4df5-81d2-81cd5152713a/Untitled.png)
66번째 줄의 `memo` 를 `memo.content` 로 바꿔줍니다.
<aside>
💡 `Text` 위젯은 `String` 자료형의 값을 받아서 보여주는 위젯입니다. 메모의 내용인 `content`를 넘겨줘야겠죠.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b2e67e54-d29e-44e2-b7a1-c343738c3f4b/Untitled.png)
72-80 번째 줄을 지워줍니다. **메모 개별 조회 (Read)** 기능은 아래에서 다시 구현하겠습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6b4bba1-52df-44af-81f8-cefb933930d5/Untitled.png)
80-92 번째 줄을 지워줍니다. **메모 생성 (Create)** 기능도 아래에서 새로 구현하겠습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ecce9a27-36f2-4061-b729-56fb45871976/Untitled.png)
7. `Restart`를 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c26d3381-5b05-433e-8429-a25e46733820/Untitled.png)
지금 화면에 보이는 메모 목록은 우리가 생성해준 더미 데이터들입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0c74725c-5ac3-41d4-89a7-cb243aa72de8/Untitled.png)
`Provider` 를 활용한 **메모 목록 조회 기능(Read)** 을 구현했습니다!
2) 메모 개별 조회 (Read)
이제 메모 데이터를 MemoService
에서 관리하니, DetailPage
가 memoList
를 따로 가지고 있을 필요가 없겠죠.
아래 코드스니펫을 복사해 main.dart
90-92번째 라인을 모두 지우고 붙여넣습니다.
[코드스니펫] DetailPage / 파라미터
DetailPage({super.key, required this.index});
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd990739-5a05-4fbe-bdf9-42dff52aabaf/Untitled.png)
2. `memoService` 에서 `memoList` 를 가져와 `index` 번째에 있는 메모를 뽑아보겠습니다.
코드스니펫을 복사해 `main.dart` 98번째 라인을 지우고 붙여넣어주세요.
- **[코드스니펫] DetailPage / context.read 로 memo 1개 조회**
```dart
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/87501087-8236-4773-b4ac-87f54e46141b/Untitled.png)
<aside>
💡 `context.read<클래스명>();`를 이용하면 위젯 트리 상단에 있는 Provider로 등록한 클래스에 접근할 수 있습니다.
변화에 따라 화면을 새로 그려줄 필요가 있을 때는 `Consumer` 를 사용하며, 화면을 새로고침할 필요없이 MemoService 의 변수나 함수만 이용하고자 한다면 `context.read<클래스명>()` 를 사용해도 됩니다.
</aside>
3. `main.dart` 125번째 라인, 157번째 라인에 있는 에러를 각각 없애보겠습니다.
아래에서 메모의 수정 및 삭제를 따로 구현할 것이기 때문에 지금은 코드를 모두 주석처리 해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b82a2e83-02c0-4217-bd24-6200214e89bd/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/733b6f3d-b91d-42b2-b0b4-61e530c90434/Untitled.png)
4. `ListTile` 을 클릭시 `DetailPage` 로 이동하도록 하겠습니다.
`main.dart` 의 71번째 라인 뒤에 아래 코드스니펫을 복사해 붙여넣어주세요.
- **[코드스니펫] HomePage 에서 DetailPage 로 이동하는 Navigator**
```dart
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aabfe72e-6353-4771-b545-54fd4db6ee19/Untitled.png)
**메모 개별 조회(Read)** 기능이 구현되었습니다. `ListTile` 을 눌러 상세페이지로 이동해보세요!
![Screen Shot 2022-09-13 at 4.41.46 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/20af2c23-98bd-4290-ba7b-55f93ea7be28/Screen_Shot_2022-09-13_at_4.41.46_AM.png)
3) 메모 작성 (Create)
먼저 코드스니펫을 복사해 MemoService
에 Memo
를 추가하는 함수를 구현해봅시다. memo_service.dart
의 19번째 라인 맨 뒤에 붙여넣습니다.
[코드스니펫] MemoService / createMemo
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
}
<aside>
💡 **MemoService**에 `memoList`에 대한 CRUD 기능을 담당하는 함수를 추가할 예정입니다. 그 중 Create를 담당하는 `createMemo` 함수를 생성하였습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c584b852-baa2-4db3-84a2-cfb725acc092/Untitled.png)
<aside>
💡 `String` 자료형의 `content` 변수를 받아 `Memo`를 생성하고, 이를 `memoList` 맨 뒤에 추가하는 로직을 작성했습니다.
</aside>
2. `HomePage` 에서 `FloatingActionButton` 을 누를 때, 방금 만든 `createMemo` 함수를 호출해 내용이 비어있는 메모가 추가되도록 하겠습니다.
아래 코드스니펫을 복사해 `main.dart` 의 87번째 라인 맨 뒤에 붙여넣습니다
- **[코드스니펫] HomePage / createMemo 호출**
```dart
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b7306c0c-3194-4004-97c0-3f562b5b79cb/Untitled.png)
<aside>
💡 `memoList` 의 맨 뒤에 memo가 추가되었으므로, `memoList.length - 1` 의 인덱스로 해당 memo에 접근합니다.
</aside>
3. 저장한 뒤 에뮬레이터에서 `FloatingActionButton` 를 눌러 메모를 추가해봅시다.
<aside>
💡 메모를 추가했지만 `HomePage` 에는 아무런 변화가 없습니다.
`Hot Reload` 를 해보면 비로소 메모가 추가되는 것을 확인할 수 있는데요. 이는 `memoList` 에 memo 가 추가되었지만 화면의 갱신이 일어나지 않았다는 것을 의미합니다.
</aside>
4. 이를 해결하기 위해 코드스니펫을 복사해 **MemoService**에 23번째 라인 맨 뒤에 붙여 넣어주세요.
- **[코드스니펫] notifyListeners**
```dart
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/66f3a459-46a5-42d7-b186-247a49975361/Untitled.png)
<aside>
💡 **MemoService**에서 `notifyListeners();`를 호출해야 `Consumer<MemoService>`의 `builder` 에 있는 함수를 호출해 화면이 갱신됩니다.
</aside>
이제 에뮬레이터에서 `FloatingActionButton` 를 누를 때마다 `HomePage` 에 빈 메모가 추가되는 것을 확인할 수 있습니다.
4) 메모 수정 (Update)
먼저 MemoService
에 메모를 수정하는 로직을 추가하겠습니다. 아래 코드스니펫을 복사해 memo_service.dart
의 25번째 라인 맨 뒤에 붙여넣습니다
[코드스니펫] MemoService / updateMemo
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
}
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b88fd71-7b53-40d9-a126-b25730ecb281/Untitled.png)
<aside>
💡 `index` 와 `content` 변수를 받아 `memoList` 의 index 번째 memo 의 `content` 를 바꿔주는 로직을 작성했습니다.
또, `notifyListeners()` 를 호출해 `Consumer<MemoService>` 의 `builder` 부분에 있는 위젯을 다시 그려줍니다.
</aside>
2. `DetailPage` 에서 `TextField` 에 텍스트를 입력할 때마다 메모 수정이 일어나도록 하겠습니다.
아래 코드스니펫을 복사해 `main.dart` 의 174번째 라인을 지우고 붙여넣습니다
- **[코드스니펫] DetailPage / TextField onChange**
```dart
memoService.updateMemo(index: index, content: value);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1debffca-b607-4396-b4ca-4fc9af942580/Untitled.png)
<aside>
💡 `TextField` 의 값이 변할 때, 즉 타이핑할때마다 `memoService` 의 `updateMemo` 를 호출하도록 했습니다.
</aside>
3. `DetailPage` 에서 메모를 작성하고 `HomePage` 로 돌아오면 변경 내역이 잘 반영되어있는 것을 확인할 수 있습니다.
5) 메모 삭제 (Delete)
먼저 MemoService
에 메모를 삭제하는 로직을 추가하겠습니다. 아래 코드스니펫을 복사해 memo_service.dart
의 31번째 라인 맨 뒤에 붙여넣습니다.
[코드스니펫] MemoService / deleteMemo
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
}
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/532b94be-5756-4b30-89ab-b22b4be58437/Untitled.png)
<aside>
💡 `index` 변수를 받아 `memoList` 의 index 번째 memo 를 삭제하는 로직을 작성했습니다.
또, `notifyListeners()` 를 호출해 `Consumer<MemoService>` 의 `builder` 부분에 있는 위젯을 다시 그려줍니다.
</aside>
2. `DetailPage` 에서 삭제 버튼을 누르면 뜨는 `Dialog` 가 뜨고, 여기서 `확인`을 누르면 삭제가 일어나도록 해봅시다. 아래 코드스니펫을 복사해 `main.dart` 의 142번째 라인을 지우고 붙여넣습니다
- **[코드스니펫] DetailPage / TextButton onPressed**
```dart
memoService.deleteMemo(index: index);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6ec414aa-b3a9-4cbb-97b9-14f85dbcca75/Untitled.png)
<aside>
💡 삭제 버튼을 누르면 뜨는 `Dialog` 가 뜨고, 여기서 `확인`을 누르면 삭제가 일어나며, 뒤로가기를 두번 (Dialog 닫기 → DetailPage 닫고 HomePage 로 이동) 실행합니다.
</aside>
3. `DetailPage` 에서 삭제 버튼을 눌러 구현이 잘 되었는지 확인합니다.
6) Provider 사용법 정리
전역적으로 사용되는 데이터를 담당할 서비스로 만들고, 해당 데이터에 대한 CRUD를 모두 해당 서비스에서 구현합니다.
class MemoService extends ChangeNotifier {
...
void deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners(); // Consumer의 builder 함수를 호출하여 화면 갱신
}
}
Provider 패키지를 이용하여 최상단 위젯 서비스를 등록해 줍니다.
위젯트리 꼭대기에 있는 Provider로 등록한 클래스에 접근 방법
Consumer<클래스명>
: 클래스 정보 갱신시 함께 새로고침 할 때 사용
context.read<클래스명>()
: 화면을 새로고침할 필요 없이, 일회성으로 서비스의 변수나 함수에 접근하고 싶은 경우에 사용
7) 리팩토링
showDeleteDialog
메소드(함수)로 분리하기
126번째 줄에 showDialog
를 클릭한 뒤 왼쪽에 전구(💡) 아이콘을 선택하고, Extract Method
를 선택해 주세요.
메소드(함수)의 이름을 입력하라고 뜨면 showDeleteDialog
라고 입력한 뒤 엔터를 누르고 저장(Ctrl/Cmd + S
)를 눌러주세요.
그러면 아래와 같이 126번째 줄이 생성된 함수를 호출하도록 바뀝니다.
그리고 153번째 라인으로 내려가보면 showDeleteDialog
라는 함수가 생성되어 있는 것을 볼 수 있습니다.
153번째 줄을 보면 함수의 반환 타입이 Future<dynamic>
이라고 되어있는데, 이 부분을 void
로 변경하고, 155번째 줄에 있는 return
을 삭제해주세요.
저장한 뒤, 메모를 하나 삭제해보면 정상적으로 작동하는 것을 확인할 수 있습니다.
최종 코드
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'memo_service.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
memo_service.dart
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
});
String content;
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
}
}
shared_preferences 패키지를 사용하는 이유
1) shared_preferences 패키지 설치
패키지를 설치해 보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.
[코드스니펫] pub.dev shared_preferences
https://pub.dev/packages/shared_preferences/install
`flutter pub add shared_preferences` 우측에 아이콘을 눌러 복사해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c859621-73d6-42cb-86eb-99d54bfeac18/Untitled.png)
2. `View` → `Terminal`을 열고 복사한 내용을 붙여 넣은 뒤 엔터를 눌러 실행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9ad5c331-063e-4fce-81d8-0148093ce7b1/Untitled.png)
`pubspec.yaml`파일을 열어서 40번째 라인에 `shared_preferences`를 확인해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4342608-4cf6-4955-a4c6-c7dd417fc167/Untitled.png)
3. 패키지 설치가 끝나셨다면 우측 상단의 정지 버튼을 눌러 앱을 끈 후
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed82c5f1-f39f-48a9-a11f-5ea54ca69fa5/Untitled.png)
`Run without debugging` 을 눌러 앱을 다시 설치해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c40bd256-eaf2-48ff-ba16-2f51c5da6701/Untitled.png)
앱이 재설치가 된 후에 `shared_preferences` 를 사용할 수 있습니다.
2) shared_preferences 사용법
사용 준비SharedPreferences
를 이용하려면 먼저 SharedPreferences
인스턴스를 가져와야합니다.
// 인스턴스 생성
SharedPreferences prefs = await SharedPreferences.getInstance();
값 저장하기
SharedPreferences는 데이터를 Key와 Value로 구성된 Map 형태로 데이터를 저장합니다. 저장시 사용되는 Key
는 원하는 이름으로 정하시면 됩니다.
prefs.setString("username", "John Doe");
값 불러오기
저장할 때 사용한 username
이라는 Key
를 이용해 다시 값을 가져올 수 있습니다. 저장된 값이 없는 경우 null
을 반환하는 점 유의해 주세요.
String? value = prefs.getString("username");
3) 기기에 memoList
데이터 저장하기
먼저 전역으로 prefs
라는 변수를 선언해 코드 어디서든 SharedPreferences
객체를 사용할 수 있도록 합니다. 아래 코드스니펫을 복사해 main.dart
의 7번째 줄을 지우고 붙여넣어주세요.
[코드스니펫] 전역 변수 prefs 선언 / main 함수 수정
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/67734020-ec2c-41f2-990d-419ef1f0cc28/Untitled.png)
7번째 라인에 `SharedPreferences`을 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library…` 를 선택해주세요. `shared_preferences` 패키지를 import 하지 않아 발생하는 문제입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fb816b4-9425-4df6-828b-a8f9e60b5305/Untitled.png)
아래와 같이 import 가 되면 에러가 해결됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a31272db-f17e-48b6-abc0-e2e027d5851b/Untitled.png)
2. `Memo` 객체를 `Map` 자료형으로 바꿔주는 함수 **toJson**, `Map` 자료형에서 다시 `Memo` 객체를 복원하는 함수 **fromJson** 을 `Memo` 클래스 내에 작성해줍니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 11번째 라인 맨 마지막에 붙여넣어주세요.
- **[코드스니펫] Memo toJson, fromJson**
```dart
Map toJson() {
return {'content': content};
}
factory Memo.fromJson(json) {
return Memo(content: json['content']);
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c8132838-be7a-42de-8395-f5e01ff2cbfc/Untitled.png)
3. **MemoService** 에 `memoList` 를 기기에 저장하는 함수 **saveMemoList** 와, 저장된 `memoList` 를 다시 복원하는 함수 **loadMemoList** 를 추가합니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 44번째 라인 뒤에 붙여넣어주세요.
- **[코드스니펫] saveMemoList / loadMemoList**
```dart
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e500d5a7-5c03-45c9-a223-9dcaae341ef9/Untitled.png)
50번째 라인에 에러가 발생한 부분에서 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library 'dart:convert'` 를 눌러 에러를 해결해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c9fe2dbe-b948-4bca-bc8c-edfbf1af9b8b/Untitled.png)
<aside>
💡 각 과정을 거치며 데이터가 어떻게 변하는지 주석으로 작성해뒀습니다. 대략적인 흐름은 아래와 같습니다.
1) `saveMemoList`
`**List<Memo>**` ⇒ (toJson) ⇒ `**List<Map>**` ⇒ (jsonEncode) ⇒ **`String`**
2) `loadMemoList`
`**String**` ⇒ (jsonDecode) ⇒ `**List<Map>**` ⇒ (fromJson) ⇒ `**List<Memo>**`
</aside>
4. 메모를 생성, 변경, 삭제할 때마다 메모를 저장하도록 합니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 `MemoService` 클래스 함수들에 있는 `notifyListeners()` 뒤에 **모두** 붙여넣어줍니다.
- **[코드스니펫] memoList 기기에 저장**
```dart
saveMemoList();
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/49bda047-bee9-4cd3-9919-c6d3e7e98da2/Untitled.png)
5. **MemoService** 를 시작할 때 저장된 메모를 불러와서 `memoList` 변수에 담아주도록 생성자를 수정해줍니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 25번째 줄 맨 뒤에 붙여넣어주세요.
- **[코드스니펫] memoList 불러오기**
```dart
MemoService() {
loadMemoList();
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d0034163-f49b-4ab4-9769-27c1c4a932ba/Untitled.png)
6. 자 이제 메모를 생성하고, 우측 상단의 `Restart` 버튼을 눌러주세요. 앱을 재부팅하는 것과 동일한 동작입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3804f24-5072-4989-9dae-d34b77c9b50d/Untitled.png)
메모가 삭제되지 않고 잘 남아있는 것을 확인할 수 있습니다. 이는 메모 데이터가 `shared_preferences` 를 통해 기기에 저장되고, 불러와지기 때문입니다.
최종 코드
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
memo_service.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
});
String content;
Map toJson() {
return {'content': content};
}
factory Memo.fromJson(json) {
return Memo(content: json['content']);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
1) 메모 핀 기능 구현
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(memo.isPinned
? CupertinoIcons.pin_fill
: CupertinoIcons.pin),
onPressed: () {
memoService.updatePinMemo(index: index);
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
memo_service.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
this.isPinned = false,
});
String content;
bool isPinned;
Map toJson() {
return {
'content': content,
'isPinned': isPinned,
};
}
factory Memo.fromJson(json) {
return Memo(
content: json['content'],
isPinned: json['isPinned'] ?? false,
);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
saveMemoList();
}
updatePinMemo({required int index}) {
Memo memo = memoList[index];
memo.isPinned = !memo.isPinned;
memoList = [
...memoList.where((element) => element.isPinned),
...memoList.where((element) => !element.isPinned)
];
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
2) 메모 수정 시간 저장
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(memo.isPinned
? CupertinoIcons.pin_fill
: CupertinoIcons.pin),
onPressed: () {
memoService.updatePinMemo(index: index);
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
trailing: Text(memo.updatedAt == null
? ""
: memo.updatedAt.toString().substring(0, 19)),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
memo_service.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
this.isPinned = false,
this.updatedAt,
});
String content;
bool isPinned;
DateTime? updatedAt;
Map toJson() {
return {
'content': content,
'isPinned': isPinned,
'updatedAt': updatedAt?.toIso8601String(),
};
}
factory Memo.fromJson(json) {
return Memo(
content: json['content'],
isPinned: json['isPinned'] ?? false,
updatedAt:
json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt']),
);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content, updatedAt: DateTime.now());
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
memo.updatedAt = DateTime.now();
notifyListeners();
saveMemoList();
}
updatePinMemo({required int index}) {
Memo memo = memoList[index];
memo.isPinned = !memo.isPinned;
memoList = [
...memoList.where((element) => element.isPinned),
...memoList.where((element) => !element.isPinned)
];
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
3) 빈 메모 삭제
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(memo.isPinned
? CupertinoIcons.pin_fill
: CupertinoIcons.pin),
onPressed: () {
memoService.updatePinMemo(index: index);
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
trailing: Text(memo.updatedAt == null
? ""
: memo.updatedAt.toString().substring(0, 19)),
onTap: () async {
// 아이템 클릭시
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
if (memo.content.isEmpty) {
memoService.deleteMemo(index: index);
}
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
if (memoList[memoService.memoList.length - 1].content.isEmpty) {
memoService.deleteMemo(index: memoList.length - 1);
}
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
memo_service.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
this.isPinned = false,
this.updatedAt,
});
String content;
bool isPinned;
DateTime? updatedAt;
Map toJson() {
return {
'content': content,
'isPinned': isPinned,
'updatedAt': updatedAt?.toIso8601String(),
};
}
factory Memo.fromJson(json) {
return Memo(
content: json['content'],
isPinned: json['isPinned'] ?? false,
updatedAt:
json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt']),
);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content, updatedAt: DateTime.now());
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
memo.updatedAt = DateTime.now();
notifyListeners();
saveMemoList();
}
updatePinMemo({required int index}) {
Memo memo = memoList[index];
memo.isPinned = !memo.isPinned;
memoList = [
...memoList.where((element) => element.isPinned),
...memoList.where((element) => !element.isPinned)
];
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
PDF 파일
단축키 모음
F5
Ctrl
+ S
command
+ S
Ctrl
+ A
command
+ A
Ctrl
+ X
command
+ X
shift
+ enter
Ctrl
+ Alt
+ L
option
+ command
+ L
Tab
Shift
+ Tab
Ctrl
+ /
command
+ /
[수업 목표]
[목차]
1) StatelessWidget
StatelessWidget 생김새
extends StatelessWidget
: StatelessWidget의 기능을 물려받습니다.생성자
: 클래스 이름과 동일한 함수build 함수
: 화면에 보여줄 자식 위젯을 반환코드스니펫을 복사한 뒤 주소창에 붙여 넣어, DartPad로 접속하여 StatelessWidget을 배워봅시다.
[코드스니펫] DartPad StatelessWidget 학습
https://dartpad.dev/?id=f8a7bf195ec729c400634d97b10f0f84
<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
StatefulWidget 생김새
기본적으로 2개의 클래스로 구성되어 있습니다.
MyApp
: StatefulWidget의 기능을 물려받은 클래스
_MyAppState
: MyApp
상태를 가진 클래스(실질적인 내용은 여기에 들어가요!)
화면을 그리는 build 함수
는 상태 클래스 (_MyAppState
) 에 있습니다.
코드스니펫을 복사한 뒤 주소창에 붙여 넣어, DartPad로 접속하여 StatefulWidget을 배워봅시다.
[코드스니펫] DartPad StatefulWidget 학습
https://dartpad.dev/?id=513188d8ad8004aaf524ce52d668d84c
<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로 접속합니다.
https://dartpad.dev/?id=185dbad13d31ea8b99d7f83fe1f8ec3a
![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>
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
![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) 에뮬레이터 실행하기
VSCode 우측 하단에 Chrome (web-javascript)
를 클릭해주세요.
Start Pixel 2 API 29 mobile emulator
를 선택해주세요.
잠시 기다리면 에뮬레이터가 실행됩니다.
VSCode 우측 상단에 아래 화살표를 눌러 Run Without Debugging
을 눌러주세요.
에뮬레이터에 아래와 같이 흰 화면이 나오면 정상적으로 실행이 완료된 것입니다!
[Android]
[iOS]
![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 만들기
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")),
);
![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의 영역에 대한 명칭은 아래와 같습니다.
우리는 아래와 같이 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),
),
],
),
![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
만들기 - 레이아웃
레이아웃 나누기
62번째 줄에 Center
를 클릭한 뒤 마우스 우클릭
→ Refactor
를 선택합니다.
Wrap with Row
을 선택합니다.
Center
위젯이 아래와 같이 Row
위젯으로 감싸집니다.
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
설정하기
이제 이미지와 텍스트의 정렬을 맞춰주면 되겠군요.
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,
![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,
),
);
}
}
최종 파일 구조
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`해 주세요.
![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),
),
],
),
)
],
),
],
),
),
],
);
}
}
최종 모습
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>
![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) 피드 리스트 만들기
ListView 위젯
출처 - dribbble
DartPad에서 ListView 위젯을 사용해 보도록 하겠습니다. 코드스니펫을 복사한 뒤 주소창에 붙여 넣어주세요.
https://dartpad.dev/?id=5aa6067a9ea6f34a6beca110cb882117
![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),
),
],
),
)
],
),
],
),
),
],
);
}
}
최종 모습
1) Feed 위젯 수정하기
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) 데이터 전달하기
화면에 보여줄 데이터를 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),
),
],
),
)
],
),
],
),
),
],
);
}
}
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
![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.
PDF 파일
단축키 모음
F5
Ctrl
+ S
command
+ S
Ctrl
+ A
command
+ A
Ctrl
+ X
command
+ X
shift
+ enter
Ctrl
+ Alt
+ L
option
+ command
+ L
Tab
Shift
+ Tab
Ctrl
+ /
command
+ /
[수업 목표]
[목차]
1) 앱 개발 방법
네이티브 앱(Native App)
Android
→ Google에서 제공하는 Android SDK(Software Development Kit)를 이용하여 개발
Java / Kotlin
Android Studio
iOS
→ Apple에서 제공하는 iOS SDK(Software Development Kit)를 이용하여 개발
Objective-C / Swift
XCode
크로스 플랫폼 앱(Cross Platform App)
React Native
JavaScript
Flutter
Dart
2) 왜 크로스 플랫폼을 사용할까요?
🛠 생산성이 월등하다.
3) 왜 Flutter를 사용할까요?
🔥 Flutter가 훨씬 핫하다 (React Native 대비. 커뮤니티 & 자료 ⬆)
→ 개발을 할 때 훨씬 편하게 찾아볼 수 있다는 뜻입니다 :)
출처 - getstream.io
📚 공식 문서가 잘 되어 있다.
👍 성능이 뛰어나다
출처 - Flutter vs Native vs React-Native: Examining performance
- [잠깐] 혹시 Windows 7를 쓰신다면 아래 가이드를 따라주세요 :)
- [링크](https://www.microsoft.com/ko-kr/p/powershell/9mz1snwt0n5d?activetab=pivot:overviewtab)에 접속하셔서 Power Shell을 설치해주세요.
**[프레임워크 설치하기]**
- 1. Flutter 설치하기
<aside>
💡 Flutter 설치 과정은 일반적인 프로그램 설치와는 조금 다릅니다. 일반적으로는 `.exe` 파일등을 실행해 프로그램을 설치하는데요.
Flutter 설치는 아래와 같은 과정으로 진행됩니다.
1. zip 파일을 다운로드 받아서 적절한 경로에 압축 해제
2. 압축 해제한 flutter 폴더 경로를 환경변수에 등록
(flutter 라는 명령어가 이 경로에 있다는 것을 윈도에게 알리는 것과 같습니다)
</aside>
- 1) 다운로드
1. 먼저 flutter 를 설치할 폴더를 만들겠습니다.
아래 사진에서 밑줄친 부분을 폴더 경로(주소)라고 하는데요.
여기에 **한글이나 공백이 들어가면** 인식을 못하고 에러가 발생합니다.
![Screenshot 2022-09-20 024333.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9b6779d0-a45f-40b8-b848-542e143236be/Screenshot_2022-09-20_024333.png)
에러를 막기 위해 `C 드라이브` 바로 아래에 `src` 라는 이름의 폴더를 생성해줍니다.
![Screenshot 2022-09-20 024147.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2090724a-a484-4960-8c32-5fa8f3e49c3b/Screenshot_2022-09-20_024147.png)
2. flutter 를 다운로드할 수 있는 [링크](https://docs.flutter.dev/get-started/install/windows)로 이동합니다.
[Windows install](https://docs.flutter.dev/get-started/install/windows)
3. 파란색 버튼(zip 파일 다운로드 링크)을 클릭합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/962ad32c-bfeb-410e-bb13-a8117bebdc90/Untitled.png)
<aside>
💡 아래 경고 메시지를 꼭 확인해주세요
1. 특수문자(한글 포함)나 공백이 설치 경로에 포함되면 안된다
2. `C:\Program Files\` 밑에는 설치하면 안된다 (상위 권한 필요)
</aside>
4. 아까 만들어준 `C:\src` 폴더에 해당 zip 파일을 다운로드 해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/22a72d03-2d30-4090-94e7-03b78ed87e80/Untitled.png)
<aside>
💡 zip 파일은 어디에 다운로드하셔도 상관 없습니다.
대신 압축을 풀어줄 때 만큼은 꼭 `C:\src` 폴더에 풀어주세요
</aside>
5. 압축을 해제해주세요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e70291ca-afad-49c1-b4ee-368d5f797f21/Untitled.png)
누르면 아래와 같이 압축 해제 대상 폴더를 지정하는 화면이 나옵니다.
여기서 `flutter_windows_…` 이 부분을 지워주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/210a3788-ccb5-4728-b9ca-5823bec0fadd/Untitled.png)
지우고 난 경로는 아래와 같습니다. 마저 압축 해제를 눌러줄까요?
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1bf07a7d-1a97-421a-837d-13248cf836e4/Untitled.png)
압축을 모두 해제하는 데 10-20분 정도 걸립니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/187db94f-296d-4810-ab42-b87792dc7d1c/Untitled.png)
압축 해제가 끝나고 나면 아래와 같이 flutter 라는 폴더가 생성되어 있을 것입니다.
(위에서 경로를 잘 지정해줬다면요!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4af3a34-0d85-4dcc-be30-8c988e71a46c/Untitled.png)
<aside>
💡 간혹가다 위에서 경로 설정을 실수해 `flutter_windows_…` 이런 이름으로 해제된 경우가 있습니다.
이 때는, 해당 폴더를 클릭해서 들어가면 비로소 `flutter` 라는 이름의 폴더를 보실 수 있습니다. 해당 `flutter` 폴더를 위 사진과 같이 밖으로 빼주세요!
(아래 환경 변수 설정을 원활히 하기 위함입니다)
</aside>
- 2) 환경 변수 설정
<aside>
💡 다운로드한 Flutter를 어디서든지 사용 가능하도록 설정하는 절차입니다.
</aside>
1. 압축이 해제된 `flutter` 폴더로 들어가 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4833238e-f729-4f8c-9071-961a744110e9/Untitled.png)
2. `bin` 폴더로 들어가주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/160d2074-5c6c-447b-96b5-6f7b6a7b24b2/Untitled.png)
3. 들어가시면 `dart`, `dart.bat`, `flutter`, `flutter.bat` 과 같은 파일들이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c811e337-c8c9-4e33-afa4-789a2b915b6e/Untitled.png)
<aside>
💡 이들이 바로 flutter 실행 파일입니다. cmd 로 이 폴더를 열고 `flutter` 라고 치면, 여기 있는 파일이 실행됩니다.
지금은 플러터가 설치된 폴더 에서만 `flutter` 라는 명령어를 실행할 수 있습니다.
우리는 이 flutter 실행 파일을 **컴퓨터 어디에서도 모두 접근 가능하게끔**, 즉 어디에서도 `flutter` 라는 명령어를 사용할 수 있게끔 해야 합니다.
환경 변수에 flutter 가 위치한 **폴더 경로를 추가**하면, 시스템의 모든 경로에서 이 flutter 파일에 접근하고, 실행할 수 있습니다.
</aside>
4. 이를 위해 우선 해당 폴더 경로를 복사하겠습니다. 위 과정을 잘 따라오셨다면 경로는 아래와 같습니다.
```
C:\src\flutter\bin
```
선택된 경로를 복사해주세요. (`Ctrl + C`)
![Screenshot 2022-09-20 033732.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3be69e5a-d3cc-4dd2-ac50-0a6337570189/Screenshot_2022-09-20_033732.png)
5. 윈도우에 `환경 변수` 라고 검색합니다. (띄어쓰기도 하셔야 해요!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8ec7c12c-472c-495c-894c-a1053c69ad96/Untitled.png)
6. 아래와 같은 창에서 `환경 변수` 버튼을 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4b070e0b-98ea-458e-a852-0eb81fe4d496/Untitled.png)
7. `사용자 변수`에 `path`를 선택한 뒤, `편집` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/85218bab-1214-48fd-be84-88664017d379/Untitled.png)
8. `새로 만들기(N)`을 선택하신 뒤 복사한 주소를 붙여 넣고 `확인` 버튼을 눌러주세요.
![Screenshot 2022-09-20 033954.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b5246c68-425b-4d7b-924c-d03b9c6a09df/Screenshot_2022-09-20_033954.png)
9. 켜져있는 창들을 모두 `확인` 버튼을 눌러 닫아주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c2ab250-9830-4b25-b116-7dbd8c6c3757/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/71c100c4-a2f5-4776-87df-d9cb79a55dc2/Untitled.png)
- 3) 설치 확인
1. `cmd`를 검색해서 `명령 프롬프트`를 실행해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/383052bb-12ac-468a-8044-7f663e4eaac2/Untitled.png)
2. 명령 프롬프트에 `flutter --version` 이라고 검색했을 때, 아래와 같은 문구가 뜨면 설치 완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/37002ed1-e60e-4265-a98f-1da627f8faf7/Untitled.png)
<aside>
💡 이제 시스템의 모든 경로에서 `flutter` 라는 명령어를 사용할 수 있습니다!
</aside>
**[에디터 설치하기]**
- 2. Visual Studio Code
<aside>
💡 Visual Studio Code (줄여서 VSCode) 앞으로 실제 코드를 작성할 편집 툴입니다.
Flutter 개발은 Android Studio와 VSCode 둘 중 원하는 툴을 사용하여 개발할 수 있는데 VSCode가 더 가볍기 때문에 앞으로 수업은 VSCode에서 진행하도록 하겠습니다.
</aside>
- 1) VSCode 설치
1. [링크](https://code.visualstudio.com/download)에 접속해 주세요.
2. Window 버튼을 클릭합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63572ab2-31fb-47de-8ee1-edb8a771ad70/Untitled.png)
3. 내 문서에 저장합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b8406fa7-10d5-4e0c-a62a-8c71df502976/Untitled.png)
4. VSCodeUserSetup 파일을 실행해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/15e64328-4565-4b1e-9997-a1caf5b97980/Untitled.png)
5. `동의합니다` 선택 후 `다음` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/26c4868e-98de-4a72-8234-0cf8cdd98c95/Untitled.png)
6. 경로를 확인하시고, `다음` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ee0ad10c-50f8-4c5e-95ce-a10813d226b1/Untitled.png)
7. `다음` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3af5829c-429a-423c-9ff8-f79400f451bd/Untitled.png)
8. `바탕 화면에 바로가기 만들기`를 선택해주시고, `다음` 버튼을 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/85d38b8f-cd2c-4f29-9dd6-e590ea63fcd1/Untitled.png)
9. `설치` 버튼을 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dd0dcbcc-e464-4f96-b883-1b225ae0ab10/Untitled.png)
10. `종료` 버튼을 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6c6ba3eb-5b6b-4089-b70a-68dc787e9d3c/Untitled.png)
11. 아래와 같이 Visual Studio Code가 실행되면, 설치 완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb31a4ee-2ed8-48df-961e-1d856ff46188/Untitled.png)
<aside>
💡 우측 하단에 한국어로 변경하라는 팝업이 뜹니다.
하지만 VSCode 사용법이나 대부분의 개발 자료는 영어로 되어 있기 때문에, 가급적 적용하지 않기를 권장 드립니다. (수업 자료도 영어 버전으로 되어있어요!)
해당 알람을 다시 보지 않으려면 `우측 톱니바퀴 ⚙` 아이콘을 누른 뒤 `Don't Show Again`을 선택해 주세요. (만약 사라져서 버튼을 누르지 못했다면 다음번에 눌러주세요!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1bbcc25a-bac1-412e-a0ee-a8302ce700e2/Untitled.png)
</aside>
- 2) Extension 설치
<aside>
💡 VSCode는 Flutter 뿐만 아니라 다양한 개발을 모두 할 수 있는 통합 에디터입니다. VSCode에서 Flutter 앱 개발을 하려면 VSCode에 Extension 탭에서 아래 목록의 Extension 들을 설치해야 합니다.
**Flutter** : VSCode에서 Flutter 개발 환경 지원
**Dart** : Flutter 개발 시 사용되는 Dart 개발 환경 지원
</aside>
1. 좌측에 extension 아이콘(동그라미)을 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/069b334c-7c6b-4eca-814c-24e159788880/Untitled.png)
2. `flutter` 라고 검색한 뒤, 해당 익스텐션을 선택하고 `install` 버튼을 눌러 설치해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3fd60cf3-786d-427f-852f-08b92eae5795/Untitled.png)
3. 위 `flutter` 익스텐션을 설치하면서 `dart` 익스텐션도 일반적으로 함께 설치가 됩니다.
`dart` 라고 검색하신 뒤 혹시 설치가 안되었다면 해당 익스텐션도 같이 설치해주세요.
`uninstall`이라고 뜨신다면 이미 설치가 된 것이니 넘어가시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b4bbc0d8-e9bc-4b7b-9fb8-ef0989d06ce0/Untitled.png)
**[IDE 설치하기]** ** IDE : 소프트웨어 애플리케이션*
- 3. Android Studio
- 1) Android Studio 설치
1. [링크](https://developer.android.com/studio)에 접속한 뒤, `Download Android Studio`를 클릭해주세요.
![Screenshot 2022-09-20 035221.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7ce46c8b-e17a-40d0-b310-104655399374/Screenshot_2022-09-20_035221.png)
2. 팝업 창이 뜨면, 쭉 읽어보시고, 바닥까지 스크롤을 내려주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/11a77b36-7bdd-4011-bc10-522b6a25894a/Untitled.png)
3. 약관에 동의하도록 체크하신 뒤, 다운로드 버튼을 눌러주세요.
![Screenshot 2022-09-20 035239.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/58b91dbe-952b-49ac-90f2-13e05d6d0c3d/Screenshot_2022-09-20_035239.png)
4. 다운로드 폴더에 다운로드를 진행해주세요.
![Screenshot 2022-09-20 035410.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d923689f-e997-4dd2-b440-1e9b09646f72/Screenshot_2022-09-20_035410.png)
5. 다운로드가 완료되면, `android-studio`를 클릭해서 실행해주세요.
![Screenshot 2022-09-20 035510.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b28f7995-98a2-4fa6-b100-d9ab0fa47387/Screenshot_2022-09-20_035510.png)
6. 아래 이미지들을 모두 `Next` 버튼을 눌러 설치를 진행해 주세요.
![Screenshot 2022-09-20 035732.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5779e09c-c112-48f2-bbb0-952057a29503/Screenshot_2022-09-20_035732.png)
![Screenshot 2022-09-20 035744.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0b3b3939-942a-4861-bdce-ae3ffdbed13f/Screenshot_2022-09-20_035744.png)
![Screenshot 2022-09-20 035803.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6762f22f-60db-4c64-939d-0df372f4867a/Screenshot_2022-09-20_035803.png)
7. `Install` 버튼을 눌러 주세요.
![Screenshot 2022-09-20 035818.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f8eca212-ff82-4d19-bd05-6cc7799c41a5/Screenshot_2022-09-20_035818.png)
8. `Next` 버튼을 눌러주세요.
![Screenshot 2022-09-20 035928.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/025bbed0-ae37-4c57-8fb2-a34844a07596/Screenshot_2022-09-20_035928.png)
9. 설치가 완료되었고, `Finish` 버튼을 눌러 Android Studio를 실행해 주세요.
![Screenshot 2022-09-20 035954.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c06fc74-6322-46f0-a89e-251455d29c79/Screenshot_2022-09-20_035954.png)
10. 아래와 같은 창이 뜨면 `OK`를 눌러주세요.
![Screenshot 2022-09-20 040050.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fe9509eb-7bb4-4cbe-8bf9-a3b54db635b1/Screenshot_2022-09-20_040050.png)
11. Android Studio 사용성 개선을 위해 Google에 데이터를 공유하고 싶으시다면, `Send usage statistics to Google` 버튼을 누르시고, 그렇지 않으시면 `Don't send` 버튼을 눌러주세요.
![Screenshot 2022-09-20 040150.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/014b2fd1-a3b1-4610-aa6c-a0a52b65caa5/Screenshot_2022-09-20_040150.png)
12. Setup Wizard가 실행되면, `Next` 버튼을 눌러주세요.
![Screenshot 2022-09-20 040252.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1c2cdfad-1ca2-4ec5-9c9b-99b4ee05f9dc/Screenshot_2022-09-20_040252.png)
13. `Next` 버튼을 눌러주세요.
![Screenshot 2022-09-20 040236.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/382ea58f-be44-4640-8347-8e6d21f13e54/Screenshot_2022-09-20_040236.png)
14. 원하시는 테마를 선택하신 뒤 `Next`를 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f7e2a5fc-217d-4e63-badc-265e7439904e/Untitled.png)
15. 만약 계정 이름이 한글이거나 띄어쓰기가 들어가 있다면`Your Android SDK location contains non-ASCII characters` 혹은 `your path contains white space etc.` 라도 뜨며 진행이 안됩니다.
<aside>
💡 에러가 없으신 분들은 `Next` 버튼을 누르고 26 순서로 넘어가 주세요
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/efaf14eb-3915-4a67-9509-a86dbad1991e/Untitled.png)
Android SDK Location 경로를 직접 만들어주도록 하겠습니다.
16. 이런 경우, 아래 그림과 같이 Android SDK Location에 `C:\Users\내 계정이름\Local`까지만 선택해 복사해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/453fce97-843d-4451-9fb7-a3dc81198db4/Untitled.png)
17. `탐색기`를 열고 주소창에 붙여 넣은 뒤 엔터를 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0f83a23d-a1ac-4975-ac74-d12b2fd756d9/Untitled.png)
18. 현재 Local이라는 폴더에 들어와 있고, 여기에 `Android`라는 폴더를 만들어 주세요. (대문자로 시작합니다)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/69dd03b0-8637-46ff-95f8-989545a5f507/Untitled.png)
19. 생성한 `Android` 폴더에 들어간 뒤 `Sdk` 라는 폴더를 만들어주세요. (대문자로 시작합니다)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/36f523e9-78f0-4bf8-ac4a-e088101e3c89/Untitled.png)
20. 생성한 `Sdk` 폴더 안으로 들어가 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/983e5efe-61c7-453f-a6f2-b794d91e14c7/Untitled.png)
21. 윈도우 검색창에 `명령`이라고 검색한 뒤 `명령 프롬프트`를 우클릭하여 `관리자 권한으로 실행`해 주세요. 그냥 실행하면 안됩니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d164c4b7-9cc1-4e36-820e-423bcce82e1e/Untitled.png)
실행할지 물어보는 팝업이 뜨면 예를 눌러서 실행해주세요.
22. 아래 명령어를 복사한 뒤 명령 프롬프트에 붙여 넣어주세요. 아직 실행하시면 안됩니다.
```bash
mklink /D C:\android-sdk
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aa888d25-2d68-41e1-a200-4ee498e8b403/Untitled.png)
다음 탐색기에서 주소창을 클릭해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c09bbf0b-9031-4dff-80ec-5995cc77b6ac/Untitled.png)
주소를 복사해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8707cb46-2d75-497e-b4af-c0f8d13b4564/Untitled.png)
복사한 경로를 명령 프롬프트 `android-sdk` 뒤에 붙여 넣어주세요. 참고로 아래 그림과 같이 사이에 띄어쓰기가 있어야합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0b2b2e71-d645-4b4e-9912-8268a6cfcdc2/Untitled.png)
명령어를 실행해 아래와 같이 기호화된 링크를 만들었다고 뜨면 성공입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/447f5527-cee0-4a38-95ef-ec8e9ea839af/Untitled.png)
<aside>
💡 Android/Sdk 폴더에 바로가기를 C 드라이브 바로 밑에 생성하는 과정입니다.
</aside>
23. 다시 Android Studio Setup Wizard로 돌아와서, 아래 그림과 같이 Android SDK Location 밑에 폴더 아이콘을 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f15a708c-8f05-4d5b-9b4c-058734307ee2/Untitled.png)
그러면 아래와 같이 경로를 선택하는 창이 뜹니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a7ea5d26-5815-42d4-a2c1-e05c8c1f0e48/Untitled.png)
24. 아래 코드를 복사해서 아래 그림과 같이 경로 선택창의 주소에 붙여 넣고 `OK`를 눌러주세요.
```bash
C:\android-sdk
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ca60021d-a9d3-4d03-a0d4-ea2e36a8b442/Untitled.png)
25. 이제 경로 관련 빨간 에러가 사라지고 활성화된 `Next` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d970a8c9-2e3f-4e48-b03f-bd534f3b942d/Untitled.png)
26. `Next` 를 눌러서 세팅을 진행해 주세요.
![Screenshot 2022-09-20 040522.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f03635e8-f1ff-4c6a-b91a-85506c8249eb/Screenshot_2022-09-20_040522.png)
27. **License Agreement** 화면이 나옵니다. 왼쪽 밑줄 친 부분을 클릭해 모두 `Accept` 를 눌러주세요
![Screenshot 2022-09-20 040605.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c88f04d6-c9be-4227-a04d-864194411d00/Screenshot_2022-09-20_040605.png)
28. `Accept` 를 모두 누르고 나면 `Finish` 버튼이 활성화됩니다. 눌러주세요.
![Screenshot 2022-09-20 041010.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/99167e06-dc28-4302-8b1a-4e7728c397eb/Screenshot_2022-09-20_041010.png)
29. 설치가 진행됩니다. 시간이 다소 소요되니 천천히 기다려주세요!
![Screenshot 2022-09-20 041021.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0d41bef0-1e89-4670-9de5-2dc25755c5f7/Screenshot_2022-09-20_041021.png)
30. 중간에 `이 앱이 디바이스를 변경할 수 있도록 허락하시겠어요?` 라는 팝업이 뜨면 `예를 선택`해주세요.
31. 모든 세팅이 완료되면 `Finish`를 클릭해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/59ef93c2-cb59-4883-9297-7d78b73b3e77/Untitled.png)
- 2) Android Command-line Tools 설치
<aside>
💡 `Android Command-line Tools`는 Flutter에서 Android에 명령을 내리기 위해 필요합니다.
</aside>
1. 아래와 같이 Android Studio가 실행되면, `More Actions`를 클릭한 뒤 `SDK Manager`를 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8375a11c-71c1-40ba-ba2a-6d5929d738b6/Untitled.png)
2. `SDK Tools`를 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/31ad9747-b889-4dc8-8b6e-efce041de413/Untitled.png)
3. `Android SDK Command-line Tools (latest)`를 선택한 뒤 `OK`를 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6faa542-270d-495f-bb77-3a7cd2b94cbf/Untitled.png)
4. Dialog가 뜨면 `OK` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5def3ca8-3748-4ae3-bec2-bb118c427cd9/Untitled.png)
5. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/32b5f758-804f-48c1-85a2-6c710144e705/Untitled.png)
- 3) Android Virtual Devices 설치
<aside>
💡 앱을 개발시 실제 휴대폰을 연결하여 개발을 진행할 때도 있지만, 대부분의 경우 Virtual Device(컴퓨터에 가상의 휴대폰을 띄우는 소프트웨어)를 이용하여 개발합니다.
</aside>
1. `More Actions` → `Virtual Device Manager` (또는 `AVD Manager`)를 선택해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/69051de0-5876-460e-8f11-d77cd168cd75/Untitled.png)
2. 이미 Device 가 있는 분들은 아래 절차를 진행하지 않으셔도 됩니다.
![Screenshot 2022-09-20 042622.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8527ebd0-b48f-4daf-ba4b-f470669a0c6b/Screenshot_2022-09-20_042622.png)
3. (Device 가 없는 경우) `Create Virtual Device`를 선택해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6e4b33d-a56d-46c0-a041-70ef779cbb25/Untitled.png)
4. 기본적으로 선택되어 있는, `Phone` → `Pixel 2` 디바이스를 `Next` 버튼을 눌러 생성합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dbb2fe70-9970-491e-a29b-204668dfc7e1/Untitled.png)
5. Release Name `Q`의 `Download`를 클릭하여 가상 기기에 설치할 OS를 다운로드 합니다.
<aside>
💡 R 버전은 Virtual Device에서 문제가 있다고 해요. 그래서 **Q 버전**으로 진행할게요!
</aside>
![Screenshot 2022-09-20 042844.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf446b06-b759-4156-af88-5e7de8b1f696/Screenshot_2022-09-20_042844.png)
6. 설치가 완료되면 `Finish` 버튼을 눌러 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a8304bbc-b8d8-42f4-a5f1-0253ab75dd3a/Untitled.png)
7. `Q` 버전의 OS를 선택한 뒤 `Next` 버튼을 눌러 주세요.
**API Level 29**인지 확인해주세요 :)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0442c914-01e1-42bb-afd2-c02bde2ffb9d/Untitled.png)
8. `Finish` 버튼을 눌러 Virtual Device 설치를 완료 해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c733f125-59d4-423b-93ae-d2434ba46a55/Untitled.png)
9. 성공적으로 추가된 Virtual Device를 확인하시고, 이제 Android Studio는 종료해주세요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3215221e-ec74-4c41-ae1c-fa738500babb/Untitled.png)
- 4) Android Licenses
1. `cmd`를 검색해서 `명령 프롬프트`를 실행해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/383052bb-12ac-468a-8044-7f663e4eaac2/Untitled.png)
2. `flutter doctor`라고 입력한 뒤 엔터를 누릅니다.
아래와 같이 `Android toolchain`의 좌측에 `[!]` 표시가 되어있습니다.
![Screenshot 2022-09-20 043142.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/504d7c2f-2725-479d-93b6-889615c328d4/Screenshot_2022-09-20_043142.png)
3. 문제를 해결하기 위해 `flutter doctor --android-licenses`를 복사해서 실행해 줍니다.
4. 실행하면 라이센스에 대한 동의를 여러번 구하는데, `y`를 입력하고 엔터를 눌러 진행해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/299f3349-5434-4687-9a37-c0b94b9ce408/Untitled.png)
5. `All SDK package licenses accepted` 라는 메시지가 뜨면 완료된 것입니다.
![Screenshot 2022-09-20 043215.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/915ccacc-e383-48bb-a183-b121b27b5096/Screenshot_2022-09-20_043215.png)
6. `flutter doctor` 를 입력했을 때 아래와 같이 `Android toolchain`의 좌측에 체크(`[v]`) 되었다면 완료!
![Screenshot 2022-09-20 043631.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64df1c29-20fa-409a-bcfe-fabaa84af36a/Screenshot_2022-09-20_043631.png)
<aside>
💡 Visual Studio 는 windows 용 앱을 개발할 때 필요한 툴입니다. 지금은 Android, iOS 앱 개발만 진행하므로 신경쓰지 않으셔도 됩니다.
</aside>
- 4. Xcode → 애플 정책상 맥환경에서만 사용 가능합니다 😞
<aside>
💡 Xcode는 iOS 앱 개발시 필요한 툴로 macOS에서만 설치가 가능하므로 넘어가도록 하겠습니다.
</aside>
<aside>
💡 [잠깐!] 그러면 window에서는 iOS 앱 개발을 할 수 없는 건가요?
애플의 정책상 iOS 앱은 macOS에서만 개발이 가능합니다. 😂
하지만 Flutter로 작성한 코드는 Android 뿐만 아니라 iOS에서도 이용할 수 있으니 향후 macOS가 생기신다면 기존 소스코드를 그대로 사용하여 iOS 앱도 출시할 수 있습니다!
</aside>
**설치를 완료하셨나요~? 잘 설치되었는지 확인해봅시다! ㅎㅎ**
- 최종 설치 확인
1. `cmd`를 검색해서 `명령 프롬프트`를 실행해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/383052bb-12ac-468a-8044-7f663e4eaac2/Untitled.png)
2. `flutter doctor` 를 입력했을 때 아래와 같이 모든 항목이 체크(`[v]`) 되었다면 설치 완료!
![Screenshot 2022-09-20 043631.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64df1c29-20fa-409a-bcfe-fabaa84af36a/Screenshot_2022-09-20_043631.png)
<aside>
💡 고생하셨습니다! 원래 개발 환경을 설정하는데 시간이 많이 들어갑니다 😂
그럼 1주 차 수업 때 뵙도록 하겠습니다 🙂
</aside>
**[프레임워크 설치하기]**
- **1. Flutter** - Android와 iOS 앱을 하나의 코드로 구현할 수 있도록 도와주는 프레임워크
<aside>
💡 Flutter 설치 과정은 일반적인 프로그램 설치와는 조금 다릅니다. 일반적으로는 `.pkg` 파일등을 실행하거나, `Applications` 폴더에 드래그 & 드롭해 설치를 진행하는데요.
Flutter 설치는 아래와 같은 과정으로 진행됩니다.
1. zip 파일을 다운로드 받아서 적절한 경로에 압축 해제
2. 압축 해제한 flutter 폴더 경로를 환경변수에 등록
(flutter 라는 명령어가 이 경로에 있다는 것을 macOS에게 알리는 것과 같습니다)
</aside>
<aside>
💡 시작하기에 앞서 **Apple Sillicon (M1, M2 등)** 을 사용하시는 분들은 **터미널**을 열고 아래 명령어를 입력해주세요 (인텔용 소프트웨어를 실행시킬 수 있는 **Rosetta** 라는 번역기를 설치하는 명령어입니다.)
```bash
sudo softwareupdate --install-rosetta --agree-to-license
```
### 터미널이란?
터미널은 마우스 클릭이 아닌 키보드로 컴퓨터에게 명령을 내리는 프로그램 입니다.
![terminal.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8def37ba-6141-4924-919d-9712a68a8981/terminal.png)
### 터미널 실행 방법
1. macOS 우측 상단에 `돋보기 🔍` 아이콘을 선택해 주세요. 그러면 아래와 같이 `Spotlight 검색` 창이 뜹니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1d6cc87e-0a8a-4477-bf51-eb6f302c89b5/Untitled.png)
2. `Spotlight`에 `terminal`이라고 검색한 뒤, 아래와 같이 하단에 `터미널` 프로그램이 보이면 엔터를 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6f1ffd0-6481-4c04-b40c-9a530a51fcb2/Untitled.png)
3. 터미널 프로그램이 실행됩니다.
![스크린샷 2021-12-08 오후 10.11.21.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b52a3237-b714-410f-baed-2372d0c844da/스크린샷_2021-12-08_오후_10.11.21.png)
</aside>
- 1) 다운로드
1. 먼저 flutter 를 설치할 폴더를 만들겠습니다. Downloads 폴더를 먼저 열어줍니다
![Screen Shot 2022-09-20 at 12.20.17 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/15210334-edd2-43ce-bf65-9e3004dd5942/Screen_Shot_2022-09-20_at_12.20.17_PM.png)
2. 여기서 `Cmd + ↑(화살표 위 버튼)` 을 눌러주세요. 상위 폴더로 이동하는 단축키입니다. 아래와 같이 유저명과 같은 이름의 폴더로 이동하면 됩니다.
![Screen Shot 2022-09-20 at 12.22.11 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ae831c6-cec3-4b99-8034-2e3e4fb6cd0a/Screen_Shot_2022-09-20_at_12.22.11_PM.png)
3. 이제 여기에 development 라는 이름의 폴더를 생성하겠습니다. 우측 상단의 `Actions` (점 3개 아이콘) 를 클릭하고 `New Folder` 를 선택해주세요.
![Screen Shot 2022-09-20 at 12.26.11 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/27a47ddd-37dc-431c-bfd9-04e6809be11a/Screen_Shot_2022-09-20_at_12.26.11_PM.png)
`**development**` 라는 이름으로 생성해줍니다.
![Screen Shot 2022-09-20 at 12.28.07 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/08feceb1-3e60-41b4-a885-ed44ba1bfa46/Screen_Shot_2022-09-20_at_12.28.07_PM.png)
4. 이제 생성한 폴더에 flutter 압축 파일을 다운로드하겠습니다.
Chrome 브라우저에서 [링크](https://docs.flutter.dev/get-started/install/macos)를 열어주세요.
5. 밑으로 조금 스크롤한 뒤, 파란색 버튼을 클릭해 다운로드를 진행해 주세요.
![Screen Shot 2022-09-20 at 12.01.03 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0bc321ce-513c-4200-baa8-251d187fbdbc/Screen_Shot_2022-09-20_at_12.01.03_PM.png)
Intel 칩을 사용하는 맥북은 왼쪽을 Apple 칩을 사용하는 맥북은 오른쪽을 선택해 주세요.
<aside>
💡 **내 mac 이 어떤 프로세서를 사용하는지 알고 싶다면**
좌측 상단 `Apple 로고` 클릭 → `이 Mac에 관하여`를 클릭하여 프로세서가 Intel 칩인지 Apple 칩인지 확인할 수 있습니다.
- Intel chip
![processor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e9a82a1-aed3-4ccf-9f4f-d7db45b782a0/processor.png)
- Apple chip
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01d99ad9-718d-4a10-8e52-9e0ea731ec38/Untitled.png)
</aside>
우선 `다운로드` 폴더에 저장하고, 다운로드가 끝나면 파일을 옮기겠습니다. 저장 버튼을 눌러주세요
![Screen Shot 2022-09-20 at 12.31.23 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3cb97280-feb8-4658-bbc3-ac556a116674/Screen_Shot_2022-09-20_at_12.31.23_PM.png)
6. 바탕화면에서 휴지통 우측에 있는 `다운로드` 폴더를 선택한 뒤 방금 다운로드한 flutter zip 파일을 우리가 만들어준 `development` 폴더로 드래그 앤 드롭합니다.
![Screen Shot 2022-09-20 at 12.42.27 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4539ac39-e0fb-47e9-b57a-3e23d9b06b83/Screen_Shot_2022-09-20_at_12.42.27_PM.png)
아래와 같이 zip 파일이 해당 폴더로 이동했는지 확인합니다.
![Screen Shot 2022-09-20 at 12.42.40 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/db5d3cc0-d64e-49ee-9bc9-155c9d96d6ed/Screen_Shot_2022-09-20_at_12.42.40_PM.png)
7. 다운받은 flutter 압축 파일을 실행해 주세요.
![Screen Shot 2022-09-20 at 12.45.08 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d4cc81c9-dfba-4829-bc01-b6286fb98a0d/Screen_Shot_2022-09-20_at_12.45.08_PM.png)
8. 아래 사진과 같이 압축이 해제되고 `flutter`라는 폴더가 생성 됩니다.
<aside>
💡 만약 압축이 해제된 폴더 이름이 아래 사진과 다른 경우 `flutter`로 변경해 주시기 바랍니다.
</aside>
![Screen Shot 2022-09-20 at 12.45.20 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/67baef48-1295-479d-96e8-096fd893ff70/Screen_Shot_2022-09-20_at_12.45.20_PM.png)
9. `flutter` → `bin` 폴더에 들어가볼까요?
`dart`, `dart.bat`, `flutter`, `flutter.bat` 등의 파일이 있는 것을 볼 수 있습니다. 이들이 flutter 실행 파일입니다.
![Screen Shot 2022-09-20 at 12.58.43 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c93cea50-d1db-4fdc-bf47-268ba856efeb/Screen_Shot_2022-09-20_at_12.58.43_PM.png)
<aside>
💡 우리는 이 flutter 실행 파일을 **컴퓨터 어디에서도 모두 접근 가능하게끔**, 즉 어디에서도 `flutter` 라는 명령어를 사용할 수 있게끔 해야 합니다.
환경 변수에 flutter 가 위치한 **폴더 경로를 추가**하면, 시스템의 모든 경로에서 이 flutter 파일에 접근하고, 실행할 수 있습니다.
</aside>
- 2) 환경변수 설정 및 설치
1. 바탕화면에서 `사과 아이콘` → `이 Mac에 관하여`를 클릭하여 macOS 버전을 확인해 주세요.
![os version.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/311d4d76-7e17-4987-beee-a4690d4db783/os_version.png)
2. macOS 버전을 확인한 뒤, 해당하는 명령어를 복사해 주세요.
<aside>
💡 mac OS 버전 순서
![Untitled.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f864e554-d9b0-4eb3-8c3e-70f0f952ace6/Untitled.png)
**macOS Mojave** 이하 버전을 사용하는 경우 **설정하는 파일이 다릅**니다.
</aside>
- macOS 카타리나(Catalina) `**이상**`버전 명령어 (최신 버전은 여기에요!)
```bash
echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.zshrc && source ~/.zshrc
```
- macOS 모하비(Mojave) **`이하`** 버전 명령어
```bash
echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.bash_profile && source ~/.bash_profile
```
<aside>
💡 어떤 명령어인지 궁금하신 분들은 아래 설명을 참고해 주세요.
1. `~/development/flutter/bin` 폴더에 있는 flutter 파일을 어디서든 실행할 수 있도록 등록(환경변수에 등록)
(~ 는 유저명과 같은 이름의 Home 폴더를 의미합니다)
> macOS 모하비 버전에서는 `.bash_profile`이라는 파일에 등록하고 이후 버전에선 `.zshrc` 파일에 등록하기 때문에 명령어가 다릅니다.
>
```bash
echo 'export PATH="$PATH:$HOME/Developments/flutter/bin"' >> ~/.zshrc
```
2. 설정 반영
```bash
source ~/.zsh
```
</aside>
3. 터미널에 단축키 `Cmd + v` 또는 마우스 우클릭하여 `붙여넣기`를 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8a6e79fa-b9a7-4591-8701-d31a13c59735/Untitled.png)
아래와 같이 붙여넣으면 아래와 같이 나오고, 엔터(enter)를 눌러 실행해 주세요.
![Screen Shot 2022-09-20 at 2.17.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9324f3b5-9935-4f32-9cd0-7620d8caac47/Screen_Shot_2022-09-20_at_2.17.02_PM.png)
4. 다음 명령어는 flutter의 버전을 확인하는 명령어입니다. 아래 명령어를 복사해 주세요.
```bash
flutter --version
```
터미널에 붙여넣고 실행해 주세요.
![Screen Shot 2022-09-20 at 2.19.36 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bf87e55f-450b-41b2-a62f-da725622a58f/Screen_Shot_2022-09-20_at_2.19.36_PM.png)
- [잠깐] 혹시 다음 팝업이 뜨면 `설치` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5e62eddb-6ee7-4664-a88b-51476ae199c2/Untitled.png)
1. 사용권 계약 팝업이 뜨면 `동의` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d8f0617d-4b21-45ec-b5e2-ba1aea1b67f8/Untitled.png)
2. 아래와 같이 설치가 완료되면 `완료` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/122d8290-ec39-4221-b23b-b25a0bfc93ef/Untitled.png)
- 3) 설치 확인
1. 터미널 창에 Flutter 버전을 확인하는 아래 명령어를 붙여넣고 실행해 주세요.
```bash
flutter --version
```
실행하면 `Building flutter tool...` 이라고 출력되고 잠시 후 아래와 같이 Flutter 버전이 출력되면 Flutter 설치 완료!
![Screen Shot 2022-09-20 at 2.19.15 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/97a87ad4-67c9-4a28-ab0c-6428707eb0de/Screen_Shot_2022-09-20_at_2.19.15_PM.png)
<aside>
💡 위 이미지에선 Flutter 3.3.2 버전이 출력되는데 시간이 지나면 최신 버전으로 업데이트 되어 버전이 다를 수 있습니다. 전혀 문제 없으니 그대로 진행해주세요.
</aside>
2. 다음 명령어를 복사해 터미널에 붙여넣고 실행해 주세요.
<aside>
💡 Flutter 개발하는데 필요한 항목들의 상태를 확인하는 명령어 입니다.
</aside>
```bash
flutter doctor
```
![_flutter doctor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2cae54a1-1763-4e82-880f-37c3f0cfccdb/_flutter_doctor.png)
<aside>
💡 Android 앱을 만드는데 필요
- Android Studio
- Android SDK
iOS 앱을 만드는데 필요
- Xcode
- CocoaPods
</aside>
위 프로그램들을 하나씩 설치해 보도록 하겠습니다.
**[에디터 설치하기]**
- 2. Visual Studio Code
<aside>
💡 Visual Studio Code (줄여서 VSCode) 는 앞으로 실제 코드를 작성할 편집 툴입니다.
Flutter 개발은 Android Studio와 VSCode 둘 중 원하는 툴을 사용하여 진행할 수 있습니다. VSCode가 더 가볍기 때문에 앞으로 수업은 VSCode를 활용해 진행하도록 하겠습니다.
</aside>
- 1) VSCode 설치
1. [링크](https://code.visualstudio.com/download)에 접속해 주세요.
2. 애플 아이콘 하단에 있는 Mac 버튼을 클릭해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/732ff958-a930-4446-9b3a-2eae43005ef4/Untitled.png)
3. `다운로드` 폴더에 저장해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fc103a3e-b9a5-400a-bcb2-fffcbbb63e28/Untitled.png)
4. 바탕화면에 다운로드 폴더를 클릭한 뒤 `Finder에서 열기` 버튼을 클릭해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e0efbc3d-75c5-4299-b0ad-94bbcfbfd511/Untitled.png)
5. 다운받은 `VSCode-darwin-universal.zip` 파일을 실행해 압축을 풀어주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/292fa7d8-ecb7-426b-913e-78e335908468/Untitled.png)
6. 압축이 풀리고 생성된 `Visual Studio Code` 파일을 드래그해서 왼쪽 `응용 프로그램`에 떨어뜨려 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d6388951-bc4a-4d04-8937-2a0783830a6a/Untitled.png)
7. 화면 우측 상단 `돋보기 🔍` 아이콘을 클릭한 뒤 `visual` 이라고 검색해 주세요. 그리고 하단에 `Visual Studio Code`가 보이면 엔터를 눌러 실행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/72e585cc-a9b0-4a46-932d-c725469e69cd/Untitled.png)
8. 아래와 같은 팝업이 뜨면 `열기` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1b1713a2-0f9a-4bbb-848b-38f76eef5292/Untitled.png)
9. 그러면 아래와 같이 VSCode가 실행되면 설치 완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64675e1d-55d2-41b9-8a4e-027d93b08eda/Untitled.png)
<aside>
💡 우측 하단에 한국어로 변경하라는 팝업이 뜹니다.
하지만 VSCode 사용법이나 대부분의 개발 자료는 영어로 되어 있기 때문에, 가급적 적용하지 않기를 권장 드립니다. (수업 자료도 영어 버전으로 되어있어요!)
해당 알람을 다시 보지 않으려면 `우측 톱니바퀴 ⚙` 아이콘을 누른 뒤 `Don't Show Again`을 선택해 주세요. (만약 사라져서 버튼을 누르지 못했다면 다음번에 눌러주세요!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1bbcc25a-bac1-412e-a0ee-a8302ce700e2/Untitled.png)
</aside>
- 2) Extension 설치
<aside>
💡 VSCode는 Flutter 뿐만 아니라 다양한 개발을 모두 할 수 있는 통합 에디터입니다. VSCode에서 Flutter 앱 개발을 하려면 VSCode에 Extension 탭에서 아래 목록의 Extension 들을 설치해야 합니다.
**Flutter** : VSCode에서 Flutter 개발 환경 지원
**Dart** : Flutter 개발 시 사용되는 Dart 개발 환경 지원
</aside>
1. 좌측에 extension 아이콘(동그라미)을 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fea993b-6a6f-4d4f-9354-fa2a33248122/Untitled.png)
2. `flutter` 라고 검색한 뒤, 해당 익스텐션을 선택하고 `install` 버튼을 눌러 설치해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c51b36d-aa33-4ad7-abde-6a334e7bb2cb/Untitled.png)
3. 위 `flutter` 익스텐션을 설치하면서 `dart` 익스텐션도 일반적으로 함께 설치가 됩니다.
`dart` 라고 검색하신 뒤 혹시 설치가 안되었다면 해당 익스텐션도 같이 설치해주세요. `uninstall`이라고 뜨신다면 이미 설치가 된 것이니 넘어가시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1ea5cf2-200a-451f-83e9-330a5d4c533e/Untitled.png)
**[IDE 설치하기]** ** IDE : 소프트웨어 애플리케이션*
<aside>
💡 **MacOS 에서는 안드로이드 환경, iOS 환경 모두에서 코드를 돌려볼 수 있습니다!**
아래의 상황에 따라 원하는 원하시는 애뮬레이터를 골라 설치해보세요! ㅎㅎ
**애뮬레이터 : 컴퓨터에서 가상으로 스마트폰 OS를 돌리는 프로그램*
1. **안드로이드와 ios 모두 확인하고 싶은 경우**
→ Android Studio 와 Xcode 모두 설치합니다.
2. **안드로이드만 확인하고 싶은 경우**
→ Android Studio만 설치합니다.
3. **ios만 확인하고 싶은 경우**
→ Xcode만 설치합니다.
**[잠깐!] Xcode 는 용량을 크게 차지하니 주의해주세요! :)**
- 용량이 부족하다면 Android Studio 만 설치하셔도 괜찮습니다.
이러면 iOS Simulator 는 사용할 수 없지만, 개발에는 큰 지장이 없습니다! ㅎㅎ
</aside>
- 3. Android Studio
- 1) Android Studio 설치
1. [링크](https://developer.android.com/studio)에 접속한 뒤, `Download Android Studio` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a9598ce0-fa0e-4c98-acc0-3222c2a34adb/Untitled.png)
2. 약관이 뜨면 아래로 쭉 스크롤 해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/95e19502-1158-45c4-9d84-9afdf2df50c7/Untitled.png)
3. Intel 칩을 사용하는 맥북은 왼쪽 `Mac with Intel chip`을 Apple 칩을 사용하는 맥북은 오른쪽 `Mac with Apple chip`을 선택해 주세요.
![Screen Shot 2022-09-20 at 2.29.56 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/39c5a846-f4a1-4a38-b5c4-7ce788a51996/Screen_Shot_2022-09-20_at_2.29.56_PM.png)
<aside>
💡 좌측 상단 `Apple 로고` 클릭 → `이 Mac에 관하여`를 클릭하여 Intel 칩인지 Apple 칩인지 확인할 수 있습니다.
- Intel chip
![processor.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e9a82a1-aed3-4ccf-9f4f-d7db45b782a0/processor.png)
- Apple chip
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01d99ad9-718d-4a10-8e52-9e0ea731ec38/Untitled.png)
</aside>
4. 다운로드 팝업이 뜨면 `저장` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c495cdb-daa3-4529-a9c0-66c42c002404/Untitled.png)
5. 바탕화면 하단에 휴지통 좌측에 있는 `다운로드` 폴더를 클릭한 뒤 `Finder`에서 열기 버튼을 클릭해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b129d044-3975-4ff7-ad8b-2dc69078b22f/Untitled.png)
6. 다운로드가 완료된 `android-studio~~.dmg` 파일을 실행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/021331d6-475a-4ddf-ae6a-c53aa3078490/Untitled.png)
7. 아래와 같은 창이 뜨면 왼쪽에 `Android Studio`를 드래그해서 `Applications`에 떨어뜨려주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c72763c1-1eec-4770-a954-bfebbeb7ada1/Untitled.png)
8. 설치가 완료되면 좌측 상단에 빨간 X를 눌러서 아래 화면을 종료해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4a1637d-6518-4aa8-b93e-e2cd52e064cd/Untitled.png)
<aside>
💡 바탕화면에 아래 사진과 같은 파일이 생겼다면 휴지통으로 드래그해 삭제하셔도 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfa41421-9575-429e-af13-3d999315cf89/Untitled.png)
</aside>
9. 우측 상단에 `돋보기🔍` 아이콘을 클릭하고, 팝업창이 뜨면 `android`라고 입력해 주세요. 그리고 아래와 같이 `Android Studio`가 자동완성으로 뜨면 엔터를 눌러 실행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a6bad6de-8aa9-4896-a9d8-5372870f604e/Untitled.png)
10. 아래와 같이 팝업이 뜨면 `열기` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/328ff0d3-085c-4bfb-ad59-f82c67659e6d/Untitled.png)
11. 아래와 같은 창이 뜬다면 `OK` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd9a712e-8000-48fd-9764-9bdec2a8aa7f/Untitled.png)
12. `Import Android Studio Settings` 팝업이 뜨면 `Do not import settings`를 선택하신 뒤 `OK` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/00c3d439-86fd-4334-b944-4b93b2633ea0/Untitled.png)
13. 안드로이드 스튜디오 사용 데이터를 구글에 전달하여 사용성 개선에 참여하고 싶다면 `Send usage statistics to Google`을 선택해주시고, 그렇지 않은 경우 `Don't send`를 선택해 주세요.
![Screen Shot 2022-09-20 at 2.35.27 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7910e8fc-9433-4cfb-a277-0b2c2dbed8e0/Screen_Shot_2022-09-20_at_2.35.27_PM.png)
14. 다음과 같은 안드로이드 스튜디오 설정 화면이 나오면 `Next` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/748967d6-689f-4aaf-af7c-f8c7da147582/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/90290328-7729-429e-bd5f-1b6fd2b48311/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/43a8a218-0bc9-4ddb-a5d4-658650c7cbb9/Untitled.png)
15. `Finish` 버튼을 눌러 Android SDK 설치를 진행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f8728adf-ae30-4237-aac8-da26ed3af750/Untitled.png)
16. **License Agreement** 화면이 나온다면 왼쪽 밑줄 친 부분을 클릭해 모두 `Accept` 를 눌러주세요
![Screenshot 2022-09-20 040605.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c88f04d6-c9be-4227-a04d-864194411d00/Screenshot_2022-09-20_040605.png)
17. `Accept` 를 모두 누르고 나면 `Finish` 버튼이 활성화됩니다. 눌러주세요.
![Screenshot 2022-09-20 041010.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/99167e06-dc28-4302-8b1a-4e7728c397eb/Screenshot_2022-09-20_041010.png)
18. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/caef348a-0104-4904-81ac-1f3d62a6e53a/Untitled.png)
19. 아래와 같은 화면이 뜨면 Android Studio 설치 완료!
![Screen Shot 2022-09-20 at 2.43.20 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2c9e3e9a-a781-482d-91a5-92deb90993d8/Screen_Shot_2022-09-20_at_2.43.20_PM.png)
- 2) Android Command-line Tools 설치
<aside>
💡 `Android Command-line Tools`는 Flutter에서 Android에 명령을 내리기 위해 필요합니다.
</aside>
1. Android Studio에서 `More Actions`를 선택한 뒤 `SDK Manager`를 선택해 주세요.
![Screen Shot 2022-09-20 at 2.43.43 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/765f159a-d30c-452b-82db-56925a758f31/Screen_Shot_2022-09-20_at_2.43.43_PM.png)
2. 그러면 아래와 같이 `Preferences for New Projects` 팝업이 뜨면 `SDK Tools` 탭을 선택 → `Android SDK Command-line Tools (latest)` 선택 → `Apply` 를 선택해 주세요.
![Screen Shot 2022-09-20 at 2.44.40 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/86cdd3d2-0c6d-4bf0-9044-3b1640723ad3/Screen_Shot_2022-09-20_at_2.44.40_PM.png)
3. 팝업이 뜨면 `OK` 버튼을 클릭해 주세요.
![Screen Shot 2022-09-20 at 2.45.33 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5b5466b0-3eda-4a24-b326-5e8f361b0aac/Screen_Shot_2022-09-20_at_2.45.33_PM.png)
4. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/179110f8-4fef-49e6-a781-8672450baef3/Untitled.png)
- 3) Android Virtual Devices 설치
<aside>
💡 앱을 개발시 실제 휴대폰을 연결하여 개발을 진행할 때도 있지만, 대부분의 경우 Virtual Device(컴퓨터에 가상의 휴대폰을 띄우는 소프트웨어)를 이용하여 개발합니다.
</aside>
1. `More Actions` → `Virtual Device Manager` (또는 `AVD Manager`)를 선택해 주세요.
![Screen Shot 2022-09-20 at 2.46.47 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e72abe9e-f703-405d-ada1-08eafbdb80e1/Screen_Shot_2022-09-20_at_2.46.47_PM.png)
2. 이미 Device 가 있는 분들은 아래 절차를 진행하지 않으셔도 됩니다.
![Screen Shot 2022-09-20 at 2.51.09 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8d5b8d36-7e78-4ef2-911d-d5a5dc7ef4a4/Screen_Shot_2022-09-20_at_2.51.09_PM.png)
3. (Device 가 없는 경우) `Create Virtual Device...`를 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/35588fa8-02ce-4a17-82b2-e4ebd4cc1924/Untitled.png)
4. 하드웨어를 선택하는 화면이 나오면 `Next`를 눌러서 기본으로 설정된 `Pixel 2` 휴대폰을 설치하도록 하겠습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fe1e55c6-ea12-4d75-b619-e41587b9e51b/Untitled.png)
5. 휴대폰에 설치할 Android OS를 선택하는 화면입니다. `Q` 옆에 있는 `Download` 버튼을 클릭하여 OS를 다운로드해 주세요.
<aside>
💡 R 버전은 Virtual Device에서 문제가 있다고 해요. 그래서 **Q 버전**으로 진행할게요!
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4ca338ba-a53c-448a-84fa-be1735599608/Untitled.png)
6. 설치가 완료되면 `Finish` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/58629f13-a9e6-4ddd-ac93-acc9441fbfab/Untitled.png)
7. `Q` 옆에 `Download` 버튼이 사라졌습니다. 우측에 현재 선택된 OS 버전이 29인지 확인한 뒤 `Next` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6d8fade8-aa03-46b7-b417-c55b3b7b2342/Untitled.png)
8. `Finish` 버튼을 눌러 Virtual Device 설치를 완료해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23afd0e5-79c6-426e-9ee3-690f6998ba43/Untitled.png)
9. 아래와 같이 `Pixel 2 API 29` 라는 Virtual Device가 추가되었습니다.
이제 좌측 상단에 빨간 버튼을 눌러 창을 종료해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1134aa4e-c890-4437-9820-264c40413b8a/Untitled.png)
10. 하단에 Android Studio 아이콘을 우클릭한 뒤 `종료` 버튼을 눌러를 Android Studio를 종료해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ffcab44-bef5-43d2-b10a-7d0c1d1b1a86/Untitled.png)
<aside>
💡 Android Studio를 클릭한 상태에서 단축키(`Cmd + Q`)를 누르셔도 됩니다.
</aside>
- 4) Android Licenses
1. 터미널에서 `flutter doctor`라고 입력한 뒤 엔터를 누릅니다.
아래와 같이 `Android toolchain`의 좌측에 `[!]` 표시가 되어있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cfa0d817-282d-4edd-b7e6-0178ad077990/Untitled.png)
2. 문제를 해결하기 위해 `flutter doctor --android-licenses`를 복사해서 터미널에 붙여 넣고 실행해 주세요. 실행시 라이센스에 대한 동의를 여러번 구하는데, `y`를 입력하고 엔터를 눌러 진행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d0492492-b2e0-4205-8875-71fd7c3bc5ce/Untitled.png)
3. 모든 동의가 완료되면 `All SDK package licenses accepted` 라고 뜹니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ef18b6df-8d01-40f7-9657-8b7cfbda449e/Untitled.png)
4. 마지막으로 터미널에 `flutter doctor` 를 입력했을 때 아래와 같이 `Android toolchain`, `Android Studio` 가 체크 완료되면 완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bfb47eed-12cb-418d-86f0-9044be87cfba/Untitled.png)
- 4. Xcode
<aside>
💡 iOS 앱을 개발하는데 필요한 Xcode를 설치해 보도록 하겠습니다.
</aside>
1. [링크](https://apps.apple.com/us/app/xcode/id497799835)를 클릭해 열어주세요.
2. 아래와 같은 팝업이 띄면 `App Store 열기` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dd013e1b-8bc8-415c-8826-7daf6173016b/Untitled.png)
3. `App Store`가 실행되고 `Xcode`가 아래와 같이 뜨면 `받기` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/63f8b6d9-4a9d-4c7d-918c-c50d2bf3299c/Untitled.png)
4. `설치` 버튼으로 변하면 한 번 더 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12804375-a9da-4223-9e13-730cfc461d7c/Untitled.png)
5. 만약 App Store에 Apple ID로 로그인이 되어있지 않아 아래와 같이 창이 뜨는 경우, 로그인을 진행해 주세요.
<aside>
💡 계정이 없다면 `Apple ID 생성`을 눌러 가입을 진행해주신 뒤, 로그인을 진행해 주세요.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/001c2d44-028a-4b49-962a-86773edc12c1/Untitled.png)
6. 로그인을 완료하면 설치가 진행 됩니다.
<aside>
💡 Xcode 설치 시간은 인터넷 상황에 따라 다르지만 보통 1시간 30분 ~ 2시간 정도 소요 됩니다. 😂
</aside>
7. 설치가 완료되면 `열기` 버튼을 눌러주세요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/adcee5ed-0d4a-4782-8879-c9945d7d9e49/Untitled.png)
8. 라이센스 동의 팝업이 뜨면 `Agree`를 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2338d57a-712a-482b-a577-27921cb7f9f1/Untitled.png)
9. 아래와 같이 암호를 입력하는 창이 뜨면 컴퓨터 시작 비밀번호를 입력한 뒤 `확인` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f3cf1771-5843-48fb-96e5-f6d1b30877bd/Untitled.png)
10. 만약 Xcode가 실행이 안되면, 다시 `열기` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/adcee5ed-0d4a-4782-8879-c9945d7d9e49/Untitled.png)
12. 아래와 같은 창이 뜨면 Xcode 설치완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c7c37404-84c3-4289-8133-7ead2d049c85/Untitled.png)
1. 설치를 완료했으니`AppStore`와 `Xcode`를 종료해 주세요.
- 하단에 App Store를 우클릭하여 `종료`를 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e4b2242-4ea0-404d-9da3-4ad29444212e/Untitled.png)
- 하단에 Xcode를 우클릭하여 `종료`를 선택해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ac0c0aeb-75bf-420a-8b63-591333ea792b/Untitled.png)
2. 이제 Homebrew 를 설치하도록 하겠습니다.
<aside>
💡 Homebrew 는 맥에서 소프트웨어를 설치 삭제할 수 있는 패키지 관리자입니다.
아래에서 brew install 명령어를 쓰기 위해 미리 설치해줍니다.
</aside>
아래 명령어를 복사해 터미널에 붙여넣고 엔터를 눌러주세요.
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
![Screen Shot 2022-09-20 at 3.11.02 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3828ed3-a3e0-4880-aa76-d312f986ecd1/Screen_Shot_2022-09-20_at_3.11.02_PM.png)
3. 다음으로 CocoaPods을 설치해 보도록 하겠습니다.
<aside>
💡 CocoaPods은 다른 사람이 만든 코드를 가져올 때 필요한 프로그램으로 Xcode와 함께 iOS 앱 개발시 필요합니다.
</aside>
아래 명령어를 복사해 터미널에서 붙여넣고 엔터를 눌러주세요.
```bash
brew install cocoapods
sudo gem install cocoapods
```
4. 아래와 같이 비밀번호를 입력하는 창이 나오면, 컴퓨터 시작시 입력하는 비밀번호를 입력한 뒤 엔터를 눌러주세요.
<aside>
💡 참고로 키보드를 눌러도 화면에 입력되는 모습은 보이지 않으니, 입력한 뒤 엔터를 누르면 됩니다.
비밀번호를 틀린 경우, `Ctrl + C`를 누르면 명령이 종료되고, 다시 명령어를 붙여넣고 실행해주세요.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e74269ff-349a-4ae5-addf-2b5d3344dd54/Untitled.png)
5. 명령이 정상적으로 실행되면 아래와 같이 뜹니다.
![Screen Shot 2022-09-20 at 3.12.24 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/06f113d2-7915-4d06-ba45-5a2f9c3b521c/Screen_Shot_2022-09-20_at_3.12.24_PM.png)
6. 아래 명령어를 복사해 터미널에서 실행해 주세요.
```bash
flutter doctor
```
그러면 아래와 같이 `Xcode` 설치가 완료된 것을 보실 수 있습니다.
![Screen Shot 2022-09-20 at 3.13.31 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dbcf4554-1f37-4303-a74e-f828f447b5ef/Screen_Shot_2022-09-20_at_3.13.31_PM.png)
**설치를 완료하셨나요~? 잘 설치되었는지 확인해봅시다! ㅎㅎ**
- 최종 설치 확인
터미널에서 `flutter doctor`라고 검색한 뒤 아래와 같이 화면이 나온다면 모든 설치가 완료하신 것입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/610c4f60-5d66-4495-baf7-7a4b82fa0d6d/Untitled.png)
<aside>
💡 다운로드 폴더에 있는 파일들은 모두 삭제하셔도 됩니다.
</aside>
<aside>
💡 고생하셨습니다! 원래 개발 환경을 설정하는데 시간이 많이 들어갑니다 😂
그럼 1주 차 수업 때 뵙도록 하겠습니다 🙂
</aside>
1) 레고 같이 조립할 수 있는 위젯(Widget)
출처 : pixabay
2) Android Material & iOS Cupertino
코드스니펫을 복사해서 새 탭에서 열면 Flutter에서 위젯을 소개하는 공식 문서가 보입니다.
https://docs.flutter.dev/development/ui/widgets
![Widget Catalog.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d3775be8-a1eb-49e0-b5a1-84e49f827086/Widget_Catalog.png)
<aside>
💡 **머터리얼 위젯(Material Widget)**은 **Android**에서 사용되는 기본 화면 구성 요소를 Flutter에서 재현한 위젯입니다.
**쿠퍼티노 위젯(Cupertino Widget)**은 **iOS**에서 사용되는 화면 구성 요소를 Flutter에서 재현한 위젯입니다.
Flutter는 특정 플랫폼에 종속되지 않은 고유의 디자인을 입힌 **커스텀 위젯(Custom Widget)**도 쉽게 만들 수 있습니다.
</aside>
<aside>
💡 **Material**, **Cupertino** 그리고 **Custom 위젯** 중 어떤 방법을 사용하든 **사용성만 해치지 않는다면** 앱을 출시하실 수 있습니다.
</aside>
<aside>
💡 Flutter에서 위젯이 어떻게 사용되는지 로그인 페이지를 만들며 배워봅시다.
</aside>
1) Flutter 프로젝트 생성하기
바탕화면에 flutter
폴더를 만들어주세요.
(이 때, 플러터 프로젝트 경로에 한글이 오지 않도록 주의해주세요!)
Visual Studio Code(VSCode)를 실행해 주세요.
View
→ Command Palette
버튼을 클릭해주세요.
명령어를 검색하는 팝업창이 뜨면, flutter
라고 입력한 뒤 Flutter: New Project
를 선택해주세요.
Application
을 선택해주세요.
프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔 flutter
폴더를 선택한 뒤 Select a folder to create the project in
버튼을 눌러 주세요.
프로젝트 이름을 hello_flutter
로 입력해주세요.
macOS의 경우 아래와 같은 팝업이 뜨면 확인
버튼을 눌러주세요.
중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요.
다음과 같이 프로젝트가 생성됩니다.
왼쪽 폴더 구조를 살짝 보고 가도록 하겠습니다.
lib
: 주로 코딩하는 폴더
pubspec.yaml
: 라이브러리 및 설정을 하는 폴더
android
: Android 프로젝트 폴더
ios
: iOS 프로젝트 폴더
web
: Web 프로젝트 폴더
아래 main.dart
코드스니펫을 복사해서 기존 코드를 모두 지운 뒤, main.dart
파일에 붙여 넣고 저장해주세요.
[코드스니펫] main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(),
),
);
}
}
다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.
analysis_options.yaml
파일을 열고, 아래 코드스니펫을 복사해서 24번째 라인 뒤(rules 뒤)에 붙여 넣고 저장해 주세요. (들여쓰기를 꼭 맞춰주세요)
[코드스니펫] analysis_options.yaml
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
<aside>
💡 상세 내용은 아래를 참고해 주세요.
- 어떤 의미인지 궁금하신 분들을 위해
`main.dart` 파일을 열어보시면 파란 실선이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
12번째 라인에 마우스를 올리면 아래와 같이 설명이 보입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수(변하지 않는 수)로 선언하라는 힌트입니다. 화면을 새로고침 할 때(화면 내 값이 변할 때마다 화면은 새로고침 됩니다) 상수로 선언된 위젯들은 새로고침을 할 필요가 없어서 성능상 이점이 있습니다.
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
지금은 학습 단계이니 눈에 띄지 않도록 `analysis_options.yaml` 파일에 아래와 같이 설정하여 힌트를 숨기도록 하겠습니다.
```dart
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
```
</aside>
2) VSCode Dart 세팅
View
→ Command Palette
를 선택해 주세요.
아래와 같이 dart recommend
라고 검색한 뒤 Dart: Use Recommended Settings
를 선택해 주세요. 그러면 저장 시 자동 줄 정렬 기능과 같이 편의 기능 설정이 적용됩니다.
3) Emulator 실행하기
View
→ Command Palatte
를 선택해주세요.
launch
를 입력한 뒤 Flutter: Launch Emulator
를 선택해 주세요.
android
에뮬레이터를 선택해주세요.
macOS의 경우, Start iOS Simulator
를 선택하실 수도 있습니다.
macOS의 경우 아래와 같은 팝업이 뜬다면 확인
버튼을 눌러주세요.
잠시 기다리시면, 에뮬레이터가 실행 됩니다. 아래와 같이 완전히 부팅이 끝날 때 까지 기다려주세요!
4) 실행하기
VSCode 우측 상단에 ⅴ
버튼을 누르고 Run Without Debugging
버튼을 클릭해주세요.
첫 번째 실행은 시간이 다소 소요되니 잠시만 기다려주세요!
드디어 첫 번째 앱 화면을 띄웠습니다! 축하드립니다👏👏
다시 켜는 방법이 궁금하다면?
VS Code 의 좌측 아래 설정 버튼을 누르고, Settings
를 눌러주세요
검색창에 widget inspect
라고 검색하시고, Dart: Open Dev Tools
옵션을 never 에서 flutter 로 변경해주세요.
그러면 앱을 실행할 때 아래와 같이 오른쪽에 Widget Inspector
탭이 열립니다.
최종 완성 이미지
1) Scaffold & Text
lib
폴더 밑에 있는 main.dart
파일에서 코딩을 해보도록 하겠습니다.
14번째 줄을 보면 home
이라고 되어있습니다. 이곳에 첫 화면에 보여줄 위젯을 넣어주는데, 여기에 Scaffold
라는 위젯이 사용되고 있습니다.
출처 - https://pixabay.com/photos/construction-work-framework-670278/
15번째 줄에 Scaffold의 appBar 영역에 AppBar()
위젯이 들어가 에뮬레이터에 상단 영역을 차지하고 있습니다.
Scaffold에 body
영역에 Text 위젯을 추가해보겠습니다. 16번째 줄에 body: Text("Hello Flutter"),
라고 입력한 뒤 저장(Ctrl/Cmd + S
)하면 에뮬레이터에 Hello Flutter
가 나타납니다.
16번째 줄에 텍스트 크기를 키워보도록 하겠습니다.
먼저 16번째 줄 “Hello Flutter"
뒤에 콤마(,)
를 추가한 뒤 저장(Ctrl/Cmd + S
)해 주세요.
그리고 17번째 줄 맨 뒤에서 아래 자동완성 단축키를 눌러주세요.
그러면 Text
위젯이 가진 속성 목록이 나옵니다. 이 중에서 style
을 이용하면 뭔가 꾸밀 수 있을 것 같다는 느낌이 옵니다. style
을 선택해 주세요.
그러면 위와 같이 style
속성이 추가되는데 여기에 무엇을 넣어야하는지 궁금하실 겁니다.
이런 경우, style 속성에 마우스를 올리면 여기에 넣는 값의 타입을 볼 수 있습니다. 아래 이미지를 보면 style
의 경우TextStyle
을 받는다고 적혀있습니다.
TextStyle
을 조금만 타이핑해 보면 자동완성으로 아래와 같이 TextStyle
을 추천해 줍니다. 아래 이미지에서 첫 번째 항목을 선택하면 TextStyle
까지만 완성해주고, 네 번째 항목을 선택하면 TextStyle()
까지 완성해 주므로 네 번째 항목을 선택해 줍니다.
그리고 저장해주면 아래와 같이 18번째 줄에 style: TextStyle(),
이 추가 됩니다.
TextStyle
에 어떤 값을 넣을 수 있는지 자동완성 기능으로 보도록 하겠습니다. 18번째 줄 TextStyle
의 소괄호 사이에서 자동완성 단축키(Ctrl + Space / Option + Esc
)를 눌러주세요.
위와 같이 추천된 속성 중에 fontSize
를 이용하면 텍스트 크기를 변경할 수 있을 것 같습니다. fontSize
를 선택해 주세요.
fontSize
에는 숫자를 넣으시면 됩니다(double
은 실수를 의미하는 자료형입니다). 28을 입력한 뒤 저장해보면 에뮬레이터에서 폰트 크기가 커진 것을 볼 수 있습니다.
2) Column & TextField
Text 위젯 밑에 ID를 입력받는 입력창을 추가해 보도록 하겠습니다. 세로 방향으로 위젯들을 나열하려면 Column
위젯을 사용해야합니다.
16번째 줄에 Text
위젯을 클릭한 뒤, 왼쪽에 있는 전구(💡) 아이콘을 클릭해 주세요.
그러면 아래와 같이 Text
위젯을 손쉽게 다른 위젯을 감싸거나 추출할 수 있도록 도와주는 리펙터(Refactor)
기능이 나타납니다.
Wrap with Column
을 선택하여 Text
위젯을 Column
위젯으로 감싸주겠습니다.
그러면 Text
위젯이 Column
위젯의 children 속성으로 들어간 것을 볼 수 있습니다.
21번째 줄 맨 뒤에, TextField(),
를 추가해주세요.
그리고 저장(Ctrl/Cmd + S
)해주면 아래와 같이 에뮬레이터에 텍스트 입력란이 생성됩니다.
iOS 에뮬레이터에서 가상 키보드 보이도록 설정하는 법
실제 모바일 환경에서는 가상 키보드가 화면에 나타나는 부분을 고려하여 화면을 만들어야 합니다. 이럴 때 아래와 같은 방법을 이용해주세요.
iOS 에뮬레이터에서 처음에 텍스트 입력이 안되는 경우, 에뮬레이터를 선택한 뒤 상단에 I/O
→ Keyboard
→ Connect Hardware Keyboard
를 선택해 주세요.
그리고 Hello Flutter
텍스트 밑에 파란 줄을 클릭하고 키보드를 입력하면 입력이 잘 됩니다.
키보드를 보이게 하고 싶은 경우, 상단에 I/O
→ Keyboard
→ Connect Hardware Keyboard
를 다시 선택하여 체크를 해제하면 키보드를 눌러도 입력되지 않고, 에뮬레이터에 나타난 가상 키보드를 입력할 수 있습니다. 키보드를 내리려면 완료
버튼을 눌러주세요.
mac OS 의 경우 Cmd + K
를 눌러 가상 키보드를 올리고 내릴 수 있습니다
TextField
에 이메일을 입력하도록 이름표를 달아 봅시다. 아래 이미지와 같이 TextField
에 decoration
속성에 InputDecoration()
을 넣고, labelText: "이메일",
를 추가해주세요.
(쉼표를 잘 찍어서 코드가 정렬되도록 합니다)
에뮬레이터 상에 “이메일”이라는 label이 생성 되었습니다.
22 ~ 26번째 줄에 TextField
를 그대로 복사해서 비밀번호 입력란을 만들어 주세요.
값을 입력해보면 비밀번호가 그대로 보입니다. 입력된 비밀번호를 보이지 않도록 만들어봅시다.
28번째 줄에 obscureText: true,
라는 속성을 주면 비밀번호가 안보이게 됩니다.TextField
위젯이 이미 속성들을 가지고 있기 때문에 우리는 해당 속성에 적절한 값만 넣어주면 됩니다!
3) Button
로그인 버튼을 만들어 봅시다.
33번째 줄(Column 내부)에 elev
라고 입력하면 자동완성으로 ElevatedButton()
이 추천되고, 해당 항목을 선택해 주세요.
아래와 같이 완성이 되고, 버튼에 필수로 입력해야하는 속성 두 가지가 뜹니다.
33번째 줄 앞쪽 onPressed
에 마우스를 올려보면, 필수로 전달하라는 의미인 required
가 보이고 전달해야하는 타입은 함수를 의미하는 void Function()
이라고 적혀 있습니다.
아직 문법을 배우지 않았으므로, 아래와 같이 onPressed
에 () {}
를 입력하고, child에 Text("로그인"),
이라고 입력해주세요.
그리고 저장(Ctrl/Cmd + S
)를 누르면 아래와 같이 정렬됩니다.
에뮬레이터 상에 버튼이 추가된 것을 확인할 수 있습니다.
4) AppBar
15번째 줄에 AppBar()
위젯에 18 ~ 21번째 줄의 Text를 넣어보도록 하겠습니다.
15번째 줄 AppBar()
의 소괄호 사이 title:
이라고 입력해 주세요.
18 ~ 21번째 줄의 Text 위젯을 15번째 줄에 title:
뒤로 이동해 주세요.
그리고 저장(Ctrl/Cmd + S
)을 눌러주시면 앱바에 title 영역으로 Hello Flutter
가 이동됩니다.
AppBar
에 centerTitle: true,
라고 넣어 두 플랫폼에서 모두 중앙 정렬이 되도록 만들어줍시다.
이제 Android에서도 중앙 정렬이 됩니다.
5) Padding
에뮬레이터 상에서 보면 TextField
가 기기 외곽에 너무 붙어 있는 것 같아 여백을 추가해 보도록 하겠습니다.
Scaffold
의 body
속성에 위젯들은 아래와 같이 배치되어 있습니다.
Column
위젯을 Padding
이라는 위젯으로 감싸면 여백을 추가할 수 있습니다.
22번째 줄에 Column
위젯을 클릭한 뒤, 왼쪽 전구(💡) 아이콘을 선택해 주세요. 그리고 Wrap with Padding
을 선택해 주세요.
Column
위젯을 Padding
위젯으로 감싸졌고, 어느정도 여백을 줄지 설정하는 23번 째 줄에 padding
속성이 추가되었습니다.
23번 째 줄에 8.0을 16으로 변경한 뒤 저장해보면 에뮬레이터에 내부에 여백이 추가된 것을 볼 수 있습니다.
6) Container
로그인 버튼을 화면 가로로 가득 채우도록 키워보겠습니다.
ElevatedButton 자체에는 width
속성이 없고, 부모 위젯에 크기를 이용해 조절 할 수 있습니다.
37번째 줄에 ElevatedButton을 클릭한 뒤 왼쪽 전구(💡) 아이콘을 클릭하고 Wrap with Container
를 선택해 주세요.
그러면 아래와 같이 Container 위젯이 ElevatedButton 위젯을 감싸게 됩니다.
다음 Container 위젯에 width: double.infinity,
라고 추가해주세요. 그러면 Container의 폭이 부모를 가득 채우게 되고, 버튼도 함께 최대 크기로 늘어납니다.
비밀번호를 입력하는 TextField와 로그인 버튼 사이에 여백을 추가해 보도록 하겠습니다.
아래 이미지와 같이 Container에 margin: EdgeInsets.only(top: 24),
라고 추가해 주세요.
7) Image & SingleChildScrollView
이메일 입력란 상단에 이미지를 넣어보겠습니다.
26번째 줄에 아래와 같이 Image.network
위젯을 추가해주세요.
Image.network("https://i.ibb.co/nngK6j3/startup.png"),
이미지에 크기를 지정하지 않았으므로, 원본 사진의 크기로 들어가게 되고, 이 상태에서 이메일 입력란을 클릭하여 가상 키보드를 올려보면 아래와 같이 하단 화면이 짤린다고 에뮬레이터에 표시됩니다.
키보드가 올라와 화면이 가려지는 경우, 스크롤을 할 수 있도록 만들어봅시다. 22번째 줄에 Padding
위젯을 클릭한 뒤 왼쪽에 전구(💡) 아이콘을 선택해 주세요. Wrap with widget...
을 선택해 Padding
위젯을 다른 위젯으로 감싸주겠습니다.
아래와 같이 Padding
위젯이 widget
이라는 익명의 위젯으로 감싸집니다.
22번째 줄에 widget
을 SingleChildScrollView
로 변경한 뒤 저장해 주세요.
이제 에뮬레이터에서 화면을 이메일을 클릭해도 화면이 넘치지 않고, 스크롤을 할 수 있습니다.
이미지를 적절한 크기로 조절해보도록 하겠습니다. width: 81,
을 Image.network
위젯에 추가해 주세요.
이미지를 Padding
으로 감싸 다른 위젯과 간격을 추가해 보겠습니다. 27번째 줄의 Image
를 선택한 뒤 오른쪽 전구(💡) 아이콘을 누르고 Wrap with Padding
을 선택해 주세요.
아래와 같이 Padding
위젯으로 Image.network
위젯이 감싸집니다.
28번째 줄에 EdgeInsets
에 8.0
을 32
로 변경한 뒤 저장해주면 완성!
최종 완성 코드
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(
"Hello Flutter",
style: TextStyle(fontSize: 28),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(32),
child: Image.network(
"https://i.ibb.co/nngK6j3/startup.png",
width: 81,
),
),
TextField(
decoration: InputDecoration(
labelText: '이메일',
),
),
TextField(
obscureText: true,
decoration: InputDecoration(
labelText: '비밀번호',
),
),
Container(
width: double.infinity,
margin: EdgeInsets.only(top: 24),
child: ElevatedButton(
onPressed: () {},
child: Text("로그인"),
),
),
],
),
),
),
),
);
}
}
Flutter 위젯(Widget) 살펴보기
에러 메시지 잘 읽고 구글링 잘하기
커뮤니티 가입하기
프로그래밍 언어 다트(Dart) 배우기
DartPad
코드스니펫을 복사한 뒤 주소창에 붙여 넣으면, DartPad로 접속합니다.
https://dartpad.dev/
```dart
void main() {
for (int i = 0; i < 5; i++) {
print('hello ${i + 1}');
}
}
```
- 실행 순서
<aside>
💡 `main`은 Dart에서 처음 시작 시 호출하는 약속된 ****함수입니다.
앞의 void 자리는 함수가 반환하는 값의 자료형을 표시하는 것입니다. 비워둬도 괜찮습니다.
</aside>
```dart
void main() {}
main2 () {}
String main3 () {
return "Hello";
}
```
![Screen Shot 2022-08-29 at 5.45.09 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/da41bae5-2351-4cea-9f8b-654647bc7de5/Screen_Shot_2022-08-29_at_5.45.09_AM.png)
- `//` : 주석으로 컴퓨터가 읽지 않습니다. 주로 개발하면서 메모할 때 사용합니다.
- `print()` : print의 소괄호 안쪽에 값을 넣으면 오른쪽 `Console`에 값이 출력 됩니다. 정상적으로 잘 작동하는지 확인(디버깅) 할 때 사용합니다.
- `;` : Dart에서는 마지막에 세미콜론(semicolon)을 찍어줍니다.
- 에러 로그 읽는 법
<aside>
💡 아래 이미지를 보면 3번 째 줄에 마지막 세미콜론이 빠졌습니다. 이 상태로 `Run`을 하게되면 우측에 에러 메세지가 나옵니다.
3:27 (3번째 줄 27번째 칸)에 에러가 발생했군요!
**에러 메세지를 보면 문제가 발생하는 위치와 해결 방법을 알 수 있습니다!** 앞으로 에러가 발생한다면 에러 메세지 부터 확인해 주세요 🙂
</aside>
![Screen Shot 2022-08-29 at 5.46.28 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4575b348-6506-4f62-b826-4de805032f07/Screen_Shot_2022-08-29_at_5.46.28_AM.png)
<aside>
💡 DartPad에서 `Reset` 버튼을 누르면, 변경한 내용이 초기화 됩니다.
![Screen Shot 2022-08-29 at 5.48.19 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/96a5d3ae-eaa8-4847-b75b-e1c11d0ac9af/Screen_Shot_2022-08-29_at_5.48.19_AM.png)
</aside>
1) 변수
변수 만들기
1) 자료형 (= 바구니에 담을 수 있는 값의 종류)
var
: 처음 담긴 값으로 타입이 지정됩니다.
String
: 문자만 담을 수 있습니다.
String?
: 문자 또는 비어있는(null
) 상태일 수 있습니다.
const
: 처음에 변수를 선언하며 담은 값을 변경할 수 없습니다.
final
: 선언하고 나중에 값을 담을 수 있으나, 한 번 담으면 변경할 수 없습니다.
void main() {
var a = 1;
String b = "hello world";
String? c = null;
const d = 1;
final e;
e = 2;
print(a);
}
2) 변수명 (= 바구니 이름)
2) 조건문
if (bool1) {
// bool1이 **true**면 실행
} else {
// bool1이 **false**면 실행
}
if (bool1) {
// bool1이 **true**면 실행
} else if (bool2) {
// bool1이 **false**이고, bool2가 **true**이면 실행
} else if (bool3) {
// bool1과 bool2가 **false**이고, bool3가 **true**이면 실행
} else {
// bool1, bool2, bool3가 모두 **false**이면 실행
}
3) 반복문
반복문 구성
1 : int i = 0
→ i
라는 변수가 0으로 시작합니다. (한 번만 실행됩니다)
2 : i < 5
→ i
의 값이 5보다 작은지 조건을 확인합니다. (false → 반복문 종료 / true → 3번)
3 : 중괄호 안쪽 영역
→ 반복해 실행하는 코드들이 들어있습니다.
4 : i++
→ i
값을 1만큼 증가 시키고 2번으로 흐름이 다시 넘어갑니다.
4) 함수(function)
함수의 호출 & 실행 순서
아래 코드 스니펫을 복사해서 DartPad에서 실행해보세요!
[코드스니펫] Dart 함수
void main() {
print("1. 시작");
say();
print("4. 종료!");
}
void say() {
print("2. 안녕");
print("3. Hello");
}
함수의 생김새
say
라고 적혀있는 부분이 함수의 이름입니다.{ }
) 안쪽 영역이 함수가 가진 실행 코드들 입니다.함수 호출 방법
함수의 표현 방법
5) 클래스(Class)
클래스 생김새
class 클래스이름 {
}
클래스의 구성 요소
class Bread {
// 생성자 함수 (클래스명과 똑같음. 클래스의 객체가 생성될 때 호출되는 함수)
Bread(String core) {
content = core; // 전달 받은 core를 content에 넣어줍니다.
}
// Bread 클래스가 가진 content 속성 (클래스 내의 변수)
String? content;
// Bread 클래스가 가진 getDescription 메소드 (클래스 내의 함수)
String getDescription() {
return "맛있는 $content빵입니다."; // 맛있는 팥빵입니다.
}
}
인스턴스(Instance)
상속(extends)
Dart의 모든 것은 Class
[Simulator Screen Recording - iPhone 13 - 2022-08-29 at 06.19.22.mp4](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a674a8d1-f48c-4b42-86fd-58d07abbf343/Simulator_Screen_Recording_-_iPhone_13_-_2022-08-29_at_06.19.22.mp4)
## 사용한 위젯
Scaffold
AppBar
Text
IconButton
Column
Padding
TextField
Icon
Divider
Expanded
ListView.builder
Card
Stack
Image.network
Container
Text
1) 실습 준비
1) Flutter 프로젝트 생성하기
View
→ Command Palette
버튼을 클릭해주세요.
명령어를 검색하는 팝업창이 뜨면, flutter
라고 입력한 뒤 Flutter: New Project
를 선택해주세요.
Application
을 선택해주세요.
프로젝트를 시작할 폴더를 선택하는 과정입니다. 미리 생성해 둔 flutter
폴더를 선택한 뒤 Select a folder to create the project in
버튼을 눌러 주세요.
프로젝트 이름을 movie_reviews
로 입력해주세요.
프로젝트가 생성되면 아래 main.dart
코드스니펫을 복사해서 기존 코드를 모두 지운 뒤, main.dart
파일에 붙여 넣고 저장해주세요.
[코드스니펫] main.dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(), // 홈페이지 보여주기
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 음식 사진 데이터
List<Map<String, dynamic>> dataList = [
{
"category": "탑건: 매버릭",
"imgUrl": "https://i.ibb.co/sR32PN3/topgun.jpg",
},
{
"category": "마녀2",
"imgUrl": "https://i.ibb.co/CKMrv91/The-Witch.jpg",
},
{
"category": "범죄도시2",
"imgUrl": "https://i.ibb.co/2czdVdm/The-Outlaws.jpg",
},
{
"category": "헤어질 결심",
"imgUrl": "https://i.ibb.co/gM394CV/Decision-to-Leave.jpg",
},
{
"category": "브로커",
"imgUrl": "https://i.ibb.co/MSy1XNB/broker.jpg",
},
{
"category": "문폴",
"imgUrl": "https://i.ibb.co/4JYHHtc/Moonfall.jpg",
},
];
// 화면에 보이는 영역
return Scaffold();
}
}
다음으로 학습 단계에서 불필요한 내용을 화면에 표시하지 않도록 설정해 주겠습니다.
analysis_options.yaml
파일을 열고, 아래 코드스니펫을 복사해서 24번째 라인 뒤에 붙여 넣고 저장해 주세요.
[코드스니펫] analysis_options.yaml
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
2) 에뮬레이터 실행하기
VSCode 우측 하단에 Chrome (web-javascript)
를 클릭해주세요.
Start Pixel 2 API 29 mobile emulator
를 선택해주세요.
잠시 기다리면 에뮬레이터가 실행됩니다.
VSCode 우측 상단에 아래 화살표를 눌러 Run Without Debugging
을 눌러주세요.
에뮬레이터에 아래와 같이 흰 화면이 나오면 정상적으로 실행이 완료된 것입니다!
2) AppBar 만들기
![simulator_screenshot_429E6845-74A6-4FFB-9C5B-31AC91DCAA3E.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed279ba5-e234-405a-a87c-16dca9a0de65/simulator_screenshot_429E6845-74A6-4FFB-9C5B-31AC91DCAA3E.png)
## 사용한 위젯
Scaffold
AppBar
IconButton
Text
Icon
![simulator_screenshot_52382C76-EC68-46A1-8F79-EF5D14DD85BE.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb77a00c-232a-483d-83ff-b377c084364b/simulator_screenshot_52382C76-EC68-46A1-8F79-EF5D14DD85BE.png)
## 사용한 위젯
Column
Padding
TextField
Icon
Divider
Expanded
ListView.builder
Card
Stack
Image.network
Container
Text
숙제 답안 (main.dart
)
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(), // 홈페이지 보여주기
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 영화 제목, 사진 데이터
List<Map<String, dynamic>> dataList = [
{
"category": "탑건: 매버릭",
"imgUrl": "https://i.ibb.co/sR32PN3/topgun.jpg",
},
{
"category": "마녀2",
"imgUrl": "https://i.ibb.co/CKMrv91/The-Witch.jpg",
},
{
"category": "범죄도시2",
"imgUrl": "https://i.ibb.co/2czdVdm/The-Outlaws.jpg",
},
{
"category": "헤어질 결심",
"imgUrl": "https://i.ibb.co/gM394CV/Decision-to-Leave.jpg",
},
{
"category": "브로커",
"imgUrl": "https://i.ibb.co/MSy1XNB/broker.jpg",
},
{
"category": "문폴",
"imgUrl": "https://i.ibb.co/4JYHHtc/Moonfall.jpg",
},
];
// 화면에 보이는 영역
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.white,
centerTitle: false,
iconTheme: IconThemeData(color: Colors.black),
title: Text(
"Movie Reviews",
style: TextStyle(
color: Colors.black,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
actions: [
IconButton(
onPressed: () {},
icon: Icon(Icons.person_outline),
)
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(
hintText: "영화 제목을 검색해주세요.",
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
suffixIcon: IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
),
),
),
Divider(height: 1),
Expanded(
child: ListView.builder(
itemCount: dataList.length,
itemBuilder: (context, index) {
String category = dataList[index]['category'];
String imgUrl = dataList[index]['imgUrl'];
return Card(
child: Stack(
alignment: Alignment.center,
children: [
Image.network(
imgUrl,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
Container(
width: double.infinity,
height: 200,
color: Colors.black.withOpacity(0.5),
),
Text(
category,
style: TextStyle(
color: Colors.white,
fontSize: 36,
),
),
],
),
);
},
),
),
],
),
);
}
}
전체 목록 바로가기
다음 강의 바로가기
Copyright ⓒ TeamSparta All rights reserved.
[수업 목표]
[목차]
1) 5주차 수업의 목표와 범위
이번주는 편하게 따라하시면 되는 것이랍니다.
런칭을 하려면 준비해야 할 몇 가지를 더 다뤄볼게요!
→ 게임에 음악 입히기
→ 스플래시 화면 붙이기
→ 광고 붙이기
→ 무료 에셋 구경하기
오늘은 머리 아픈 것 없습니다! 찬찬히 따라하면 끝~! 😎
2) 오늘 만들 것
인트로 + 음악 + 광고까지 붙인 르탄이 카드 뒤집기 게임
3) 4주차 르탄이 카드 뒤집기 게임
작동 확인하기
→ 잘 작동하네요! 이제, 시작합니다!
1) 시작씬을 만들어봅니다.
Scenes 폴더 → StartScene 만들기
화면 색 바꾸기
카메라 색 : rgb ⇒ 90, 90, 225으로 설정 해줍니다.
이미지 넣기
오른쪽 클릭 - > UI → Image 클릭 → 이름은 rtans라고 지정해 줍니다.
크기는 width: 400, height: 400으로 설정 합니다.
타이틀 만들기
[코드스니펫] 폰트 다운로드
[https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/game_new/week04/BMHANNA_11yrs_ttf.ttf](https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/game_new/week04/BMHANNA_11yrs_ttf.ttf)
Assets → fonts 폴더 생성 → 폰트 파일 넣어줍니다.
font-size: 70, y: 350으로 설정 해줍니다.
width: 760, height: 200으로 설정 해줍니다.
text에 르탄이를 찾아라 라고 적어 줍니다.
폰트를 적용 해주고, 폰트 사이즈는 70으로 적용시켜 줍니다.
가운데 정렬로 맞춰줍니다.
color: (255, 255, 255, 255)로, 컬러는 흰색을 맞춰 줍니다.
버튼 만들기
Canvas → 오른쪽 클릭 → UI → Image 만들어서
이름은 startBtn로 변경
사이즈와 위치는 width: 360, height: 120, PosY: -400 로 적용해 줍니다.
add component → shadow 컴포넌트 추가
distance는 x: 10, y: -10
, 색 (color)은 250, 250, 0 , 200로 설정 해줍니다.
Title 복사( ctrl+d
)해서 startBtn 아래에 두기.
PosY: 0, width: 360, height: 120, color: (0, 0, 0, 255)로 설정 해줍니다.
2) 르탄이 이미지 애니메이션 넣기
Animations 폴더 → rtans 만들기 → rtans 이미지에 붙이기
애니메이션 만들기
→ 애니메이션 더블클릭하고, 르탄이 이미지 전체(0~7)를 끌어다놓기
→ 오른쪽 바를 끌어서 40에 맞추기. 르탄이가 빠르게 회전하는 것을 확인!
3) startBtn 에 기능 만들기
startBtn.cs
만들기
using UnityEngine.SceneManagement;
public void startGame()
{
SceneManager.LoadScene("MainScene");
}
버튼 컴포넌트 만들고 클릭 붙이기
잘 되는지 확인하기
→ 완료! 이제 시작해볼까요?
1) 스플래시 이미지란?
예) 카카오톡 시작할 때 뜨는 이미지
같은 것이랍니다.
이 씬은 우리가 만드는 게 아니라, 유니티에서 몇 가지 세팅만으로 만들 수가 있어요.
→ 즉, Scene 만들어 SceneManager.Loadscene..
하는 게 아닙니다. 😉
다만! 무료버전에서는 유니티 로고가 함께 노출된답니다.
→ 아쉽게도 로고를 지우려면 Pro
버전을 구매해야..
→ 한편, made with Unity
라고 쓰여진 게임들은 모두 무료버전의 유니티로 제작했음을 우리도 알 수 있겠죠! 자, 그럼 직접 세팅해볼까요?
2) 이미지 세팅하기
Edit → Project settings → Player → Splash Image로 접근하기
Preview를 눌러 확인하기
→ Splash Style : 배경 / 로고 색
→ Animation : Dolly - 잠깐 커짐 / Static - 일정 크기
Images 폴더에 로고 준비하기
→ Mesh Type : Full Rect ⇒ Apply 클릭
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e8396268-b03d-47a3-946e-d62074c3fc89/Untitled.png)
4. 스플래시 화면 세팅하기
→ Animation : Static 으로 맞추기
→ Draw Mode : All Sequential 로 맞추기
→ 이미지 : select → spartaMsg 클릭
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b10603a3-0039-4afd-8dcf-ce3c35c2c45a/Untitled.png)
5. preview 눌러서 확인하기
→ 잘 되네요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6bd7db93-b51d-4669-817e-f171f56fd666/Untitled.png)
1) 사운드 구상하기
2) 음원 준비하기
다운받아서 적당한 폴더에 풀고 → 파일을 들어봅니다.
Unity에 Sounds
폴더를 만들고 가져다놓기
3) 소리 재생하기: 카드 뒤집기
card
에 AudioSource
컴포넌트 달기
card.cs
준비하기
→ AudioClip, AudioSource 받기
public AudioClip flip;
public AudioSource audioSource;
적절한 순간에 재생되게 하기
→ flip을 한번만 재생되게 하기
public void openCard()
{
audioSource.PlayOneShot(flip);
anim.SetBool("isOpen", true);
transform.Find("front").gameObject.SetActive(true);
transform.Find("back").gameObject.SetActive(false);
if (gameManager.I.firstCard == null)
{
gameManager.I.firstCard = gameObject;
}
else
{
gameManager.I.secondCard = gameObject;
gameManager.I.isMatched();
}
}
4) 소리 재생하기: 카드 맞췄을 때
gameManager.cs
에서 같게 세팅하기
→ AudioSource 컴포넌트 붙이고
→ AudioSource, AudioClip 받고
public AudioSource audioSource;
public AudioClip match;
재생하기
public void isMatched()
{
string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
if (firstCardImage == secondCardImage)
{
audioSource.PlayOneShot(match);
firstCard.GetComponent<card>().destroyCard();
secondCard.GetComponent<card>().destroyCard();
int cardsLeft = GameObject.Find("cards").transform.childCount;
if (cardsLeft == 2)
{
endTxt.SetActive(true);
Time.timeScale = 0.0f;
}
}
else
{
firstCard.GetComponent<card>().closeCard();
secondCard.GetComponent<card>().closeCard();
}
firstCard = null;
secondCard = null;
}
5) 조금 특별하게 - audioManager 만들기
gameManager처럼, audioManager를 만들어 줍니다.
→ 스크립트도 audioManager.cs
를 만들어 붙여주세요!
AudioSource 컴포넌트를 붙이고, 준비합니다.
public AudioSource audioSource;
public AudioClip bgmusic;
Start()
에서 실행해줍니다.
→ 이번엔 계속 실행될 예정이니, 아래 두 줄을 넣어주세요!
audioSource.clip = bgmusic;
audioSource.Play();
1) 마켓에 올리기 전 확인해야 하는 설정들
Edit
→ Preference
→ External Tools
체크
Android 빌드를 위해서는 JDK, NDK, SDK 설정이 필수
→ Unity Hub에서 1주차에 설치시 함께 완료했습니다!
만약 설치하지 않았다면?
→ Unity Hub 를 통해서 추가 설치가 가능합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/040f86ea-7f52-4eb0-8246-abac147403e2/Untitled.png)
- `Edit` → `Project Settings` → `Player`
- Company Name 과 Product Name, 그리고 Version 을 적절히 입력해주세요
→ Company Name : `SpartaCodingClub`
→ Product Name : `findRtan`
→ Version : `1.0`
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a869b62f-360f-4d94-81a7-9dc081c8d1fd/Untitled.png)
- Icon
- Select아이콘을 누르고 spartaMsg를 선택.
- Resolution and Presentation
- 안드로이드로 바꾸고, Landscape Right, Left를 꺼줍니다. (우리는 세로형 게임!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a464db26-3bfb-4f7e-b47d-339f98e05d62/Untitled.png)
- Other Settings
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/94bb555c-663b-4a1f-93ff-04dc2173df80/Untitled.png)
- 안드로이드 마켓에 배포하려면 64 bit 지원이 필수가 되었기 때문에 Scripting Backend 를 IL2CPP 로 변경합니다
- Target Architectures 에서 ARM64 를 체크하도록 합니다.
- Publishing Settings
<aside>
💡 Keystore란? 안드로이드에서 이 앱을 배포할 수 있는 권리!
</aside>
1. `Keystore Manager` 눌러서 만들기
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f2d9ddf3-c84b-43a2-9a0d-83b737d301f6/Untitled.png)
2. Keystore → Create New → Anywhere 클릭
→ `spartakey` 로 바탕화면에 저장해주세요
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1309b92a-3604-44b0-a683-f86d890844aa/Untitled.png)
3. 그 외 입력하기
→ Alias : `spartakey`
→ Password : `123456` (간단하게 설정해주세요)
→ Add Key 클릭!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89834ffa-9f34-4d09-8c6b-1527a294d72e/Untitled.png)
4. `Yes` 를 클릭
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2d783adf-1ab1-41a5-933d-45b4e9e6347a/Untitled.png)
2) 원하는 OS 대상으로 Switch Platform
하고 빌드하기
Build Settings 항목 클릭
Scenes in Build에 씬 추가하기
Switch Platform
눌러주기 (한참 시간이 걸립니다)
다시 화면 사이즈를 정해주고
다시, 우측 하단 Build
를 눌러서 빌드해보기 (한참 시간이 걸립니다!)
→ apk 파일 이름은 myFirstGame
으로 할게요!
→ 아래와 같이 파일이 생성되면 완료!
[참고] 빌드시 Can not sign the application 오류가 뜨는 경우!
keystore의 비밀번호를 틀린 경우이니 keystore의 비밀번호를 다시 확인해주세요!
3) 배포하려면
(1) 안드로이드 폰이 있다면 → 바로 볼 수 있어요!
컴퓨터에 폰을 usb로 연결하고 → 개발자 옵션 → USB디버깅을 켜주기!
target device
를 설정하고 → build and run
을 누르면 끝!
(2) 방금 만든 .apk
파일을 "구글플레이스토어"나, "애플 앱스토어"에 올리면 되는 것!
구글플레이에는 → 누구나 올릴 수 있습니다!
→ 참고: $25 (1회)의 개발자 등록 비를 내야 한답니다.
애플 앱스토어에 올리려면 → Mac PC가 있어야 해요!
→ 참고: 1년 129,000원의 개발자 등록비를 내야 한답니다.
참고자료
1) 애플 개발자 등록
애플 개발자 사이트에 들어가서 계정을 생성 및 라이센스를 구매합니다
하단의 Join the Apple Developer Program을 눌러 개발자 라이센스 구매절차를 따릅니다
개인을 눌러주세요( 혹시 회사나 단체면 그에 맞는 것을 눌러주세요
2) iOS 빌드
expo build:ios
우린 배포를 할거니 아카이브를 선택합니다. 그리고 애플 계정을 만들고, 개발자 라이센스도 구매하였다면! 여기서 Y를 누릅니다
다음과 같이 계정을 입력하면, 추가 인증을 할 수도 있습니다. 자동으로 맥에서 추가인증 코드가 뜨거나 문자로 받아 6자리 숫자를 알려주니, 이걸 그대로 터미널에 입력 후 엔터!
그러면 다음과 같이 인증키(안드로이드에서의 싸인키 정도의 의미)를 여러분들이 직접 추가할지, 엑스포가 알아서 하게 할지 결정하라고 합니다. 엑스포에게 맡겨줍니다.
혹시 기존에 앱을 만들었던 적이 있으면 다음과 같이 기존 키를 사용할것인가? 라고 선택하는 문구를 볼수도 있는데, 이땐 새로 추가를 하시면 됩니다. 그럼 방금 전의 화면처럼 새로 만들 키를 엑스포가 알아서 만들어 추가하게 할지 말지를 결정하는 화면을 보게됩니다.
키생성을 엑스포에 위임했다면 엑스포가 추후에 앱에서 사용할 수도 있는 노티피케이션(푸시 알람) 기능을 위한 푸시 키까지 생성을 도와줍니다. 이것 또한 엑스포가 알아서 생성하고 처리할 수 있게 위임합니다. 이것 또한 기존에 만들었던게 있으면 다음과 같이 n을 누르고 엑스포에 위임한다!를 선택하시면 됩니다.
프로비져닝도 엑스포에 위임
이 모든 과정이 끝나면 빌드 단계에 들어갑니다. ipa 파일을 생성하고 있는 겁니다. 그럼 끝다길 기다립니다.
빌드가 완료됐다!라는 터미널 화면을 보게되면 엑스포 대시보드에 로그인하여 artifact 파일을 내려봤습니다. 쉽게 말해 앱 파일입니다. 그럼 드디어 우린 ipa 앱 파일을 다운 받게됩니다.
3) 앱스토어에서 개발중인 앱 선택 및 작업 공간 활성화
플랫폼은 iOS
이름은 여러분이 원하는데로!
기본언어는 한국어!
번들 ID는 여러분들이 방금 엑스포에서 빌드(배포)단계를 거쳤다면 표시가 나옵니다. 그걸 누르세요!
SKU는 유니크한 앱 아이디를 쓰면 되는데, app.json 에 썼던 "bundleIdentifier": "com.sparta.psytest", 의 값을 넣어주세요. com.sparta.psytest 이거요!
그리고 전체 엑세스!
4) 트랜스포터로 앱 파일을 앱스토어로 전송
앱 스토어에서 해당 프로그램을 다운받고 실행합니다.
Expo 대시보드에서 내려받은 ipa 파일(대시보드 상에서 보았던 artifact 파일)을 선택하고 전송을 누릅니다.
전송이 완료되면, 앱 스토어 관리자 페이지 활동 내역에서 처리중인것을 확인이 가능합니다. 좀 기다리면 처리중에서 사용 가능한 단계로 바뀝니다.
우리가 안드로이드 배포때 만든 앱로고도 보이죠?
5) 이미지 및 최종 정보 등록 후 승인 요청
그럼 실제 사용 단계 전까지 기다리는 동안 나머지 이미지들과 정보를 기입합니다.
이미지는 권고사항을 그대로 따라야 합니다. (권고 사항 보기)
이 또한 온라인 포토샵 또는 디자인 툴로 준비를 해서 차근차근 업로드를 합니다.
프로모션, 설명, 키워드는 앱 관련해서 적고 싶은 정보를 기입하고, 지원 URL, 마케팅 URL은 일단! 스파르타코딩 클럽 홈페이지 주소로 넣습니다. 혹시 심사가 이것때문에 통과되지 않는다면 관련 홈페이지를 간단하게 준비하셔야해요!
앱클립, iMessage, Apple Watch 모두 넘어갑니다. 우립 앱을 만들고 있으니까요. 그리고 빌드 파일 선택은 위에서 우리가 트랜스포터로 전송 시킨 앱이 처리중에서 사용가능 단계가 되면 선택을 할 수 있게 됩니다. 일단 넘어갑니다.
다음 정보에선 등급만 설정합니다.
저희 앱은 전부 아니오를 체크하면 됩니다. 해당사항이 없습니다 😂
앱 심사 정보는 앱이 제대로 통과가 안되면, 안된 사유를 알려주기 위해 혹은 커뮤니케이션 할 사람의 정보를 기입하면됩니다.
유의할점은 전화번호 형식은+8210~과 같이 국가 번호를 입력해야합니다.
이번 절차 또는 추후(앱 재배포)에 앱 심사를 거친다음 앱을 본인이 직접 버튼을 눌러 앱 스토어에 제출할건지 아니면 자동으로 제출하게 할건지 결정합니다.
광고 식별자는 우리에게 해당사항이 없으므로 아니요!
그리고 저장을 누릅니다
저장을 눌렀으면 왼쪽에 앱 정보를 누릅니다.
개인정보 처리방침 URL은 스파르타코딩 클럽 홈페이지 주소를 넣습니다.
( 사실 이렇게하면 결과적으로 앱 심사를 통과할 수 없습니다. iOS 앱 심사는 정말 까다롭기 때문에, 실체적이고 유효한 값들을 넣어야 하는데, 일단 그 과정만 같이 하는 것에 의의를 두고 추후에 앱을 정말 배포 하실땐 관련 정보를 제대로 준비하셔야 합니다.)
깃허브 리파짓 토리를 파고 다음 사례처럼 README.md 에 작성하여 URL로 사용할 수 있습니다.
(사례)
부재도 적당히 기입해줍니다.
카테고리도 앱 성격에 맞게 선택해주시고, 콘텐츠 권한도 심리테스트 앱에서 타사 콘텐츠에 엑세스 안함을 선택해줍니다.
그리고 저장!
왼쪽의 가격 및 사용 가능 여부를 선택한다음 무료를 선택해주세요
그리고 저장!
자 이제 상단의 App Store 탭을 누르면 우리가 초반에 작성했던 페이지를 보게됩니다.
암호화를 사용하고 있지 않음을 눌러줍니다
그 다음 마지막으로 우측 상단에 심사를 위해 제출을 누릅니다
그러면 왼쪽에 심사 대기중을 보게 됩니다.
[준비하기]
1) Unity Ads 란?
2) Unity 에디터 내에서 Unity Ads 추가하기
Windows
→ General
→ Services
탭을 클릭하여 Service 메뉴 보기
→ General Settings 클릭
organizations 드롭다운 해서 선택 → Create Project ID
클릭
Ads의 off 클릭
패키지 매니저 사용하기
[참고] 추가 설치를 왜 진행 하나요?
유니티 업데이트로 기본 Advertisement 설치시 4.4.1 버전이 설치됩니다.
이 버전으로 설치하시고 진행하시면 오류가 발생하니
강의 영상과 동일한 3.7.5 버전의 패키지 설치를 위해 패키지 매니저를 사용합니다!
1) 14세 이하 ... **No**
2) Window → Package Manager로 패키지 매니저 열고 Packages: Unity Registry 클릭합니다.
![unity registry.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8f21317e-2e20-474c-986d-bc69be5f3c3b/unity_registry.jpg)
3) Advertisement install 클릭합니다 ( 3.7.5 버전인지 꼭 확인! )
![advertisement1.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b8f9f85-1350-47b4-9f6a-b22cbd945de9/advertisement1.jpg)
4) Service로 돌아와서 **off** 를 **on** 으로 변경 합니다.
5) **on** 이라고 뜨면 일단 준비 끝납니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dacd38c9-8bc1-4224-8569-70b4d76eeff3/Untitled.png)
→ 강의 영상과 달리 **Install Lastest Version** 은 클릭하실 필요 없습니다!
6) Test mode 체크하기!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0d642823-e5ee-484f-893e-282a6b436997/Untitled.png)
7) Ads Package 클릭 → **Install Lastest Version** 해주기
[게임 끝나면 붙이기]
1) adsManager 준비하기
adsManager
만들기
단, 이번엔 꽤 작업할 것이 많아서, 붙여놓고 살펴볼게요!
adsManager를 만들고, adsManager.cs
를 붙여줍니다.
→ 아래를 먼저 추가하고, 코드스니펫 추가!
using UnityEngine.Advertisements;
[코드스니펫] adManager.cs 코드
public static adsManager I;
string adType;
string gameId;
void Awake()
{
I = this;
if (Application.platform == RuntimePlatform.IPhonePlayer)
{
adType = "Rewarded_iOS";
gameId = "iOS 아이디";
}
else
{
adType = "Rewarded_Android";
gameId = "Android 아이디";
}
Advertisement.Initialize(gameId, true);
}
public void ShowRewardAd()
{
if (Advertisement.IsReady())
{
ShowOptions options = new ShowOptions { resultCallback = ResultAds };
Advertisement.Show(adType, options);
}
}
void ResultAds(ShowResult result)
{
switch (result)
{
case ShowResult.Failed:
Debug.LogError("광고 보기에 실패했습니다.");
break;
case ShowResult.Skipped:
Debug.Log("광고를 스킵했습니다.");
break;
case ShowResult.Finished:
// 광고 보기 보상 기능
Debug.Log("광고 보기를 완료했습니다.");
break;
}
}
몇가지 세팅하기
Start()
와 Update()
는 지워주세요.
iOS 아이디
, Android 아이디
부분에 내 아이디 (숫자)를 적으세요
→ 어디있냐고요?
→ Window → General → Services → Ads 클릭 → 아랫쪽에!
광고 활성화 시켜주기
Dashboard를 누르고 로그인 합니다.
b. Get Started 를 누르고 활성화를 시켜줍니다.
c. Project Setup 으로 진행 합니다.
d. 완료하면 아래 화면에서 ID를 확인할 수 있습니다 😎
e. Finish setup 이후에는 Ad Units에서 확인하실 수 있습니다.
2) 끝 텍스트를 눌렀을 때 광고 뜨게 하기
gameManager.cs
에서 30초 → 3초에 끝나게 하기 😁
if (time > 3.0f)
{
endTxt.SetActive(true);
Time.timeScale = 0.0f;
}
endTxt.cs
에서 넘어가던 씬을 gameManager.cs
의 함수로 만들기
→ 우선, 따라해볼게요!
using UnityEngine.SceneManagement;
public void retryGame()
{
SceneManager.LoadScene("MainScene");
}
adsManager.cs
에서 보상을 적어두기
void ResultAds(ShowResult result)
{
switch (result)
{
case ShowResult.Failed:
Debug.LogError("광고 보기에 실패했습니다.");
break;
case ShowResult.Skipped:
Debug.Log("광고를 스킵했습니다.");
break;
case ShowResult.Finished:
// 광고 보기 보상 기능
gameManager.I.retryGame();
break;
}
}
endTxt.cs
에서 마지막으로 텍스트가 눌렸을 때 광고를 보게 하기
public void retryGame()
{
adsManager.I.ShowRewardAd();
}
끝
텍스트를 눌렀을 때 아래와 같이 나오면 완성!
3) 주의사항
Advertisement.Initialize(gameId, true);
→ 런칭할 때는 여기 true 를 false 로만 바꿔줘야 합니다. (true는 테스트를 하겠다는 뜻)
여기도 체크 해제해줘야겠죠!
그리고 다 했으면!
→ 게임 종료 조건 3.0f
를 다시 → 30.0f
로 바꿔둬야겠죠! 😎
[무료 에셋스토어 구경하기]
1) 무료 에셋스토어란?
2) 유명한 곳 둘러보기 (1)
[코드스니펫] OpenGameArt.org
https://opengameart.org/
UI가 조금 올드해보여도 이만한 데가 없답니다!
Browse → 2D Art → CCO
에 클릭하고 다시 검색을 눌러 둘러볼까요?
[우선 이 정도로만 알아둘게요]
→ CC-BY , GPL , ... ⇒ 사용에 뭔가 조건이 있음
→ CC0 ⇒ 사용에 아무런 조건이 없음
[사용법]
→ 눌러서 다운로드 받아 사용하면 된답니다.
3) 유명한 곳 둘러보기 (2)
[코드스니펫] 유니티에셋스토어
https://assetstore.unity.com/2d?category=2d&free=true&orderBy=1&rows=264
[코드스니펫] 예제 import 해보기
https://assetstore.unity.com/packages/audio/sound-fx/free-casual-game-sfx-pack-54116
→ `Add to My Assets` 클릭 → Accept 클릭 → Open in Unity 클릭
→ `Download` 클릭 → `Import` 클릭
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b87411f-9557-466b-8262-5890203dfae5/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7943cbea-eb76-42aa-8145-04e1d90eda08/Untitled.png)
→ Import 를 클릭하면 폴더가 생긴답니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/117be35f-6d63-4f1c-907f-c3d87b79b776/Untitled.png)
→ 여기서 쓰고 싶은 것만 남기고 나머지를 삭제하셔도 무방합니다. (또는 다른 곳에 보관!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfe81a5d-1619-444e-83ed-f562464a0906/Untitled.png)
4) 팁 - 사용 방법
배경이미지
, 효과음
, 배경음악
을 가져다 쓰는 게 좀 더 적당하답니다.OpenGameArt.org
에서 가져온 것![우리가 만들만한 게임의 범위]
1) 되짚어보기 + 더 공부한다면?
1주차: 유니티의 기본 사용법 - 빗물받는르탄이
2주차: 유니티 사용법 복습1 + 데이터저장 - 풍선을 지켜라
3주차: 유니티 사용법 복습2 + 레벨 구현 - 고양이 밥주기
4주차: 퍼즐게임 만들기 & 로직체험 - 르탄이 맞추기
5주차: 소리, 광고
서버에 데이터 저장하기( firebase ), 3D로 카메라를 활용하기
⇒ 이것으로 딱 만들만한 것은, 하이퍼캐주얼 게임
!
2) 하이퍼캐주얼 게임이란?
대부분 1) 세로로 플레이, 2) 원 버튼(터치, 슬라이드), 3) 30~60초에 한 판
아래와 같은 게임들 많이 보셨죠!
막간상식) 하이퍼캐주얼 게임으로 굉장히 유명해진 회사들도 많아요!
→ 한 달에 3~4개씩 게임을 만들어, 몇 개가 초 대박을 터뜨리면서 큰! 회사가 됐답니다.
→ Voodoo
, Ketchapp
, Lion studio
와 같은 회사들이 있어요!
3) 내가 게임을 기획해본다면?
쫄깃 류 : 아주 간단한데, 콤보가 핵심이라, 10 → 11 → 12콤보 갈 때에 심장이 쫄깃!
통쾌 류 : 다다다다다 없앨 때 느끼는 쾌감
경쟁 류 : 내가 죽나 네가 죽나 해보자! (지금 수준에선 만들기 어려워요!)
콘텐츠 류 : 더 궁금해. 궁금해!
퍼즐 류 : 으으으. 깨고 싶다 깨고 싶어!
[코드스니펫] 구글플레이 스토어
https://play.google.com/store/search?q=ketchapp&c=apps
힌트요정 - 👻
→ 우선 Ketchapp
이라는 회사로 맞춰뒀어요.
→ 여기서부터 출발해서, 유사한 콘텐츠를 타고 돌아다녀보세요!
예) 제출 링크 예시
https://play.google.com/store/apps/details?id=com.ketchapp.dunkshot
마음에 드는 게임의 URL을 제출하면 끝!
[수업 목표]
[목차]
1) 4주차 수업의 목표와 범위
여태까지 배웠던 것들로 만들거예요!
카드 뒤집기 게임을 만들면서 보드게임 만들기의 기초를 배워봅니다.
→ 보드게임은 우리가 만들었던 게임과 살~짝 다르게, "로직"이 중요하답니다.
→ 지난 주차에 했던 것들이 새록새록 기억 날 거예요!
2) 오늘 만들 것 : 르탄이 카드 뒤집기 게임
같은 모양의 르탄이를 뒤집는 게임! 해본 적 있죠? 😎
3) 만들 순서
[기본 씬 구성하기]
1) 기본 세팅하기
windows → 2x3 layout 클릭! free aspect → phone 클릭!
→ rgb ⇒ 90, 90, 225
로 맞출게요! (MainCamera)
2) 타이머 만들어두기
UI → Text 클릭 → timeTxt 로 만들어두기
→ font size: 70 , y: 400, width: 200, height: 200, Color: (255, 255, 255, 255)
1) 르탄이 이미지 받아두기
[코드스니펫] 르탄이 이미지 모음
https://s3.ap-northeast-2.amazonaws.com/materials.spartacodingclub.kr/game_new/week04/rtan.zip
Images 폴더 아래에 rtan 이미지 풀어두기
2) 카드 한 장만 만들어두기
Cards (Create Empty!) 아래에 → card (Create Empty) 한 장만 만들어둘게요
→ 앞면(front)은 르탄이 이미지가 들어가고, 뒷면(back)은 ?
물음표가 들어갑니다.
→ card 아래에 front/back 로 sprite를 만들어둘게요. 아래처럼!
front
스프라이트에 rtan0 이미지를 끌어다 놓습니다.
→ 앗, 너무 크네요! 이미지를 클릭해서 pixels per unit
을 450으로 맞춥니다.
→ 다른 르탄이들도 모두 450으로 맞춰주세요! (shift 눌러서 한번에 잡아 바꾸기)
우선 front
는 꺼두겠습니다.
back 아래에 Canvas를 만들어 Text로 ?
표시를 넣어주세요
Canvas UI를 만듭니다.
Render-Mode를 World Space
로 변경 해줍니다.
Transform Reset 버튼을 통해 Rect Transform을 초기화합니다.
Order in layer를 1
로 바꾸어 줍니다.
Text에서 Font Size는 50으로 설정 합니다.
Scale 부분을 (0.01 , 0.01)로 맞추어 줍니다.
마지막으로 Card의 Scale을 1.3으로 바꾸기
1) gameManager 세팅하기 (쉬운 것부터!)
gameManager 만들기
시간이 가게 하기
public Text timeTxt;
float time = 0.0f;
void Update()
{
time += Time.deltaTime;
timeTxt.text = time.ToString("N2");
}
Play 해서 확인해보기!
1) 배치 전략
카드를 16장 만들어서 직접 배치하는 방법은 → 100장이면 너무 힘들잖아요!
지금 카드 사이즈가 x:1.3, y:1.3
이니까, 1.4
만큼씩 띄워서 붙여주려고요!
2) 자동으로 카드 생성하기
card를 프리팹으로 만들기
→ 기존 것은 과감히 삭제하기!
카드 생성하기 전에 반복문
을 구경해보기 (튜터만 할게요!)
void Start()
{
for (int i = 0; i < 16; i++)
{
Debug.Log(i);
}
}
card를 새로 만들어서 cards 아래에 붙이기
→ 실행해서 확인해볼까요?
public GameObject card;
void Start()
{
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
}
}
3) 카드 위치 잡아주기
전략을 생각하기
[예를 들어 1번째
라고 하면]
→ x : 1을 4로 나눈 몫 = 0
→ y : 1을 4로 나눈 나머지 = 1
⇒ (0,1) 위치 ⇒ (1.4씩 곱해주면) ⇒ (0, 1.4)
위치
[예를 들어 7번째 라고 하면]
→ x : 7을 4로 나눈 몫 = 1
→ y : 7을 4로 나눈 나머지 = 3
⇒ (1,3) 위치 ⇒ (1.4씩 곱해주면) ⇒ (1.4, 4.2)
위치
카드 위치 잡아주기
void Start()
{
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f;
float y = (i % 4) * 1.4f;
newCard.transform.position = new Vector3(x, y, 0);
}
}
카드를 전체적으로 옮겨주기
→ x, y를 적당히 빼줘서 전체를 가운데에 위치하게 하기
→ 여기서는 x: -2.1f, y: -3.0f
void Start()
{
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
}
}
완성된 배치 구경하기
[르탄이 넣기]
1) 랜덤으로 섞기 전략
2) 리스트를 랜덤으로 섞기
우선 리스트 만들고 출력하기
→ 카드가 만들어지면서 Debug.Log
해보면 되겠죠?
void Start()
{
int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
Debug.Log(rtans[i]);
}
}
리스트를 섞기
→ 아래를 맨 위에 추가
using System.Linq;
[코드스니펫] 랜덤하게 섞기
rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
```csharp
void Start()
{
int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
Debug.Log(rtans[i]);
}
}
```
3. `Play` 해서 확인해보기
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7c6a4f13-1954-4a0f-8d85-7026bee3c74c/Untitled.png)
3) 르탄이 붙여주기
이미지를 꺼내오려면? → Resources 폴더에 옮겨두기
르탄이 붙이기
string rtanName = "rtan" + rtans[i].ToString();
→ 르탄이 이름(이미지 이름)을 만들어두기
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite =
→ 새 카드 아래에 front
를 찾아서, sprite를 변경
Resources.Load<Sprite>(rtanName);
→ Resources
폴더에 있는 rtanName 이미지를 가져오자
void Start()
{
int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
string rtanName = "rtan" + rtans[i].ToString();
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
}
}
확인해보기
→ 프리팹에서 front를 켜고, back을 끈 다음 확인합니다.
→ 잘 나오네요! 😎
[카드 애니메이션]
1) 기본 애니메이션 만들기 card_idle
Animations 폴더 생성 후 card_idle 애니매이션 만들기
→ 프리팹 card를 열어서 붙여놓기, Loop Time 체크하는 것 잊지 말기!
카드를 꺼내놓기
→ 안보이면 만들기 어려우니까!
애니메이션 레코딩하기
→ 0:20
에만 rotation z:3
을 만들어놓기
2) 뒤집기 애니메이션 만들기 card_flip
Animations 폴더 생성 후 card_flip 애니메이션 만들기
→ 프리팹을 열어 card에 붙여놓기
→ 뒤집기는 한번
이므로 loop time 체크할 필요 없음!
애니메이션 만들기
→ 0:10
부분만 Scale 을 x:1.2, y:1.2
로 만들어두기
→ 살짝 눌린 것처럼!
3) 애니메이션 조건 만들기
Animator 를 열고, transition 만들어 줍니다.
카드의 오른쪽 클릭하고 make transition 을 클릭합니다.
오는 것과 가는 것 두 개의 transition을 생성합니다.
각 transition에 대해서, has exit time 에 체크 해제하고, transition duration을 0으로 변경합니다.
파라미터 만들기
→ bool 형식(true / false)의 isOpen
transition에 파라미터 조건 붙이기
→ idle ⇒ flip
: bool이 true 가 되면 발동
→ flip ⇒ idle
: bool이 false 가 되면 발동
[뒤집기]
1) 카드 클릭하면 뒤집어지기 (=쉬어가기 🙂)
card c# 스크립트에서 button 속성 붙이기
card.cs
만들어서, 클릭하면 front가 보이게 해줍니다.
public void openCard()
{
transform.Find("front").gameObject.SetActive(true);
transform.Find("back").gameObject.SetActive(false);
}
b.온클릭 함수에 card를 붙여 넣은 후, card에, openCard button 함수를 붙여 줍니다.
![opencard.jpg](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/22262d1e-0413-4224-898c-55db44be50fc/opencard.jpg)
1. 카드에 animtion 적용하기
```csharp
public Animator anim;
public void openCard()
{
anim.SetBool("isOpen", true);
transform.Find("front").gameObject.SetActive(true);
transform.Find("back").gameObject.SetActive(false);
}
```
2. `Play` 해서 잘 뒤집어지는지 실행해보기
→ 잘 되네요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e93f57c8-9ad7-4f2d-af7d-cbf43ef78a90/Untitled.png)
[카드 매칭하기]
1) 카드 매칭하기 - 전략
2) 카드 매칭하기
우선, gameManager 싱글톤화
→ 이제 다른 곳에서 막 부를 것이니까!
public static gameManager I;
void Awake()
{
I = this;
}
gameManager에서 카드 이름을 저장해둘 수 있게 하기 + 매칭 로직 함수 만들어두기
public GameObject firstCard;
public GameObject secondCard;
public void isMatched()
{
Debug.Log("판단하자");
}
card.cs
에서 openCard하면 firstCard 또는 secondCard에 나를 넣기
public void openCard()
{
anim.SetBool("isOpen", true);
transform.Find("front").gameObject.SetActive(true);
transform.Find("back").gameObject.SetActive(false);
if (gameManager.I.firstCard == null)
{
gameManager.I.firstCard = gameObject;
}
else
{
gameManager.I.secondCard = gameObject;
gameManager.I.isMatched();
}
}
gameManager.cs
에서 isMatched()
만들기
→ front
를 찾아서 카드 이미지 이름을 받아오기
→ 다 끝났으면 다시 비워주기
public void isMatched()
{
string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
if (firstCardImage == secondCardImage)
{
Debug.Log("같다!");
}
else
{
Debug.Log("같지 않다!");
}
firstCard = null;
secondCard = null;
}
card 행동 준비하기
→ card.cs
안에 없애기, 뒤집어주기 함수 만들어두기
→ 같으면 → 1초 후에 둘 다 없애기 / 다르면 → 1초 후에 둘 다 뒤집어주기
→ 1초 후에 실행해야하니까, invoke를 따로 만들어줘야 하겠죠?
public void destroyCard()
{
Invoke("destroyCardInvoke", 1.0f);
}
void destroyCardInvoke()
{
Destroy(gameObject);
}
public void closeCard()
{
Invoke("closeCardInvoke", 1.0f);
}
void closeCardInvoke()
{
anim.SetBool("isOpen", false);
transform.Find("back").gameObject.SetActive(true);
transform.Find("front").gameObject.SetActive(false);
}
gameManager.cs
에서 같을 때 / 같지 않을 때 적절한 함수 불러주기
firstCard.GetComponent<card>().destroyCard()
→ firstCard에 붙어있는 card.cs 에서 destoryCard 를 불러라!
public void checkMatched()
{
string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
if (firstCardImage == secondCardImage)
{
firstCard.GetComponent<card>().destroyCard();
secondCard.GetComponent<card>().destroyCard();
}
else
{
firstCard.GetComponent<card>().closeCard();
secondCard.GetComponent<card>().closeCard();
}
firstCard = null;
secondCard = null;
}
확인해보기 ⇒ 잘 되네요! 😎
1) 카드가 모두 없어지면 게임 끝내기
끝! 텍스트를 미리 만들어두기
fontsize 20, timeTxt 를 [윈도우] ctrl+d
[맥] command + D 해서 만들면 편하겠죠!
width: 300, height: 300, posY: 0
으로 설정 해주세요.
color : (220, 255, 0, 255)
도 설정해줍니다.
안보이게 세팅 하여 감춰두세요!
gameManager.cs
에서, 매칭 됐을 때 남은 카드를 체크하기
→ cards를 찾아서, 아래에 몇 개의 자식이 있는지를 판단하면 끝!
→ 실행해보기!
public void checkMatched()
{
string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
if (firstCardImage == secondCardImage)
{
firstCard.GetComponent<card>().destroyCard();
secondCard.GetComponent<card>().destroyCard();
int cardsLeft = GameObject.Find("cards").transform.childCount;
Debug.Log(cardsLeft);
}
else
{
firstCard.GetComponent<card>().closeCard();
secondCard.GetComponent<card>().closeCard();
}
firstCard = null;
secondCard = null;
}
실행해보면, 지금 맞춘 두 장을 포함해서 나오는 것을 알 수 있어요
→ 즉, 2
가 나오면 마지막이라는 뜻!
게임 끝내기!
public GameObject endTxt;
public void isMatched()
{
string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
if (firstCardImage == secondCardImage)
{
firstCard.GetComponent<card>().destroyCard();
secondCard.GetComponent<card>().destroyCard();
int cardsLeft = GameObject.Find("cards").transform.childCount;
if (cardsLeft == 2)
{
endTxt.SetActive(true);
Time.timeScale = 0.0f;
}
}
else
{
firstCard.GetComponent<card>().closeCard();
secondCard.GetComponent<card>().closeCard();
}
firstCard = null;
secondCard = null;
}
끝
버튼에 다시시작하기 버튼 붙이기!
→ (1) 버튼 컴포넌트 추가하기
→ (2) endTxt.cs
만들고, endTxt에 붙이기
using UnityEngine.SceneManagement;
public void retryGame()
{
SceneManager.LoadScene("MainScene");
}
→ (3) gameManager.cs
start에서 TimeScale을 다시 되돌려놓는 것도 잊지말기
Time.timeScale = 1.0f;
확인하기 ⇒ 잘 되네요!
2) 미세 조정하기
이렇게, 다 만들고 변수들을 조정해서 마지막으로 게임을 손 본답니다!
card.cs
에서 없어지거나 / 다시 뒤집히는 시간을 0.5f
로 바꿔둘게요!
public void destroyCard()
{
Invoke("destroyCardInvoke", 0.5f);
}
void destroyCardInvoke()
{
Destroy(gameObject);
}
public void closeCard()
{
Invoke("closeCardInvoke", 0.5f);
}
void closeCardInvoke()
{
anim.SetBool("isOpen", false);
transform.Find("back").gameObject.SetActive(true);
transform.Find("front").gameObject.SetActive(false);
}
이렇게 되면 완성!
힌트요정 - 👻
[수정해야할 부분]
gameManager.cs
의 Update()
부분만 수정하면 된답니다!
→ 어떤 조건이 되면 아래 코드가 실행되면 되겠죠!
endTxt.SetActive(true);
Time.timeScale = 0.0f;
gameManager.cs
부분
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Linq;
public class gameManager : MonoBehaviour
{
public Text timeTxt;
float time = 0.0f;
public GameObject endTxt;
public GameObject card;
public static gameManager I;
public GameObject firstCard;
public GameObject secondCard;
void Awake()
{
I = this;
}
// Start is called before the first frame update
void Start()
{
Time.timeScale = 1.0f;
int[] rtans = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
rtans = rtans.OrderBy(item => Random.Range(-1.0f, 1.0f)).ToArray();
for (int i = 0; i < 16; i++)
{
GameObject newCard = Instantiate(card);
newCard.transform.parent = GameObject.Find("cards").transform;
float x = (i / 4) * 1.4f - 2.1f;
float y = (i % 4) * 1.4f - 3.0f;
newCard.transform.position = new Vector3(x, y, 0);
string rtanName = "rtan" + rtans[i].ToString();
newCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(rtanName);
}
}
// Update is called once per frame
void Update()
{
time += Time.deltaTime;
timeTxt.text = time.ToString("N2");
if (time > 30.0f)
{
endTxt.SetActive(true);
Time.timeScale = 0.0f;
}
}
public void isMatched()
{
string firstCardImage = firstCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
string secondCardImage = secondCard.transform.Find("front").GetComponent<SpriteRenderer>().sprite.name;
if (firstCardImage == secondCardImage)
{
firstCard.GetComponent<card>().destroyCard();
secondCard.GetComponent<card>().destroyCard();
int cardsLeft = GameObject.Find("cards").transform.childCount;
if (cardsLeft == 2)
{
endTxt.SetActive(true);
Time.timeScale = 0.0f;
}
}
else
{
firstCard.GetComponent<card>().closeCard();
secondCard.GetComponent<card>().closeCard();
}
firstCard = null;
secondCard = null;
}
}
Copyright ⓒ TeamSparta All rights reserved.
[수업 목표]
[목차]
1) 3주차 수업의 목표와 범위
스파르타가 만들어둔 이미지를 가지고 게임을 만들어봅니다.
→ 이미지들을 진짜 디자이너
에게 받았다고 생각하고 만들어보세요!
3주차 내용도 1, 2주차의 것과 80% 같고, 20%만 새롭답니다.
→ 한번 더 복습해볼까요? 이번주까지 하고나면, 꽤 익숙해질거예요!
2) 오늘 만들 것 : 고양이 밥주기 게임
배고픈 고양이들에게 밥을 주고, 생선가게를 지켜보아요!
→ 새롭게 배우는 것: 여러 Scene 만들기!, 슬라이더 바, 레벨시스템
3) 만들 순서
[기본 씬 구성하기]
1) 기본 세팅하기 & 배경 만들기
windows → 2x3 layout 클릭! free aspect → phone 클릭!
MainScene으로 변경하는 것도 잊지마세요!
메인카메라의 size를 5 → 25로 바꿀게요!
→ 약간 더 멀리서 보겠다는 이야기! 이미지들이 이 사이즈에 맞춰 작업되어 있습니다. 😎
카메라 색은 hex ⇒ FFF0B2
으로 할게요! 그러면, 배경과 같은 효과!
→ 이렇게도 할 수 있다는 사실! 알고갈게요!
2) 이미지 받아두기
3) 오브젝트 배치하기
생선가게 : y: -22
강아지 : y: -16.1
위치를 지정하신 후, Inspector의 Sprite에 이미지를 드래그해주세요!
1) 시작씬 구성하기
Scenes 폴더 안에 create → Scene 이름은 StartScene
그리고 더블클릭!
마찬가지로 Camera size ⇒ 25
sprite 이미지 만들어서 ⇒ intro
이미지 끌어다넣기
UI → Image를 만들어서 startBtn
해두기
→ startBtn 이미지 끌어다넣기
→ posY: -300
→ width: 300, height: 100
2) 씬 넘어가기
startBtn 에 버튼 컴포넌트 달기
→ 참고) 버튼 컴포넌트는 "sprite"가 아니라, 반드시 "UI Image"에 붙여야 작동한다는 사실!
스크립트 만들기
→ Scripts 폴더 만들고, startBtn.cs
만들기 → startBtn에 붙여두기
using UnityEngine.SceneManagement;
public void GameStart()
{
SceneManager.LoadScene("MainScene");
}
onclick에 붙이기
3) Play
해서 확인해보기!
→ 잘 되네요! 이제 MainScene으로 이동해서 작업할게요! (더블클릭!)
1) 밥 만들어두기
→ sprite → circle 클릭, 이름 : food
→ scale: x: 6, y: 6
→ Sprite : Knob 으로 설정하고, 색은 컬러추출기
를 이용해서 강아지 색으로 설정하기
2) 밥 복사해서 쏘기
food.cs
만들어서 붙여두기. 생성하면 무조건 위로 직진!
void Update()
{
transform.position += new Vector3(0.0f, 0.5f, 0.0f);
}
프리팹으로 만들어두기
→ Prefabs 폴더 만들고 끌어다놓기! 과감하기 원래 있던 food는 삭제!
GameManager 만들기
→ 0.2초 마다 밥을 쏘기
→ 강아지 위치에서 y 좌표만 2.0f 높아진 곳에서 쏘기
→ Quaternion.identity
는 회전 없다는 뜻! (no rotation)
public GameObject food;
public GameObject dog;
void Start()
{
InvokeRepeating("makeFood", 0.0f, 0.2f);
}
void makeFood()
{
float x = dog.transform.position.x;
float y = dog.transform.position.y + 2.0f;
Instantiate(food, new Vector3(x,y,0), Quaternion.identity);
}
앗, 그런데 food가 안 없어진다!
→ food.cs
에서 y 좌표가 26.0f 가 넘으면 없어지게 하기
void Update()
{
transform.position += new Vector3(0.0f, 0.5f, 0.0f);
if (transform.position.y > 26.0f)
{
Destroy(gameObject);
}
}
3) 강아지 움직이기
강아지 마우스 따라 움직입니다.
dog.cs 만들어주세요.
마우스 좌표 중 X 좌표만 가져옵니다. (2주차에서 썼던 코드 입니다.)
y 좌표는 transform.position.y 내 좌표를 그대로 씁니다.
여기서 잠깐! 외워서 쓰는 게 아니라, 검색해보고 쓴다!라고 생각하세요.
void Update()
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
transform.position = new Vector3(mousePos.x, transform.position.y, 0);
}
fishShop을 벗어나지 않게 합니다.
x 좌표 -8.5f ~ 8.5f 로 고정합니다.
void Update()
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
float x = mousePos.x;
if (x > 8.5f)
{
x = 8.5f;
}
if (x < -8.5f)
{
x = -8.5f;
}
transform.position = new Vector3(x, transform.position.y, 0);
}
1) normalCat 만들기
create empty → normalCat
만들기
normalCat 안에 full / hungry로 sprite를 만들어 붙이기
hungry 안에 canvas → image를 만들고 back을 만들기!
→ Canvas를 만들 때, RenderMode를 World Space로 변경해주기!
→ width: 4, height: 0.5
→ canvas의 position을 y:-4
로 고정하기
front는 back을 (ctrl+d)해서 복사한 뒤 작업시작!
→ 색 : 추출기로 fish-shop 색 추출
→ pivot의 x 값을 0 으로 만들고, scale의 x 값을 0.4로 조정해보기!
→ 이제 x scale만 조정해주면 게이지를 만들 수 있음. 이것이 바로 hp 바 만들기!
2) normalCat에 애니메이션 붙이기
Animations 폴더 만들고 normalCat
만들기
→ Loop time 체크 잊지 말기!
normalCat에 붙여넣고, 더블클릭해서 녹화시작하기
→ 0:10에 normalCat_2
, 0:20에 다시 normalCat_1
3) fatCat 만들기
→ ctrl+d 를 이용해서 만들거예요! 두 번째는 처음부터 만들 필요가 없겠죠 😎
→ (1) hungry, full 의 이미지를 교체해주기
→ (2) Canvas의 x 좌표를 -0.5
해두기
→ (3) animation 을 새로 만들어서 넣어두기 (기존에 있던 animator를 먼저 삭제하기)
4) 두 개를 모두 프리팹 화 해두기
→ fatCat은 game view에서는 삭제해두기
→ 참고) 프리팹을 고칠 때는 프리팹을 더블클릭해서 작업합니다!
1) 고양이 내려오게 하기
cat.cs
만들기
임의의 위치에서 내려오게 하기
→ y 좌표는 30.0f 화면 밖에서 내려오게 하기
void Start()
{
float x = Random.Range(-8.5f, 8.5f);
float y = 30.0f;
transform.position = new Vector3(x, y, 0);
}
// Update is called once per frame
void Update()
{
transform.position += new Vector3(0.0f, -0.05f, 0.0f);
}
[추가 팁!] 고양이가 너무 빨라요!
왜 이런 현상이 발생 하나요?
어떻게 해결 하나요?
2) 고양이와 밥 충돌하게 하기
밥에 tag 주기
밥에 rigidbody 2D
, circle collider 2D
달아주기
→ 참고) 충돌 = 한쪽에 rigidbody + 양쪽에 collider
→ 단! Body Type을 Kinematic
으로 잡아주기 = 중력의 영향을 안 받겠다는 뜻!
→ 그리고 isTrigger
에 체크! 중력의 영향을 안 받을 때에는 이것을 체크해주세요!
(옵션을 체크하지 않으면 충돌감지가 안됩니다!)
고양이에 box collider 2D 달아주기 (나중에 fatCat에도 달아야겠죠?)
→ collider size를 조정해줘야 해요! x:4, y:7
로 맞춰볼까요?
cat.cs
에 충돌 구현하기
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
Debug.Log("맞았다!");
}
}
3) 충돌하면 에너지를 채워주기
기본 에너지 세팅
float full = 5.0f;
float energy = 0.0f;
밥 충돌하면 에너지 올라가게 하기 + 밥은 없애기
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
energy += 1.0f;
Destroy(coll.gameObject);
}
}
energy 와 full이 같아지면 배불렀다!
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
}
else
{
Debug.Log("배가 다 찼어요");
}
}
}
게이지 채우기
→ 시작 게이지를 0으로 해둘게요! (front의 x scale을 0으로)
→ hungry 밑에, Canvas 밑에, front를 찾아서, x scale을 enery/full 값으로 조절
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}
else
{
Debug.Log("배가 다 찼어요");
}
}
}
게이지가 모두 찼으면, hungry는 안보이고, full이 보이게 하기!
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}
else
{
gameObject.transform.Find("hungry").gameObject.SetActive(false);
gameObject.transform.Find("full").gameObject.SetActive(true);
}
}
}
에너지 꽉 차면 옆으로 비키게 하기
→ 단, 화면 오른쪽에 있었으면(x>0) 오른쪽으로, 왼쪽이면 왼쪽으로 비키게 하기
void Update()
{
if (energy < full)
{
transform.position += new Vector3(0.0f, -0.05f, 0.0f);
}
else
{
if (transform.position.x > 0)
{
transform.position += new Vector3(0.05f, 0.0f, 0.0f);
}
else
{
transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
}
}
}
[참고] 고양이 이미지 변경 시점이 다른 이유는?
게임 설계상 OnColliderEnter2D
와 Update
에서 ‘따로’ energy를 체크 하기 때문에 고양이의 이미지가 변경되는 시점과 옆으로 비키는 시점이 조금 다릅니다.
어떻게 다를지 로직을 체크해봅니다.
3초 뒤에 없애기
void Update()
{
if (energy < full)
{
transform.position += new Vector3(0.0f, -0.05f, 0.0f);
}
else
{
if (transform.position.x > 0)
{
transform.position += new Vector3(0.05f, 0.0f, 0.0f);
}
else
{
transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
}
Destroy(gameObject, 3.0f);
}
}
4) gameManager에서 고양이 부르기
등장시키기
→ 1.0초에 한 마리씩 등장. 이제 normalCat
을 Hierachy에서 없애도 됩니다!
public GameObject normalCat;
void Start()
{
InvokeRepeating("makeFood", 0.0f, 0.2f);
InvokeRepeating("makeCat", 0.0f, 1.0f);
}
void makeCat()
{
Instantiate(normalCat);
}
order-in-layer 조정하기
→ 실행시켜보면 고양이의 에너지바가 fish-shop보다 위에 온답니다.
→ dog와 fish-shop의 order-in-layer를 1
로 조정하기!
5) 특정 y 좌표 밑으로 내려오면 게임 종료
retryBtn 만들고, 숨겨두기
→ Image로 만들기
→ width: 300, height: 100
→ button 컴포넌트 달기
→ retryBtn.cs
만들어서 넣고, onclick에 연결하기
using UnityEngine.SceneManagement;
public void ReGame()
{
SceneManager.LoadScene("MainScene");
}
gameManager 싱글톤 화
→ 어딘가에서 나를 부르기 전에는 반드시!
public static gameManager I;
void Awake()
{
I = this;
}
gameManager 내에 gameOver() 함수 만들기
public GameObject retryBtn;
public void gameOver()
{
retryBtn.SetActive(true);
Time.timeScale = 0.0f;
}
cat.cs
에서 특정 y 좌표 밑으로 내려가면 gameOver() 부르기
void Update()
{
if (energy < full)
{
transform.position += new Vector3(0.0f, -0.05f, 0.0f);
if (transform.position.y < -16.0f)
{
gameManager.I.gameOver();
}
}
else
{
if (transform.position.x > 0)
{
transform.position += new Vector3(0.05f, 0.0f, 0.0f);
}
else
{
transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
}
Destroy(gameObject, 3.0f);
}
}
[레벨업 표기하기]
1) 레벨 UI 만들기
오른쪽 위에 레벨 표기하기
level 폴더를 Canvas 게임오브젝트로 묶어 배치합니다.
Rect Transform을 먼저 리셋해주시고 위치는 x:250, y:500 으로 설정 해줍니다.
back / front는 width: 100, height: 15, y: -70 으로 설정 해줍니다.
Text는 width: 20, height: 100, font size: 40, font style: Bold, x: -50 으로 설정 합니다.
2) 5마리 당 레벨 1씩 올리기
gameManager에 레벨업 함수 만들기
int level = 0;
int cat = 0;
public void addCat()
{
cat += 1;
level = cat / 5;
}
고양이가 배부를 때 addCat()
함수 부르기
→ 다만, 이렇게 하면 안됩니다!
→ 이유는, 이렇게하면 옆으로 빠질 때 계속 점수가 올라가겠죠!
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}
else
{
gameManager.I.addCat();
gameObject.transform.Find("hungry").gameObject.SetActive(false);
gameObject.transform.Find("full").gameObject.SetActive(true);
}
}
}
그래서! 약간 수정을 합니다.
→ 상태
를 isFull 로 만들고, 제어합니다.
bool isFull = false;
if (isFull == false)
{
gameManager.I.addCat();
gameObject.transform.Find("hungry").gameObject.SetActive(false);
gameObject.transform.Find("full").gameObject.SetActive(true);
isFull = true;
}
3) 레벨업 표기해주기
레벨 업 라벨 초기화해주기
→ 레벨업 텍스트 0으로
→ 레벨업 front 바 x scale 0으로
레벨 업 라벨 바꿔주기
→ 레벨업 텍스트, front 바
using
public Text levelText;
public GameObject levelFront;
public void addCat()
{
cat += 1;
level = cat / 5;
levelText.text = level.ToString();
levelFront.transform.localScale = new Vector3((cat - level * 5) / 5.0f, 1.0f, 1.0f);
}
[레벨 반영하기]
1) Lv.1
, Lv.2
: 더 많은 고양이 출현시키기
gameManager.cs
에서 확률에 따른 추가 고양이 출현
→ 단, 빠른 진행을 위해 makeFood를 0.2f
→ 0.1f
로 바꿔둘게요!
→ 이미 생각보다 쉽지 않을걸요!
void makeCat()
{
Instantiate(normalCat);
if (level == 1)
{
float p = Random.Range(0, 10);
if (p < 2) Instantiate(normalCat);
}
else if (level >= 2)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
}
}
2) Lv.3
: fatCat 출현시키기
cat.cs
에 고양이 타입을 만들기!
→ type: 0
은 normalCat, type: 1
은 fatCat !!
→ 아래와 같이 써두면, 오브젝트에서 타입을 조절할 수 있습니다.
→ 다시 normalCat 프리팹에 가서 type: 0
으로 만들어보기
public int type;
fatCat 프리팹 준비하기
→ front의 x scale: 0
으로 만들기
→ 스크립트 붙이고 type: 1
로 만들기
→ box collider 2D를 붙이는 것도 잊지 말기!
고양이 타입을 고려한 cat.cs
만들기
→ type:1
(fatCat)인 경우 full: 10
이고, 내려오는 속도가 늦음
void Start()
{
float x = Random.Range(-8.5f, 8.5f);
float y = 30.0f;
transform.position = new Vector3(x, y, 0);
if (type == 1)
{
full = 10.0f;
}
}
// 업데이트 구문 안의 if문
if (energy < full)
{
if (type == 0)
{
transform.position += new Vector3(0.0f, -0.05f, 0.0f);
}
else if (type == 1)
{
transform.position += new Vector3(0.0f, -0.03f, 0.0f);
}
if (transform.position.y < -16.0f)
{
gameManager.I.gameOver();
}
}
fatCat
을 등장시키기
void makeCat()
{
Instantiate(normalCat);
if (level == 1)
{
float p = Random.Range(0, 10);
if (p < 2) Instantiate(normalCat);
}
else if (level == 2)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
}
else if (level >= 3)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
Instantiate(fatCat);
}
}
1) startScene으로 가서 게임을 즐겨봅니다!
→ 앗, 그런데 Replay
후에 총알 발사가 안되네요!
2) gameManager 시작할 때 timeScale = 1.0f
로 바꿔주기
void Start()
{
Time.timeScale = 1.0f;
InvokeRepeating("makeFood", 0.0f, 0.1f);
InvokeRepeating("makeCat", 0.0f, 1.0f);
}
해적고양이의 속성
x:0.8
, y:0.8
-0.1f
해적고양이 준비하기 (튜터와 함께)
normalCat 프리팹을 가져와서, 오른쪽 키 → unpack 합니다.
이미지 교체해주고, pirateCat의 scale을 작게하기
pirateCat의 animator를 떼어내고, 새로 붙여주기
cat.cs
스크립트 붙이고 type: 2
로 바꿔두기
프리팹 화 해두고 과감히 삭제하기
이렇게 되면 완성!
→ 제법 흥미진진하죠?
힌트요정 - 👻
[수정해야할 부분]
cat.cs
의 Update 부분 : type 나오는 곳에 아래를 추가해주기!
transform.position += new Vector3(0.0f, -0.1f, 0.0f);
gameManager.cs
의 makeCat 부분 : level >= 4
만들어주기!
→ level >= 3
부분은 level == 3
으로 바꿔야겠죠?
else if (level >= 4)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
Instantiate(fatCat);
Instantiate(pirateCat);
}
gameManager.cs
부분
→ makeCat()
부분을 보세요
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class gameManager : MonoBehaviour
{
public GameObject food;
public GameObject dog;
public GameObject normalCat;
public GameObject fatCat;
public GameObject pirateCat;
public GameObject retryBtn;
int level = 0;
int cat = 0;
public Text levelText;
public GameObject levelFront;
public static gameManager I;
void Awake()
{
I = this;
}
// Start is called before the first frame update
void Start()
{
Time.timeScale = 1.0f;
InvokeRepeating("makeFood", 0.0f, 0.1f);
InvokeRepeating("makeCat", 0.0f, 1.0f);
}
void makeFood()
{
float x = dog.transform.position.x;
float y = dog.transform.position.y + 2.0f;
Instantiate(food, new Vector3(x,y,0), Quaternion.identity);
}
void makeCat()
{
Instantiate(normalCat);
if (level == 1)
{
float p = Random.Range(0, 10);
if (p < 2) Instantiate(normalCat);
}
else if (level == 2)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
}
else if (level == 3)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
Instantiate(fatCat);
}
else if (level >= 4)
{
float p = Random.Range(0, 10);
if (p < 5) Instantiate(normalCat);
Instantiate(fatCat);
Instantiate(pirateCat);
}
}
// Update is called once per frame
void Update()
{
}
public void gameOver()
{
retryBtn.SetActive(true);
Time.timeScale = 0.0f;
}
public void addCat()
{
cat += 1;
level = cat / 5;
levelText.text = level.ToString();
levelFront.transform.localScale = new Vector3((cat - level * 5) / 5.0f, 1.0f, 1.0f);
}
}
cat.cs
부분
→ Update()
부분을 보세요
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cat : MonoBehaviour
{
float full = 5.0f;
float energy = 0.0f;
bool isFull = false;
public int type;
// Start is called before the first frame update
void Start()
{
float x = Random.Range(-8.5f, 8.5f);
float y = 30.0f;
transform.position = new Vector3(x, y, 0);
if (type == 1)
{
full = 10.0f;
}
}
// Update is called once per frame
void Update()
{
if (energy < full)
{
if (type == 0)
{
transform.position += new Vector3(0.0f, -0.05f, 0.0f);
}
else if (type == 1)
{
transform.position += new Vector3(0.0f, -0.03f, 0.0f);
}
else if (type == 2)
{
transform.position += new Vector3(0.0f, -0.1f, 0.0f);
}
if (transform.position.y < -16.0f)
{
gameManager.I.gameOver();
}
}
else
{
if (transform.position.x > 0)
{
transform.position += new Vector3(0.05f, 0.0f, 0.0f);
}
else
{
transform.position += new Vector3(-0.05f, 0.0f, 0.0f);
}
Destroy(gameObject, 3.0f);
}
}
void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}
else
{
if (isFull == false)
{
gameManager.I.addCat();
gameObject.transform.Find("hungry").gameObject.SetActive(false);
gameObject.transform.Find("full").gameObject.SetActive(true);
isFull = true;
}
}
}
}
}
[수업 목표]
[목차]
1) 2주차 수업의 목표와 범위
1주차에 배웠던 것을 복습 → 복습 → 복습 하는 게 전체적인 수업의 구성이에요!
→ 이번 주는 조금 더 익숙해진 자신을 볼 수 있을 것이랍니다.
→ 참고로 오늘, 쉽습니다! (복습이 많기 때문에! 😎)
Rise Up! 이란 게임을 유사하게 만들어보면서, 유니티의 기초를 다시 다집니다.
실제 게임 모습 보기: 전세계 1억 다운로드 이상의 히트작이랍니다.
2) 오늘 만들 것 : 풍선을 지켜라
네모가 풍선에 닿으면 끝! 오래 살아남는 게 목표랍니다.
→ 새롭게 배우는 것: 마우스로 제어하기, 베스트스코어 기록, 애니메이션 전환
3) 만들 순서
[기본 씬 구성하기]
1) 기본 세팅하기 & 배경 만들기
windows → 2x3 layout 클릭! free aspect → phone 클릭!
배경 색은 rgb ⇒ 20, 20, 80
으로 할게요! 사이즈는 x: 6, y: 10
2) 풍선, 마우스 만들기
간단한 풍선을 만들어둡니다. (balloon)
→ position x: 0, y: -3.2
에 맞춰두기!
마우스를 만들어둡니다. (shield)
→ scale x: 0.5, y: 0.5
로 세팅해두기! position은 그대로 둘게요!
→ rgb ⇒ 0, 0, 255
로 가겠습니다!
3) 타이머 만들기
시간 라벨 만들기 (timeTxt)
→ UI → Text를 활용!
→ font size 70
, position x: 0, y: 450
으로 맞춰주세요!
→ width: 200, height :200
→ posY: 450으로 맞춰주세요!
→ color: 255, 0, 0 (빨강)
1) 애니메이션 더하기
Animations 폴더 → 애니메이션 만들기 (balloon_idle)
→ 이따가 풍선이 "터지는" 애니메이션도 만들어야 하니, 이것은 idle(기본 상태)로 둘게요!
→ Loop Time에 체크하는 것 잊지 말기!
풍선에 끌어다 놓고 애니메이션 만들기
→레코드 (빨간색 동그라미!)를 눌러서 같이 세팅합시다!
→ 0:00, 0:40은 처음 모습 그대로
→ 0:20은 rgb ⇒ 200, 200, 255
로 세팅하기
Play
버튼을 눌러서 확인하기
2) 마우스에 움직임 더하기
Scripts → shield.cs 만들기 + shield에 붙이기
마우스 포인터를 따라 움직이게 하기
→ 외우지 말고, 나중에도 보고 쓰는 코드랍니다. 튜터도 외우고 있지 않아요!
→ mouse 의 좌표계를 카메라 좌표계로 바꾸고, shield의 위치에 넣어주기
void Update()
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
transform.position = new Vector3(mousePos.x, mousePos.y, 0);
}
Play
해서 움직임 확인하기
[내려오기]
1) 네모 만들기
sprite → square 로 만들고, 이름 바꿔두기 (square)
→ position x:0, y:3
에 맞추기
order in layer 맞추기
→ 네모, 마우스, 풍선 모두 order in layer를 1로 바꿔줍니다! (참고로 배경은 0 !)
2) 네모 떨어지게 하기/충돌효과 주기
→ rigidbody 2D 와, box collider 2D 를 줍니다.
3) 풍선, 마우스에도 충돌효과 주기
풍선, 마우스에 circle collider 2D
달기
Play
버튼을 눌러 마우스와 네모가 부딪히는지 보기
[나타나기]
1) gameManager 만들기
→ gamaManager gameObject 와 script 를 만들고 서로 붙여줍니다!
→ 이후 이 gameManager에서 네모를 만들어 줄 예정!
2) 네모 랜덤으로 나타내기
square.cs
만들고, 네모에 붙이기
랜덤 위치에서 생성하기
→ x: -3.0f ~ 3.0f
, y: 3.0f ~ 5.0f
→ 저장 후 Play
해서 확인하기
void Start()
{
float x = Random.Range(-3.0f, 3.0f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector3(x, y, 0);
}
랜덤 사이즈로 생성하기
→ size: 0.5f ~ 1.5f
→ 저장 후 Play
해서 확인하기
void Start()
{
float x = Random.Range(-3.0f, 3.0f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector3(x, y, 0);
float size = Random.Range(0.5f, 1.5f);
transform.localScale = new Vector3(size, size, 1);
}
3) 네모를 prefab으로 만들기
Prefabs 폴더를 만들고 → 끌어다넣기
기존의 square 오브젝트는 과감하게 삭제!
4) gameManager.cs
에서 네모를 만들기
반복 실행하게 하기
→ 0.5f 마다 makeSquare 함수를 실행!
void Start()
{
InvokeRepeating("makeSquare", 0.0f, 0.5f);
}
void makeSquare()
{
Debug.Log("반복한다!");
}
네모 만들기
→ square 프리팹을 받아서, 복제하기
public GameObject square;
void makeSquare()
{
Instantiate(square);
}
1) 시간 올리기
UI text 받기
using UnityEngine.UI;
public Text timeTxt;
시간 올리기
float alive = 0f;
void Update()
{
alive += Time.deltaTime;
timeTxt.text = alive.ToString("N2");
}
[판넬 만들기]
1) 게임 종료 판넬 만들기
Image 만들기
→ 사이즈 x: 450, y: 600
→ shadow 효과주기 : rgba ⇒ 255, 255, 0, 150
(Add Component로 추가!)
→ 그림자 위치는 x: 15, y: -15
로 맞추기
폰트 가져오기
[코드스니펫] 배달의민족 주아체
http://pop.baemin.com/fonts/jua/BMJUA_ttf.ttf
→ Fonts 폴더 만들고 끌어다넣기
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf440841-5349-4ef3-b30a-6a4e3cd9a845/Untitled.png)
3. 끝 메시지, 현재 스코어, 최고 스코어 만들기
→ 폰트 사이즈는 메시지는 50, 라벨은 40으로 해주세요!
→ position (-100, 100), (150, 100), (-100, 0), (150, 0)으로 맞춰주세요!
→ ctrl+d (복제) 를 이용하면 무척 편하답니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e3a20e24-4dfc-4150-8cb4-1ffb415b5aaa/Untitled.png)
4. retry 버튼 만들기
→ retryBtn 오브젝트 안에 만들게요!
→ 이미지 색은 `rgb ⇒ 80, 80, 200` 으로 할게요!
→ width: 300, height: 100
→ posY : -200
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4d01b527-7cf4-4328-a0b9-fa70ead600d2/Untitled.png)
5. 버튼에 `button` 속성 달고 Image 끌어다놓기
→ 그래야 클릭 할 때 color 틴트가 일어납니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/40e041f9-18bc-4402-b2e5-530955df6e1e/Untitled.png)
6. 우선, 판넬 전체를 숨겨둡니다.
→ `SetActive(true)` 로 나중에 켤 것이랍니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/44088764-b8e5-4a8d-b560-030ec83fc768/Untitled.png)
[판넬 나타내기]
1) gameManager 싱글톤 처리하기
public static gameManager I;
void Awake()
{
I = this;
}
2) 게임 종료하기
gameManager.cs
에 종료 함수 만들어두기
public GameObject endPanel;
public void gameOver()
{
Time.timeScale = 0.0f;
endPanel.SetActive(true);
}
square.cs
네모가 풍선과 부딪히면 게임 종료하기
→ 우선, 풍선에 "balloon"이라는 tag를 주기
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "balloon")
{
gameManager.I.gameOver();
}
}
Play
해서 확인하기
3) 현재 점수 보여주기
thisScoreText 가져오기
public Text thisScoreTxt;
gameOver() 수정하기
public void gameOver()
{
Time.timeScale = 0.0f;
thisScoreTxt.text = alive.ToString("N2");
endPanel.SetActive(true);
}
한걸음 더 : Update() 함수를 멈추게 하기
→ Update()와 gameOver() 간의 약간의 시간차가 있기 때문에, 이것을 제어해보겠습니다.
→ 그래야 timeTxt
와 thisScoreTxt
가 같은 값으로 나온답니다!
bool isRunning = true;
void Update()
{
if (isRunning)
{
alive += Time.deltaTime;
timeTxt.text = alive.ToString("N2");
}
}
public void gameOver()
{
isRunning = false;
Time.timeScale = 0.0f;
thisScoreTxt.text = alive.ToString("N2");
endPanel.SetActive(true);
}
4) 다시하기 만들기
gameManager.cs
- 다시하기 함수 만들기
using UnityEngine.SceneManagement;
public void retry()
{
SceneManager.LoadScene("MainScene");
}
시간을 다시 켜주기
→ 이렇게 다시 할 때에는 반드시 "시간"도 되돌려 놓아야 합니다!
void Start()
{
Time.timeScale = 1.0f;
InvokeRepeating("makeSquare", 0.0f, 0.5f);
}
다시하기 버튼에 retry() 함수 붙이기
1) 데이터를 보관하는 방법: PlayerPrefs
데이터 저장하기
PlayerPrefs.SetFloat("bestScore", 어떤숫자값);
PlayerPrefs.SetString("bestScore", 어떤문자열);
데이터 불러오기
어떤숫자값 = PlayerPrefs.getFloat("bestScore");
어떤문자열 = PlayerPrefs.getString("bestScore");
데이터를 저장했었는지 확인
→ 있으면 true
없으면 false
PlayerPrefs.HasKey("bestScore")
데이터를 모두 지우기
PlayerPrefs.DeleteAll();
2) 최고 점수 보여주기
로직 생각하기
if (최고 점수가 없으면)
{
최고점수 = 지금점수
}
else
{
if (최고점수 < 지금점수)
{
최고점수 = 지금점수
}
}
구현하기
public void gameOver()
{
isRunning = false;
Time.timeScale = 0.0f;
thisScoreTxt.text = alive.ToString("N2");
endPanel.SetActive(true);
if (PlayerPrefs.HasKey("bestScore") == false)
{
PlayerPrefs.SetFloat("bestScore", alive);
}
else
{
if (PlayerPrefs.GetFloat("bestScore") < alive)
{
PlayerPrefs.SetFloat("bestScore", alive);
}
}
}
최고 점수 띄워주기
public Text bestScoreTxt;
bestScoreTxt.text = PlayerPrefs.GetFloat("bestScore").ToString("N2");
1) 풍선이 터지면서
끝나게 하기
터지는 애니메이션(balloon_die) 만들고 balloon에 끌어다 놓고 add New Clip 해주기
0:20 에 기록하기
→ x:2, y:2
그리고 rgba ⇒ 255, 0, 0, 125
으로, 터지는 것처럼!
balloon animator를 열어서, idle → die로 transition 만들기
→ 마우스 오른쪽 클릭후 make transition 하면 됩니다!
Parameters에, bool 형식의 isDie
를 만들기
transition을 클릭하고 아래와 같이 세팅하기
→ has exit time 을 체크 해제해야 : 즉시 전환됩니다!
2) 풍선 애니메이션 전환하기
gameManager.cs
에서 - animator를 받기
public Animator anim;
gameOver()
할 때 isDie
값을 바꿔주기
public void gameOver()
{
anim.SetBool("isDie", true);
isRunning = false;
Time.timeScale = 0.0f;
thisScoreTxt.text = alive.ToString("N2");
endPanel.SetActive(true);
if (PlayerPrefs.HasKey("bestScore") == false)
{
PlayerPrefs.SetFloat("bestScore", alive);
}
else
{
if (PlayerPrefs.GetFloat("bestScore") < alive)
{
PlayerPrefs.SetFloat("bestScore", alive);
}
}
bestScoreTxt.text = PlayerPrefs.GetFloat("bestScore").ToString("N2");
}
확인하기 : 앗, 안된다!
→ 그 이유는, 애니메이션이 나올 틈이 없이 시간을 멈추기 때문
→ 0.5초 후에 시간을 멈추도록 Invoke 로 처리하기!
public void gameOver()
{
anim.SetBool("isDie", true);
isRunning = false;
Invoke("timeStop", 0.5f);
thisScoreTxt.text = alive.ToString("N2");
endPanel.SetActive(true);
if (PlayerPrefs.HasKey("bestScore") == false)
{
PlayerPrefs.SetFloat("bestScore", alive);
}
else
{
if (PlayerPrefs.GetFloat("bestScore") < alive)
{
PlayerPrefs.SetFloat("bestScore", alive);
}
}
bestScoreTxt.text = PlayerPrefs.GetFloat("bestScore").ToString("N2");
}
void timeStop()
{
Time.timeScale = 0.0f;
}
현재 상황
→ "뜨악" 시간이 지나면 네모가 계속 쌓이고 있었네요..!
→ 화면을 넘어가면 square를 Destroy 해줄 수 있을까요?
이렇게 되면 완성!
→ 화면에 보여지는 네모와 square(clone)
수가 일치하면 완성!
힌트요정 - 👻
→ square.cs
만 수정하면 된답니다!
→ Update() 안에 딱 세 줄만 넣으면 됩니다! 딱 5분만 더 해보면 될 거예요!
→ y좌표 구하기 ⇒ transform.position.y
기억나시죠!
→ 없애라 ⇒ Destroy(gameObject)
기억나시죠!
Update()
{
if (만약에 y좌표가 -5.0f 보다 작다면)
{
없애라;
}
}
square.cs
코드
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class square : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
float x = Random.Range(-3.0f, 3.0f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector3(x, y, 0);
float size = Random.Range(0.5f, 1.5f);
transform.localScale = new Vector3(size, size, 1);
}
// Update is called once per frame
void Update()
{
if (transform.position.y < -5.0f)
{
Destroy(gameObject);
}
}
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "balloon")
{
gameManager.I.gameOver();
}
}
}
[스파르타코딩클럽] 게임개발 종합반 - 1주차
[수업 목표]
[목차]
1) 게임개발종합반 수업의 목표와 범위
"어차피 게임 개발자들도 모든 유니티 코드를 외우고 있지 않습니다.
결국, 어떻게 동작하는지 대략적인 기능을 이해하고,
내가 필요한 부분을 찾아서 만들 수 있는 단계로 오르는 것이 중요합니다."
2) raindrop - 친환경 게임: 빗물 받는 르탄이
3) 5주 강의 구성
빗물 받는 르탄이
: 유니티 세팅, 기초 문법 연습풍선을 구해라! 백만 다운로드 게임 따라만들기
: 유니티 기초 복습고양이 밥주기 게임
: hp바, 레벨 연습하기르탄이 카드 뒤집기 게임
: 보드 게임 기초 구현하기주변 기능 학습
: 스플래시 화면 구성, 광고붙이기, 배포하기, 무료 에셋 구경하기4) 오늘 만들 순서
(1) 유니티 - 기본 세팅, 씬 구성하기
(2) 캐릭터 왔다 갔다 하게 하기 + 클릭 시 방향 전환 구현
(3) 비 내리기 구현
(4) 비 충돌 구현
(5) UX (남은 시간 / 숫자합) 구현
(6) 게임 오버(팝업) 구현
1) OS별 각각의 다운로드 링크(Window / Mac)를 클릭해서 Unity-hub를 다운받습니다.
[코드스니펫] (Window) 유니티허브 다운로드
https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe?_ga=2.197600431.1066071928.1631537679-831002153.1627910894
[코드스니펫] (Mac) 유니티허브 다운로드
https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.dmg
2) 로그인 진행
로그인하기 클릭 후 회원가입 진행 → 구글로 로그인
3) 라이선스 발급받기
라이선스 관리 클릭
새 라이선스 활성화 클릭 → Personal → 완료
아래와 같은 화면이 나오면 발급 완료!
4) 유니티 설치하기
뒤로가기 눌러서 메인으로 돌아온 뒤 → 설치 → '추가' 클릭
아래 화면에서 '다음' 클릭
VisualStudio 클릭 + Android build supprt 클릭
맥의 경우 'Mac build support' / 윈도우의 경우 'Windows build support' 클릭
(모듈은 추후에도 추가할 수 있으니 잘못 체크했을까봐 너무 걱정 마세요!)
동의 → 동의 → 완료 클릭. 꽤 오랜 시간 (최장 20~30분까지) 기다리면 설치 완료!
마지막으로, 한국어 세팅하면 끝!
1) 유니티에서 개발하기
게임 개발에 최적화된 개발 환경. 특히 2D 게임은 거의 100% 유니티로 개발한 것으로 생각하면 됨. 최근엔 대놓고 unity 로고를 보이는 게임들도 많음. 그림판 vs 포토샵.
프로젝트 생성 후 window → 아래와 같이 뷰 환경을 세팅!
project → 오른쪽 클릭 → one column layout 클릭
2) 각각 뭐 하는 뷰일까?
3) 배경 세팅하기
메인 씬 이름 바꾸기
→ project에서 오른쪽 클릭 후 MainScene으로 변경
Game 씬 사이즈 바꾸기
→ +
버튼을 클릭하고 760 x 1280 Phone을 입력 → Phone 으로 변경
배경 입히기
→ 2D Object → Sprite → Square 클릭 → background로 이름 바꾸기
→ 색을 255,255,220,255
로 맞추기
→ Scale을 X:6, Y:10
으로 맞추기
4) UI박스(점수) 세팅하기
검은색 박스 만들기
→ 2D Object → Sprite → Square 클릭 → ground로 이름 바꾸기
→ 색을 50, 50, 50, 255
로 맞추기
→ Scale을 X:6, Y:1.5
으로 맞추기 + Position은 Y:-4.3
으로 맞추기
→ order in layer를 1로 맞추기
'에셋'에 캐릭터 넣어두기
→ Assets 에서 Images 폴더 생성 → 르탄이 이미지 압축 풀고 끌어다놓기
3. 르탄이 캐릭터 만들기
→ 2D Object → Sprite → Square 클릭 → rtan으로 이름 바꾸기
→ Sprite 부분에 르탄이1 이미지 끌어다놓기
→ Order in Layer를 1로 바꾸기
→ position `Y:-2.9`
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/93acc45d-b094-43e3-ad72-bb05e3781ee6/Untitled.png)
5) 간단한 애니메이션을 입혀보기
애니메이션 폴더 만들기 (Asset → Animation)
애니메이션 파일을 만들고, Loop Time에 체크
이것을 만들어둔 르탄 캐릭터에 sprite에 끌어다놓기
Controller가 생긴 것을 확인!
→ Controller는 : Animation을 컨트롤 하는 것 (예 - 보통 상태 / 맞을 때 / 뛸 때 어떤 애니메이션을 써라)
→ Animation은 : 동작 파일
6) 기본 Animation 만들어보기
rtan_run.anim
더블 클릭 후 → 르탄이 캐릭터 클릭
르탄이1, 2파일을 적당한 시간 간격으로 끌어다두기
Animator 간단 설명
→ 시작하면 무조건 rtan_run을 실행하게 되어있고
→ rtan_run은 끝이 없는 애니메이션임
실행해보면, 움직인다!
1) 먼저 세팅하기 : Visual Studio
윈) Edit → Preferences → External Tools → Visual Studio Community 2019로 맞추기
맥) Unity → Preferences → External Tools → Visual Studio for mac
2) 캐릭터에 코딩을 더하는 법
→ 유니티에서는, 캐릭터가 코드를 갖고 있을 수 있음
→ 즉, 캐릭터에 코드를 입히는 것. "너는 태어날 때 여기서 태어나고, 매 순간 이렇게 작동해라"
→ 가장 중요한 두 가지 함수가 있음. start (너는 태어날 때) / update (매 순간 이렇게 해라)
3) Script 만들기
→ Assets 우클릭 → Create → Folder (이름 Scripts) → Create → C# script (이름 rtan)
→ C#은? Microsoft가 개발한 코딩 개발 언어. 희한하게 유니티에서만 주류로 쓰이고 있다.
4) 캐릭터 좌우 움직임 코딩하기
1) 캐릭터 오른쪽으로 이동하기
→ 아래와 같이 입력하고 캐릭터에 스크립트를 끌여다 놓기
[코드스니펫] 캐릭터 오른쪽으로 이동하기
void Update()
{
transform.position += new Vector3(0.05f, 0, 0);
}
```csharp
void Update()
{
transform.position += new Vector3(0.05f, 0, 0);
}
```
→ Play 버튼을 눌러보기 (캐릭터가 오른쪽으로 이동한다!)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/40d4b96e-31cf-42d1-9270-5fe26045e685/Untitled.png)
→ transform의 의미: 캐릭터의 위치와 중, position을 계속 바꿔달라는 것
→ `transform.position += new Vector3(0.05f, 0, 0);`
→ 트랜스폼 안의 포지션을, Vector3 방향으로 계속 더해주세요
→ float 란? 소수점을 나타내는 자료형. 즉, 소수를 쓰고 싶으면 뒤에 f 를 붙여줘야 함
→ 위에 변수를 선언해서 이렇게 쓸 수도 있음!
```csharp
float direction = 0.05f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.position += new Vector3(direction, 0, 0);
}
```
- 2) 캐릭터가 벽에 닿으면 다른 방향을 보게 하기
→ 2-0) Debug.Log 로 위치 보기
- **[코드스니펫] Debug.log**
```csharp
Debug.Log(transform.position.x);
```
```csharp
Debug.Log(transform.position.x);
```
C#
→ 2-1) 760보다 클 때 다른 방향 보게 하기
- **[코드스니펫] 760보다 클 때 다른 방향 보게 하기**
```csharp
float direction = 0.05f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (transform.position.x > 2.8f)
{
direction = -0.05f;
}
transform.position += new Vector3(direction, 0, 0);
}
```
```csharp
float direction = 0.05f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (transform.position.x > 2.8f)
{
direction = -0.05f;
}
transform.position += new Vector3(direction, 0, 0);
}
```
→ 2-2) 0보다 작을 때 다른 방향 보게 하기
```csharp
float direction = 0.05f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (transform.position.x > 2.8f)
{
direction = -0.05f;
}
if (transform.position.x < -2.8f)
{
direction = 0.05f;
}
transform.position += new Vector3(direction, 0, 0);
}
```
→ 📝(직접 해보기) 2-3) 방향 전환하기
(힌트1: "유니티 2d 좌우반전하기"로 검색)
(힌트2: 이런 코드를 만나면 굿! `transform.localScale = new Vector3(-1, 1, 1);` )
- **[코드스니펫] 방향 좌우반전하기**
```python
transform.localScale = new Vector3(-1, 1, 1);
```
- 펼치면 답!
```csharp
float direction = 0.05f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (transform.position.x > 2.8f)
{
direction = -0.05f;
transform.localScale = new Vector3(-1, 1, 1);
}
if (transform.position.x < -2.8f)
{
direction = 0.05f;
transform.localScale = new Vector3(1, 1, 1);
}
transform.position += new Vector3(direction, 0, 0);
}
```
5) 클릭 시 움직임 바꾸기
위의 코드를 조금만 예쁘게 다듬고,
float direction = 0.05f;
float toward = 1.0f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (transform.position.x > 2.8f)
{
direction = -0.05f;
toward = -1.0f;
}
if (transform.position.x < -2.8f)
{
direction = 0.05f;
toward = 1.0f;
}
transform.localScale = new Vector3(toward, 1, 1);
transform.position += new Vector3(direction, 0, 0);
}
마우스 클릭하면 → 움직이는 방향/이미지 방향 바꾸기
[코드스니펫] 마우스 클릭시 방향 바꾸기
if (Input.GetMouseButtonDown(0))
{
toward *= -1;
direction *= -1;
}
```csharp
if (Input.GetMouseButtonDown(0))
{
toward *= -1;
direction *= -1;
}
```
[빗방울 내리게 하기]
1) 빗방울 특징
2) 빗방울 그리기
Sprite → Circle 클릭 → rain 으로 이름 바꾸기
→ 색을 150,150,255,255
으로 맞추기
→ Position Y:4
세팅하기
3) 빗방울 떨어지게 하기
rigidbody 2D를 달아 중력의 영향을 받게 하기
4) 땅에 닿으면 없어지게 하기(충돌 세팅)
circle collider 2d를 달고, 반경 조정. 자세히 보면 초록색 선이 보임!
바닥에도 box collider 2d를 달아주기
게임을 실행하면 땅과 충돌을 합니다.
5) 땅에 닿으면 없어지게 하기(충돌 조작)
"땅"인지 알 수 있게, ground 라고 tag를 주기
땅에 닿았는지 확인하기
→ rain
스크립트 만들고, 빗방울에 붙이기
→ OnCollisionEnter2D
함수는 다른 콜라이더에 부딪혔을 때 실행되는 내장함수
→ coll (부딪힌 것의) tag 가 ground 이면!
[코드스니펫] 땅에 닿았는지 확인하기
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "ground")
{
Debug.Log("땅이다!");
}
}
```csharp
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "ground")
{
Debug.Log("땅이다!");
}
}
```
3. 비가 없어지게 하기
→ Debug.Log 대신, `Destroy(gameObject)`
→ gameObject는 나 자신
- **[코드스니펫] 비가 없어지게 하기**
```python
Destroy(gameObject);
```
```csharp
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "ground")
{
Destroy(gameObject);
}
}
```
[빗방울 랜덤하게 나타나게 하기]
1) 랜덤하게 위치 잡아주기
start() 함수에 랜덤 position 세팅하기
void Start()
{
float x = Random.Range(-2.7f, 2.7f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector3(x, y, 0);
}
2) 랜덤하게 사이즈(큰/중간/작은) 잡아주기
어떤 사이즈로 나올지 생각하고 →
사이즈 변경: transform.localScale = new Vector3(size, size, 0);
색 변경: GetComponent<SpriteRenderer>().color = new Color(100 / 255f, 100 / 255f, 255 / 255f, 255 / 255f);
(참고 : 255.0f 로 나눠주는 게 핵심!)
int type;
float size;
int score;
// Start is called before the first frame update
void Start()
{
float x = Random.Range(-2.7f, 2.7f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector3(x, y, 0);
type = Random.Range(1, 4);
if (type == 1)
{
size = 1.2f;
score = 3;
GetComponent<SpriteRenderer>().color = new Color(100 / 255f, 100 / 255f, 255 / 255f, 255 / 255f);
}
else if (type == 2)
{
size = 1.0f;
score = 2;
GetComponent<SpriteRenderer>().color = new Color(130 / 255f, 130 / 255f, 255 / 255f, 255 / 255f);
}
else
{
size = 0.8f;
score = 1;
GetComponent<SpriteRenderer>().color = new Color(150 / 255f, 150 / 255f, 255 / 255f, 255 / 255f);
}
transform.localScale = new Vector3(size, size, 0);
}
[코드스니펫] type이 1일 때
size = 1.2f;
score = 3;
GetComponent<SpriteRenderer>().color = new Color(100 / 255f, 100 / 255f, 255 / 255f, 255 / 255f);
[코드스니펫] type이 2일 때
size = 1.0f;
score = 2;
GetComponent<SpriteRenderer>().color = new Color(130 / 255f, 130 / 255f, 255 / 255f, 255 / 255f);
[코드스니펫] type이 3일 때
size = 0.8f;
score = 1;
GetComponent<SpriteRenderer>().color = new Color(150 / 255f, 150 / 255f, 255 / 255f, 255 / 255f);
[빗방울 계속 나오게 하기]
1) GameManager 만들기
예) 점수 / 다시 시작 / 3번 째 다시 시작에 부스터 / 광고보기 등
빈 곳에 object를 만들고 "gameManager"로 만들어둡니다.
마찬가지로 스크립트도 만들어 붙입니다. (어떻게 알았는지 아이콘 모양이 다르네요!)
2) 빗방울 복제하기 - Prefabs
프리팹 구현하기 (폴더 만들고 끌어다 놓기 & 오브젝트는 삭제)
3) 빗방울 복제하기 - Instantiate
gameManager에서 : 빗방울을 받기 + 프리팹 끌어다놓기
public GameObject rain;
0.5초마다 한번씩 실행되는 코드
[코드스니펫] InvokeRepeating 함수
InvokeRepeating("makeRain", 0, 0.5f);
[코드스니펫] makeRain 함수
void makeRain()
{
Debug.Log("비를 내려라!");
}
```csharp
void Start()
{
InvokeRepeating("makeRain", 0, 0.5f);
}
void makeRain()
{
Debug.Log("비를 내려라!");
}
```
3. 빗방울 프리팹을 복제하기
- **[코드스니펫] Instantiate 함수**
```csharp
Instantiate(rain);
```
```csharp
void makeRain()
{
Instantiate(rain);
}
```
1) 점수 보드 만들기
폰트 적용하기
→ Assets에 fonts 폴더 만들고 옮겨두기
[코드스니펫] 배민-한나체
http://pop.baemin.com/fonts/hanna11yrs/BMHANNA_11yrs_ttf.ttf
Sprite vs UI 그리고 Canvas
→ UI → Text
클릭 → 아래 설정을 따라하기 (폰트사이즈, 위치 등)
text를 네 번 복사→붙여넣기 해서 아래와 같이 맞추기
2) gameManager - 싱글톤 화
[코드스니펫] 싱글톤 화
public static gameManager I;
void Awake()
{
I = this;
}
```csharp
public static gameManager I;
void Awake()
{
I = this;
}
```
3) gameManager - 점수 올라가는 함수 만들기
int totalScore = 0;
public void addScore(int score)
{
totalScore += score;
}
4) 빗방울 - 캐릭터에 맞으면 점수 올라가게 하기
캐릭터에 tag 주기 + collider 주기
빗방울 - 캐릭터에 맞으면 점수 올라가고 + 사라지기
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "ground")
{
Destroy(gameObject);
}
if (coll.gameObject.tag == "rtan")
{
gameManager.I.addScore(score);
Destroy(gameObject);
}
}
gameManager - addScore 함수에 Debug.Log를 걸어서 확인 → 잘된다!
5) gameManager - 올라가는 점수 표기하기
UI Text 받기
using UnityEngine.UI;
public Text scoreText;
Text 바꿔주기
public void addScore(int score)
{
totalScore += score;
scoreText.text = totalScore.ToString();
}
처음 스코어는 0으로 만들어주기
1) Retry
판넬 만들기
정리하기 : 아래와 같이 세팅하기
→ image 사이즈: 400 / 250
→ txt 사이즈: 80
→ 글자 색상 (255, 255, 255, 255)
→ 배경 색상 (232, 52, 78, 255)
→ Inactive로 만들어두기
2) gameManager - 시간이 가게 하기
시간이 흐르게 하기
void Update()
{
limit -= Time.deltaTime;
timeTxt.text = timeLimit.ToString("N2");
}
멈추게 하기
void Update()
{
limit -= Time.deltaTime;
if (limit < 0)
{
Time.timeScale = 0.0f;
limit = 0.0f;
}
timeTxt.text = timeLimit.ToString("N2");
}
3) 0초에 Retry 판넬 나오게하기
Panel 받기
public GameObject panel;
Panel 나오게 하기
void Update()
{
limit -= Time.deltaTime;
if (limit < 0)
{
limit = 0.0f;
panel.SetActive(true);
Time.timeScale = 0.0f;
}
timeText.text = limit.ToString("N2");
}
4) 판넬 클릭하면 다시 시작하게 하기
판넬에 button 달기
씬 불러오는 것은 중앙에서 해야할 일! (gameManager.cs)
using UnityEngine.SceneManagement;
public void retry()
{
SceneManager.LoadScene("MainScene");
}
클릭하면 작동 할 함수 만들기 (panel.cs)
[코드스니펫] panel.cs
public void retry()
{
gameManager.I.retry();
}
```csharp
public void retry()
{
gameManager.I.retry();
}
```
4. onclick 연결하기
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8f3c7d45-7553-4b37-a00f-766acd4c1427/Untitled.png)
5) gameManager - 초기화 함수를 만들기
초기화 해야 할 요소들
→ timeScale, timeLimit, totalScore
void Start()
{
InvokeRepeating("makeRain", 0, 0.5f);
initGame();
}
void initGame()
{
Time.timeScale = 1.0f;
totalScore = 0;
limit = 30.0f;
}
6) 수업 전체 코드
사이즈는 0.8
로 해주세요!
색은 new Color(255 / 255.0f, 100.0f / 255.0f, 100.0f / 255.0f, 255.0f / 255.0f);
이렇게!
이렇게 되면 완성!
힌트요정 - 👻
→ rain.cs
만 수정하면 된답니다!
→ type = Random.range( ..
여기부터, if else
까지!
new Color
new Color(255 / 255.0f, 100.0f / 255.0f, 100.0f / 255.0f, 255.0f / 255.0f);
rain.cs
코드
[유머] 만우절 장난치는 여친