비동기 프로그래밍: Future와 async/await

비동기 프로그래밍: Future와 async/await

Dart의 라이브러리는 FutureStream 객체를 돌려주는 함수로 가득 차 있어요. 이런 함수를 비동기(asynchronous) 함수라 부르는데, 파일 입출력 같은 시간이 걸리는 작업을 완료를 기다리지 않고 시작해 놓고 바로 반환합니다. asyncawait 키워드를 쓰면 이런 비동기 코드를 동기 코드처럼 읽히는 모양으로 작성할 수 있어요. 이 글에서는 Future를 다루는 방법과 async 함수 선언, Stream을 받아오는 방법을 차례로 살펴볼게요.

출처: Dart 공식 문서 — Asynchronous programming

Future 다루기

완료된 Future의 결과가 필요할 때는 두 가지 방법을 써요. 하나는 여기서 설명할 async/await이고, 다른 하나는 dart:async 문서에 나오는 Future API를 쓰는 방법이에요.

asyncawait로 작성한 코드는 비동기지만 겉보기에는 동기 코드와 아주 비슷해요. 예를 들어 비동기 함수의 결과를 await로 기다리는 코드를 볼까요.

await lookUpVersion();

await를 쓰려면 그 코드가 async 함수, 즉 async로 표시된 함수 안에 있어야 해요.

Future<void> checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

async 함수는 시간이 오래 걸리는 작업을 할 수는 있어도, 그 작업이 끝날 때까지 기다리지는 않아요. 대신 첫 await 표현식을 만날 때까지 실행하다가, 거기서 Future 객체를 반환하고, await 표현식이 완료된 뒤에야 실행을 이어갑니다.

await를 쓰는 코드에서 에러와 정리를 다룰 때는 try, catch, finally를 사용해요.

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

async 함수 안에서는 await를 여러 번 쓸 수 있어요. 다음 코드는 세 함수의 결과를 순서대로 기다립니다.

var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await _expression_에서 _expression_의 값은 보통 Future예요. 만약 Future가 아니라면 그 값은 자동으로 Future 안에 감싸집니다. 이 Future는 어떤 객체를 돌려주겠다는 약속을 나타내고, await _expression_의 값은 바로 그 돌려받은 객체예요. await 표현식은 그 객체를 사용할 수 있을 때까지 실행을 멈추게 합니다.

await를 쓸 때 컴파일 타임 에러가 난다면, await가 async 함수 안에 있는지부터 확인해 보세요. 예를 들어 앱의 main() 함수에서 await를 쓰려면 main()의 본문이 async로 표시되어 있어야 해요.

void main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

위 예시는 checkVersion()이라는 async 함수를 결과를 기다리지 않고 호출하는데, 코드가 함수가 끝났다고 가정하면 문제가 생길 수 있어요. 이런 문제를 피하려면 unawaited_futures 린터 규칙을 쓰면 됩니다.

async 함수 선언하기

async 함수는 본문이 async 수정자로 표시된 함수예요. 함수에 async 키워드를 붙이면 그 함수는 Future를 반환하게 됩니다.

예를 들어 String을 반환하는 아래 동기 함수를 보겠습니다.

String lookUpVersion() => '1.0.0';

이 함수를 async 함수로 바꾸면 — 예를 들어 나중에 구현이 시간이 걸리게 될 때 — 반환값은 Future가 돼요.

Future<String> lookUpVersion() async => '1.0.0';

함수 본문에서 Future API를 직접 쓸 필요는 없어요. 필요하면 Dart가 Future 객체를 알아서 만들어 줍니다. 함수가 유용한 값을 반환하지 않는다면 반환 타입을 Future<void>로 만들어 주는 게 좋아요.

Stream 다루기

Stream에서 값을 받아올 때도 두 가지 방법이 있어요. 하나는 async비동기 for 루프(await for)를 쓰는 것이고, 다른 하나는 dart:async 문서에 나오는 Stream API를 쓰는 방법이에요.

await for를 쓰기 전에, 그것이 코드를 더 명확하게 하고 정말로 스트림의 모든 결과를 기다리려는 것인지 확인해야 해요. 예를 들어 UI 이벤트 리스너에는 보통 await for를 쓰지 않는 게 좋아요. UI 프레임워크가 끝없는 이벤트 스트림을 보내기 때문이에요.

비동기 for 루프의 형태는 다음과 같아요.

await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

_expression_의 값은 Stream 타입이어야 해요. 실행은 다음과 같이 진행됩니다.

  1. 스트림이 값을 내보낼 때까지 기다린다.
  2. 변수에 그 값이 들어간 상태로 for 루프의 본문을 실행한다.
  3. 스트림이 닫힐 때까지 1과 2를 반복한다.

스트림 듣기를 멈추려면 breakreturn 문을 쓰면 돼요. 이들은 for 루프를 빠져나가면서 스트림 구독도 해제합니다.

비동기 for 루프를 만들 때 컴파일 타임 에러가 난다면 await for가 async 함수 안에 있는지 확인해 보세요. 예를 들어 main() 함수에서 비동기 for 루프를 쓰려면 main()의 본문이 async로 표시되어 있어야 해요.

void main() async {
  // ...
  await for (final request in requestServer) {
    handleRequest(request);
  }
  // ...
}

더 알아보기