Flutter

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

흐성진 2025. 1. 31. 16:50
반응형

이미지 출처 : 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 는 함수의 본문 앞에 사용하여 함수를 비동기식으로 표현할 수 있다.

awaitasync 와 짝꿍으로 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

 

반응형