본문 바로가기
flutter

플러터 - Future, FutureBuilder

by 들풀민들레 2023. 3. 13.

본 글은 [Do it! 깡샘의 플러터&다트 프로그래밍] 의 내용을 발췌한 것입니다.

 

 

책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.

 

퓨처 — Future


다트 언어에서 제공하는 Future, Stream, await, async는 모두 비동기 프로그래밍을 지원하는 기능입니다. 비동기 프로그래밍이란 시간이 오래 걸리는 작업을 실행한 후 끝날 때가지 기다리지 않고 다음 작업을 실행하는 것입니다. 비동기 프로그래밍과 반대되는 개념은 동기 프로그래밍으로 어떤 작업을 실행하고 끝날 때까지 기다렸다가 그다음 작업을 수행하는 것입니다.
먼저 동기로 실행되는 예를 들어 비동기 프로그래밍이 왜 필요한지 알아보겠습니다. 다음 코드에서 버튼을 클릭하면 onPress() 함수가 호출되고 onPress() 함수에서 sum() 함수를 호출합니다. 여기서는 sum() 함수에서 단순히 더하기를 처리했지만, 시간이 오래 걸리는 작업이라고 가정해 보겠습니다.

void sum() {
    var sum = 0;
    Stopwatch stopwatch = Stopwatch();
    stopwatch.start();
    for (int i = 0; i < 500000000; i++) {
    	sum += i;
    }
    stopwatch.stop();
    print("${stopwatch.elapsed}, result: $sum");
}
void onPress() {
    print('onPress top...');
    sum();
    print('onPress bottom...');
}

실행 결과를 보면 onPress top이 출력되고 sum() 함수의 결과가 출력된 후에 onPress bottom이 출력됐습니다. 즉, sum() 함수가 끝나야 onPress() 함수에서 sum() 함수를 호출한 그다음 줄이 실행됩니다. 결국 onPress() 함수는 sum() 함수의 실행이 끝날 때까지 대기합니다.
오른쪽 그림은 A가 실행되다 B로 넘어온 다음 B 실행을 마쳐야 다시 A의 나머지를 실행하는 동기 구조를 나타냅니다. 자연스러운 흐름처럼 보이지만 B 작업이 오래 걸린다면 문제가 됩니다. 왜냐하면 B를 마칠 때까지 A 작업의 나머지를 수행할 수 없기 때문입니다.

이처럼 시간이 오래 걸리는 작업은 다양한데 네트워킹 또는 파일을 읽거나 쓰는 경우가 대표적입니다. 이런 작업을 동기로 프로그래밍하면 그 작업이 끝날 때까지 사용자 이벤트나 화면을 처리할 수 없습니다. 따라서 앱의 성능이 떨어지는 문제가 있습니다.
반면에 시간이 오래 걸리는 작업이 처리되는 동안 다른 작업도 함께 처리하는 것을 비동기 프로그래밍이라고 하며, 이때 Future 클래스를 사용합니다. Future는 다트 언어에서 제공하는 클래스이며 미래에 발생할 데이터를 의미합니다.
오른쪽 그림은 A에서 B를 실행한 후 곧바로 A에게 Future를 넘겨줘 A의 나머지 작업도 함께 처리하는 비동기 프로그래밍을 보여 줍니다. 이때 Future는 미래에 발생할 데이터를 담을 수 있는 상자 정도로 이해할 수 있습니다.

Future를 만들 때 아직 B 작업이 끝나지 않았으므로 구체적인 데이터는 없습니다. 하지만 미래의 데이터를 담을 수 있는 Future를 만들어 A 작업을 계속 실행하고, B 작업이 끝나면 그때 구체적인 데이터를 Future에 담습니다.
결국 정리하면 시간이 오래 걸리는 부분에서 미래의 데이터를 담을 수 있는 Future 상자를 만들어 다른 작업도 함께 실행하고, 실제 데이터가 발생하는 시점에 Future에 담아 이용할 수 있게 하는 것입니다.
앞에 살펴본 sum() 함수에서 Future를 이용해 onPress()가 대기하지 않게 작성해 보면 다음과 같습니다. 코드를 보면 onPress() 함수는 이전과 차이가 없습니다. 그런데 sum() 함수의 반환 타입을 Future로 선언했으며 실행되자마자 Future 객체를 반환합니다. 이렇게 하면 sum() 함수를 호출한 곳은 호출하자마자 결과를 받으므로 sum() 호출문 그다음 줄을 바로 실행합니다.

Future<int> sum() {
    return Future<int>(() { // 미래의 데이터를 담을 상자 반환
        var sum = 0;
        Stopwatch stopwatch = Stopwatch();
        stopwatch.start();
        for (int i = 0; i < 500000000; i++) {
            sum += i;
        }
        stopwatch.stop();
        print("${stopwatch.elapsed}, result: $sum");
        return sum; // 실제 데이터를 상자에 담기
    });
}
void onPress() {
    print('onPress top...');
    sum();
    print('onPress bottom...');
}

실행 결과를 보면 마지막 줄에 sum() 함수의 실행이 끝나서 출력한 기록이 있습니다. 그런데 sum() 함수의 실행이 끝나기 전에 onPress bottom이 먼저 출력됐다는 점이 중요합니다. 결국 sum() 함수가 실행되는 동안 onPress() 함수도 함께 실행됐다는 의미입니다.
sum() 함수에서 시간이 오래 걸리는 작업은 Future 생성자의 매개변수로 지정한 함수에서 처리하게 구성했습니다. 이 함수에서 반환하는 값이 Future에 담기는 미래의 데이터입니다. 예에서는 Future에 sum값을 담았습니다.
Future를 선언할 때 제네릭을 이용해 미래에 발생할 데이터 타입을 지정합니다. Future<int>로 선언하면 int 타입의 데이터를 담을 수 있는 Future 객체를 의미합니다.

퓨처 빌더 — FutureBuilder


플러터 앱에서 Future 데이터는 대부분 화면에 출력합니다. 그런데 Future는 미래에 발생할 데이터이므로 화면에 바로 출력할 수 없습니다. 결국 결과가 나올 때까지 대기했다가 화면에 출력해 주는 위젯이 필요한데, FutureBuilder가 그 역할을 해줍니다.
다음은 플러터 API 문서에 나오는 FutureBuilder의 생성자 부분입니다.

const FutureBuilder<T>(
    { Key? key,
    Future<T>? future,
    T? initialData,
    required AsyncWidgetBuilder<T> builder }
)

FutureBuilder는 위젯이지만 자체 화면을 가지지는 않습니다. FutureBuilder가 출력하는 화면은 생성자 매개변수로 지정되는 AsyncWidgetBuilder에 명시합니다. FutureBuilder는 Future 데이터를 화면에 출력하는 위젯이므로 Future 데이터를 future 매개변수에 지정해야 합니다.
이 future 매개변수에 지정한 Future 객체에서 미래에 데이터가 발생하면 AsyncWidgetBuilder 부분을 실행해 해당 데이터로 화면을 구성합니다.
다음은 플러터 API 문서에 나오는 AsyncWidgetBuilder 정의입니다.

AsyncWidgetBuilder<T> = Widget Function(
    BuildContext context,
    AsyncSnapshot<T> snapshot
)

AsyncWidgetBuilder는 함수이며 이 함수의 반환 타입이 위젯입니다. FutureBuilder가 이 위젯을 화면에 출력합니다. AsyncWidgetBuilder의 두 번째 매개변수 타입이 AsyncSnapshot이며 이곳에 Future 데이터를 전달해 줍니다.
FutureBuilder를 이용해 Future 데이터를 화면에 출력하는 코드는 다음처럼 작성할 수 있습니다.

body: Center(
    child: FutureBuilder(
        future: calFun(),
        builder: (context, snapshot) {
        	if (snapshot.hasData) {
        		return Text('${snapshot.data}');
       		}
        	return CircularProgressIndicator();
        },
    ),
),

AsyncSnapshot 객체의 hasData 속성으로 데이터가 있는지 판단할 수 있으며 실제 데이터는 data 속성으로 얻습니다. AsyncWidgetBuilder의 반환 타입은 위젯이어야 하는데 Future 데이터를 이용하므로 실제 데이터가 발생하기까지 시간이 걸릴 수 있습니다. 따라서 데이터 발생 전에 출력할 위젯을 별도로 명시해 줘야 하는데, 이 예에서는 화면에 빙글빙글 돌아가는 원을 표현하는 CircularProgressIndicator 위젯을 이용했습니다. 이 위젯이 먼저 출력되고 이후에 실제 Future 데이터가 발생하면 if 문 안에서 반환한 Text 위젯이 출력됩니다.

 

 

 

책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.

 

 

'flutter' 카테고리의 다른 글

플러터 - InheritedWidget  (0) 2023.03.13
플러터 - Isolate  (0) 2023.03.13
플러터 - dio  (0) 2023.03.13
플러터 - Navigation  (0) 2023.03.13
플러터 - Scaffold  (0) 2023.03.13