들어가기 전에

자바스크립트 개발자라면 알아야 할 33가지 개념 #25 자바스크립트 : 바보를 위한 Promise

Javascript Promise 는 사실 어렵지 않습니다. 하지만, 처음에는 많은 사람들이 Promise를 이해하기 어렵다고 생각합니다. 그래서, 저자는 어떻게 Promise를 이해했는지 한번 그대로 적어보겠습니다. 바보가 이해했던 그 방법을요.

Promise 이해하기

Promise는 요약하자면 다음과 같습니다.

"여러분이 아이라고 상상해보세요. 여러분의 어머니가 새로운 스마트폰을 다음 주에 사주기로 약속합니다."

여러분은 다음 주에 정말로 새로운 스마트폰을 가질 수 있을지 알 수 없습니다. 어머니는 진짜로 새로운 스마트폰을 사줄 수도 있고, 만일 기분이 좋지 않다면, 약속을 어기고, 스마트폰을 사주는 것을 잠시 보류할 수도 있습니다.

이게 바로 Promise(약속) 입니다. Promise는 3가지 상태를 가집니다.

  1. Pending(미결) : 여러분은 새로운 스마트폰을 가질 수 있을지 알 수 없습니다.
  2. Fulfilled(이행) : 어머니의 기분이 괜찮아서, 스마트폰을 사줬습니다.
  3. Rejected(거절) : 어머니가 기분이 괜찮지만, 스마트폰은 사주지 않기로 했습니다.

Promise 만들기

이제 위에서 배웠던 내용을 자바스크립트 코드로 만들어봅시다!

// ES 5 //
var is MomHappy = false;

// Promise
var willIGetNewPhone = new Promise(
  function (resolve, reject) {
    if(isMomHappy) {
      var phone = {
        brand: 'Samsung',
        color: 'black'
      };
      resolve(phone); //fulfilled
    }
    else {
      var reason = new Error('mom is not happy');
      reject(reason); //reject
    }
  }
);

코드만 봐도 대략 무슨 내용인지 알기 쉽습니다.

// promise의 문법은 대략 다음과 같이 생겼습니다.
new Promise(/_ executor _/ function (resolve, reject) { ... });

Promise 사용하기

이제 Promise를 만들어보았으니, 사용해봅시다.

// ES 5 //
...

// Promise 호출
var askMom = function () {
  willGetNewPhone
    .then(function (fulfilled) {
      // 와! 새 폰을 얻었다!
      console.log(fulfilled);
    // output: { brand: 'Samsung', color: 'black' }
    })
    .catch(function (error) {
      // 이런. 엄마가 폰을 안사주네..
      console.log(error.message);
    // output: 'mom is not happy'
    });
}

예제를 실행해보고 결과를 봅시다.

데모는 여기서 할 수 있습니다.

promisetest1.webp

Promise 연계(Chaining)하기

Promise는 연계 가능(Chainable)합니다.

여러분은 아이(Kid) 입니다. 새로운 스마트폰을 사면 친구들에게 보여주기로 약속(Promise) 했다고 생각해봅시다.

새로운 Promise를 작성해봅시다.

...

// 2nd promise
var showOff = function (phone) {
  return new Promsie(
    function (resolve, reject) {
      var message = 'Hey friend, I have a new ' +
          phone.color + ' ' + phone.brand + ' phone';

      resolve(message);
    };
  );
};

더 짧게 작성하면 다음과 같습니다.

// shorten it
...

// 2nd promise
var showOff = function (phone) {
  var message = 'Hey friend, I have a new ' + 
              phone.color + ' ' + phone.brand + ' phone';

  return Promise.resolve(message);
};

이제 Promise를 연계해봅시다. willGetNewPhone Promise를 수행한 이후에만, showOff Promise를 수행할 수 있습니다.

...

// call our promise
var askMom = function () {
  willGetNewPhone
  .then(showOff) // 여기서 연계합니다.
  .then(function (fulfilled) {
    console.log(fulfilleded);
    // output: 'Hey friend, I have a new black Samsung phone.'
  })
  .catch(function (error) {
    // oops, mom don't buy it
    console.log(error.message);
  // output: 'mom is not happy'
  });
};

Promise를 연계하는 것은 이렇게나 쉽습니다.

Promise는 비동기(Asynchronous)다.

Promise는 비동기입니다. Promise를 호출한 전과 이후에 메시지를 로깅해봅시다.

var askMom = function () {
  console.log('before asking Mom'); // log before
  willGetNewPhone
    .then(showOff)
    .then(function (fulfilled) {
      console.log(fulfilled);
    })
    .catch(function (error) {
      console.log(error.message);
    });
  console.log('after asking Mom');
}

출력 순서가 어떻게 될까요? 맞춰보세요.

아마 다음과 같을 수 있겠죠?

1. before asking mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

하지만 사실 실제 출력은 다음과 같이 이뤄집니다.

1. before asking mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

promisetest2.webp

왜냐구요? JS는 누구도 기다려주지 않으니까요.

여러분도 어머니가 새로운 스마트폰을 사준다는 약속을 계속 기다리기만 하진 않을 것입니다. 그 동안 밖에 나가서 뛰어 놀기도 하고 그러겠죠? 아닌가요? 그게 우리가 말하는 비동기(Asynchronous) 입니다. 코드는 어떠한 방해나 결과에 대한 기다림 없이 돌아갈 것입니다. Promise는 오직 코드가 흘러가길 기다릴 뿐입니다. 여러분은 .then을 작성하여 코드가 흘러갔을 때 추가적으로 해야할 일을 코딩할 수 있습니다.

ES5, ES6/2015, ES7에서의 Promise

ES5 - 주류 브라우저들

위에 작성했던 데모 코드들은 만일 여러분들이 Bluebird Promise 라이브러리만 설치했다면, ES5 환경(모든 주류 브라우저 + 노드JS 환경을 말합니다.)에서 돌아갑니다. ES5는 자체적으로는 Promise를 지원하지 않습니다. Bluebird외에도 다른 유명한 Promise 라이브러리인 Q가 있으니 참고하세요.

ES6 / ES2015 - 현대 브라우저들과 NodeJs v6

위의 작성했던 데모 코드들이 라이브러리 없이도 작동합니다. 왜냐하면 ES6는 Promise를 네이티브하게 지원하기 때문입니다. 추가로 ES6 함수들과 함께라면, 화살표 함수를 이용하여 코드를 훨씬 더 간단히 만들 수 있습니다. 그리고 constlet과 같은 선언문으로 변수 선언도 가능합니다.

ES6의 코드 예제는 다음과 같습니다.

// ES 6 //
const isMomHappy = true;

// Promise
const willGetNewPhone = new Promise(
  (resolve, reject) => { // 화살표 함수
    if (isMomHappy) {
      const phone = {
        brand: 'Samsung',
        color: 'black'
      };
      resolve(phone);
    }
    else {
      const reason = new Error('mom is not happy');
      reject(reason);
    }
  }
);

const showOff = function (phone) {
  const message = 'Hey friend, I have a new ' +
            phone.color + ' ' + phone.brand + ' phone';
  return Promise.resolve(message);
};

// call our promise
const askMom = function () {
  willGetNewPhone
    .then(showOff)
    .then(fulfilled => console.log(fulfilled))
    .catch(error => console.log(error));
};

askMom();

ES7 - Async Await이 문법을 더 예쁘게 만들어줍니다.

ES7은 asyncawait 문법을 도입했습니다. 두가지 문법은 비동기 문법을 더 예쁘고 이해하기 쉽게 만들어줍니다. 심지어 .then.catch도 필요 없습니다.

우리 예제를 ES7으로 재작성해봅시다.

/_ ES7 _/
const isMomHappy = true;

// Promise
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 2nd promise
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// call our promise
async function askMom() {
    try {
        console.log('before asking Mom');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

(async () => {
    await askMom();
})();

왜 Promise이고 언제 써야 할까요?

우리는 왜 Promise가 필요할까요? Promise가 있기 전에는 어떻게 코딩을 했을까요? 이러한 지룸넹 대답하기 전에 다시 근본적인 부분으로 넘어가봅시다.

일반 함수 vs 비동기 함수

두 가지 예제를 봅시다. 두 예제 모두 두 숫자를 더하는 예제입니다. 하나는 일반 함수를 이용해 더하고 하나는 원격에서 더합니다.

일반 함수로 두 숫자 더하기

// add two numbers normally

function add (num1, num2) {
    return num1 + num2;
}

const result = add(1, 2); // you get result = 3 immediately

비동기 함수로 두 숫자 더하기

// add two numbers remotely

// get the result by calling an API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// you get result  = "undefined"

여러분이 만일 일반 함수로 두 숫자를 더한다면, 결과를 즉시 볼 수 있을 것입니다. 하지만, 여러분이 원격 서버를 이용하여 두 숫자 더하기에 대한 결과 값을 구한다면, 여러분은 기다려야 합니다. 즉시 값을 얻진 못합니다.

또는 이렇게 설명할 수도 있습니다. 여러분은 서버가 다운되거나 응답 지연 때문에 값을 얻을 수 있을지 없을지도 모릅니다. 여러분은 서버가 값을 반환할 때까지 모든 프로세스를 잠시 멈춰두길 원하진 않을 것입니다. 그래서 비동기가 필요합니다.

API를 호추하는 일, 파일을 다운로드하는 일, 파일을 읽는 일 등은 주로 비동기 함수를 사용하여 코딩합니다.

Promise가 있기 전에 코딩하던 방법 : Callback

우리가 반드시 비동기 호출을 사용해야 할까요? 정답은 아닙니다. Promise 전에 우리는 callback을 사용했습니다. Callback은 여러분이 결과 값을 받으면 수행할 함수입니다. callback의 개념을 받아들이기 위해 이전 예제를 수정해봅시다.

// add two numbers remotely
// get the result by calling an API

function addAsync (num1, num2, callback) {
    // use the famous jQuery getJSON callback API
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback
    const result = success; // you get result = 3 here
});

문법은 그럭저럭 괜찮아보입니다. 그렇다면 우리는 왜 Promise를 사용해야 할까요?

만일 여러분이 연속되는 비동기 액션을 수행하려면 어떻게 해야 할까요?

숫자들을 한 번 더하지 않고, 이번에는 숫자를 세 번 더해보겠습니다. 일반 함수에서요. 우린 다음과 같이 코딩할 것입니다.

// add two numbers normally

let resultA, resultB, resultC;

 function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // you get resultA = 3 immediately
resultB = add(resultA, 3); // you get resultB = 6 immediately
resultC = add(resultB, 4); // you get resultC = 10 immediately

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

콜백을 사용하면 어떻게 될까요?

// add two numbers remotely
// get the result by calling an API

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // use the famous jQuery getJSON callback API
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback 1
    resultA = success; // you get result = 3 here

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // you get result = 6 here

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // you get result = 10 here

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

데모는 여기있습니다.

문법이 사용자에게 친화적이지 못합니다. 좋은 말로는, 피라미드처럼 보인다고 합니다. 하지만 사람들은 주로 이렇게 된 코드를 "콜백 지옥"이라고 합니다. 왜냐하면 콜백이 다른 콜백 안에 계속 중첩되어 있기 때문입니다. 여러분이 10개의 콜백을 가지고 있다고 가정하면, 10번 중첩을 시켜야 합니다.

콜백 지옥에서 빠져나오기

Promise가 우리를 구하러 왔습니다. Promise 버전의 예제를 봅시다.

// add two numbers remotely using observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // use ES6 fetch API, which return a promise
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

데모는 여기에 있습니다.

Promise와 .then을 사용하여, 우리는 피라미드 모양의 콜백을 빳빳히 펴냈(Flatten)습니다. 중첩된 부분이 없어서 훨씬 보기 좋아졌습니다. 물론, ES7의 async 문법을 사용하면, 우리는 예제를 더 더 깔끔히 작성할 수 있습니다. 이 예제는 여러분들이 직접 작성해보면 좋을 것입니다.

새로운 친구 : Observable

Promise 라는 친구를 놓아주기 전에, 비동기 데이터를 더욱 쉽게 다루게 해줄 수 있는 Observable 이라는 친구에 대해 알아봅시다.

Observable은 0개 혹은 그 이상의 이벤트를 내보내는 lazy event stream입니다. 그리고 Observable을 끝낼 수도 있고 안 끝낼 수도 있습니다.

Promise와 Observable의 몇가지 중대한 차이가 있습니다.

두려워하지 마세요. Observable로 쓰인 새로운 데모를 봅시다. 이 예제에서, 우리는 Observable을 만들기 위해서 RxJS를 이용할 것입니다.

let Observable = Rx.Observable;
let resultA, resultB, resultC;

function addAsync(num1, num2) {
  // ES6의 fetch API를 사용합니다. fetch API는 promise를 반환합니다.
  const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then (x => x.json());

  return Observable.fromPromise(promise);
}

addAsync(1, 2)
.do(x => resultA = x)
.flatMap(x => resultAsync(x, 3))
.do(x => resultB = x)
.flatMAp(x => resultAsync(x, 4))
.do(x => resultC = x)
.subscribe(x => {
  console.log('total: ' + x);
  console.log(resultA, resultB, resultC);
});

데모는 여기에 있습니다.

알아둬야 할 것은

Observable은 더 멋진 일을 쉽게 할 수 있는 친구입니다. 예를 들면, delay 추가 함수를 3초로 설정하여 지연을 둘 수도 있고 또한 일정 횟수 이후에 다시 호출할 수도 있습니다.

...
addAsync(1,2)
  .delay(3000) // delay 3 seconds
  .do(x => resultA = x)
  ...

Observable은 나중에 더 이야기 해봅시다!

요약

여러분은 Promise와 callback과 친해졌습니다. 이해하고 사용해보세요. Observable에 대해서는 별로 생각 안하셔도 됩니다. 3가지 모두는 상황에 따라 여러분의 개발에 사용될 수 있습니다.

우린 이해를 위해 mom promise to buy phone에 대한 데모코드를 몇개 작성해보았습니다.

그게 끝입니다. 자바스크립트 Promise를 길들이는데 많은 도움이 되었길 바랍니다. Happy Coding!