
이번 시간에는 Dart언어의 비동기 처리 방식에 대해 알아본다.
Flutter를 사용하여 개발한 모바일 앱은 네트워크 통신, 데이터베이스 조회, 파일 읽기/쓰기 등 시간이 오래 걸리는 작업을 자주 수행한다. 만약 이러한 작업들을 동기 방식으로 처리한다면 화면이 멈추거나 앱이 응답하지 않는 현상이 발생할 수도 있다.
Dart의 비동기 처리 방식을 활용하면 시간이 소요되는 작업을 백그라운드에서 수행하는 동안 **사용자 인터페이스(UI)**는 계속 동작할 수 있어 더욱 부드럽고 쾌적한 **사용자 경험(UX)**를 제공할 수 있다.
따라서 비동기 프로그래밍은 성능 좋고 안정적인 앱 개발을 위한 필수 기술이다.
비동기 처리란?
비동기(Asynchronous) 란 언제 끝날지 모르는 작업을 기다리지 않고 다음 작업을 처리하게 하는 것을 의미한다. 예를 들어 네트워크 요청, 파일 I/O처럼 시간이 오래 걸리는 작업을 백그라운드에서 처리하는 동안 다른 작업을 병행하여 프로그램의 응답성을 유지하는 방식을 비동기 처리 방식이라 한다.
비동기 처리를 지원하지 않고 동기 방식으로만 처리한다면 어떤 작업에 대해 시간이 오래 걸린다면 사용자는 실행이 멈춘 것이라 판단하고 프로그램을 종료할 수 있다.
다트 언어는 비동기 처리 방식을 지원한다.

비동기 처리의 작동 방식
다트는 async와 await 키워드를 이용해 비동기 처리를 구현한다. 함수 이름 뒤 본문이 시작하는 중괄호 { 앞에 async 키워드를 붙여 비동기 함수임을 나타낸다. async 키워드는 함수가 비동기 작업을 수행하고 Future 객체를 반환한다는 뜻이다.
비동기 함수 안에서 언제 끝날지 모르는 작업 앞에 await 키워드를 붙인다. await 키워드는 비동기 작업이 완료될 때 까지 기다렸다가 결과를 반환한다. 비동기 작업의 결과를 받으려면 비동기 함수 이름 앞에 Future(값이 여러개면 Stream) 클래스를 지정한다.
다음은 비동기 처리 방식 구현 코드 예제이다. 다음 코드를 읽고 출력 결과를 예측해보자.
void main() {
checkVersion();
print('end process');
}
Future checkVersion() async {
var version = await lookupVersion();
print(version);
}
int lookupVersion() {
return 12;
}
일반적으로 보았을 때 main() 함수에서 가장 먼저 checkVersion() 함수를 호출했으므로 checkVersion() 함수에 있는 lookupVersion() 함수가 호출되어 12를 전달받은 후 12를 출력한 다음, main() 함수의 print() 문이 실행될 것 같다. 하지만 출력 결과는 다음과 같다.
end process
12
왜 이런 결과가 나올까?
코드를 하나하나 분석해보자. 먼저 main() 함수에서 checkVersion() 함수를 호출한다. 이때 checkVersion() 함수는 async 키워드로 선언한 비동기 함수이다. 그리고 함수 내부에서 await lookupVersion() 함수가 값을 반환하면 await 키워드는 해당 값을 version 변수에 할당한다. checkVersion() 함수는 version 값을 출력하고 main() 함수로 돌아간다.
main() 함수는 checkVersion() 함수의 실행이 완료될 때 까지 기다리지 않고, print() 문을 호출한다. 이후 checkVersion() 함수의 작업이 완료되면 최종적으로 12를 출력하게 된다.
좀 더 자세하게 분석
void main() {
printOne();
printTwo();
printThree();
}
void printOne() {
print('one');
}
void printTwo() async {
Future.delayed(Duration(seconds: 1), () {
print('Future');
});
print('two');
}
void printThree() {
print('three');
}
해당 코드의 결과는 다음과 같다.
one
two
three
Future
printTwo() 함수에서 Future.delayed() 에 의해 1초 후에 print('Future')를 수행하라고 명령을 한 후 이어서 print('two')를 실행한다. 그리고 여전히 main() 에서는 다음 함수인 printThree() 를 호출한다. 따라서 1초가 지나기 전에 모든 함수의 print()문이 실행되어 one, two, three가 출력되고 이후 main() 함수가 실행된 후 1초가 지난 시점에 Future가 출력되게 된다.
그러면 아래와 같이 코드를 수정해보면 결과가 어떻게 달라질까?
void main() {
printOne();
printTwo();
printThree();
}
void printOne() {
print('one');
}
void printTwo() async {
await Future.delayed(Duration(seconds: 2), () { // await 추가
print('Future');
});
print('two');
}
void printThree() {
print('three');
}
printTwo() 함수에 await 키워드가 추가되었다. await 키워드에 의해 해당 함수를 중단하고 main() 으로 돌아간다. printThree() 함수를 먼저 호출하게 되고 이후 2초가 지난 후 Future 출력, two 출력 후 종료한다.
one
three
Future
two
결론
await가 없으면 비동기 작업을 던져놓고 그냥 다음 줄로 진행하지만, await가 있으면 그 작업이 끝날 때까지 현재 함수를 중단하고 제어권을 호출자에게 넘긴다. 그래서 await가 있을 때는 printThree()가 printTwo()의 print('two')보다 먼저 실행되는 것이다.