자바스크립트의 비동기 처리 톺아보기 - (2) generator, async/await

wookhyung·2022년 9월 18일
4

톺아보기

목록 보기
2/8
post-thumbnail

https://velog.io/@ctdlog/JS-자바-스크립트의-비동기-처리-톺아보기

이전 글과 이어지는 글입니다.

아마도 자바스크립트 비동기라는 주제에 대해서 Callback, Promise, async/await 라는 키워드들은 대부분 들어본 적이 있으실 겁니다. 하지만 Generator 는 아마도 들어본 적이 있는 사람도 있을 것이고, 처음 들어본 사람도 있을거로 생각합니다.

본격적으로 Generator 를 설명하기 전에, 이전 주제였던 Callback, Promise 로 잠깐 되돌아 가봅시다.

Callback 의 문제점 중에는 콜백 지옥이 있었습니다. 이 콜백 지옥은 단순히 가독성이 떨어진다는 이유로 읽기가 힘든 것일까요? 물론, 그것도 문제점이겠지만 비동기 처리 방식 자체에 한계점이 있습니다.

일반적인 인간의 사고방식은 코드가 위에서 아래로 순차적으로 실행되는 것이 더 친숙하게 다가옵니다.

다음과 같은 코드가 있습니다.

function A(callback) {
  console.log('A');
  setTimeout(() => callback(), 0);
}

function B() {
  console.log('B');
}

function C() {
  console.log('C');
}

A(B);

C();

출력 결과가 어떻게 될지 추론해봅시다.

아마도, A -> C -> B 라는 순서대로 출력될 것이라고 추론하는 데에는 그렇게 많은 시간이 걸리지 않을 것입니다.

하지만, setTimeout과 같은 비동기 코드가 점점 많아지면 어떻게 될까요? 개발자 입장에서 코드가 순차적으로 읽히지 않고 다양한 인과관계를 추론하면서 디버깅 작업을 해야 하므로 많은 시간이 소요될 것입니다.

결국, 비동기 방식의 한계점은 코드의 흐름 제어를 순차적/동기적으로 표현할 수 없다는 데 있습니다.

Promise는 Callback에 비해서 가독성 측면에서 더 나아졌지만, 비동기 코드 자체의 가독성 문제를 해결하지 못했기 때문에 개발자들은 비동기 코드를 동기 코드처럼 표현할 방법을 탐구했습니다.


3️⃣ 제네레이터(Generator)

기존 ES5까지는 자바스크립트에서 함수가 실행되기 시작하면 완료될 때까지 계속 실행되며 도중에 다른 코드가 끼어들어 실행되는 법은 없었다고 생각했습니다.

그러나 ES6부터 제네레이터, 함수 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재시작할 수 있는 특수한 함수가 등장했습니다.

현재 실행중인 함수에서 제네레이터의 next() 를 호출하면 제너레이터로 제어권을 넘기고 제네레이터는 본인의 역할을 수행하다가 yield 를 실행하면 다시 제어권을 넘겨준 함수로 제어권을 양보하는 구조가 제네레이터의 동작 방식입니다.

이게 무슨 말일까요? 제네레이터의 예제 코드를 살펴봅시다.

var x = 1;

// 제네레이터 함수를 작성하기 위해 function 키워드에 * 을 붙입니다.
function* foo() {
  x++;
  yield;
  console.log("x : ", x);
}

function bar() {
  x++;
}

// 제네레이터 함수를 호출하며, 제네레이터 객체가 반환됩니다. 제네레이터 객체는 이터러블이면서, 동시에 이터레이터입니다.
// 반환된 'generator' 객체는 next() 메서드를 가지고 있습니다.
var generator = foo();

// foo()는 여기서 시작됩니다.
generator.next();
console.log(x); // 2
bar();
console.log(x); // 3
generator.next(); // x : 3

앞서, yield 를 실행하면 제어권을 양보한다고 했습니다. x++ 가 실행된 이후에 yield 가 실행되면서 함수가 멈추고 함수 호출자에게 제어권을 양보(yield)합니다.

이제 함수 호출자인 generator 객체가 제어권을 가졌으며, 필요한 시점에 next()가 호출되면 함수가 중지됐던 시점부터 다시 함수가 실행되며, 다음 yield 표현식을 만나면 함수는 다시 멈춥니다.

즉, 다음과 같은 일련의 과정을 반복해서 거치는 것입니다.

yield -> generator.next() -> yield -> generator.next() -> yield -> generator.next() -> ... -> return

🛠 그렇다면, 이런 특징을 가진 제네레이터가 비동기 처리에서 어떻게 활용될 수 있을까요?

request('url')
  .then(response => {
    render(response.data);
  });

일반적으로 Promise를 사용하여 비동기 처리 코드를 작성한다면, 다음과 같이 구현할 것입니다.

이 코드는 제네레이터를 활용하여 다음과 같이 리팩토링할 수 있습니다.

function* main() {
  const response = yield request('url');
  render(response.data);
}

const generator = main();
const request = generator.next().value; /* request(...)을 받는다. */
request.then(response => {
  generator.next(response); /* generator에 res를 넘겨주면서 main을 재실행시킨다. */
});

뭐야, 코드가 더 길어졌는데요?

네, 코드가 길어진건 사실이지만 코드의 지저분함보다 일단 main 제네레이터에 집중해봅시다.

function* main() {
  const response = yield ajaxRequest('url');
  render(response.data);
}

이전의 Promise를 활용한 코드와 비교했을 때, 마치 동기적으로 작동하는 코드처럼 보이지 않나요?

처음 글을 시작하면서 말했던 비동기 처리 방식 자체의 문제점, 인간에게 친숙한 사고방식인 순차적/동기적으로 표현할 수 없다는 문제가 해결되었습니다. 이는 개발자의 입장에서 코드의 가독성을 높여주고 코드 실행 과정을 추론하는 시간을 줄이게 됩니다.

결과적으로, Promise의 신뢰성과 Generator의 가독성이 합쳐져 더 나은 코드를 작성할 수 있게 되었습니다.

하지만, 앞서 말했듯이 코드가 길어진 건 사실입니다. yield 를 통해서 함수가 중지되고 나서, 다시 실행해주려면 특정 함수가 실행되고 나서 then 을 통해 next() 를 호출해줘야 하기 때문입니다.

이를 해결하기 위해서는 반복되는 과정인 next() 호출을 도와줄 함수를 구현해야 합니다. 이 함수는 제네레이터가 생성한 객체가 yield 한 무언가가 귀결될 때까지 기다린 후, 귀결된 값을 다시 제네레이터에 돌려주면 됩니다.

이 과정을 도와주는 게 이번 글의 마지막 주제, 바로 ES2017(ES8)에서 추가된 async/await 입니다.

4️⃣ async/await

async/await은 Promise와 Generator를 합친 일종의 Syntactic sugar로 소개되었습니다.

위에서 구현했던 코드를 다시 리팩토링해 봅시다.

async function main() {
  const response = await ajaxRequest('url');
  render(response.data);
}

이전보다 훨씬 가독성 측면에서 좋아졌습니다.

또한, 앞서 1편에서도 말했지만, 콜백 패턴을 이용하여 비동기 처리를 하게 되면 에러를 처리하기 쉽지 않았습니다.

하지만, async/await은 await을 통해 Promise가 수행된 결과 '값'을 전달받기 때문에, 외부에서 에러 처리가 용이합니다.

async function main() {
  try {
    const response = await ajaxRequest('잘못된 URL');
    render(response.data);
  } catch (error) {
    console.error(error) // 에러 발생
  }
}

하지만, 모든 비동기 처리에 await 키워드를 사용하는 것은 주의해야 합니다.

async function foo() {
  const a = await new Promise((resolve) => setTimeout(() => resolve(1), 3000));
  const b = await new Promise((resolve) => setTimeout(() => resolve(2), 2000));
  const c = await new Promise((resolve) => setTimeout(() => resolve(3), 1000));
  
  console.log(a, b, c); // 1 2 3
}

foo();

여기서 foo 함수가 수행하는 a, b, c 3개의 비동기 처리는 서로 연관이 없고 순서를 보장할 필요가 없습니다. 하지만 await을 사용했으므로, 이전 비동기 처리가 끝날 때까지 대기하며 다음 코드가 실행되어 최종적으로 출력될 때까지 총 6초의 시간이 소요됩니다.

이러한 상황에는, 다음과 같이 Promise.all 메서드를 활용하는 것이 좋습니다.

async function foo() {
  const res = Promise.all([
      new Promise((resolve) => setTimeout(() => resolve(1), 3000)),
      new Promise((resolve) => setTimeout(() => resolve(2), 2000)),
      new Promise((resolve) => setTimeout(() => resolve(3), 1000)),
  ]);

  console.log(res); // 1 2 3
}

foo();

async/await 은 새로운 기술이 아닌, 기존에 있었던 Promise와 Generator의 Syntactic sugar입니다. 먼저, 해당 기술들에 대한 선행 지식이 있어야 상황에 맞춰 적절하게 사용할 수 있을 것 같습니다.


🧐 생각 정리

자바스크립트의 비동기 처리, Callback부터 시작해서 Promise, generator, async/await까지 살펴보았습니다. 최근까지 많은 기술 스택들을 익히면서, '내가 이 기술을 제대로 이해하고, 사용하고 있나?' 라는 생각이 들었습니다. 자바스크립트에 대한 깊은 이해가 없는 상태에서 새로운 기술 스택들만 익히는 데 급급했던 것 같아, 이번 발표 스터디를 통해서 자바스크립트 기초 지식을 탄탄히 하고자 합니다.

그 중 첫 번째로 자바스크립트의 비동기 처리를 택한 이유는, 많은 비동기 코드를 작성하면서 async/await에 대해서 제대로 알고 사용하지 않고 남발했던 경우가 많았던 것 같습니다. 글로 정리하면서 생각보다 비동기 처리에 대해서 모르는 점이 많았고, generator와 같이 기존에 잘 알지 못했던 것도 많이 알게 되어 공부를 시작하길 잘했다는 생각이 듭니다.

특히 async/await의 경우, Promise와 Generator에 대한 Syntactic sugar이기 때문에 이번 기회를 통해 Promise와 Generator에 대해서 제대로 이해하지 않은 채 사용했더라면, 좋은 코드를 작성하지 못했을 것이라는 생각이 듭니다. 모든 상황에 완벽하게 적용할 수 있는 비동기 처리는 존재하지 않으므로 각각의 상황에 따라 적절하게 처리해야 될 것 같습니다.

앞으로는 기술을 선택하기에 앞서, '왜' 사용해야 하는지에 집중하고 고민하는 개발자가 되겠습니다.

지금까지 긴 글 읽어주셔서 감사합니다.

Reference

https://velog.io/@hjkdw95/왜-비동기적-프로그래밍을-해야하는가
https://www.howdy-mj.me/javascript/async/
https://www.howdy-mj.me/javascript/asynchronous-programming/
https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous/Introducing
https://ko.javascript.info/async
https://youtu.be/s1vpVCrT8f4
https://velog.io/@koseungbin/비동기
https://fe-churi.tistory.com/35
https://poiemaweb.com/es6-generator
https://suhwan.dev/2018/04/18/JS-async-programming-with-promise-and-generator/
https://devowen.com/292?category=721812
https://jeonghwan-kim.github.io/2016/12/15/coroutine.html
https://sosocodingday.tistory.com/290
https://chodragon9.github.io/blog/callback/
https://youtu.be/fsmekO1fQcw
https://youtu.be/3uuBHt_SNTA

profile
Front-end Developer

0개의 댓글