Develop/Flutter

[Flutter] 플러터 비동기 Future 와 Stream

JunJangE 2021. 12. 20. 19:21

앱 개발을 하면서 비동기는 아주 중요한 개념이라고 생각하여 Flutter에서 주로 사용되는 비동기인 Future와 Stream의 개념을 다시 한번 집고 넘어가려고 한다.

비동기란 동기 처리가 끝난 후에 실행되며 Firebase 같은 DB에서 데이터를 가져오거나 API, 크롤링 등 외부에서 데이터를 가져오는 경우와 반응형 프로그래밍을 할 때 쓰인다.

Flutter에서 비동기 처리를 하기 위해서는 Future와 Stream에 대해서 잘 알아야 한다.

Future는 이전에 설명했듯이 앨범에서 이미지 가져오기, 현재 배터리 표시, 파일 가져오기, http 요청 등 일회성 응답에 사용된다.

Stream은 위치 업데이트, 음악 재생, 스톱워치 등 일부 데이터를 여러 번 가져올 때 사용된다. 즉, 계속해서 데이터의 변화를 감지하고 그에 맞춰 적절한 처리를 할 때 사용된다.

FutureBilder

우선 FutureBilder에 대해서 알아보자.

밑에 예시 코드는 이전에 사용했던 FutureBuilder 예시 코드에서 조금 수정한 코드로 동행복권 api에서 로또 당첨 번호를 가져와 화면에 뿌려주는 UI를 구현하고자 한다.

코드를 작성하기 전에 머릿속으로 어떤 UI를 만들지 구상부터 해보자.

  • StatelessWidget을 통해 MateralApp을 실행시키자.
  • 앱의 기능을 하는 코드를 작성했다면 앱이 실행될 때 표시할 화면을 구성하자.
  • 구성하는 화면을 동적으로 변화시킬 것인지 생각한다.
  • 동적으로 변화시키기 위해서는 StaetfulWidget을, 동적으로 변화시키지 않을 거라면 StatelessWidget을 사용한다.
  • 앱이 실행될 때 표시할 위젯은 비동기 처리를 할 때 화면을 동적으로 변화시키기 위해 StatefulWidget을 사용하자.
  • appBar의 title은 'Flutter Study'라는 텍스트를 가운데 정렬을 하고 그림자를 없애서 UI에 뿌리도록 하자.
  • 앱의 body 부분에는 Center 위젯과 Column 위젯을 통해 가운데에 열을 기준으로 구현하고 가운데 정렬을 하자.
  • 앱의 body 부분의 형식을 생각했으면 어떤 위젯을 통해 구성할지 고민한다.
  • 앱의 body 부분에 Text 위젯으로 '로또 당첨 번호' 를 UI에 뿌리도록 하자.
  • 앱의 body 부분에 SizeBox 위젯을 통해 위에 Text 위젯과 다음 위젯 사이에 공간을 만들자. SizeBox에 높이는 20.0으로 하자.
  • 앱의 body부분에 FutureBilder를 통해 비동기 처리를 한 데이터를 가져와서 UI에 뿌리도록 하자.
  • 인터넷에서 데이터를 가져와야 하기 때문에 http 패키지를 추가하자.
  • 인터넷에서 가져온 데이터를 수동으로 직렬화 시키기 위해 convert 패키지를 추가하자.
  • 마지막에는 비동기 처리 함수를 만들어서 인터넷에 있는 로또 당첨 번호를 가져오자.

위에 글은 예시일 뿐이고 코드를 작성하기 전에 어떤 UI를 만들지 구상을 한 후에 코드를 작성하는 것이 자신이 무엇을 하고자 하는 것인지, 먼저 무엇을 해야 하는지 알기 쉬워지는 것 같다.

코드를 작성하기 전에 먼저 http 패키지를 추가하자.

http 패키지를 사용하면 인터넷으로부터 데이터를 쉽게 가져올 수 있다.

http 패키지를 설치하기 위해서는 pubspec.yaml의 의존성 부분에 다음 코드를 추가해줘야 한다.

다음 코드를 입력하면 http 패키지를 최신 버전으로 추가할 수 있다.

flutter pub add http

 

http 패키지를 추가했다면 주석을 확인해보면서 코드를 작성해보자.

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // http 패키지 추가
import 'dart:convert'; // json을 수동으로 직렬화 시키기 위해 convert 패키지 추가


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

// 비동기 처리를 할 때 화면을 동적으로 변화시키기 위해 StatefulWidget를 사용
class MyHomePage extends StatefulWidget {

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Study'),
        centerTitle: true,
        elevation: 0.0,
      ),
      
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '로또 당첨 번호',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(
              height: 20,
            ),

            // FutureBuilder 예시 코드
            FutureBuilder(
                future: fetchPost(),
                builder: (context, snapshot) {
   
                  // 해당 부분은 data를 아직 받아 오지 못했을 때 실행되는 부분
                  if (snapshot.hasData == false) {
                    return CircularProgressIndicator(); // CircularProgressIndicator : 로딩 에니메이션
                  }

                  // error가 발생하게 될 경우 반환하게 되는 부분
                  else if (snapshot.hasError) {
                    return Text('Error: ${snapshot.error}'); // 에러명을 텍스트에 뿌려줌
                  }

                  // 데이터를 정상적으로 받아오게 되면 다음 부분을 실행하게 되는 부분
                  else {
                    return Text(snapshot.data.toString());
                  }
                }),

          ],
        ),
      ),
    );
  }
}

// 비동기를 통해 네트워크 요청
Future fetchPost() async{ 

await Future.delayed(Duration(seconds: 2)); // 비동기 과정을 보여주기 위해 시간을 딜레이 시킨다.

int page = 994; // 페이지 번호는 동적으로 바꿀 수 있게 한다.

String baseUrl = 'https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=$page'; // 동행복권 Lotto api url

final response = await http.get(Uri.parse(baseUrl)); // http 데이터를 가져온다.

  var LottoNums = []; // 날짜, 회차, 로또 당첨 번호 

  // 만약 서버 상태 코드가 200과 함께 OK 응답을 반환하면, JSON을 파싱한다. 
  if (response.statusCode == 200){
    final nums = await json.decode(response.body);

    // 필요한 정보만을 가져온다.
    LottoNums.add(nums['drwNoDate']); // 날짜
    LottoNums.add(nums['drwNo']);     // 회차
    LottoNums.add(nums['drwtNo1']);   // 번호1
    LottoNums.add(nums['drwtNo2']);   // 번호2
    LottoNums.add(nums['drwtNo3']);   // 번호3
    LottoNums.add(nums['drwtNo4']);   // 번호4
    LottoNums.add(nums['drwtNo5']);   // 번호5
    LottoNums.add(nums['drwtNo6']);   // 번호6
    LottoNums.add(nums['bnusNo']);    // 보너스 번호

    return LottoNums; 
 
  }else{
    return "Error";
    }

}

 

코드의 흐름을 확인해보면 FutureBuilder 메소드를 통해 비동기 처리 함수를 불러오게 되는 것을 확인할 수 있고 처음에 data를 받아 오지 못했으므로 snapshot.data가 false 가 되어 로딩 애니메이션이 나오게 된다. 이때 비동기 처리 함수를 불러오게 되는데 네트워크 요청을 통해 실행되며 동행복권 로또 당첨 번호 api를 통해 데이터를 가져오게 된다. 데이터를 가져오게 되면 필요한 정보만을 가져와 UI에 뿌려주게 된다. 여기서 비동기 과정을 좀 더 이해하기 쉽게 하기 위해 시간을 딜레이 한 것도 확인할 수 있다.

<플러터 FutureBuilder를 사용한 네트워크 비동기 처리>

StreamBilder

다음으로 StreamBilder에 대해서 알아보자.

밑에 예시 코드는 StreamBilder를 통해 1초씩 타이머가 흐르는 데이터를 감지하여 텍스트를 바꿔주는 UI를 구현하고자 한다.

위와 같이 코드를 작성하기 전에 머릿속으로 어떤 Ul를 만들지 구상부터 하자.

  • StatelessWidget을 통해 MateralApp을 실행시키자.
  • 앱의 기능을 하는 코드를 작성했다면 앱이 실행될 때 표시할 화면을 구성하자.
  • 구성하는 화면을 동적으로 변화시킬 것인지 생각한다.
  • 동적으로 변화시키기 위해서는 StatefulWidget을, 동적으로 변화시키지 않을 거라면 StatelessWidget을 사용한다.
  • 앱이 실행될 때 표시할 위젯은 비동기 처리를 할 때 화면을 동적으로 변화시키기 위해 StatefulWidget을 사용하자.
  • appBar의 title은 'Flutter Study'라는 텍스트를 가운데 정렬을 하고 그림자를 없애서 UI에 뿌리도록 하자.
  • 앱의 body 부분에는 Center 위젯과 Column 위젯을 통해 가운데에 열을 기준으로 구현하고 가운데 정렬을 하자.
  • 앱의 body 부분의 형식을 생각했으면 어떤 위젯을 통해 구성할지 고민한다.
  • 앱의 body 부분에 Text 위젯으로 'Hello' 을 UI에 뿌리도록 하자.
  • 앱의 body부분에 StreamBilder를 통해 비동기 처리를 한 데이터를 가져와서 UI에 뿌리도록 하자.
  • 마지막에는 비동기 처리를 위한 함수를 작성하자.

위에 글도 예시일 뿐이고 앞으로 코드를 작성하기 전에 어떤 UI를 만들지 구상을 한 후에 코드를 작성해보자.

그럼 주석을 확인해보면서 코드를 작성해보자.

import 'package:flutter/material.dart';

void main() => runApp(MyApp()); // MyApp을 실행시킬거야

class MyApp extends StatelessWidget { // MyApp이라는 상태가 없는 위젯을 상속받는 클래스를 생성

  @override // 상속 받은 메서드를 재정의
  Widget build(BuildContext context){ 
    return MaterialApp( // MaterialApp으로 리턴한다.
    
      title: "MyApp", // 앱의 이름은 "MyApp"으로 정하자.
      debugShowCheckedModeBanner: false, // 디버그 체크 모드 배너를 없애
      theme: ThemeData( // 테마를 설정
        primaryColor: Colors.blue // 기본 테마는 blue로 정하자.
      ),
      home: MyWidget(), // 앱이 실행 될 때 MyWidget()을 호출해
    );

  }
}

class MyWidget extends StatefulWidget{ // MyWidget이라는 상태가 있는 위젯을 상속받는 클래스를 생성

  @override // 상속 받는 메서드를 재정의
  _MyWidgetState createState() => _MyWidgetState(); // createState() 메서드를 통해 상태를 생성한다.

}

class _MyWidgetState extends State<MyWidget>{ // _MyWidgetState는 MyWidget의 상태를 상속받는다.

  @override // 상속 받는 메서드를 재정의
  Widget build(BuildContext context){ 
    return Scaffold( // Scaffold를 리턴해

      appBar: AppBar( // appBar를 설정하자.
        title: Text("Flutter Study"), // appBar의 title은 Text에 "Flutter Study"로 정하자.
      centerTitle: true, // appBar의 title은 center에 두자.
      elevation: 0.0, // appBar의 그림자 효과를 0.0으로 두자.

      ),
      body: Center( // 앱의 body부분을 Center 위젯으로 구현하자.
      child: Column( // 구성은 Column 위젯을 가져와서 열로 구현하자.
        mainAxisAlignment: MainAxisAlignment.center, // 가운데 중심선 기준으로 가운데 정렬을 하자.
        children: <Widget>[ // 그에 자식들은 다음 위젯이야.
          Text("Hello"), // Text 위젯으로 "Hello"를 화면에 뿌려주자.

          StreamBuilder( // StreamBuilder를 통해 비동기 처리를 해주자.
            stream: _stream(),
            builder: (BuildContext context, AsyncSnapshot snapshot){

              // 매초마다 변화된 데이터를 감지해서 화면에 뿌려준다.
              return Text('${snapshot.data} seconds passed'); 
              
            },
          )        
        ],
      ),
      )
    );
  }
}

// 비동기 처리
// 1초에 한번씩 업데이트(20초까지)
Stream _stream(){
  return Stream.periodic(const Duration(seconds: 1), (int x) => x).take(21);

}

 

코드의 흐름을 확인해보면 StreamBuilder 메소드를 통해 비동기 처리 함수를 불러오게 되는 것을 확인할 수 있고 비동기 처리 함수에서 1초에 한 번씩 업데이트되는 것을 감지하여 시간 초를 UI에 뿌려주는 것을 확인할 수 있다.

<플러터 StreamBuilder를 사용한 비동기 처리>

참고

 

FutureBuilder class - widgets library - Dart API

Widget that builds itself based on the latest snapshot of interaction with a Future. The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.bui

api.flutter.dev

 

StreamBuilder class - widgets library - Dart API

Widget that builds itself based on the latest snapshot of interaction with a Stream. Widget rebuilding is scheduled by each interaction, using State.setState, but is otherwise decoupled from the timing of the stream. The builder is called at the discretion

api.flutter.dev

 

인터넷에서 데이터 가져오기

인터넷을 통해 데이터를 가져오는 것은 대부분의 앱에서 필수적입니다. 다행스럽게도, Dart와 Flutter는이러한 유형의 작업을 위해 도구를 제공합니다.여기서는 아래와 같은 단계로 진행합니다: 1.

flutter-ko.dev