본문 바로가기
javascript

ES6의 Promise

by 초특급하품 2019. 10. 16.

Javascript에서는 실행 결과를 받을 때까지 기다리지 않고 다음 작업을 이어서 하는 비동기 요청을 자주 쓴다. 요청과 완료 시점이 다르기 때문에 코딩은 더 어렵게 느껴지지만 javascript가 실행되는 single-thread 환경을 생각해보면 자주 사용할 수밖에 없다.

예를 들어 browser에서 동기적으로 작업을 한다면 그 작업이 진행되는 동안에 사용자의 액션은 freezing 될 것이고, 이런 browser는 아무도 사용하지 않을 것이다.

 

비동기를 자주 사용하는 환경인 만큼 이를 구현하는 방법도 시간에 따라 진화하고 있다.

최신 표준에서는 어떻게 구현을 하는지 알아보기 앞서서 진화해온 과정을 익히기 위해 옛날 방법부터 시작해서 ES6에 등장하는 Promise까지 알아보자.

 

Callback

javascript에서 비동기 작업을 구현할 때 가장 원초적인 방법은 콜백을 이용하는 방법이다. 익숙하게 사용하던 setTimeout()나 jquery의 ajax요청이 이에 해당된다.

어느 기술이든 평범한 경우에는 문제가 잘 드러나지 않지만, 요구조건이 까다로워지면 문제점이 보이기 마련이다. 콜백 작업의 의존성이 있어서 연속되는 경우에 콜백 안에 콜백이 이어지는 콜백 지옥이 펼쳐진다. 코드로 생각해보면 콜백이 한 개 이어질 때마다 괄호와 tab이 추가될 것이고, 우리가 집중하고 싶은 각각의 콜백 로직의 가독성은 점점 산으로 갈 것이다. 가독성에서 더 나아가 비동기 작업이 중첩되면서 에러를 제어하기도 힘들어진다.

 

Promise

콜백의 단점을 보완하기 위해 ES6에서 Promise가 등장했다.

Promise는 미래에 얻어지는 비동기 작업의 결괏값을 사용하기 위한 방법을 정의했다. 우선 그 방법으로 각 비동기 작업의 성공/실패를 제어하기 위해 두 가지를 나눠서 인터페이스를 강제했다.

 

결과를 넘기는 데에는 성공/실패 두 가지 방법이 있기 때문에 Promise의 선언은 인자로 resolvereject를 받는 것으로 시작한다. 받을 때와 마찬가지로 비동기 작업 후 해당 작업이 성공했을 때는 resolve(), 실패했을 때는 reject()를 호출까지 해야 한다.

이런 약속이 있기 때문에 비동기 작업을 시키는 입장에서 어떻게 수행되는지는 몰라도 그 결과로 두 함수중 하나는 반드시 한번 호출된다는 확신을 가질 수 있다.

 

의존적인 여러 개의 비동기 작업

asyncJob1을 수행한 후 결괏값을 받아서 asyncJob2를 수행하는 작업을 Promise를 이용해서 구현해보면 아래와 같다.

doAsyncJob1 = () => new Promise((resolve, reject) => {
    asyncJob1((result) => {
      if(result.success){
        resolve(result.data);
      }
      reject(new Error('[fail] asyncJob1'));
    })
  });

doAsyncJob2 = (data) => new Promise((resolve, reject) => {
    asyncJob2(data, (result) => {
      if(result.success){
        resolve(result.data);
      }
      reject(new Error('[fail] asyncJob2'));
    })
  });

doAsyncJob1()
  .then(doAsyncJob2)

Promise의 인터페이스에 맞게 비동기 작업이 성공하면 resolve를, 실패하면 reject를 호출했고, 이 규약을 지켰기 때문에 각 비동기 작업은 then을 통해서 연결할 수 있다.

 

콜백 구현의 단점이었던 에러를 제어하기 위해서는 catch를 사용한다.

doAsyncJob1()
  .then(doAsyncJob2)
  .catch(err => handleError(err))

  .then(doAsyncJob3)
  .then(doAsyncJob4)

 

catch를 사용해서 이전의 비동기 작업에서 발생하는 error를 잡고, 논리적으로 판단해서 그대로 진행시킬지, 또 error를 던질지 입맛에 맞게 구현하면 된다. 비동기로 실행되는데도 불구하고 마치 동기적인 코드의 try-catch와 같이 사용할 수 있다.

추가로 then에서 반환되는 값이 당연히 Promise 객체임을 예상할 수 있기 때문에 매번 new Promise를 하지 않고 일반 데이터를 반환해도 Promise로 감싸준다.

 

이를 이용하면 간단한 코드의 경우 아래와 같이 arrow function과 함께 간결하게 쓸 수 있다.

doAsyncJob1()
  .then(result => result * 2)

 

독립적인 여러 개의 비동기 작업

순서가 중요한 비동기 작업은 앞서 then으로 연결해서 그 순서를 보장시켰다.

순서가 상관없는 여러개의 Promise작업은 하나의 Array로 묶어서 한 번에 작업을 요청할 수 있다. 이에 대한 응답도 역시 Promise라서 그 뒤에 비동기 작업을 이어서 하고 싶으면 역시 then으로 연결할 수 있다. 이때 인자로는 앞에서 Array로 수행한 여러 Promise들의 결괏값이 Array로 전달된다.

Promise.all([doAsyncJob1, doAsyncJob2])
  .then(doAsyncJobAfterAll)

 

정리

Promise는 새로 생긴 스펙이지만 사실 기존에 비슷한 기능을 하는 라이브러리들이 있었고, 이것도 비동기 작업을 구현하는 하나의 패턴일 뿐이다. 그러기 때문에 완벽할 수는 없고 개발자의 호불호에 맞게 사용하는 게 맞는 것 같다.

그래도 콜백과 비교를 해보면 장점이 많이 보인다.

  1. 각 비동기 작업을 별도로 관리해서 로직에 집중할 수 있다.
  2. 많은 라이브러리가 Promise를 지원하기 때문에 도입 비용이 낮다.
  3. then, all 등과 같은 매력적인 함수가 지원된다.
  4. 여러 비동기 작업에 대한 에러 핸들링이 쉽다.

'javascript' 카테고리의 다른 글

React class vs function component 차이점  (0) 2020.05.17
Typescript 타입 정의 파일  (0) 2020.03.29
CommonJS / ES 모듈 로딩 방식  (1) 2020.03.29
ES8의 async와 await  (0) 2019.10.16
ES6의 Iterator와 Generator  (0) 2019.10.16

댓글