비동기 처리 방식 알아보기

Dart의 async, await을 활용한 비동기 처리를 공부합니다.
Posted on 2022-02-07 by GKSRUDTN99
Flutter Dart Asynchronous

비동기란?

다트는 비동기 처리를 지원하는 언어입니다.

비동기란 언제 끝날지 모르는 작업을 기다리지 않고 다음 작업을 처리하게 만드는 것을 의미합니다.


모든 작업을 동기적으로 진행한다면 특정 작업(네트워크 통신)이 오래걸릴 경우 다른 작업(UI 처리)이

진행되지 않아 사용자는 실행이 멈춘 것으로 생각할 수 있습니다.

더 나은 사용자 경험을 위해 네트워크 통신, DB CRUD 등의 작업들은 비동기로 처리해야 합니다.


비동기 프로세스의 작동 방식

다트는 async와 await 키워드를 통해 비동기 처리를 구현합니다. 구현하는 방법은 다음과 같습니다.


  1. 함수 이름 뒤, 본문이 시작하는 중괄호 { 앞에 async 키워드를 붙여 비동기 코드블럭임을 나타낸다.
  2. 비동기 코드블럭 안에서 언제 끝날지 모르는 작업 앞에 await 키워드를 붙인다.
  3. 비동기 작업의 결과를 함수의 리턴값으로 반환하고 싶다면, Future(값이 여러 개면 Stream) 클래스를 사용할 수 있다.


아래는 비동기 처리의 예시 코드입니다.


void main() {
  checkVersion();
  print('end process');
}

Future checkVersion() async {
  var version = await lookUpVersion();
  print(version);
}

int lookUpVersion() {
  return 12;
}

만약, checkVersion함수가 비동기 함수가 아니라면, main함수에서 제일 먼저 checkVersion함수를 호출했으므로

checkVersion 안에 있는 lookUpVersion이 호출되어 12를 전달받아 출력한 다음,

다시 main함수로 돌아와 'end process'가 출력될 것입니다.


하지만, 실제 실행결과는 아래와 같습니다.



이런 결과가 나오는 이유는 checkVersion함수가 비동기 함수였기 때문입니다.


async 키워드를 통해 checkVersion함수를 비동기 함수로 만들었으므로

main함수를 실행하다 checkVersion함수를 만났을 때,

checkVersion함수를 실행하기 위해 main함수의 진행이 멈추는 것이 아니라,

checkVersion함수는 비동기적으로 따로 실행되고, main함수의 나머지 부분을 먼저 실행합니다.


앞선 코드에서 lookUpVersion함수 앞에 await키워드가 붙어있는 것을 확인할 수 있습니다.


await 키워드는 처리를 완료하고 결과를 반환할 때까지 이후 코드의 처리를 멈춥니다.


그러므로 lookUpVersion함수가 실행되어 12를 반환한 후에야 print(version)구문을 실행하게 됩니다.


await 키워드를 통해 네트워크 지연 등으로 제대로 된 값을 반환받지 못한 채 이후 과정이 실행되는 것을 방지할 수 있습니다.


비동기 함수가 반환하는 값 활용하기


비동기 함수가 반환하는 값을 처리하려면 then()함수를 이용합니다.


다음은 then()함수를 활용한 예시코드입니다.


void main() async {
  await getVersionName().then((value) => {
    print(value)
  });
  print('end process');
}

Future<String> getVersionName() async {
  var versionName = await lookUpVersionName();
  return versionName;
}

String lookUpVersionName() {
  return 'Android 0';
}

실행 결과는 아래와 같습니다.



getVersionName함수의 반환값을 Futer<String>으로 정해두고, async키워드를 통해 비동기 함수로 만들면,

이 함수가 값을 성공적으로 반환했을 때 호출하는 쪽에서는 then함수를 이용해 처리할 수 있습니다.


then함수 외에 error함수를 통해 에러 상황도 처리할 수 있습니다.


다트와 스레드

다트는 하나의 스레드로 동작하는 언어입니다.


그러므로 앞에서 살펴본 await 키워드를 적절하게 사용해야 합니다.


다음은 await 키워드의 활용 예시 입니다.


void main() {
  printOne();
  printTwo();
  printThree();
}

void printOne() => print('One');

void printThree() => print('Three');

void printTwo() {
  Future.delayed(Duration(seconds: 1), () {
    print('Future!');
  });
  print('Two');
}


실행결과는 아래와 같습니다.



Future.delayed함수는 첫번째 parameter만큼 기다린 뒤에

두번째 parameter로 받은 구문을 실행하라는 의미입니다.


Future.delayed함수는 비동기 함수이므로,

그 특성에 따라 'Two'가 먼저 출력됩니다.


그 후에 'Three'를 출력하고 'Future!'가 가장 늦게 출력됩니다.


printTwo()함수를 다음과 같이 수정해보겠습니다.


void main() {
  printOne();
  printTwo();
  printThree();
}

void printOne() => print('One');

void printThree() => print('Three');

void printTwo() async {
  await Future.delayed(Duration(seconds: 1), () {
    print('Future!');
  });
  print('Two');
}


실행결과는 아래와 같습니다.



Future.delayed코드 앞에 await 키워드를 붙였으므로 이후 코드의 실행이 멈춥니다.


그럼 printTwo 함수를 벗어나 main함수의 나머지 코드를 실행하고,

그 다음 await가 끝나면 printTwo함수의 나머지 구문을 실행합니다.


이처럼 await키워드를 이용하면 호출한 쪽의 프로세스가 끝나거나,

해당 프로세스가 또다시 await키워드를 만나기 전까지 기다리게 됩니다.


그러므로 이를 잘 고려하여 프로그램을 작성해야 합니다.


그럼, 아래 코드의 출력결과를 예상해보는 것으로 오늘 공부를 마무리하겠습니다.


void main() async {
  printOne();
  printTwo();
  printThree();
  await Future.delayed(Duration(seconds: 2), () {
    print('Main Future!');
  });
}

void printOne() => print('One');

void printThree() => print('Three');

void printTwo() async {
  await Future.delayed(Duration(seconds: 1), () {
    print('Future!');
  });
  print('Two');
}


실행 결과입니다.