사전캠프 11일차
오늘은 스레드 앱 데이터 CRUD 사이클과 API에 대해 학습했다.
그리고 API를 활용한 실습까지 한가지 완료했다.
✅ 소스코드 제거
FirebaseFirestore.instance.collection('feeds').get().then((value) { print(value.docs.length); });
- 테스트용으로 넣어 둔 소스코드를 제거한다.
📌 ThreadFeedWriteController FirebaseFirestore instance 설정
late CollectionReference feedsCollectionRef; // void onInit() { super.onInit(); feedsCollectionRef = FirebaseFirestore.instance.collection('feeds'); }✅ 스레드 앱에 스레드를 등록하는 과정을 통해 파이어 베이스 데이터베이스로 데이터를 저장한다.
feed를 등록하는ThreadFeedWriteController에 파이어 베이스 데이터베이스를 사용할 수 있도록onInit함수에서 설정한다.
💡GetxController에onInit함수는 라이프 사이클의 가장 처음 호출되는 메서드이다.
controller가context에 등록이 될 때 onInit이 불린다고 생각하면 된다.
📌 save 함수 데이터베이스 저장
void save() { var feedModel = FeedModel( contents: contents, images: selectedImages?.map<File>((e) => File(e.path)).toList() ?? [], ); feedsCollectionRef.add(feedModel); // Get.back(result: feedModel); }✅ 이제 변수
feedsCollectionRef로 데이터베이스에 연결하여 사용할 수 있도록 했다.
이제 실제적으로 저장되는 함수인 save 함수에서 feed 데이터를 저장한다.
이 소스코드 한 줄로 데이터베이스에 저장된다.
하지만 저장을 시도하면Map<Stirng, dynamic>으로 저장해야 하는데FeedModel로 저장하고 있다는 오류가 발생한다.
📌 FeedModel 형변환
import 'dart:io'; import 'package:uuid/uuid.dart'; // class FeedModel { String id; String contents; List<File> images; DateTime createdAt; // FeedModel({ required this.contents, required this.images, }) : id = Uuid().v4(), createdAt = DateTime.now(); // Map<String, dynamic> toMap() { return { 'id': id, 'contents': contents, 'createdAt': createdAt.toIso8601String(), }; } }✅ 오류를 해결하기 위해
FeedModel을Map<String, dynamic>형태로 형 변환해준다.
📌 save함수 저장시 toMap 사용
void save() { var feedModel = FeedModel( contents: contents, images: selectedImages?.map<File>((e) => File(e.path)).toList() ?? [], ); feedsCollectionRef.add(feedModel.toMap()); // Get.back(result: feedModel); }✅ createdAt의 경우 createdAt인 DateTime 클래스로 저장할 수 없기 때문에 toIso8691String()으로 변환해서 저장해야한다.
feedsCollectionRef에add할 때 feedModel을 사용하는 대신toMap함수를 호출해서 Map 타입으로 변환된 데이터를 저장시키도록 처리 한 것이다.
📌 HomeFeedListcontroller 조회 메서드 구현
late CollectionReference feedsCollectionRef; // void onInit() { super.onInit(); feedsCollectionRef = FirebaseFirestore.instance.collection('feeds'); loadAllFeeds(); } // void loadAllFeeds() async { var feedData = await feedsCollectionRef.get(); feedList = feedData.docs .map<FeedModel>( (data) => FeedModel.fromJson(data.data() as Map<String, dynamic>)) .toList(); update(); }✅ HomeFeedListcontroller에서도 feedsCollection을 사용할 수 있도록 onInit 함수에서 초기화를 해줬으며, 해당
feedsCollectionRef변수를 통해서 데이터베이스의 데이터를 불러올 것 이다.
onInit함수 마지막 부분에loadAllFeeds함수를 호출하게 해서 앱이 실행 시에도 데이터를 자동으로 채워질 수 있도록 처리한다.
feedsCollectionRef에get함수를 통해 documents 들을 불러왔으며, 해당 documents들을map함수를 사용해서FeedModel로 다시 형 변환을 해주는 작업한다.
📌 FeedModel에 fromJson 함수 구현
import 'dart:io'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:uuid/uuid.dart'; // class FeedModel { String id; String contents; List<File> images; DateTime createdAt; // FeedModel({ String? id, required this.contents, required this.images, DateTime? createdAt, }) : id = id ?? Uuid().v4(), createdAt = createdAt ?? DateTime.now(); // factory FeedModel.fromJson(Map<String, dynamic> json) { return FeedModel( id: json['id'], contents: json['contents'], images: [], createdAt: DateTime.parse(json['createdAt']), ); } // Map<String, dynamic> toMap() { return { 'id': id, 'contents': contents, 'images': images, 'createdAt': createdAt.toIso8601String(), }; } }✅ fromJson이라는 함수가 아직 FeedModel에 구현되어 있지 않아서 오류가 발생하고 있어, 함수를 구현한다.
앱을 새로고침을 하게 되면 데이터가 휘발되지 않고, 클라우드 서버(파이어 베이스)로부터 데이터를 받아와 화면에 저장시킨 피드를 볼 수 있게 된다.
📌 addFeed 함수 삭제 후 reload함수로 변경
void reload() { feedList.clear(); loadAllFeeds(); }✅ 피드를 등록하고 나서 돌아왔을 때도 데이터 갱신이 이루어지도록 수정한다.
이전에는 피드 등록 후addFeed라는 함수로 저장시킨 데이터를 상태 관리 목적으로 배열에 담아주는 방식으로 처리했었는데, 그 부분을 수정한다.
📌 home 화면 reload 함수 호출
if (result != null) { Get.find<HomeFeedListcontroller>().reload(); }✅ 호출하던 화면 쪽에 오류가 발생하는데,
addFeed함수를 참조하고 있는데 방금 전 addFeed를 삭제했기 때문에 오류가 발생한다. 이 부분을reload로 변경한다.
📌 더보기 아이콘 클릭시 feedModel 전달
GestureDetector( onTap: () { _showCupertinoActionSheet(model); }, child: Icon( Icons.more_horiz, color: Color(0xff999999), ), )✅ 더보기 아이콘을 누를 때 id만 전달하고 있었지만, feedModel 전체를 전달하도록 수정한다.
📌 _showCupertinoActionSheet FeedModel 로 수정
void _showCupertinoActionSheet(FeedModel feedModel) { showCupertinoModalPopup( context: Get.context!, builder: (BuildContext context) => CupertinoActionSheet( actions: <CupertinoActionSheetAction>[ CupertinoActionSheetAction( onPressed: () async {}, child: Text('수정'), ), CupertinoActionSheetAction( onPressed: () { Navigator.pop(context); Get.find<HomeFeedListcontroller>().removeFeed(feedModel.id); }, isDestructiveAction: true, child: Text('삭제'), ), ], cancelButton: CupertinoActionSheetAction( onPressed: () { Navigator.pop(context); }, child: Text('취소'), ), ), ); }✅
_showCupertinoActionSheet함수에서도 FeedModel을 받아주도록 수정한다.
📌 _showCupertinoActionSheet 수정 로직 개발
CupertinoActionSheetAction( onPressed: () async { var result = await Get.to<FeedModel?>(ThreadWritePage(), binding: BindingsBuilder(() { Get.put(ThreadFeedWriteController(editFeedModel: feedModel)); })); if (result != null) { Get.find<HomeFeedListcontroller>().reload(); } }, child: Text('수정'), ),✅ 피드 수정이 작동되도록 수정한다.
📌 ThreadFeedWriteController 생성자 만들기
class ThreadFeedWriteController extends GetxController { ThreadFeedWriteController({this.editFeedModel}); final FeedModel? editFeedModel;✅
ThreadFeedWriteController를Getx로 의존성을 주입할 때, 생성자(constructor)에서editFeedModel을 받을 수 있게 해서editFeedModel이 있으면 수정 모드로, 그렇지 않으면 생성 모드로 처리 하도록 한다.
editFeedModel이nullable변수로 선언을 했기 때문에 기존 생성 로직에 지장이 없다.
📌 ThreadFeedWriteController onInit 함수 수정
void onInit() { super.onInit(); feedsCollectionRef = FirebaseFirestore.instance.collection('feeds'); if (editFeedModel != null) { contents = editFeedModel!.contents; } }✅
onInit함수에서editFeedModel에 따라content값을 추가한다.
피드 수정 버튼을 통해 수정 상태로 진입하는데, 페이지에서는 수정된 데이터를 기준으로 데이터를 채워 주고 있지 않아서, 화면이 수정할 내용이 채워지지 않은 상태가 되어있다.
📌 TextEditingController 생성
class ThreadFeedWriteController extends GetxController { ThreadFeedWriteController({this.editFeedModel}); final TextEditingController contentsTextController = TextEditingController();✅ 위의 문제를 수정해준다.
📌 TextEditingController 에 text 값으로 수정
void onInit() { super.onInit(); feedsCollectionRef = FirebaseFirestore.instance.collection('feeds'); if (editFeedModel != null) { contents = editFeedModel!.contents; contentsTextController.text = contents; } }📌 thread_write_page 수정모드 대응 TextField 수정
TextField( controller: Get.find<ThreadFeedWriteController>() .contentsTextController, cursorHeight: 16, decoration: InputDecoration( isDense: true, hintText: '새로운 소식이 있나요?', hintStyle: TextStyle( color: Color(0xff9a9a9a), fontSize: 14, ), contentPadding: EdgeInsets.zero, border: InputBorder.none, ), onChanged: (value) { Get.find<ThreadFeedWriteController>() .setContent(value); }, ),✅
TextField의 경우 초깃값을 채우기 위해서는TextEditingController를 통해 넣을 수 있다.
ThreadFeedWriteController클래스 내의 변수로 생성한다.
📌 save 함수 수정
void save() async { var feedModel = FeedModel( id: editFeedModel?.id, contents: contents, images: selectedImages?.map<File>((e) => File(e.path)).toList() ?? [], ); if (editFeedModel != null) { var doc = await feedsCollectionRef.where('id', isEqualTo: feedModel.id).get(); feedsCollectionRef.doc(doc.docs.first.id).update(feedModel.toMap()); } else { feedsCollectionRef.add(feedModel.toMap()); } // Get.back(result: feedModel); }✅
editFeedModel의 값이 있고 없고 차이로add를 할지update를 할지 분기 처리로 개발한다.
update의 경우 document의id값을 알아야 수정할 수 있다.
업데이트 처리 전feedModel.id를 통해 피드를 조회하고 조회된 document의 id를 추출해서 updqte를 처리하면 된다. 피드를 수정하면 정상적으로 작동한다.
💡 초기 모델 설계를docId까지 모두 저장시키는 방법도 있다.
그렇게 되면 업데이트 시 불필요한 데이터 조회를 줄일 수 있다.
📌 bottomSheet 함수 수정
onPressed: () async { var result = await Get.to<FeedModel?>(ThreadWritePage(), binding: BindingsBuilder(() { Get.put(ThreadFeedWriteController(editFeedModel: feedModel)); })); Get.back(); // 추가 if (result != null) { Get.find<HomeFeedListcontroller>().reload(); } },✅ 하지만 수정하고 돌아왔는데 bottomSheet가 여전히 활성화되어 있다.
위와 같이 소스코드를 수정해주면 정상적으로 처리된다.
📌HomeFeedListController 데이터베이스 Delete 개발
void removeFeed(String feedId) async { var doc = await feedsCollectionRef.where('id', isEqualTo: feedId).get(); feedsCollectionRef.doc(doc.docs.first.id).delete(); feedList.removeWhere((feed) => feed.id == feedId); update(); }✅ Update를 진행했던
BottomSheet에 삭제 버튼을 눌러home_feed_list_controller에 삭제 프로세스를 타게 될 텐데 그 부분을 수정해주면 된다.
업데이트와 마찬가지로 documentId가 필요하므로get데이터를 하고delete를 한다.
📚 API(Application Programming Interface)
소프트웨어 애플리케이션이 서로 통신하고 데이터를 공유할 수 있도록 하는 규칙, 프로토콜이다.
보통 서버와 클라이언트 간 소통을 목적으로 진행되며, 클라이언트는 서버 내부를 몰라도 api 규칙에 맞춰 요청을 하면 원하는 데이터를 얻을 수 있다.
이를 통해 개발 프로세스를 단순화하고, 협업을 촉진하며, 다양한 플랫폼 간의 통합을 지원한다.
📚 API 구성 요소와 개념
📕 Methods
✔GET(데이터 검색),POST(새 데이터 생성),PUT(기존 데이터 업데이트),DELETE(데이터 제거)등 API를 사용하여 수행할 수 있는 작업이다.
📙 Request
✔ 클라이언트(애플리케이션)가API에 요청하여, 원하는 메서드, 엔드포인트 및 필요한 데이터를 지정한다. 클라이언트가 의도를 API에 전달하는 방법이다.
📒 Response
✔ 요청을 처리한 후 API는 응답을 다시 클라이언트로 보낸다. 이 응답에는 일반적으로 요청된 데이터, 성공 또는 실패를 나타내는 상태 코드 및 관련 오류 메시지가 포함된다.
📗 데이터 형식
✔ API는 종종 클라이언트와 API 간에 데이터를 교환하기 위해JSON(JavaScript Object Notation)또는XML(eXtensible Markup Language)과 같은 표준 데이터 형식을 사용한다.
🔍 플러터에서 API 통신을 할 수 있도록 가장 기본이 되는 http 라이브러리부터 dio까지 제공하고 있다.
http 라이브러리를 활용해서 간단한 api 통신으로 데이터를 받아와 화면에 그린다.
📕 Get
http.get
✔ GET 메서드는 리소스에 대한 정보를 검색하는 데 사용된다.
읽기 전용 작업이며, 리소스를 수정하거나 부작용이 없어야 한다.
GET 요청을 사용할 때, 필요한 매개변수는 일반적으로 URL의 쿼리 문자열을 통해 전달된다.
✔ http 패키지의Get함수//http 패키지의 정의된 get함수 Future<Response> get(Uri url, {Map<String, String>? headers}) //사용 예제 http.get('https://sample.com/posts', headers : {'Authorization' , 'Auth Key'});✔
url: 요청할 url
✔header:http.get함수 호출시 요청 헤더에 필요한 정보를 담을 수 있다.
보통 인증 정보나content-type과 같은 내용을 담아 요청한다.
📙 Post
http.post
✔ POST 메서드는 새 리소스를 만드는 데 사용된다.
일반적으로 서버가 처리하고 새 리소스를 생성하는데 사용하는 요청 본문에 데이터를 보내야한다.
이에 대한 응답으로 서버는 일반적으로 생성된 리소스 또는 확인 메시지를 반환한다.
✔ http 패키지의post함수//http 패키지의 정의된 post함수 Future<Response> post( Uri url, { Map<String, String>? headers, Object? body, Encoding? encoding, }) //사용 예제 http.post('https://sample.com/post', headers : {'Authorization' , 'Auth Key'}, body : {'name':'개남'})✔
url: 요청할 url
✔header:http.post함수 호출시 요청 헤더에 필요한 정보를 담을 수 있다.
보통 인증 정보나content-type과 같은 내용을 담아 요청한다.
✔body: 새로운 데이터를 생성할 정보를 담아 전달 할 수 있다.
📒 Put
http.put
✔ PUT 메서드는 기존 리소스를 새 데이터로 업데이트하는데 사용된다.
POST 메서드와 마찬가지로 일반적으로 요청 본문에 데이터를 보내야 한다.
서버는 데이터를 처리하고 그에 따라 리소스를 업데이트한다.
PUT 메서드는 동일한 요청을 여러 번 수행해도 동일한 결과가 나타난다.
✔ http 패키지의put함수//http 패키지의 정의된 put함수 Future<Response> put( Uri url, { Map<String, String>? headers, Object? body, Encoding? encoding, }) //사용 예제 http.put('https://sample.com/post/1', headers : {'Authorization' , 'Auth Key'}, body : {'name':'개남'})✔
url: 요청할 url
✔header:http.put함수 호출시 요청 헤더에 필요한 정보를 담을 수 있다.
보통 인증 정보나content-type과 같은 내용을 담아 요청한다.
✔body: 업데이트할 데이터를 담아 전달한다.
📗 Delete
http.delete
✔ DELETE 메서드는 리소스를 제거하는 데 사용된다.
요청 본문에 데이터를 보낼 필요가 없다.
서버는 요청을 처리하고, 리소스를 삭제하며, 일반적으로 확인 메시지 또는 상태 코드를 반환한다.
✔ http 패키지의delete함수//http 패키지의 정의된 delete함수 Future<Response> delete( Uri url, { Map<String, String>? headers, Object? body, Encoding? encoding, }) //사용 예제 http.delete('https://sample.com/post/1', headers : {'Authorization' , 'Auth Key'});✔
url: 요청할 url
✔header:http.delete함수 호출시 요청 헤더에 필요한 정보를 담을 수 있다.
보통 인증 정보나content-type과 같은 내용을 담아 요청한다.
📌 http패키지 설치
flutter pub add http✔ Terminal에 입력해 설치한다.
📌 빈 프로젝트 준비
import 'package:flutter/material.dart'; // void main() { runApp(const MyApp()); } // class MyApp extends StatelessWidget { const MyApp({super.key}); // Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'API Communication Sample'), ); } } // class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); // final String title; // State<MyHomePage> createState() => _MyHomePageState(); } // class _MyHomePageState extends State<MyHomePage> { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( '홈', ), ], ), ), ); } }
📌 JSONPlaceholder - Guide
✔https://jsonplaceholder.typicode.com/posts를 주소창에 입력하면, 자동으로 Get 매서드 된다.
✔
posts의 첫번째를 반환해달라는 의미이다.
https://jsonplaceholder.typicode.com/posts/1를 입력하면, 첫번째 정보가 넘어오는 것을 알 수있다.
📌 FutureBuilder 위젯 사용
class _MyHomePageState extends State<MyHomePage> { Future<String> fetchData() async { await Future.delayed(const Duration(milliseconds: 2000)); return '데이터 로드 완료'; } // Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: FutureBuilder<String>( future: fetchData(), builder: (BuildContext context, AsyncSnapshot<String> snapshot) { if (snapshot.hasData) { return Text(snapshot.data!); } else { return const Center( child: CircularProgressIndicator(), ); } }, ), ), ); } }✔
FutureBuilder는 플러터에서 api를 활용해서 화면에 보이도록 하는 위젯이다.
정확히는 api 활용 위젯은 아니고, 비동기식의 프로세스를 통해 화면에 갱신 처리를 할 수 있도록 도와주는 위젯이다.
이 소스코드를 살펴보면 단순히 fetchData 함수에서 2초가량 대기 후에 return으로 ‘데이터 로드 완료’를 보내주고 있습니다.
✔FutureBuilder를 사용하게 되면 위젯이 그려지는 순간builder가 호출되는데, 이때는snapshot에 데이터가 없다.
그렇기 때문에else문으로 분기되어 로딩 화면이 표시되는 것이다.
이후 2초 뒤에 ‘데이터 로드 완료’ 데이터를 반환해주게 되면, 다시 한번builder가 호출된다.
이때는hasData가 존재하기 때문에true분기로 진행되어data를 출력하게 된다.
📌 http Import
import 'package:http/http.dart' as http;
📌 http 패키지 이용하여 JSONPlaceholder호출
Future<String> fetchData() async { final url = Uri.parse('https://jsonplaceholder.typicode.com/posts'); final response = await http.get(url); print(response.body); return '데이터 로드 완료'; }✔ futureData 함수를 이용하여 api 통신을 한다.
✔ 앱을 재실행하면 로그로 데이터를 출력하고 있음을 확인할 수 있다.
📌 Post 모델 설계
class Post { final int id; final int userId; final String title; final String body; // Post({required this.id, required this.userId, required this.title, required this.body}); // factory Post.fromJson(Map<String, dynamic> json) { return Post( id: json['id'], userId: json['userId'], title: json['title'], body: json['body'], ); } }✔ 이 데이터를
fetchData반환할 때 전달하도록 한다.
원활하게 전달하기 위해 모델을 설계해서 전달하는 것이 좋다.
post_model.dart파일을 만들어 코드를 넣어준다.
📌 fetchData 모델로 데이터 변환
Future<List<Post>?> fetchData() async { final url = Uri.parse('https://jsonplaceholder.typicode.com/posts'); final response = await http.get(url); if (response.statusCode == 200) { List jsonResponse = json.decode(response.body); return jsonResponse.map((post) => Post.fromJson(post)).toList(); } }✔ 모델에
return하도록 한다.
📌 FutureBuilder 수정
body: Center( child: FutureBuilder<List<Post>?>( future: fetchData(), builder: (BuildContext context, AsyncSnapshot<List<Post>?> snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, index) { return ListTile( title: Text(snapshot.data![index].title), subtitle: Text(snapshot.data![index].body), ); }, ); } else { return const Center( child: CircularProgressIndicator(), ); } }, ), ),✔
return반환이String에서Post로 변경되었기 때문에,FutureBuilder부분에도 수정이 필요하다.
이제 4주차 마지막 강의 3-3 알람앱 + 날씨 API만 남았다.
생각보다 실습은 어렵지 않았으나, 나중에 홀로 할 때 헷갈리지 않을까 걱정된다.
스레드 앱 데이터 부분이 어려웠던 것 같다.
형 변환을 해준다던가, 한 곳을 바꾸면 나머지 다른 곳도 찾아서 바꿔줘야 된다던가 하는 연결성이 어려웠다.
그리고 API도 연결하는 방법, 규칙이 있다보니까 잘 살펴봐야 할 것 같다.
API는 이론 위주였던 것 같아서 생각보다 정리할 내용이 많았고, 실습이 적었지만 내용이 복잡했다.