자바스크립트에서 제너레이터와 async/await

Jin·2022년 3월 1일
0

Javascript

목록 보기
18/22

자바스크립트 (이하 JS)에서는 이제껏 당연히 일단 함수가 실행되기 시작하면 완료될 때까지 계속 실행되며 도중에 다른 코드가 끼어들어 실행되는 법은 없었습니다.

하지만, ES6부터 이러한 완전 실행 법칙을 따르지 않는, 제너레이터라는 전혀 새로운 종류의 함수가 등장하였습니다.

var x = 1;

function *foo() {
  x++;
  yield;
  console.log("x: ", x);
}

function bar() {
  x++;
}

var it = foo();
it.next();
console.log(x); // 2
bar();
console.log(x); // 3
it.next(); // x: 3

함수 이름 앞에 * 표시를 붙이게 되면 JS 엔진은 이 것을 제너레이터로 인식합니다.

제너레이터를 이 제너레이터의 실행을 제어할 이터레이터 객체가 필요한데 여기서는 it가 그 역할을 수행합니다.

it.next()를 하게 되면 yield가 있는 곳까지 함수의 로직을 수행한 후 멈추게 됩니다.

그리고 다시, it.next()를 하면 해당 yield 이후의 로직을 수행합니다.

이렇듯 제너레이터는 1회 이상 시작/중지를 거듭할 수 있으면서도 반드시 끝까지 실행할 필요는 없는 특별한 함수입니다.

제너레이터에 있어서 중요한 지점은 여기 있습니다.

function *foo(x, y) {
  return x * y;
}

var it = foo(6, 7);
var res = it.next();
console.log(res.value); // 42

it은 일반 함수와 다르게 foo 함수의 결괏값을 담지 않습니다. 왜냐하면 foo 함수 자체가 제너레이터이기 때문입니다. foo가 실행하는 것은 비로소 그 이터레이터인 it가 next() 되었을 때입니다. 또한, value 프로퍼티로 일종의 중간 반환 값을 받을 수 있습니다.

yield와 value 프로퍼티에 대해 좀 더 들어가보겠습니다.

function *foo(x) {
  var x = yield 2;
  z++;
  var y = yield (x * z);
  console.log(x, y, z);
}

var z = 1;
var it1 = foo();
var it2 = foo();

var val1 = it1.next().value; // 2
var val2 = it2.next().value; // 2

val1 = it1.next(val2 * 10).value; // 40 <-- x: 20, z: 2
val2 = it2.next(val1 * 5).value; // 600 <-- x: 200, z: 3

it1.next(val2 / 2); // 20 300 3
it2.next(val1 / 4); // 200 10 3

처리 로직은 이렇습니다.

  • *foo() 인스턴스 2개를 동시에 실행하고 두 next() 호출 모두 yield 2 지점에서 2 값을 넘겨받습니다.
  • val2 * 10은 2 * 10이므로 이 값은 it1에 전달되어 x 값은 20이 됩니다. z 값은 1에서 2로 증가하고 20 * 2를 yield 하므로 val1은 40이 됩니다.
  • val1 * 5는 40 * 5이고 이 값은 it2에 전달되어 x 값은 200이 됩니다. z 값은 다시 2에서 3으로 증가하고 200 * 3을 yield 하면 val2는 600이 됩니다.
  • val2 / 2는 600 / 2고 it1로 전달되어 y 값은 300이 되고 콘솔 창엔 x y z 값이 20 300 3으로 각각 표시됩니다.
  • val1 / 4는 40 / 4고 it2로 전달되어 y 값은 10이 되고 콘솔창엔 200 10 3이 표시됩니다.

여기서, 우리가 캐치할 수 있는 중요한 특징은 다음과 같습니다.

  • value 프로퍼티를 통해 yield 바로 뒤에 있는 값을 반환받을 수 있다.
  • n번째 yield로 들어가는 값은 n + 1번째 next 함수의 인자로 들어간다.
  • 제너레이터는 다중 인스턴스화가 가능하다.
  • 제너레이터의 로직이 정상적으로 종료되려면 next 함수 호출의 수는 yield보다 1개 더 많아야 한다.

이렇게 yield와 value 프로퍼티로 우리는 실행과 중지를 반복되며 중간 결괏값을 반환받을 수 있고 혹은 값을 넣으면서 로직의 흐름을 완벽히 제어할 수 있게 됩니다. 또한, 우리는 yield와 next로 양방향으로 값을 보내고 받을 수 있기 때문에 양방향 메시징 체계로도 실질적인 활용이 가능합니다.

만약, foo 제너레이터의 로직을 도중에 그만두고 싶다면 it.next() 대신 it.return()을 호출하면 됩니다.

그리고 yield에 반환 값이 없는 경우에는 엄밀히 말하면 yield undefined가 수행되는 것이므로 value 프로퍼티로 가져올 시 undefined를 가져오게 됩니다.


async, await

제너레이터는 프라미스와도 좋은 호흡을 보여줬습니다.

제너레이터가 프라미스를 yield 하고 나중에 이 프라미스가 제너레이터의 이터레이터를 제어하여 끝까지 진행하는 이 패턴은 매우 강력하고 유용하였습니다. ES7에서는 이런 비동기 흐름 제어와 관련하여 굉장히 유용한 유틸리티가 등장하는데 그것이 바로 async와 await입니다.

function foo(x,y) {
  return ajax(`url?x=${x}&y=${y}`);
}

async function main() {
  try {
    var text = await foo(11, 31);
    console.log(text);
  } catch (err) {
    console.log(err);
  }
}

위의 코드에서 text가 콘솔로 찍히기 위해서는 우리는 그동안 setTimeout이던지 콜백이던지 하는 다양한 방법을 시도했었지만 그 나름의 크리티컬한 단점들도 같이 받아들여야 했습니다. 하지만, await와 async는 마치 동기적인 코드를 보는 듯 간단하면서도 강력한 기능을 제공합니다.

async function 하면 프라미스를 await 할 경우 해야 할 일, 즉 프라미스가 귀결될 때까지 이 함수를 멈추게 할 거란 사실을 자동으로 인식하기 때문입니다.

profile
배워서 공유하기

0개의 댓글