자바스크립트 비동기적 프로그래밍

2018-02-28

자바스크립트 애플리케이션은 단일 스레드에서 동작한다. 즉, 자바스크립트는 한 번에 한 가지 일만 할 수 있다. 자바스크립트가 싱글 스레드라고 할 수 있는 일이 제한된다고 느낄지도 모르지만, 사실 멀티스레드 프로그래밍에서 겪어야 하는 골치 아픈 문제를 신경쓰지 않아도 된다는 장점이 있다. 물론 부드럽게 동작하는 소프트웨어를 만들기 위해서는 사용자의 입력 뿐만 아니라 여러 문제를 비동기적 관점에서 생각해야 한다. 자바스크립트의 비동기적 프로그래밍에는 뚜렷이 구분되는 세 가지 패러다임이 있다. 이는 콜백, 프로미스(promise), 제너레이터이다.


사용자 입력 외에 비동기적 프로그래밍을 사용해야 하는 경우는 크게 세가지이다.

1) Ajax 호출을 비롯한 네트워크 요청
2) 파일을 읽고 쓰는 등의 파일시스템 작업
3) 의도적으로 시간 지연을 사용하는 기능

1. 콜백

2. 스코프와 비동기적 실행

  • 함수를 호출하면 항상 클로저가 만들어진다. 매개변수를 포함해 함수 안에서 만든 변수는 모두 무언가가 자신에 접근할 수 있는 한 계속 존재한다.
  • 콜백은 자신을 선언한 스코프(클로저)에 있는 것에 접근할 수 있다.
  • 스코프 예제 (for 루프 밖에서 선언된 let 변수)
  • 스코프 예제2 (for 루프 안에서 선언된 let 변수)


3. 오류 우선 콜백

  • 노드가 점점 인기를 얻어가던 시기에 우선 오류 콜백(error-first callback)이라는 패턴이 생김
  • 콜백을 사용하면 예외 처리가 어려워지므로 콜백과 관련된 에러를 처리할 방법의 표준이 필요함
  • 이에 따라 나타난 패턴이 콜백의 첫 번째 매개변수에 에러 객체를 쓰자는 것. 에러가 null이거나 undefined인 경우에는 에러가 없는 것
  • 오류 우선 콜백을 다룰 때는 에러 매개변수를 체크하고 그에 맞게 반응한다
  • 오류 우선 콜백 예제


4. 콜백 헬

  • 콜백 헬 예제
  • 콜백 예외처리 (잘못된 콜백 함수 예외처리)
  • 위의 예제에서는 콜백이 우연히 두번 호출되거나 아예 호출되지 않았을 경우를 방지하는 장치도 없다.
  • 비동기적 코드가 늘어나면 늘어날수록 관리하기 어렵다. 이런 문제 때문에 프라미스가 등장

5. 프라미스(promise)

  • 콜백의 단점을 해결하기 위해 만들어짐. 일반적으로 안전하기 관리하기 쉬운 코드를 만들 수 있음
  • 프라미스가 콜백을 대체하는 것은 아님. 프라미스에서도 콜백을 사용하며, 프라미스는 콜백을 예측 가능한 패턴으로 만들어줌
  • 프라미스는 콜백을 사용했을 때 나타날 수 있는 엉뚱한 현상이나 찾기 어려운 버그를 상당수 해결
  • 프라미스 기반 비동기적 함수를 호출하면 그 함수는 Promise 인스턴스를 반환한다. 프라미스는 성공하거나 실패하거나 두가지 뿐이다.
  • 프로미스는 다음 중 하나의 상태를 가집니다.
    1) 대기중(pending): 초기상태, 이행 또는 거부되지 않은
    2) 이행됨(fulfilled): 연산이 성공적으로 완료됨
    3) 거부됨(rejected): 연산이 실패
/*
 * 형식
 */
p.then(onFulfilled, onRejected);

p.then(function(value){
  // 이행 
  }, function(reason){
  // 거부
 })


/*
 * 예제
 */
var p1 = new Promise(function(resolve, reject){
  resolve("success!");
  // 또는
  // reject("Error!")
});

p1.then(function(value){
  console.log(value); //성공
}, function(reason){
  console.log(reason); //오류
})

6. 프라미스 체인

  • 프로미스는 체인으로 연결할 수 있다
  • 프로미스가 완료되면 다른 프로미스를 반환하는 함수를 즉시 호출할 수 있다.
  • 프로미스 체인 예제


7. 제너레이터와 프라미스의 만남

  • 제너레이터는 함수와 호출자 사이의 양방향 통신을 가능하게 한다.
  • 현재 실행중인 함수에서 제너레이터의 next()를 호출하면 제너레이터로 제어권을 넘기고, 제너레이터는 자신의 기능을 수행하다가 yield를 실행하면 다시 제어권을 양보한다.
  • 제너레이터는 원래 동기적인 성격을 가졌지만, 프라미스와 결합하면 비동기 코드를 효율적으로 관리할 수 있다.
  • 이터레이터를 생성해서 next()를 실행하면 결과의 value 값으로 프라미스를 반환하고, 프라미스의 then() 메서드에서 다시 이터레이터의 next() 함수를 실행한다. 이런식으로 이터레이터가 done: true가 될 때까지 순환하면서 호출하게된다.
  • 즉 next() -> yield() -> then() -> next()의 순환 흐름이 된다.
  • 제너레이터 예제


8. co 라이브러리


참고

김정환님 블로그, 제너레이터와 프라미스를 이용한 비동기 처리