[Flutter] 스나이퍼팩토리 15일차

KWANWOO·2023년 2월 14일
1
post-thumbnail

스나이퍼팩토리 플러터 15일차

15일차에는 정말 중요햔 http 통신과 비동기 처리에 대해 학습했다.

학습한 내용

  • HTTP 통신
  • http 패키지와 dio 패키지
  • 동기와 비동기
  • Future, async, await

추가 내용 정리

http 패키지와 dio 패키지

http 패키지와 dio 패키지는 모두 HTTP 통신을 지원하는 패키지이다. 아래 링크를 통해 각 패키지의 자세한 내용을 확인할 수 있다.

동기와 비동기

  • 동기(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");
}

Future, async, await

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의 사용법 정도만 작성했지만 비동기는 많은 내용을 가지고 있기 때문에 더 학습이 필요하다...(나중에 더 많은 내용을 포스팅 할 예정)

JWT 토큰

Authentication(인증)은 로그인과 같이 사용자가 권한이 주어진 사용자임을 인증 받는 것을 의미한다. Authorization(인가)은 사용자가 한 번 인증을 받은 후에 그 사용자가 특정 리소스에 엑세스 할 수 있는지 여부를 결정하는 것이다.

이러한 인가에서 주로 사용되는 방식은 세션(Session)이다. 세션은 세션 id를 통해서 사용자가 서버에 로그인이 지속되는 상태를 말하는데 아래와 같은 방식으로 인가를 한다.

세션의 인가 과정
1. 사용자가 로그인에 성공하면 세션을 발행한다.
2. 세션을 브라우저에 저장하고, 서버 메모리에도 저장한다.
3. 인가가 필요한 요청을 보낼 때 서버에 세션 값을 같이 보낸다.
4. 서버는 메모리에 저장된 값과 세션 값을 비교하여 맞는 값이 있으면 인가를 수행한다.

세션은 아래와 같은 단점을 가지고 있다.

세션의 단점
1. 세션은 서버에도 저장되어 있기 때문에 사용자가 동시 다중 접속 했을 때 메모리가 부족해 질 수 있다.
2. 서버가 재부팅하면 세션이 날아가 사용자가 다시 로그인을 해야한다.
3. 분산된 서버의 경우 세션 유지가 제대로 되지 않아서 서버 확장이 어렵다.

JWT는 JSON Web Token의 약자로 사용작 로그인하면 토큰을 발행하는데 세션과 다른점은 서버는 이 토큰을 저장하지 않는다는 점이다.

JWT는 아래와 같은 형태를 가진다.

JWT 예시

  • 암호화된 3가지 데이터를 이어붙인 형태로 구성되어 있다.
  • Header: 알고리즘, 타입 값이 들어간다.
  • Payload: 토큰이 가지는 데이터이다.
  • Signature: 헤더에 정의된 알고리즘을 통해 암호화한 비밀 값으로 서버만 알고 있다.

하지만 JWT는 세션처럼 모든 사용자들의 상태를 기억하고 있지 않기 때문에 대상들의 상태를 언제나 제어할 수 없다.

만약 하나의 기기에서만 로그인 가능한 서비스를 만들 경우 pc에서 로그인하면 핸드폰에서 세션값을 사용 못하게 하는 제어를 할 수 없고, 해커에게 토큰을 빼앗기면 무효화할 방법도 없다.

이를 해결하기 위해 accessTokenrefreshToken을 주는 방법을 사용한다.

  • accessToken: 인가를 받을 때 사용하는 토큰(보통 수명이 짧다.)
  • refreshToken: accessToken이 수명을 다했을 때 accessToken을 다시 발행 받기 위한 토큰(보통 2주 정도로 긴 수명을 가짐)

15일차 과제

  1. HTTP 통신의 status code
  2. dio 패키지를 활용하여 스나이퍼팩토리의 비밀 URL 호출
  3. javascript로 작성된 백엔드 코드를 보고 문제 해결
  4. 주어진 URL에 데이터를 요청하여 결과물 제작 1
  5. 주어진 URL에 데이터를 요청하여 결과물 제작 2

1. HTTP 통신의 status code

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가지에 대해 살펴보자

200 OK

요청이 성공했음을 의미한다. 정보는 요청에 따른 응답으로 반환되며, 주로 서버가 요청한 페이지를 제공했다는 의미로 사용된다.

400 Bad Request

잘못된 요청을 의미하며, 잘못된 문법의 요청으로 서버가 구문을 인식하지 못했거나 사기성이 있는 요청의 라우팅 등의 경우에 사용된다.

404 Not Found

서버가 요청받은 리소스를 찾을 수 없음을 의미한다. 예를 들어 서버에 존재하지 않는 페이지에 대한 요청이 있을 경우 서버는 이 코드를 제공한다.

또는 API의 종점은 적절하지만 리소스 자체가 존재하지 않음을 의미할 수도 있다. 서버들은 인증받지 않은 클라이언트로부터 리소스를 숨기기 위해 이 응답을 403 대신 사용할 수도 있다.

403 Forbidden
클라이언트는 컨텐츠에 접근할 권리를 가지고 있지 않습니다. 서버는 클라이언트가 누구인지를 알고 있다.

예를 들어 미승인된 클라이언트를 서버가 거절하기 위해 사용한다.

500 Internal Server Error

웹 사이트 서버에 문제가 있음을 의미하지만 서버는 정확한 문제에 대해 구체적으로 설명이 불가능하다. 즉, 서버에서 처리 방법을 알 수 없는 상황이 발생했음을 의미한다.

503 Service Unavailable

서버가 요청을 처리할 준비가 되지 않은 상태를 의미한다. 일반적인 원인은 유지보수를 위해 작동이 중단되거나 과부하가 걸린 서버이다.

이 응답은 응답과 함께 문제를 설명하는 사용자 친화적인 페이지가 전송되어야 한다. 또한 임시 조건에서 사용되어야 하며 Retry-After: HTTP 헤더는 가능하면 서비스를 복구하기 전 예상 시간을 포함해야 한다.

웹 마스터는 이러한 일시적인 조건 응답을 캐시하지 않아야 하기 때문에 응답과 함께 전송되는 캐싱 관련 헤더에도 주의를 기울여야 한다.

301 Moved Permanently

요청한 리소스의 URI가 영구적으로 변경되었음을 의미한다. 새로운 URI가 응답으로 제공될 수도 있다.

303 See Other

클라이언트가 요청한 리소스를 다른 URI에서 GET 요청을 통해 얻어야 하는 경우 이를 지시하기 위해 서버가 클라이언트로 직접 보내는 응답이다.

2. dio 패키지를 활용하여 스나이퍼팩토리의 비밀 URL 호출

dio 패키지를 활용해 스나이퍼팩토리에 존재하는 비밀 URL을 찾아 호출하고자 한다. 비밀 URL은 아래와 같다.

"https://sniperfactory.com/sfac/http_{20부터 50사이정수}"
  • ex) 0이라면, http_0으로 요청할 것
  • 반드시 반복문을 통하여 해결할 것

코드 작성

dio를 테스트 하기 위해 간단하게 테스트 버튼을 가진 UI를 만들었다.

  • pubspec.yaml
dependencies:
	dio: ^5.0.0

dependenciesdio 넣어 패키지를 설치했다.

  • 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 위젯을 호출한다.

  • home_page.dart
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)});

3. javascript로 작성된 백엔드 코드를 보고 문제 해결

아래 이미지는 javascript로 작성된 벡엔드에서 동작하는 코드이다.

다음 조건을 맞추어 문제를 해결하는 코드를 작성하고자 한다.

  • 100부터 150 사이의 정수를 찾아서 아래의 URL에 접근하시오.
"https://sniperfactory.com/sfac/http_assignment_{100부터 150사이정수}"
  • 정답코드를 받기위한 코드를 작성하시오.

코드 작성

2번의 코드와 마찬가지로 dio 패키지를 사용하였고, main.dart의 코드는 같다. 또한 home_page.dart에서 화면의 UI는 그대로 사용하고 URL에 접근하여 정답 코드를 가져오는 getData() 함수만 다시 작성하였다.

  • home_page.dart
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에서는 headersuser-agentSniperFactoryBrowser를 설정해 해당 브라우저에서만 요청을 받도록 했고, authorization을 임의의 값으로 설정해 JMT 토큰을 전달했다.

then() 메소드를 사용해 요청이 성공했을 경우 결과 값과 URL을 출력해 주었다. 예외 처리를 하는 catchError는 이번에도 따로 기능을 작성하지는 않았다.

결과

2번과 같은 UI를 가진 화면에서 버튼을 클릭하면 콘솔창에 아래와 같이 출력된다.

숨겨진 페이지는 119번 페이지 이고 정답 코드는 5292304이다!!

4. 주어진 URL에 데이터를 요청하여 결과물 제작 1

아래의 URL에 네트워크 데이터 요청을 하고, 응답 데이터를 활용하여 결과물 예시와 같은 앱을 만들고자 한다.

  • URL
    https://sniperfactory.com/sfac/http_json_data

  • 결과물 예시


  • 이 때, StatefulWidget을 생성하고 다음의 코드를 사용할 수 있도록 합니다. (getData는 네트워크에 데이터를 요청하는 코드입니다.)

코드 작성

이번 과제는 두 가지 방식을 사용하여 작성해 보았다. 우선 두 가지 방식 모두 아래와 같은 main.dart 파일을 작성했다.

  • 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()를 호출하는 방식이다.

  • 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> {
  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()로 화면을 그리기 때문이라고 생각된다.)

5. 주어진 URL에 데이터를 요청하여 결과물 제작 2

아래의 URL에 네트워크 데이터 요청을 하고, 응답 데이터를 활용하여 결과물 예시와 같은 앱을 만들고자 한다.

코드 작성

이번에도 4번 과제와 마찬가지로 FutureBuilder를 사용하는 방식과 사용하지 않는 방식 두 가지를 작성했다.

우선 main.dart는 아래와 같다.

  • 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.dartHomePage()를 호출하는 역할을 한다.

첫 번째 방식은 initState()에서 데이터를 받아 오고 받아온 다음 setState()를 호출하는 방식이다.

  • 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> {
  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으로 구성해 이미지와 타이틀 텍스트를 출력했다.


두 번째 방식은 `FutureBuilder`를 사용하는 방식이다.
  • 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 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분이네...ㅠㅠ 뭔가 예상하지 못한 오류가 계속 나서 좀 걸렸다. 어떻게든 마무리하긴 했는데 제대로 한게 맞는 건지 헷갈리는 것도 처음이다. ㅋㅋㅋㅋ 네트워크 통신이 좀 어렵네 😭😭

📄 Reference

profile
관우로그

2개의 댓글

comment-user-thumbnail
2023년 7월 25일

7시 50분,, 역시 대단하십니다! 저는 현재시각 10시 04분입니다,, 혹시 어떻게 하셨나 보러 놀러왔다가 차이를 실감하고 다시 돌아갑니다,,

1개의 답글

관련 채용 정보