[flutter] 비동기 병렬 처리(Future.wait) flutter 앱 적용기

[flutter] 비동기 병렬 처리(Future.wait) flutter 앱 적용기

들어가는 말


최근 진행했던 flutter 프로젝트에는 화면 당 초기에 2-3번 많게는 4-5번의 api 호출 을 하게 됩니다. 이처럼 여러 번의 api 호출 시 응답 속도를 개선한 경험이 있어 공유합니다.

이때 flutter의 비동기 처리 방식이 javascript와 유사한 부분이 많아 비교 참조 하였습니다.


javascript의 비동기 처리 방식

어플리케이션에서 네트워크 요청, 파일 읽기/쓰기 등과 같은 작업을 할 때 javascript에서는 비동기 방식으로 요청을 처리합니다.

javascript는 단일 스레드(single-thread) 기반의 언어이기 때문에 한 번에 하나의 명령만 실행 할 수 있지만 네트워크 요청이나 파일 I/O 작업 등의 처리를 별도의 이벤트 루프(Event loop)에게 위임하면서 시간이 오래걸리는 작업을 어플리케이션 동작 정지 없이 효율적으로 처리 할 수 있습니다.

setTimeout 예시 다이어그램
API 호출 예시 다이어그램

이때 javascript에서는 아래의 상태를 가지는 Promise 라는 객체를 사용합니다.

  1. 대기(Pending): 비동기 처리 로직이 아직 완료되지 않은 상태
  2. 이행(Fulfilled): 비동기 처리가 성공적으로 완료되어 프로미스가 결과값을 반환한 상태
  3. 거부(Rejected): 비동기 처리가 실패하거나 오류를 발생한 상태

위 도식에서 Promise가 대기(Pending) 상태에서 이행(Fulfilled) 또는 거부(Reject) 상태가 되면 Microtask Queue에 쌓인 후 Event loop는 이를 확인하고 Call Stack 으로 이동시켜 실행합니다.

일반적으로 Microtask QueueTask Queue 보다 높은 우선순위를 가집니다.

Task 종류 예시
Task setTimeout setInterval
Microtask Promise callback process.nextTick

Call Stack에서 이행(Fulfilled) 또는 거부(Reject)된 Promise를 처리하는 방식은 크게 두 가지가 있습니다.

  1. 프로미스 체이닝 .then 사용
  2. async await 사용

이 두 가지 방식을 이용하여 jsonplaceholder.typicode.com에서 제공하는 todo를 호출하는 예제 코드는 아래와 같습니다.

// 프로미스 체이닝 사용

fetch(`https://jsonplaceholder.typicode.com/todos/1`)
      .then(response => {
        if (!response.ok) {
          throw new Error(response.statusText);
        }
        return response.json();
      })
      .then(json => console.log(json))
      .catch(error => console.error(error));
// async await 사용

async function fetchTodo() {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/todos/1`);
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    const json = await response.json();
    console.log(json);
  } catch (error) {
    console.error(error);
  }
}

fetchTodo();

만약 여러 번의 api 호출을 한다면 async await 방식을 사용해서 아래와 같이 작성 할 수 있습니다.

const fetchAll = async () => {
  const startTime = performance.now();
  
  try {
    const todoResponse = await fetch(`https://jsonplaceholder.typicode.com/todos/1`);
    const todo = await todoResponse.json();
    console.log('Todo', todo);
    
    const postResponse = await fetch(`https://jsonplaceholder.typicode.com/posts/1`);
    const post = await postResponse.json();
    console.log('Post', post);
    
    const commentResponse = await fetch(`https://jsonplaceholder.typicode.com/comments/1`);
    const comment = await commentResponse.json();
    console.log('Comment', comment);
    
  } catch(error) {
    console.error(error);
  } finally {
    const endTime = performance.now();
    console.log(`Time taken: ${endTime - startTime} milliseconds`);
  }
}

fetchAll();

이러한 방식은 먼저 Todo API를 호출한 후 Promise가 resolved 되면 Post Api를 호출하는 순차적인 방식입니다.

이때 Timer로 Console에 출력된 값은 463 milliseconds 였습니다.

Promise.all( )

위에서 본 것 처럼 다수의 API 호출 등 여러 개의 Promise를 처리할 때는 Promise.all 메서드를 활용하여 여러 비동기 요청을 동시에 진행 할 수 있습니다.

const startTime = performance.now();

Promise.all([
  fetch(`https://jsonplaceholder.typicode.com/todos/1`),
  fetch(`https://jsonplaceholder.typicode.com/posts/1`),
  fetch(`https://jsonplaceholder.typicode.com/comments/1`),
]).then(response => Promise.all(response.map(res => res.json())))
  .then(([todo, post, comment]) => {
  console.log('Todo:', todo);
  console.log('Post:', post);
  console.log('Comment', comment);
  const endTime = performance.now();
  console.log(`Time taken: ${endTime - startTime} milliseconds`);
})

Promise.all() 메서드는 Promise 객체를 배열로 받은 후 API 호출 작업을 동시에 진행합니다. 이때 Timer로 Console에 출력된 값은 160 milliseconds 로 속도가 단축되었습니다.

만약 다수의 Promise 객체 중 하나라도 reject 상태가 된다면 나머지 Promise도 reject 처리 됩니다. 위 예제 코드에서 첫 번째 fetch 함수의 url을 틀리게 작성한 후 테스트 한 결과는 error 출력이었습니다.


flutter에서의 비동기 병렬처리

최근 진행한 flutter api 호출 속도 개선 역시 async await를 활용한 순차적인 api 호출에서 병렬처리 방식으로 바꾸는 방향으로 진행되었습니다.

flutter 개발에 쓰이는 dart 언어는 javascript 처럼 단일 스레드(single-thread) 기반의 언어이고 event loop를 사용합니다. javascriptPromise에 대응 되는 개념은 dart에서는 Future 로 구현되었습니다.

또한 비동기 병렬처리를 위해 사용한 Promise.all() 메서드를 dart에서는 Future.wait() 메서드로 구현할 수 있습니다.

위에서 javascript로 비동기 병렬처리를 구현한 내용을 flutter로 구현해보면 아래와 같습니다.

  • async await를 사용한 순차적인 API 호출(752 milliseconds)
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> fetchSequentially() async {
  final startTime = DateTime.now();
  
  try {
    final todoResponse = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1'));
    final todo = jsonDecode(todoResponse.body);
    print('Todo: $todo');

    final postResponse = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
    final post = jsonDecode(postResponse.body);
    print('Post: $post');

    final commentResponse = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/comments/1'));
    final comment = jsonDecode(commentResponse.body);
    print('Comment: $comment');
  } catch (error) {
    print('Error: $error');
  } finally {
    final endTime = DateTime.now();
    final timeTaken = endTime.difference(startTime);
    print('Time taken: ${timeTaken.inMilliseconds} milliseconds');
  }
}

void main() {
  fetchSequentially();
}
💡
위 코드는 아래 링크에서 테스트 해 볼 수 있습니다.
DartPad
An online Dart editor with support for console and Flutter apps.
  • Future.wait를 사용한 비동기 병렬처리(170 milliseconds)
import 'dart:convert';
import 'package:http/http.dart' as http;

void fetchAll() async {
  final startTime = DateTime.now();

  try {
    final responses = await Future.wait([
      http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos/1')),
      http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1')),
      http.get(Uri.parse('https://jsonplaceholder.typicode.com/comments/1')),
    ]);

    final todo = jsonDecode(responses[0].body);
    final post = jsonDecode(responses[1].body);
    final comment = jsonDecode(responses[2].body);

    print('Todo: $todo');
    print('Post: $post');
    print('Comment: $comment');
  } catch (error) {
    print('Error: $error');
  } finally {
    final endTime = DateTime.now();
    final timeTaken = endTime.difference(startTime);
    print('Time taken: ${timeTaken.inMilliseconds} milliseconds');
  }
}

void main() {
  fetchAll();
}
💡
위 코드는 아래 링크에서 테스트 해 볼 수 있습니다.
DartPad
An online Dart editor with support for console and Flutter apps.

Read more

[Terraform 튜토리얼 1-6] 중복 없애다 망한 썰 – DRY 원칙, 정말 항상 맞을까?

[Terraform 튜토리얼 1-6] 중복 없애다 망한 썰 – DRY 원칙, 정말 항상 맞을까?

개발자라면 한 번쯤 들어봤을 말, "Don't Repeat Yourself", 줄여서 DRY 원칙. 이건 소프트웨어 개발에서 아주 중요한 원칙이에요. 중복을 줄이면 버그도 줄고, 유지 보수도 쉬워지고, 코드도 깔끔해지죠. 그런데… Terraform 같은 IaC 세계에서도 DRY가 무조건 좋을까요? 🤔 DRY가 뭔데? DRY 원칙의 핵심은 딱 하나: "같은 걸 반복해서 쓰지 마." * 상수 값, 로직, 설정

By Chansong
[Terraform 튜토리얼 1-5] 선언형 vs 명령형: Terraform은 왜 선언형인가?

[Terraform 튜토리얼 1-5] 선언형 vs 명령형: Terraform은 왜 선언형인가?

Terraform을 쓰다 보면 종종 듣게 되는 말이 하나 있어요: Terraform은 "선언형(Declarative)" 도구다. 그런데 이게 도대체 무슨 뜻일까요? "명령형(Imperative)"이랑은 뭐가 다르고, 왜 Terraform은 선언형일까요? 🧠 선언형과 명령형, 개념부터 정리 구분 선언형 (Declarative) 명령형 (Imperative) 핵심 개념 무엇이 되어야 하는지만 선언 어떻게 할지를 하나하나 명령 예시 “난 스테이크 먹을래” “고기

By Chansong
[Terraform 튜토리얼 1-4] 모듈이 정리가 안 될 땐, 응집도(Cohesion)를 의심하라

[Terraform 튜토리얼 1-4] 모듈이 정리가 안 될 땐, 응집도(Cohesion)를 의심하라

인프라를 코드로 관리할 때, 좋은 모듈을 만드는 핵심 개념 중 하나가 바로 응집도(Cohesion)입니다. 이 단어가 좀 낯설게 느껴질 수 있지만, 사실 우리는 매일 응집도가 높은 시스템 속에서 살고 있어요. ✅ 응집도란? 하나의 모듈이 하나의 책임 또는 기능에만 집중하는 정도 응집도가 높은 모듈은 말 그대로 한 가지 일에만 집중합니다. 그래서

By Chansong
[Terraform 튜토리얼 1-3] 내부는 감추고, 필요한 것만 보여줘 – 캡슐화란?

[Terraform 튜토리얼 1-3] 내부는 감추고, 필요한 것만 보여줘 – 캡슐화란?

Encapsulation, 캡슐화라고 하면 뭔가 개발자스럽고 어려워 보이죠? 객체지향 프로그래밍 할 때나 듣던 단어 같고요. 그런데 이 개념은, Terraform 같은 Infrastructure as Code(IaC)에서도 아주 중요합니다. 이번 글에서는 "캡슐화가 무엇이고", "왜 필요한지", 그리고 "Terraform에서는 어떻게 쓰이는지"를 쉽게 풀어보겠습니다. ✅ 캡슐화, 한 마디로 뭐야? “필요한 것만 보이게 하고, 나머지는 숨기는 것”

By Chansong