[Dart/Flutter] Future, Stream의 차이와 async, then의 차이

2024년 06월 25일
 | 
Info-Geon
Dart_Flutter basic img

Dart를 사용하여 코딩을 하다 보면 Future, Stream을 많이 사용하게 됩니다. 또한 이러한 비동기 함수들을 받기 위해서는 async/await, then 등을 많이 사용하는 것을 확인할 수 있어요.

Future는 FutureBuilder와 함께 사용되며, Stream은 StreamBuilder와 함께 사용됩니다.

초보자의 경우에는 각각이 어떻게 다른지, 어떻게 사용되는지 헷갈릴수 있습니다!

그런 사람들을 위해 이번 글을 작성해보았습니다.

1. Future

Future는 Dart에서 비동기 작업*(아래에 설명하겠습니다)이 완료될 때 하나의 값을 반환합니다. 예를 들어, Firebase로부터의 데이터 fetch, 파일 읽기, 타이머 등등 시간이 걸리는 작업을 수행할 때 사용됩니다.

Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 3));
  return 'Fetched Data';
}

이러한 Future와 함께 쓰이는 것이 FutureBuilder입니다.

이는 Flutter에서 UI를 비동기 데이터에 맞춰 동적으로 업데이트를 하는 위젯이며, Future가 완료될 때 까지의 상태 변화에 따라 다른 UI를 빌드합니다.

Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 3));
  return 'Fetched Data';
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: fetchData(), // 비동기 작업을 나타내는 Future
      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          // Future가 아직 완료되지 않은 경우 (로딩 중)
          return CircularProgressIndicator();
        } else if (snapshot.hasError) {
          // Future 실행 중 에러가 발생한 경우
          return Text('Error: ${snapshot.error}');
        } else {
          // Future가 성공적으로 완료된 경우
          return Text('Result: ${snapshot.data}');
        }
      },
    );
  }
}

하지만 중요한 것은 FutureBuilder는 가져오는 Data에 변화가 있을 때 마다 실시간으로 반영이 되는 것이 아니라, refresh를 하여 Data를 새로 가져 왔을 때만 변화가 있다는 것입니다.

즉, 새로고침 후 Data에 변화가 있으면 UI에 반영이 되는 것입니다.

 

그럼 새로고침을 할 필요가 없이, Data에 변화가 있으면 실시간으로 UI에 반영이 되게 하기 위해서는 어떻게 하면 될까요? 이럴 때 Stream이 사용됩니다.

2. Stream

Stream은 Dart에서 일련의 비동기 데이터 이벤트를 처리할 수 있게 해주는 객체입니다. 여러 값들을 순차적으로 반환할 수 있고, 실시간 데이터 처리등에 사용됩니다.

Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

void main() async {
  await for (var value in countStream(5)) {
    print(value); // 1부터 5까지 1초 간격으로 출력
  }
}

이러한 Stream과 함께 쓰이는 것이 StreamBuilder입니다.

이는 Flutter에서 비동기 데이터를 UI에 동적으로 표시하기 위해 사용되는 위젯이며, Stream의 변화에 따라 UI를 실시간으로 업데이트 합니다.

Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    yield i;
    await Future.delayed(Duration(seconds: 1));
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: countStream(5), // 비동기 스트림
      builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator(); // 스트림 데이터 대기 중
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}'); // 에러 발생
        } else if (snapshot.hasData) {
          return Text('Count: ${snapshot.data}'); // 스트림 데이터 표시
        } else {
          return Text('No data'); // 데이터 없음
        }
      },
    );
  }
}

Stream은 Future과 다르게 새로고침을 할 필요 없이 Data가 변하면 실시간으로 반영되어 UI를 변경하게 됩니다.

그럼 무조건 Stream이 좋은 것인가? 라고 하면 상황에 따라 다를 수 있습니다.

데이터의 변화를 실시간으로 보여줘야할 경우(주식 차트, 추천 검색어 등)에는 Stream이 좋지만, 데이터를 검색했을 때만 최신화 시켜 보여줘야 하는 경우(검색된 상품 등)에는 Future이 더 좋을 수 있으니, 본인의 프로젝트에 맞는 Builder를 선택하시면 됩니다.

 

그렇다면 이러한 비동기 데이터를 받기 위해서는 await와 then이 사용되는데, 이들은 각자 어떠한 차이점이 있을까요?

3. await와 then의 차이

이 차이는 바로 아래 코드로 보여드리도록 하겠습니다.

// then을 사용한 비동기 작업
Future<String> fetchData() async {
  return Future.delayed(Duration(seconds: 3), () => 'Fetched Data');
}

void main() {
  fetchData().then((data) {
    print(data);  // 비동기 작업 완료 후 데이터를 출력
  });

  print('Data fetched');
}
// async/await를 사용한 비동기 작업
Future<void> fetchData() async {
  await Future.delayed(Duration(seconds: 3));
  print('Fetched Data');
}

void main() async {
  print('Fetching data...');
  await fetchData();  // 비동기 작업이 완료될 때까지 기다림
  print('Data fetched');
}

위에서 보는 것과 같이, then의 경우 콜백 방식을 사용하여 비동기 작업을 처리합니다.

하지만 async/await는 비동기 작업을 동기 작업 코드처럼 작성할 수 있게 해주어, 가동성을 높여줍니다.

두가지 방식 다 작동하는 것은 같지만, then은 많이 사용하다보면 유명한 콜백 지옥(callback hell)을 유발할 수 있기 때문에, 가독성과 유지 보수성이 높은 async/await를 사용하는 것이 좋습니다.

참고) 비동기/동기 작업이란 무엇인가?

동기 작업(synchronous operation): 작업이 순차적으로 수행되며, 현재 작업이 완료될 때 까지 다음 작업이 시작되지 않음.

비동기 작업(asynchronous operation): 작업이 시작되면 그 작업이 완료되기를 기다리지 않고, 즉시 다음 작업이 수행됨. 비동기 작업이 완료되면, 콜백 함수가 호출됨.

이렇게만 보면 “그럼 future나 stream의 경우 동기작업이어야 작업(데이터 수신 등)이 완료 될 때 까지 기다린 후, 화면에 표시가 되는거 아닌가?” 라는 물음을 가질 수 있습니다.

하지만 실제로는 비동기 작업(Future) 도중에도 플러터의 UI는 1초에 몇번이나 비동기 작업의 완료 여부와 상관없이 계속해서 랜더링 되고, 업데이트 됩니다. 그래서 만일 동기 작업으로 Future나 stream을 처리하게 되면 랜더링이 되지 않게 되어 오류가 발생 하게 됩니다. 그러므로 비동기 작업으로 처리하여, 계속하여 다음 작업(랜더링)이 수행되게 하고, 비동기 작업이 완료가 되면 이를 콜백함수(then, await)를 통하여 알려주어 이를 랜더링에 반영하는 형식으로 작동됩니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다