15일차에는 정말 중요햔 http 통신과 비동기 처리에 대해 학습했다.
학습한 내용
- HTTP 통신
- http 패키지와 dio 패키지
- 동기와 비동기
- Future, async, await
http 패키지와 dio 패키지는 모두 HTTP 통신을 지원하는 패키지이다. 아래 링크를 통해 각 패키지의 자세한 내용을 확인할 수 있다.
- http 패키지
https://pub.dev/packages/http
- dio 패키지
https://pub.dev/packages/dio
동기(Synchronous): 동시에 일어나는
동기는 동시에 일어난다는 뜻으로 요청과 그 결과가 동시에 일어난다는 약속이다.
즉, 요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야 한다.
장점: 설계가 매우 간단하고 직관적이다.
단점: 결과가 주어질 때까지 대기해야 한다.
- 코드 예시
void main() { print("It's first"); print("I'm second"); print("Last one"); }
비동기(Asynchronous): 동시에 일어나지 않는
비동기는 동시에 일어나지 않는다는 뜻으로 요청과 결과가 동시에 일어나지 않는다.
즉, 요청한 자리에서 결과가 주어지지 않는다.
장점: 결과가 주어지는데 시간이 많이 걸려도 그 시간 동안 다른 작업을 수행할 수 있기 때문에 자원을 효율적으로 사용할 수 있다.
단점: 동기보다 설계가 복잡하다.
- 코드 예시
void main() { print("It's first"); Future.delayed(Duration(seconds: 0), () => print("I'm second")); print("Last one"); }
Flutter에서 비동기식을 사용하는 이유는 CRUD등의 외부 데이터를 조작하는 동안 메인에서 다른 작업을 수행할 수 있기 때문이다.
아래의 예시 코드를 통해 살펴보자
void main() {
getCoffee();
}
getCoffee() async {
String menu = await todayCoffee();
print("Today Coffee is $menu");
}
Future<String> todayCoffee() {
return Future.delayed(Duration(seconds: 1), () => "Latte");
}
// 결과 : 1초 뒤에 Today Coffee is Latte 출력
getCoffee()
에서 async
가 사용되었는데 이는 await
를 사용하기 위해 반드시 필요하다.
await
는 비동기 함수가 끝날 때까지 기다린다는 의미이다. await
가 끝난 뒤 다음 라인의 코드가 실행된다. 그렇기 때문에 1초 뒤에 "Today Coffee is Latte"가 출력되는 것이다.
(await
는 반환값이 Future
인 함수 앞에 쓰지만 Future
가 아니어도 사용 가능하고, async
가 있는데 await
를 사용하지 않는 것은 가능하다.)
만약 이처럼 동기로 요청과 처리를 그 자리에서 수행하지 않고 요청을 한 뒤 응답을 받기 전에 다른 코드를 실행하는 비동기식 처리를 하고 싶다면 Future
를 반환하는 함수에 await
를 사용하지 않으면 된다.
await
를 사용하지 않는 다면 해당 함수가 끝났을 때 Callback
함수를 사용하여 알릴 수 있다.
해당 포스팅에서는 간단히 동기, 비동기의 정의와 Future
await
async
의 사용법 정도만 작성했지만 비동기는 많은 내용을 가지고 있기 때문에 더 학습이 필요하다...(나중에 더 많은 내용을 포스팅 할 예정)
Authentication(인증)은 로그인과 같이 사용자가 권한이 주어진 사용자임을 인증 받는 것을 의미한다. Authorization(인가)은 사용자가 한 번 인증을 받은 후에 그 사용자가 특정 리소스에 엑세스 할 수 있는지 여부를 결정하는 것이다.
이러한 인가에서 주로 사용되는 방식은 세션(Session)이다. 세션은 세션 id를 통해서 사용자가 서버에 로그인이 지속되는 상태를 말하는데 아래와 같은 방식으로 인가를 한다.
세션의 인가 과정
1. 사용자가 로그인에 성공하면 세션을 발행한다.
2. 세션을 브라우저에 저장하고, 서버 메모리에도 저장한다.
3. 인가가 필요한 요청을 보낼 때 서버에 세션 값을 같이 보낸다.
4. 서버는 메모리에 저장된 값과 세션 값을 비교하여 맞는 값이 있으면 인가를 수행한다.
세션은 아래와 같은 단점을 가지고 있다.
세션의 단점
1. 세션은 서버에도 저장되어 있기 때문에 사용자가 동시 다중 접속 했을 때 메모리가 부족해 질 수 있다.
2. 서버가 재부팅하면 세션이 날아가 사용자가 다시 로그인을 해야한다.
3. 분산된 서버의 경우 세션 유지가 제대로 되지 않아서 서버 확장이 어렵다.
JWT는 JSON Web Token의 약자로 사용작 로그인하면 토큰을 발행하는데 세션과 다른점은 서버는 이 토큰을 저장하지 않는다는 점이다.
JWT는 아래와 같은 형태를 가진다.
JWT 예시
- 암호화된 3가지 데이터를 이어붙인 형태로 구성되어 있다.
- Header: 알고리즘, 타입 값이 들어간다.
- Payload: 토큰이 가지는 데이터이다.
- Signature: 헤더에 정의된 알고리즘을 통해 암호화한 비밀 값으로 서버만 알고 있다.
하지만 JWT는 세션처럼 모든 사용자들의 상태를 기억하고 있지 않기 때문에 대상들의 상태를 언제나 제어할 수 없다.
만약 하나의 기기에서만 로그인 가능한 서비스를 만들 경우 pc에서 로그인하면 핸드폰에서 세션값을 사용 못하게 하는 제어를 할 수 없고, 해커에게 토큰을 빼앗기면 무효화할 방법도 없다.
이를 해결하기 위해 accessToken
과 refreshToken
을 주는 방법을 사용한다.
- accessToken: 인가를 받을 때 사용하는 토큰(보통 수명이 짧다.)
- refreshToken: accessToken이 수명을 다했을 때 accessToken을 다시 발행 받기 위한 토큰(보통 2주 정도로 긴 수명을 가짐)
- HTTP 통신의 status code
- dio 패키지를 활용하여 스나이퍼팩토리의 비밀 URL 호출
- javascript로 작성된 백엔드 코드를 보고 문제 해결
- 주어진 URL에 데이터를 요청하여 결과물 제작 1
- 주어진 URL에 데이터를 요청하여 결과물 제작 2
HTTP status code는 특정 HTTP 요청이 성공적으로 완료되었는지 여부를 나타낸다. 이러한 status code는 아래와 같이 5개로 그룹화 시킬 수 있다.
- 100~199 (정보) : 요청을 받았으며 프로세스를 계속 진행합니다.
- 200~299 (성공) : 요청을 성공적으로 받았으며 인식했고 수용했습니다.
- 300~399 (리다이렉션) : 요청 완료를 위해 추가 작업 조치가 필요합니다.
- 400~499 (클라이언트 오류) : 요청의 문법이 잘못되었거나 요청을 처리할 수 없습니다.
- 500~599 (서버 오류) : 서버가 명백히 유효한 요청에 대한 충족을 실패했습니다.
각 요청에 맞는 status code의 자세한 내용은 아래의 링크를 참고
HTTP response status codes - MDN Web Docs - Mozilla
이러한 status code 중 200, 400, 404, 500, 503, 301, 303 총 7가지에 대해 살펴보자
요청이 성공했음을 의미한다. 정보는 요청에 따른 응답으로 반환되며, 주로 서버가 요청한 페이지를 제공했다는 의미로 사용된다.
잘못된 요청을 의미하며, 잘못된 문법의 요청으로 서버가 구문을 인식하지 못했거나 사기성이 있는 요청의 라우팅 등의 경우에 사용된다.
서버가 요청받은 리소스를 찾을 수 없음을 의미한다. 예를 들어 서버에 존재하지 않는 페이지에 대한 요청이 있을 경우 서버는 이 코드를 제공한다.
또는 API의 종점은 적절하지만 리소스 자체가 존재하지 않음을 의미할 수도 있다. 서버들은 인증받지 않은 클라이언트로부터 리소스를 숨기기 위해 이 응답을 403 대신 사용할 수도 있다.
403 Forbidden
클라이언트는 컨텐츠에 접근할 권리를 가지고 있지 않습니다. 서버는 클라이언트가 누구인지를 알고 있다.예를 들어 미승인된 클라이언트를 서버가 거절하기 위해 사용한다.
웹 사이트 서버에 문제가 있음을 의미하지만 서버는 정확한 문제에 대해 구체적으로 설명이 불가능하다. 즉, 서버에서 처리 방법을 알 수 없는 상황이 발생했음을 의미한다.
서버가 요청을 처리할 준비가 되지 않은 상태를 의미한다. 일반적인 원인은 유지보수를 위해 작동이 중단되거나 과부하가 걸린 서버이다.
이 응답은 응답과 함께 문제를 설명하는 사용자 친화적인 페이지가 전송되어야 한다. 또한 임시 조건에서 사용되어야 하며 Retry-After: HTTP
헤더는 가능하면 서비스를 복구하기 전 예상 시간을 포함해야 한다.
웹 마스터는 이러한 일시적인 조건 응답을 캐시하지 않아야 하기 때문에 응답과 함께 전송되는 캐싱 관련 헤더에도 주의를 기울여야 한다.
요청한 리소스의 URI가 영구적으로 변경되었음을 의미한다. 새로운 URI가 응답으로 제공될 수도 있다.
클라이언트가 요청한 리소스를 다른 URI에서 GET 요청을 통해 얻어야 하는 경우 이를 지시하기 위해 서버가 클라이언트로 직접 보내는 응답이다.
dio
패키지를 활용해 스나이퍼팩토리에 존재하는 비밀 URL을 찾아 호출하고자 한다. 비밀 URL은 아래와 같다.
"https://sniperfactory.com/sfac/http_{20부터 50사이정수}"
- ex) 0이라면, http_0으로 요청할 것
- 반드시 반복문을 통하여 해결할 것
dio
를 테스트 하기 위해 간단하게 테스트 버튼을 가진 UI를 만들었다.
dependencies:
dio: ^5.0.0
dependencies
에 dio
넣어 패키지를 설치했다.
import 'package:first_app/page/home_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// root Widget
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(), // 홈 페이지 호출
);
}
}
main.dart
에서는 HomePage
위젯을 호출한다.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
void getData() async {
var url = "https://sniperfactory.com/sfac/http_"; //URL
var dio = Dio();
for (var i = 20; i <= 50; i++) {
await dio
.get(url + i.toString())
.then((value) => print("$value \nurl은 ${url + i.toString()}입니다!"))
.catchError((_) {});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('15일차 과제'),
),
body: Center(
child: ElevatedButton(
onPressed: () => getData(),
child: Text('test 버튼'),
),
),
);
}
}
화면의 UI는 가운데 ElevatedButton
을 가지고 있다. 해당 버튼을 누르면 dio
로 페이지를 호출한다.
URL을 호출하는 함수는 getData()
로 작성했다. 우선 URL에서 뒤의 숫자만 빼고 url
변수에 저장하고 Dio
객체를 선언했다. 반복문을 사용하여 url
뒤에 숫자를 20부터 50까지 붙여 호출해 보았다. 요청은 get()
메소드를 사용했고 성공했을 경우 then()
을 통해서 가져온 값과 URL이 호출된다. 오류가 났을 경우에는 catchError
로 예외 처리를 하는데 해당 기능은 따로 추가하지 않았다.
위와 같은 화면을 가진 페이지에서 가운데 버튼을 클릭하면 콘솔에 아래와 같은 결과가 출력된다. (35번 페이지가 숨겨진 페이지였습니다!)
위의 코드에서는 따로 예외 처리를 하지 않았지만 만약 아래 코드처럼 catchError
에 에러를 출력해 본다면 다른 페이지들에서는 404 에러가 발생한다.
await dio
.get(url + i.toString())
.then((value) => print("$value \nurl은 ${url + i.toString()}입니다!"))
.catchError((e) {print(e)});
아래 이미지는 javascript로 작성된 벡엔드에서 동작하는 코드이다.
다음 조건을 맞추어 문제를 해결하는 코드를 작성하고자 한다.
- 100부터 150 사이의 정수를 찾아서 아래의 URL에 접근하시오.
"https://sniperfactory.com/sfac/http_assignment_{100부터 150사이정수}"
- 정답코드를 받기위한 코드를 작성하시오.
2번의 코드와 마찬가지로 dio
패키지를 사용하였고, main.dart
의 코드는 같다. 또한 home_page.dart
에서 화면의 UI는 그대로 사용하고 URL에 접근하여 정답 코드를 가져오는 getData()
함수만 다시 작성하였다.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
void getData() async {
var url = "https://sniperfactory.com/sfac/http_assignment_"; //URL
var dio = Dio();
for (var i = 100; i <= 150; i++) {
await dio
.post(
url + i.toString(),
options: Options(
headers: {
'user-agent': 'SniperFactoryBrowser',
'authorization': 'Bearer ey-1234567890',
},
),
)
.then((value) => print("$value \nurl은 ${url + i.toString()}입니다!"))
.catchError((_) {});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('15일차 과제'),
),
body: Center(
child: ElevatedButton(
onPressed: () => getData(),
child: Text('test 버튼'),
),
),
);
}
}
우선 접근할 URL을 주어진 URL로 선언했는데 뒤의 숫자 부분은 빼고 저장했다. 그리고 2번과 마찬가지로 Dio
객체를 선언했다.
이번에는 반복문을 100부터 150까지 수행했다. 주어진 백엔드 코드를 보면 POST
요청만 받고 있기 때문에 post()
메소드를 사용하여 요청을 보냈다. URL은 저장해 놓은 값에 반복하는 숫자를 더해 주었고, option
으로 조건들을 맞게 설정해 주었다.
option
에서는 headers
의 user-agent
로 SniperFactoryBrowser
를 설정해 해당 브라우저에서만 요청을 받도록 했고, authorization
을 임의의 값으로 설정해 JMT 토큰을 전달했다.
then()
메소드를 사용해 요청이 성공했을 경우 결과 값과 URL을 출력해 주었다. 예외 처리를 하는 catchError
는 이번에도 따로 기능을 작성하지는 않았다.
2번과 같은 UI를 가진 화면에서 버튼을 클릭하면 콘솔창에 아래와 같이 출력된다.
숨겨진 페이지는 119번 페이지 이고 정답 코드는 5292304이다!!
아래의 URL에 네트워크 데이터 요청을 하고, 응답 데이터를 활용하여 결과물 예시와 같은 앱을 만들고자 한다.
- URL
https://sniperfactory.com/sfac/http_json_data
- 결과물 예시
- 이 때, StatefulWidget을 생성하고 다음의 코드를 사용할 수 있도록 합니다. (getData는 네트워크에 데이터를 요청하는 코드입니다.)
이번 과제는 두 가지 방식을 사용하여 작성해 보았다. 우선 두 가지 방식 모두 아래와 같은 main.dart
파일을 작성했다.
import 'package:first_app/page/home_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// root Widget
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(), // 홈 페이지 호출
);
}
}
첫 번째 방식은 initState()
에서 데이터를 받아 오고 받아온 다음 setState()
를 호출하는 방식이다.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
dynamic data; // 화면을 구성할 데이터들을 저장할 변수
getData() async {
var url = "https://sniperfactory.com/sfac/http_json_data"; //URL
var dio = Dio();
var res = await dio.get(url); //리소스 요청
setState(() {
data = res.data; //응답을 data에 저장
});
}
void initState() {
super.initState();
getData(); //네트워크에 데이터 요청
}
Widget build(BuildContext context) {
return Scaffold(
body: data != null //데이터를 받아온 경우 Card 출력
? Card(
margin: EdgeInsets.symmetric(horizontal: 80, vertical: 150),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0), //<-- SEE HERE
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//이미지
Expanded(
child: Image(
image: NetworkImage(
data['item']['image'],
),
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
),
//타이틀 텍스트
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
style: TextStyle(
fontWeight: FontWeight.bold,
),
data['item']['name'].toString(),
),
),
//구분선
Divider(
height: 1,
indent: 8,
endIndent: 8,
),
//설명 텍스트
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
data['item']['description'].toString(),
),
),
//신청 버튼
Container(
width: double.infinity,
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {},
child: Text(
'${data['item']['price'].toString()}원 결제하고 등록',
),
),
)
],
),
)
: Center(child: CircularProgressIndicator()), //데이터를 받아오기 전 로딩 출력
);
}
}
initState()
에서 데이터를 받아오기 위해 StatefulWidget
으로 작성했다. 우선 요청한 데이터를 저장할 변수 data
를 선언하고, 데이터를 요청하는 getData()
함수를 작성했다. 요청이 완료되어 응답이 온 다음에는 setState()
를 통해 화면을 다시 그려주었고, data
에 응답의 데이터를 저장했다.
이러한 데이터 요청 함수를 initState()
에서 실행했다.
본문에서는 data
에 값이 있을 경우에 카드를 출력하고, 없을 경우에는 프로그레스를 출력해 로딩을 표현했다. 카드는 Card
위젯을 사용해 둥근 테두리를 적용했으며 Column
으로 구성해 이미지와 텍스트들은 받아온 데이터를 사용해 출력했다. 신청 버튼은 ElevatedButton
을 사용해 만들었다.
두 번째 방식은 FutureBuilder
위젯을 사용하는 것이다. 이 코드 역시 main.dart
는 일치한다. home_page.dart
는 아래와 같이 작성했다.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future data; // 화면을 구성할 데이터들을 저장할 변수
Future getData() async {
var url = "https://sniperfactory.com/sfac/http_json_data"; //URL
var dio = Dio();
var res = await dio.get(url); //리소스 요청
return res.data; //응답 데이터를 반환
}
void initState() {
super.initState();
data = getData(); //네트워크에 데이터 요청(data에 저장)
}
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: data,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 80, vertical: 150),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0), //<-- SEE HERE
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//이미지
Expanded(
child: Image(
image: NetworkImage(
snapshot.data['item']['image'],
),
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
),
//타이틀 텍스트
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
style: TextStyle(
fontWeight: FontWeight.bold,
),
snapshot.data['item']['name'].toString(),
),
),
Divider(
height: 1,
indent: 8,
endIndent: 8,
),
//설명 텍스트
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
snapshot.data['item']['description'].toString(),
),
),
//신청 버튼
Container(
width: double.infinity,
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {},
child: Text(
'${snapshot.data['item']['price'].toString()}원 결제하고 등록',
),
),
)
],
),
);
} else {
return CircularProgressIndicator(); //데이터를 받아오기 전 로딩 출력
}
},
),
);
}
}
두 번째 방법에서는 먼저 Future
타입을 저장하는 data
를 나중에 초기화 할 수 있도록 late
를 사용해 선언했다.
이번에는 getData()
에서 data
를 저장하지 않고 응답의 데이터를 리턴해 주었다. 리턴 타입은 Future<dynamic>
이다.
그 후 initState()
에서 getData()
의 값을 data
에 저장했다.
본문에서 UI 구성은 같지만 Card
위젯을 FutureBuilder
에서 builder
의 리턴 값으로 만들었다. future
속성에 앞에서 선언한 data
를 넣어 응답을 확인할 수 있도록 했다.
builder
에서 snapshot
을 사용해 값이 있으면 Card
를 반환하고 값이 아직 없으면 프로그레스를 출력해 로딩을 표현해 주었다.
두 방식 모두 결과는 동일하게 출력되었다.
두 코드 모두 동일하게 위와 같은 결과가 나왔지만 FutureBuilder
를 사용했을 때는 프로그레스 바가 표시 되지 않고 거의 바로 화면이 출력되었고, 첫 번째 방식은 프로그레스 바가 약간 보인 뒤 화면이 바뀌었다. 즉, FutureBuilder
를 사용했을 때가 조금 더 빠르게 느껴졌다. (아마도 첫 번째 방식은 데이터를 불러온 후 다시 setState()
로 화면을 그리기 때문이라고 생각된다.)
아래의 URL에 네트워크 데이터 요청을 하고, 응답 데이터를 활용하여 결과물 예시와 같은 앱을 만들고자 한다.
- URL
https://jsonplaceholder.typicode.com/photos?albumId=1
- 결과물 예시
- 위 URL은 이미지와 타이틀을 List형태로 보내주는 무료 API입니다.
- 아래로 스크롤이 가능하도록 받은 데이터를 전부 나열하세요.
이번에도 4번 과제와 마찬가지로 FutureBuilder
를 사용하는 방식과 사용하지 않는 방식 두 가지를 작성했다.
우선 main.dart는 아래와 같다.
import 'package:first_app/page/home_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// root Widget
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(), // 홈 페이지 호출
);
}
}
항상 똑같이 main.dart
는 HomePage()
를 호출하는 역할을 한다.
첫 번째 방식은 initState()
에서 데이터를 받아 오고 받아온 다음 setState()
를 호출하는 방식이다.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var dataList; // 화면을 구성할 데이터들을 저장할 변수
getData() async {
var url = "https://jsonplaceholder.typicode.com/photos?albumId=1"; //URL
var dio = Dio();
var res = await dio.get(url); //리소스 요청
setState(() {
dataList = res.data; //응답 데이터를 저장
});
}
void initState() {
super.initState();
getData(); //네트워크에 데이터 요청
}
Widget build(BuildContext context) {
return Scaffold(
// 데이터 리스트에 결과를 받아온 후 리스트뷰를 출력
body: dataList != null
? ListView.builder(
itemCount: dataList.length,
itemBuilder: (context, index) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//이미지
Image(
image: NetworkImage(dataList[index]['url']),
),
//타이틀 텍스트
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
style: TextStyle(
fontWeight: FontWeight.bold,
),
dataList[index]['title'].toString(),
),
),
],
),
);
},
)
: CircularProgressIndicator(), //데이터가 없을 경우 프로그레스 바
);
}
}
방식은 4번 과제와 동일한데 URL만 다르게 설정했다. getData()
함수는 데이터를 요청하고 요청의 응답이 오면 setState()
로 화면을 다시 그리며 데이터를 dataList
에 저장한다. 해당 함수는 initState()
에서 호출한다.
본문은 dataList
가 비어있지 않을 경우 리스트 뷰를 출력하며 ListView.builder
를 사용해 만들었다. 내부 요소들은 Card
위젯으로 만들었고 Column
으로 구성해 이미지와 타이틀 텍스트를 출력했다.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future dataList; // 화면을 구성할 데이터들을 저장할 변수
Future getData() async {
var url = "https://jsonplaceholder.typicode.com/photos?albumId=1"; //URL
var dio = Dio();
var res = await dio.get(url); //리소스 요청
return res.data; //응답 데이터를 반환
}
void initState() {
super.initState();
dataList = getData(); //네트워크에 데이터 요청(dataList에 저장)
}
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: dataList,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//이미지
Image(
image: NetworkImage(snapshot.data[index]['url']),
),
//타이틀 텍스트
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
style: TextStyle(
fontWeight: FontWeight.bold,
),
snapshot.data[index]['title'].toString(),
),
),
],
),
);
},
);
} else {
return CircularProgressIndicator(); //데이터 응답이 없을 때 프로그레스 바
}
},
),
);
}
}
이번에도 역시 4번 과제의 FutureBuilder
를 사용한 방식과 같다. getData()
는 리소스를 요청하고 응답의 데이터 값을 반환한다. 이 값은 initState()
에서 호출되어 dataList
에 저장된다.
저장된 값을 사용하여 FutureBuilder
를 만들고 응답이 오면 ListView.builder
를 리턴했다. 내부 요소는 첫 번째 방식과 코드가 동일하다.
두 코드의 결과는 아래와 같이 동일한 결과를 보여준다.
오늘은 진짜 오래 걸렸다. 15일차를 진행했는데 역대 가장 오래 걸렸다. ㅠㅠ 심지어 6시까지가 기본적으로 정해진 학습 시간인데 그 시간 안에도 다 끝내지 못했다. ㅋㅋㅋㅋ 지금 시간이...7시 50분이네...ㅠㅠ 뭔가 예상하지 못한 오류가 계속 나서 좀 걸렸다. 어떻게든 마무리하긴 했는데 제대로 한게 맞는 건지 헷갈리는 것도 처음이다. ㅋㅋㅋㅋ 네트워크 통신이 좀 어렵네 😭😭
7시 50분,, 역시 대단하십니다! 저는 현재시각 10시 04분입니다,, 혹시 어떻게 하셨나 보러 놀러왔다가 차이를 실감하고 다시 돌아갑니다,,