Flutter 에서의 비동기 프로그래밍 Future, async/await

2025. 1. 31. 16:50· 📱Flutter
목차
  1. 비동기 프로그래밍 이란?
  2. Future란?
  3. async / await 이란?
  4. 결론
  5. 참고
반응형

이미지 출처 : https://kkhcode.tistory.com/6

 

맨 처음 iOS를 공부하면서 가장 어렵게 느껴졌던 개념이 비동기 프로그래밍이었다.

내용만 듣고 이걸 왜 쓰는 거지?라는 의문이 들어서 대충 흘려보냈다가 프로젝트에 들어가면서 그 중요성에 대해서 알게 되었다.

그렇게 비동기 프로그래밍의 중요성을 알게 되었고 Flutter를 시작하면서도 다행히 iOS와 크게 다른 점이 없어서 적응할 수 있었지만,

Flutter, Dart에 특화된 비동기 프로그래밍에 대해서는 자세히 알지 못해서 공부하면서 알아보려고 한다.

 

비동기 프로그래밍 이란?

비동기
앞에서 행하여진 사상(事象)이나 연산이 완료되었다는 신호를 받고 비로소 특정한 사상이나 연상이 시작되는 방식. [네이버 국어사전]

즉 비동기 프로그래밍이란 특정 코드의 처리가 완료되기 전, 처리하는 도중에도 아래로 계속 내려가며 수행하는 것이다.

 

동기 vs 비동기

  • 동기 처리 : 태스크를 순차적으로 실행하며, 한 작업이 완료될 때까지 다음 작업을 대기
    • 예 : 맛집에서 줄을 서서 차례대로 주문하기
  • 비동기 처리 : 태스크의 완료를 기다리지 않고 다음 작업을 진행
    • 예 : 카페에서 주문 후 진동벨을 받고 자리에더 다른일 하기

 

https://dart.dev/libraries/async/async-await

 

Asynchronous programming: futures, async, await

Learn about and practice writing asynchronous code in DartPad!

dart.dev

코드랩의 내용에 따르면 비동기 프로그래밍은 다음과 같은 경우에 많이 쓰인다고 한다.

  • 네트워크를 통해 데이터 가져오기
  • 데이터베이스에 쓰기
  • 파일에서 데이터 입출력
  • 이미지 다운로드
  • 위치 정보 가져오기
  • 블루투스 통신

 

Future란?

- Future<T> 인스턴스는 T 타입의 값을 생성한다.

- Future가 사용 가능한 값을 생성하지 않으면 Future의 유형은 Future<void>로 한다

- Future를 반환하는 함수를 호출하면 함수는 완료해야 할 작업을 대기열에 추가한다.

- Future 연산이 완료되면 Future는 값 또는 오류와 함께 완료된다.

 

예를 들어 Future<int> 라는 선언이 있을 때 지금은 아무 동작도 하지 않지만

나중에 사용을 하게 되면 int의 값이나 error 가 나오게 될 것이다.

error가 나올 경우를 대비해서 catchError 메서드를 같이 사용해 주는 게 좋다.

 

우선 Future를 기본적으로 사용하는 예시를 보면

Future<void> fetchUserOrder() {
  // 이 함수가 다른 서비스나 데이터베이스에서 사용자 정보를 가져온다고 가정해 보겠습니다.
  return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
}

void main() {
  fetchUserOrder();
  print('Fetching user order...');
}

 

fetchUserOrder() 라는 함수를 만들어서 main 에서 실행한다고 가정해 보자.

이렇게 되면 어느 부분이 먼저 출력될까?

 

동기프로그래밍이라면 fetchUserOrder 가 점유하고 있기 때문에 Large Latte, Fetching user order... 이 순차적으로 출력될 것이다.

하지만 이번엔 Future를 적용했기 때문에 비동기 프로그래밍이므로 fetchUserOrder() 가 완료되기 전에 Fetching user order... 이 먼저 출력된 후 Large Latte가 출력될 것이다.

 

여기서 만약 오류가 발생한다면 어떻게 될까?

오류를 대비하는 방법도 코드 예시를 통해 확인할 수 있다.

import 'dart:async';

Future<int> futureNumber() {
  // 3초 후 100이 상자에서 나온다
  return Future<int>.delayed(Duration(seconds: 3), () {
    return 100;
  });
  // 오류가 발생하는 코드
  // return Future<int>.delayed(Duration(seconds: 3), () {
  //	throw 'Error'!;
  //})
}

void main() {
  // future 라는 변수에서 미래에(3초 후에) int가 나올 것 이다
  Future<int> future = futureNumber();

  future.then((val) {
    // int가 나오면 해당 값을 출력
    print('val: $val');
  }).catchError((error) {
    // error가 해당 에러를 출력
    print('error: $error');
  });

  print('기다리는 중');
}

 

futureNumber 함수는 3초가 되기 전까지는 닫혀있다가 3초가 되면 100이 나오며 Future<int> 를 반환해야 한다.

메인 함수에서 future 변수에 해당 함수의 반환 값을 넣어 저장하면 3초 후에 future 는 int로 값이 바뀌는 것이 아니다.

future 는 계속해서 Future<int> 이다. 그렇게 때문에 future 가 100으로 바뀌는 것이 아니다.

그렇다면 어떻게 그 Future<int> 에서 나오는 값을 다룰수 있을까?

그럴 땐 then 함수를 통해서 다룰 수 있다.

위 코드에서 보면 future.then(...) 을 통해서 다루고 있다.

then 내부에는 또 다른 함수가 들어가 있으며 이 함수로 Future<int> 로 부터 나오는 값을 다룰 수 있는 것이다.

then 내부 함수에서 val 에 Future<int> 라는 상자가 열렸을 때 나오는 값이 들어갈 것이므로 val: 100 이 출력된다.

 

위 코드를 수행하면

기다리는 중, val: 100 의 순서대로 출력되고 그 외의 값이 생기게 되면 catchError 를 통해서 에러값이 나오게 될 것이다.

 

 

그래서 Future 는?

예를 들어서 main 함수의 가장 마지막 줄에 print 가 있고 해당 코드가 1000줄의 코드라고 가정해 보자.

동기적으로 처리했을 경우 Future<int> 에서 값이 나올 때까지 1000줄의 코드는 동작하지 않고 정지해 있을 것이다.

하지만 비동기로 처리를 한다면 어떻게 될까? Future<int>에서 값이 나오지 않아도 계속해서 동작해서 계속해서 print 문이 출력될 것이다.

이것이 비동기 함수를 사용하는 가장 큰 이유이다.

 

Future 에 대한 공식문서

https://api.dart.dev/dart-async/Future-class.html

 

Future class - dart:async library - Dart API

The result of an asynchronous computation. An asynchronous computation cannot provide a result immediately when it is started, unlike a synchronous computation which does compute a result immediately by either returning a value or by throwing. An asynchron

api.dart.dev

 

async / await 이란?

async 와 await 또한 Dart에서 비동기 처리를 위한 것으로 Future 를 조금 더 용이하게 다루기 위해서 사용된다.

비동기 함수를 정의하고 그 결과를 사용할 수 있는 선언적 방법을 제공하며, async 와 await 을 사용할 때는 다음 두가지 기본 조건을 명심해야 한다.

 

비동기 함수를 정의하려면 함수의 본문 앞에 async 를 추가해야 한다.

void main() async {
	...
}

Future<void> main() async {
	...
}

 

 

awit 키워드는 async 함수에만 작동한다.

print(await doSomeThing());

 

함수의 본문 앞에 async를 추가해 주었다면

이제 async 함수가 생겼으므로 await 키워드를 사용하여 나중에 완료될 때까지 기다리게 할 수 있다.

 

동기식 함수의 경우

String createOrderMessage() {
  var order = fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // 이 기능이 더 복잡하고 느리다고 가정해보자
    Future.delayed(
      const Duration(seconds: 2),
      () => 'Large Latte',
    );

void main() {
  print('Fetching user order...');
  print(createOrderMessage());
}

 


Fetch user order...
Your order is: Instance of '_Future<String>'

 

비동기 함수의 경우

Future<String> createOrderMessage() async {
  var order = await fetchUserOrder();
  return 'Your order is: $order';
}

Future<String> fetchUserOrder() =>
    // 이 기능이 더 복잡하고 느리다고 가정해보자
    Future.delayed(
      const Duration(seconds: 2),
      () => 'Large Latte',
    );

Future<void> main() async {
  print('Fetching user order...');
  print(await createOrderMessage());
}

 


Fetching user order ...
Your order is: Large Latte

 

동기와 비동기의 예제는 크게 세 가지의 포인트가 다르다.

  • createOrderMessage() 의 반환 유형이 String 에서 Future<String> 으로 변경 된다.
  • async 키워드는 createOrderMessage() 와 main() 의 함수 본문 앞에 추가된다.
  • await 키워드는 비동기 함수인 fetchUserOrder() 와 createOrderMessage() 앞에 사용한다.

async 는 함수의 본문 앞에 사용하여 함수를 비동기식으로 표현할 수 있다.

await 은 async 와 짝꿍으로 async 함수 내에서만 작동을 할 수 있다.

 

async 와 await을 사용한 예제

다음 예제를 보면서 async 함수 내부에서 어떻게 실행되는지 한번 살펴보자

 

Future<void> printOrderMessage() async {
  print('Awaiting user order...');
  var order = await fetchUserOrder();
  print('Your order is: $order');
}

Future<String> fetchUserOrder() {
  // 이 기능이 더 복잡하고 느리다
  return Future.delayed(const Duration(seconds: 4), () => 'Large Latte');
}

void main() async {
  countSeconds(4);
  await printOrderMessage();
}

void countSeconds(int s) {
  for (var i = 1; i <= s; i++) {
    Future.delayed(Duration(seconds: i), () => print(i));
  }
}

 

이 함수를 실행하게 되면 다음의 결과를 얻게 된다.


Awaiting user order ...
1
2
3
4
Your order is: Large Latte

 

그러면 앞의 예제에서 printOrderMessage() 에서

var order = await fetchUserOrder();
print('Awaiting user order...');

 

두 개의 순서를 바꿔서 실행해 보자.

그렇게 되면 다음의 결과를 갖게 된다.


1
2
3
4
Awaiting user order ...
Your order is: Large Latte

 

print('Awaiting user order') 가 printOrderMessage() 의 첫 번째 await 키워드 뒤에 나타나므로 출력 타이밍이 바뀌게 되는 것이다.

 

플러터에서의 활용

실제 플러터에서는 다음과 같이 비동기 함수를 활용해서 유저의 정보를 받아오고

그 데이터를 표현해 줄 수가 있다.

 

// 사용자 데이터 모델
class User {
  final String name;
  final String email;
  
  User({required this.name, required this.email});
}

// 사용자 관련 서비스 클래스
class UserService {
  // 비동기로 사용자 정보를 가져오는 메서드
  Future<User> fetchUser() async {
    try {
      // 실제로는 여기서 API 호출이 일어난다
      // 예시를 위해 2초 지연을 시뮬레이션을 설정
      await Future.delayed(const Duration(seconds: 2));
      
      // 서버에서 받아온 데이터를 시뮬레이션
      return User(
        name: '홍길동', 
        email: 'hong@example.com'
      );
    } catch (e) {
      // 에러가 발생하면 더 구체적인 에러 메시지와 함께 예외를 던진다
      throw Exception('사용자 조회 실패: $e');
    }
  }
}

class UserProfileWidget extends StatelessWidget {
  final UserService userService;
  
  const UserProfileWidget({required this.userService, super.key});
  
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      // 비동기 데이터를 가져오는 Future를 지정
      future: userService.fetchUser(),
      builder: (context, snapshot) {
        // 데이터를 기다리는 동안 로딩 표시
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
        
        // 에러가 발생한 경우 에러 메시지 표시
        if (snapshot.hasError) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.error, color: Colors.red),
                const SizedBox(height: 16),
                Text(
                  '오류가 발생했습니다\n${snapshot.error}',
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          );
        }
        
        // 데이터가 성공적으로 로드된 경우 사용자 정보 표시
        final user = snapshot.data!;
        return Card(
          margin: const EdgeInsets.all(16),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Text('이름: ${user.name}'),
                const SizedBox(height: 8),
                Text('이메일: ${user.email}'),
              ],
            ),
          ),
        );
      },
    );
  }
}

 

에러 처리(try-catch 구문의 사용)

비동기 작업에서는 다양한 에러가 발생할 수 있다.

그 에러가 어떤 에러인지 알수있으면 해당 에러에 대한 처리가 훨씬 수월해진다.

Future<void> handleAsyncOperation() async {
  try {
    await riskyOperation();
  } on NetworkException catch (e) {
    print('네트워크 오류: $e');
  } on TimeoutException catch (e) {
    print('시간 초과: $e');
  } catch (e) {
    print('예상치 못한 오류: $e');
  } finally {
    print('작업 완료');
  }
}

 

안정적인 비동기 함수 설계 방법

타임아웃 설정

긴 작업이 실행된 경우 무한 대기를 방지하기 위해서 타임아웃을 설정해 줄 수 있다.

Future<void> fetchWithTimeout() async {
  try {
    await fetchData().timeout(
      const Duration(seconds: 5),
      onTimeout: () => throw TimeoutException('요청 시간 초과'),
    );
  } catch (e) {
    print('오류: $e');
  }
}

 

취소 가능한 작업

사용자가 화면을 벗어나가거나 작업을 중단할 수 있을 경우에 작업을 취소할 수 있어야 한다.

Future<void> cancelableOperation() async {
  final completer = Completer<void>();
  
  // 작업 취소 가능하도록 설정
  final operation = CancelableOperation.fromFuture(
    longRunningOperation(),
    onCancel: () => print('작업이 취소되었습니다'),
  );

  try {
    await operation.value;
  } catch (e) {
    print('오류: $e');
  }
}

 

동시 실행 제어

여러 비동기 함수를 실행해야 되는경우 다음과 같이 관리할 수 있다.

Future<void> loadDashboardData() async {
  try {
    // 여러 비동기 작업을 동시에 실행
    final results = await Future.wait([
      getUserProfile(),     // 사용자 프로필
      getRecentActivity(), // 최근 활동
      getNotifications(),  // 알림
    ], eagerError: true);  // 첫 번째 에러 발생 시 즉시 중단
    
    // 결과 처리
    final profile = results[0];
    final activities = results[1];
    final notifications = results[2];
    
    // UI 업데이트
    updateDashboard(
      profile: profile,
      activities: activities,
      notifications: notifications,
    );
    
  } catch (e) {
    // 에러 처리
    handleError(e);
  }
}

 

결론

비동기 프로그래밍은 앱 개발에 있어서 필수적인 요소이다.

Flutter와 Dart 에서는 Future, async/await, Stream 등의 도구들을 사용하여 효율적이면서 반응성이 좋은 앱을 만들 수 있다.

이러한 개념들을 잘 이해하고 적용하면 유저의 입장에서 사용성이 크게 향상될 수 있을 것이다.

특히 네트워크 통신이나 파일 처리와 같이 시간이 걸리는 작업에 대해서 더 효율적으로 관리할 수 있게 된다.

 

참고

https://dart.dev/libraries/async/async-await

 

Asynchronous programming: futures, async, await

Learn about and practice writing asynchronous code in DartPad!

dart.dev

 

https://velog.io/@jintak0401/FlutterDart-%EC%97%90%EC%84%9C%EC%9D%98-Future-asyncawait

 

Flutter/Dart 에서의 Future, async/await

Flutter 와 Dart 를 공부하면서 깨달은 Future, async / await 에 대한 설명과 고민에 대한 답을 작성한 포스트입니다.

velog.io

 

반응형
저작자표시 비영리 변경금지 (새창열림)

'📱Flutter' 카테고리의 다른 글

Flutter 의 annotation 에 대해  (0) 2025.02.12
Flutter Firebase Crashlytics 적용해보기  (1) 2025.02.06
Dart로 알아보는 SOLID 원칙  (0) 2025.01.09
Flutter Equatable의 이해  (0) 2024.12.29
Flutter와 Native의 AppLifeCycle 비교  (6) 2024.12.28
  1. 비동기 프로그래밍 이란?
  2. Future란?
  3. async / await 이란?
  4. 결론
  5. 참고
'📱Flutter' 카테고리의 다른 글
  • Flutter 의 annotation 에 대해
  • Flutter Firebase Crashlytics 적용해보기
  • Dart로 알아보는 SOLID 원칙
  • Flutter Equatable의 이해
흐성진
흐성진
개발자 흐성진의 블로그 입니다.
반응형
흐성진
망각의 코딩러 흐성진
흐성진
전체
오늘
어제
  • 분류 전체보기 (57)
    • 📱Flutter (17)
    • 🌐 Web (1)
      • ⚛️ React (0)
      • 🇯🇸‌JavaScript (1)
    • 💡CS (2)
    • iOS (8)
    • Capstone (22)
      • Capstone1 - 2019-2학기 (13)
      • Caostone2 - 2020-1학기 (9)
    • Linux, Mac (4)
    • Me (3)

인기 글

최근 글

hELLO · Designed By 정상우.v4.2.1
흐성진
Flutter 에서의 비동기 프로그래밍 Future, async/await
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.