본문 바로가기
Flutter 독학으로 기본기 익히기

07. 비동기 상태에 따라 UI를 바꾸는 FutureBuilder 알아보기

by daco2020 2025. 5. 8.

오늘 배울 것

FutureBuilder 와 일반 Builder 와의 차이점을 비교하고 FutureBuilder 의 목적과 언제 사용하면 좋은지 알아봅니다.

 

 

 

FutureBuilder 란?

FutureBuilder Future 타입의 비동기 작업을 감지해서 '결과값이 오기 전', '도착했을 때', '에러가 났을 때' 각각의 상태에 따라 다른 위젯을 보여줄 수 있게 해주는 빌더입니다.

 

비동기 작업 결과를 기다렸다가 화면에 반영해야 할 때 유용하게 사용할 수 있습니다.

 

 

 

일반 Builder 와 차이점

일반 Builder FutureBuilder
이미 가지고 있는 데이터로 빌드 비동기 데이터가 '나중에' 올 때까지 기다렸다가 빌드
동기적 데이터 기반 비동기 (Future) 기반
반복적인 UI 생성 비동기 작업의 상태에 따른 UI 생성

 

Builder는 단순 반복적으로 UI를 생성하는데 FutureBuilder는 상태에 따라 지정된 UI를 생성합니다.

 

 

 

FutureBuilder 를 왜 사용할까?

FutureBuilder 를 사용하면 setState() 처럼 상태를 수동으로 갱신할 필요 없기 때문에 코드가 깔끔해집니다.

 

또한 Future 상태를 자동으로 감지하기 때문에 편하게 UI를 구현할 수 있죠. 예를 들어 ConnectionStatesnapshot 을 이용해 아래와 같이 상태 분기 처리가 가능합니다. (try-catch 를 사용하지 않고도 분기를 처리할 수 있는 것도 장점입니다)

FutureBuilder<String>(
  future: _future,
  builder: (context, snapshot) {
    if (snapshot.connectionState ==
        ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.hasError) {
      return Text('에러 발생: ${snapshot.error}');
    } else if (snapshot.hasData) {
      return Text(snapshot.data!);
    } else {
      return Text('버튼을 눌러서 데이터를 가져오세요');
    }
  },
)

 

 

*잠깐! snapshot은 뭔가요?

더보기

snapshot은 Future나 Stream의 현재 상태와 데이터를 담고 있는 객체를 말합니다.

 

FutureBuilder나 StreamBuilder를 사용할 때 builder 함수는 항상 (context, snapshot) 인자를 받습니다. 이때, snapshot 객체는 아래처럼 다양한 상태 정보가 들어 있습니다.

 

snapshot 상태 정보 설명
connectionState 현재 연결 상태 (none, waiting, active, done)
hasData 데이터가 있는지 여부 (true or false)
data 실제로 받은 데이터
hasError 에러 발생 여부
error 에러가 있다면 그 에러 내용

 

 

 

FutureBuilder 는 언제 쓰면 좋을까?

비동기 요청에 대한 결과값을 UI로 구현해야 하는 경우 FutureBuilder 사용하는 것이 좋습니다. 

 

예를 들어, API 요청 결과를 받아서 화면에 보여줄 때, 초기 앱 설정을 로드할 때, 파일 시스템에서 비동기로 데이터 읽을 때 FutureBuilder 를 효과적으로 사용할 수 있습니다. 

 

 

 

FutureBuilder 실습

아래는 실습 전체 코드입니다. 코드를 main.dart 에 붙여 넣기 한 후 앱을 실행하여 직접 확인해 보세요.

 

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'Future/Await 실습', home: MyHomePage());
  }
}

Future<String> fetchMessage() async {
  await Future.delayed(Duration(seconds: 2));
  return "데이터 도착! 👏";
}

Future<String> deleteMessage() async {
  await Future.delayed(Duration(seconds: 2));
  return "데이터 삭제! 👏";
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<String>? _future;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('FutureBuilder 예제')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _future = fetchMessage();
                    });
                  },
                  child: Text('외부 API 데이터 조회'),
                ),
                SizedBox(width: 12),
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _future = deleteMessage();
                    });
                  },
                  child: Text('로컬 파일 데이터 삭제'),
                ),
              ],
            ),
            SizedBox(height: 20),
            FutureBuilder<String>(
              future: _future,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return CircularProgressIndicator(); // 로딩 중
                } else if (snapshot.hasError) {
                  return Text('에러 발생: ${snapshot.error}');
                } else if (snapshot.hasData) {
                  return Text('결과: ${snapshot.data}'); // 데이터 도착
                } else {
                  return Text('버튼을 눌러서 데이터를 가져오세요');
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

 

 

중요한 부분을 나눠 설명해 보겠습니다.

 

fetchMessage 함수는 가상의 '외부 API 데이터 조회' 함수, 밑에 deleteMessage 함수는 가상의 '로컬 파일 데이터 삭제' 함수라고 생각해 주세요. (실제로는 2초의 딜레이가 있는 비동기 함수입니다)

Future<String> fetchMessage() async {
  await Future.delayed(Duration(seconds: 2));
  return "데이터 도착! 👏";
}

Future<String> deleteMessage() async {
  await Future.delayed(Duration(seconds: 2));
  return "데이터 삭제! 👏";
}

 

 

아래 코드로 구현된 두 개의 버튼을 통해 각각의 비동기 함수를 호출할 수 있습니다.

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    ElevatedButton(
      onPressed: () {
        setState(() {
          _future = fetchMessage();
        });
      },
      child: Text('외부 API 데이터 조회'),
    ),
    SizedBox(width: 12),
    ElevatedButton(
      onPressed: () {
        setState(() {
          _future = deleteMessage();
        });
      },
      child: Text('로컬 파일 데이터 삭제'),
    ),
  ],
),

 

 

마지막으로 snapshot을 통해 비동기 응답의 상태 분기처리하여 UI를 그려주는 FutureBuilder 구현 코드입니다.

FutureBuilder<String>(
  future: _future,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator(); // 로딩 중
    } else if (snapshot.hasError) {
      return Text('에러 발생: ${snapshot.error}');
    } else if (snapshot.hasData) {
      return Text('결과: ${snapshot.data}'); // 데이터 도착
    } else {
      return Text('버튼을 눌러서 데이터를 가져오세요');
    }
  },
),

 

실제로 앱을 실행하여 버튼을 누르면 약 2초간 CircularProgressIndicator 가 로딩 UI를 그려주고, 2초 후에는 비동기 응답인 "데이터 도착! 👏" 또는 "데이터 삭제! 👏" 메시지가 화면에 나타납니다.

 

 

 

 

Recap

- FutureBuilderFuture의 상태를 감지해서 자동으로 UI를 갱신해 줍니다. 
- snapshot을 이용해 비동기 응답 상태를 분기 처리합니다.
- 비동기 작업 결과를 기다렸다가 화면에 반영해야 할 때 유용합니다.