Asynchronous Javascript(비동기 자바스크립트)

JavaScript

Synchronous Javascript?

Asynchronous Javascript(비동기 자바스크립트)를 소개하기 전, Synchronous Javascript(동기 자바스크립트)에 대해 짚고 넘어가면 좋겠다.

자바스크립트는 single-threaded인 언어라, 메인 스레드 한 개에서 한 번에 한 가지 작업만 처리할 수 있고, 그 외의 작업들은 해당 활성 작업이 끝날 때까지 블락 상태를 유지한다.

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

위의 코드는 이런 순서로 처리된다.

  1. <button> 에 대한 레퍼런스를 받아온다. 해당 element는 DOM에 정의되어 있으므로, 바로 가져올 수 있다.
  2. 1의 버튼에 click 이벤트 리스너를 생성하여 이벤트가 트리거될(버튼이 클릭될) 때 아래의 기능을 실행하게 만든다.
    1. 'You clicked me!' 라는 alert 을 보여준다.
    2. alert 메세지가 닫히면 <p> element를 생성한다.
    3. 생성된 <p>textContext 를 부여한다.
    4. document.body에 해당 <p> 문단을 추가한다.

Asynchronous Javascript?

많은 웹 브라우저들은 synchronous하게 실행되면 안 되는 API나 기능들을 가진다. 예를 들면 네트워크를 통한 정보/파일 불러오기, 데이터베이스를 통한 CRUD 작업 등을 할 때에는 동기적으로 실행이 불가한 경우가 하다.

let response = fetch('myImage.png');
let blob = response.blob();
... 
// blob 이미지를 UI에 보여주는 코드

myImage.png 파일의 용량이나 네트워크의 속도에 따라 이미지를 다운로드 받는 시간에는 차이가 있겠지만, 이미지를 받아오는 시간이 얼마나 소요될 지 모르기 때문에 response.blob() 을 실행하면 에러가 발생할 수 있다. response 가 아직 준비되지 않았기 때문이다. 안전한 코드를 작성하려면 response 가 반환되기 전까지 .blob() 의 실행을 기다리게 해야 한다.

이렇게, 동기적으로 실행되면 안 되는 기능들을 어떤 이벤트를 걸어 한꺼번에, 비동기적으로 처리를 할 수 있는 장치가 필요해진다. 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가며, 안 좋을 경우 무수한 에러를 남기게 된다.

비동기적인 코드는 별도의 요청, 실행 대기, 보류 등을 다룬다. 예시로는:

  1. setTimeout : 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류
  2. addEventListener : 사용자의 직접적인 개입이 있을 때에야 어떤 함수를 실행하도록 대기
  3. XMLHttpRequest : 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 함수를 실행하도록 대기
  4. Promise(ES6) : 비동기 연산이 종료된 이후에 성공(resolve) 또는 실패(reject) 여부를 받아올 때까지 다음 구문의 실행을 대기
  5. Generator(ES6) : Iterator.next 메서드와 yield 구문을 통해 어떤 함수의 실행을 멈추고 대기
  6. async/await(ES2017) : 함수에 async를 표기하고 await로 비동기 작업을 명시하여 해당 내용이 처리된 이후에 다음 함수를 실행하도록 대기

Callback Hell

Callback Hell(콜백 지옥)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다. 주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 콜백 함수를 많이 사용하게 되는데, 가독성이 떨어질뿐더러 에러를 발견하고 코드를 수정하기도 어렵다. 웹의 복잡도가 높아질수록 비동기 코드가 많이 사용되기에 콜백 지옥에 빠지기 훨씬 쉬워졌다.

Callback Function?

Callback Function(콜백 함수)은 다른 코드의 인자로 넘겨주는 함수다. 다른 코드(함수/메서드)에게 함수를 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수라고 볼 수 있다. 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행한다.

어떤 함수 A를 호출하면서 ‘특정 조건일 때 함수 B를 실행해서 나에게 알려달라’ 라는 요청을 함께 보낸다. 이 요청을 받은 함수 A의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 B를 직접 호출한다.

Callback Hell Example, and how to fix it

setTimeout(function(name){
  var coffeeList = name;
  console.log(coffeeList);

  setTimeout(function(name){
    coffeeList += ', ' + name;
    console.log(coffeeList);

    setTimeout(function(name){
      coffeeList += ', ' + name;
      console.log(coffeeList);

      setTimeout(function(name){
        coffeeList += ', ' + name;
        console.log(coffeeList);
      }, 500, '카페라떼');
		
    }, 500, '카페모카');
	
  }, 500, '아메리카노');

}, 500, '에스프레소');
> '에스프레소'
> '에스프레소, 아메리카노'
> '에스프레소, 아메리카노, 카페모카'
> '에스프레소, 아메리카노, 카페모카, 카페라떼'

각 콜백은 커피 이름을 전달하고 목록에 이름을 추가한다. 목적 달성에는 지장이 없지만 값이 전달되는 순서가 ‘아래에서 위로’ 향하고 있어 가독성이 떨어지고 이해하기 어렵다.

위에 설명했던 Promise, Generator, async/await 을 써서 위의 코드를 수정해보자.

Promise

new Promise(function(resolve){
  setTimeout(function(){
    var name = '에스프레소';
    console.log(name); // '에스프레소'
    resolve(name);
  }, 500);
}).then(function(prevName){
  return new Promise(function(resolve){
    setTimeout(function(){
      var name = prevName + ', 아메리카노';
      console.log(name); // '에스프레소, 아메리카노'
      resolve(name);
    }, 500);
  });
}).then(function(prevName){
  return new Promise(function(resolve){
    setTimeout(function(){
      var name = prevName + ', 카페모카';
      console.log(name); // '에스프레소, 아메리카노, 카페모카'
      resolve(name);
    }, 500);
  });
}).then(function(prevName){
  return new Promise(function(resolve){
    setTimeout(function(){
      var name = prevName + ', 카페라떼';
      console.log(name); // '에스프레소, 아메리카노, 카페모카, 카페라떼'
      resolve(name);
    }, 500);
  });
});

new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch로 넘어가지 않는다.

비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.

Generator

var addCoffee = function(prevName, name){
  setTimeout(function(){
    coffeeMaker.next(prevName ? prevName + ', ' + name : name);
  }, 500);
};
var coffeeGenerator = function*(){
  var espresso = yield addCoffee('', '에스프레소');
  console.log(espresso); // '에스프레소'
  var americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano); // '에스프레소, 아메리카노'
  var mocha = yield addCoffee(americano, '카페모카');
  console.log(mocha); // '에스프레소, 아메리카노, 카페모카'
  var latte = yield addCoffee(mocha, '카페라뗴');
  console.log(latte); // '에스프레소, 아메리카노, 카페모카, 카페라떼'
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

*가 붙은 함수 coffeeGenerator 가 바로 Generator 함수다. Generator 함수를 실행하면 Iterator 가 반환되는데, Iteratornext라는 메서드를 가지고 있다. 이 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다. 이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그다음에 등장하는 yield에서 함수의 실행을 멈춘다.

비동기 작업이 완료되는 시점마다 next 메서드를 호출한다면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행된다.

Promise + Async / Await

var addCoffee = function(name){
  return new Promise(function(resolve){
    setTimeout(function(){
      resolve(name);
    }, 500);
  });
};
var coffeeMaker = async function(){
  var coffeeList = '';
  var _addCoffee = async function(name){
    coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
  };
  await _addCoffee('에스프레소');
  console.log(coffeeList); // '에스프레소'
  await _addCoffee('아메리카노');
  console.log(coffeeList); // '에스프레소, 아메리카노'
  await _addCoffee('카페모카');
  console.log(coffeeList); // '에스프레소, 아메리카노, 카페모카'
  await _addCoffee('카페라떼');
  console.log(coffeeList); // '에스프레소, 아메리카노, 카페모카, 카페라떼'
};
coffeeMaker();

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로도 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에 다음으로 진행된다.

Promisethen 과 흡사한 효과를 얻을 수 있다.


출처
JavaScript: The Hard Parts (Will Sentance)
코어 자바스크립트 (정재남)
코드 예시 : developer.mozilla.org