JavaScript 의Promise를 이해하는 방법

소개

자바스크립트 프로미스는 이해하기 어려울 수 있습니다. 그래서 저는 프로미스를 이해하는 방법을 적어보려고 합니다.

프로미스 이해하기

프로미스란 간단히 말해:

“당신이 아이라고 상상해보세요. 엄마가 약속하셨죠, 다음 주에 새 폰을 사주신다고.”

당신은 모릅니다 다음 주까지 그 폰을 받을지 말지. 엄마는 진짜로 새 폰을 사줄 수도 있고, 사주지 않을 수도 있습니다.

그것이 약속입니다. 약속에는 세 가지 상태가 있습니다. 그것들은:

  1. 대기 중: 당신은 모릅니다 그 폰을 받을지 말지
  2. 이행됨: 엄마가 기뻐하며, 당신에게 새 폰을 사줍니다
  3. 거부됨: 엄마가 슬퍼하며, 폰을 사주지 않습니다

프로미스 만들기

이것을 자바스크립트로 바꿔봅시다.

// ES5: 1부

var isMomHappy = 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
        }

    }
);

코드 자체가 상당히 표현적입니다.

아래는 일반적으로 프로미스 구문이 어떻게 보이는지 입니다:

// 프로미스 구문은 이렇게 생겼습니다
new Promise(function (resolve, reject) { ... } );

프로미스 사용하기

이제 프로미스가 있으니, 사용해보겠습니다:

// ES5: 2부

var willIGetNewPhone = ... // 1부에서 이어짐

// 프로미스 호출
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // 예이, 새 폰을 샀어요
            console.log(fulfilled);
             // 출력: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // 이런, 엄마가 안 사주셨어요
            console.log(error.message);
             // 출력: '엄마가 행복하지 않아요'
        });
};

askMom();

예제를 실행해보고 결과를 확인해봅시다!

데모: https://jsbin.com/nifocu/1/edit?js,console

Promise 연결하기

Promise는 연결 가능합니다.

예를 들어, 당신이 아이로서, 엄마가 새 휴대폰을 사주면 친구에게 약속하고 보여줄 것입니다.

이것은 또 다른 약속입니다. 이를 작성해 봅시다!

// ES5

// 두 번째 약속
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

참고: 위의 코드를 아래와 같이 줄여서 작성할 수 있습니다:

// 줄이기

// 두 번째 약속
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

Promise를 연결해 봅시다. 당신, 아이는 willIGetNewPhone Promise가 끝난 후에만 showOff Promise를 시작할 수 있습니다.

// Promise 호출
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // 여기에 연결
    .then(function (fulfilled) {
            console.log(fulfilled);
         // 출력: 'Hey friend, I have a new black Samsung phone.'
        })
        .catch(function (error) {
            // 이런, 엄마가 사주지 않았어요
            console.log(error.message);
         // 출력: 'mom is not happy'
        });
};

이렇게 하면 Promise를 연결할 수 있습니다.

약속은 비동기적이다

약속은 비동기적입니다. 약속을 호출하기 전과 후에 메시지를 기록해 보겠습니다.

// 약속 호출
var askMom = function () {
    console.log('before asking Mom'); // 호출 전 로그
    willIGetNewPhone
        .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.

엄마의 약속(새 폰)을 기다리는 동안 놀이를 멈추지 않을 것입니다. 이것이 바로 비동기적이라고 부르는 것입니다: 코드는 결과를 차단하거나 기다리지 않고 실행됩니다. 약속을 기다려야 진행되는 모든 것은 .then에 넣습니다.

다음은 ES5의 전체 예제입니다:

// ES5: 전체 예제

var isMomHappy = true;

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

    }
);

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

    return Promise.resolve(message);
};

// promise 호출
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // 여기서 연결
    .then(function (fulfilled) {
            console.log(fulfilled);
            // 출력: '안녕 친구야, 새로운 검은색 삼성 폰이 생겼어.'
        })
        .catch(function (error) {
            // 어이쿠, 엄마가 안 사주셨네
            console.log(error.message);
            // 출력: '엄마가 행복하지 않아'
        });
};

askMom();

ES5, ES6/2015, ES7/Next의 Promises

ES5 – 대부분의 브라우저

데모 코드는 Bluebird promise 라이브러리를 포함한다면 ES5 환경(모든 주요 브라우저 + NodeJs)에서 작동합니다. 왜냐하면 ES5는 기본적으로 promises를 지원하지 않기 때문입니다. 또 다른 유명한 promise 라이브러리는 Kris Kowal의 Q입니다.

ES6 / ES2015 – 최신 브라우저, NodeJs v6

데모 코드는 ES6가 기본적으로 프로미스를 지원하기 때문에 바로 작동합니다. 또한, ES6 함수를 사용하면 화살표 함수로 코드를 더 간단하게 만들고 constlet을 사용할 수 있습니다.

다음은 ES6 코드의 전체 예제입니다:

//_ ES6: 전체 예제_

const isMomHappy = true;

// 프로미스
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);
        }

    }
);

// 두 번째 프로미스
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    return Promise.resolve(message);
};

// 프로미스 호출
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // 화살표 함수
        .catch(error => console.log(error.message)); // 화살표 함수
};

askMom();

모든 varconst로 대체되었고, function(resolve, reject)(resolve, reject) =>로 간소화되었습니다. 이러한 변경으로 인해 몇 가지 이점이 있습니다.

ES7 – Async/Await

ES7에서는 asyncawait 구문이 도입되어 비동기 구문을 .then.catch 없이 더 쉽게 이해할 수 있게 되었습니다.

ES7 구문으로 예제를 다시 작성해 보겠습니다:

// ES7: 전체 예제
const isMomHappy = true;

// 프로미스
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);
        }

    }
);

// 두 번째 프로미스
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// ES7 async await 스타일로 프로미스 호출
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 사용
(async () => {
    await askMom();
})();

프로미스와 사용 시기

왜 프로미스가 필요한가? 프로미스가 없었을 때 세상은 어땠을까? 이 질문에 답하기 전에 기본부터 살펴보자.

일반 함수 VS 비동기 함수

두 가지 예제를 살펴보자. 두 예제 모두 두 숫자의 덧셈을 수행한다: 하나는 일반 함수를 사용하여 더하고, 다른 하나는 원격으로 더한다.

두 숫자를 더하는 일반 함수

// 일반적으로 두 숫자를 더함

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

const result = add(1, 2); // 결과 = 3을 즉시 얻음
두 숫자를 더하는 비동기 함수
// 두 숫자를 원격으로 더함

// API를 호출하여 결과를 얻음
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// 결과 = "undefined"를 얻음

일반 함수로 숫자를 더하면 결과를 즉시 얻을 수 있다. 하지만 원격 호출을 통해 결과를 얻으려면 기다려야 하며, 결과를 즉시 얻을 수 없다.

서버가 다운되거나 응답이 느려질 수 있기 때문에 결과를 받을지 여부를 알 수 없습니다. 결과를 기다리는 동안 전체 프로세스가 차단되지 않기를 원합니다.

API 호출, 파일 다운로드 및 파일 읽기는 수행할 몇 가지 일반적인 비동기 작업입니다.

비동기 호출에 프로미스를 사용할 필요는 없습니다. 프로미스 이전에는 콜백을 사용했습니다. 콜백은 결과를 받을 때 호출하는 함수입니다. 이전 예제를 수정하여 콜백을 허용해 보겠습니다.

// 두 숫자를 원격으로 더하기
// API를 호출하여 결과 얻기

function addAsync (num1, num2, callback) {
    // 유명한 jQuery getJSON 콜백 API 사용
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // 콜백
    const result = success; // 여기서 result = 3을 얻습니다
});

후속 비동기 작업

숫자를 하나씩 더하는 대신 세 번 더하고 싶습니다. 일반 함수에서는 다음과 같이 할 것입니다:-

// 두 숫자를 일반적으로 더하기

let resultA, resultB, resultC;

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

resultA = add(1, 2); // 즉시 resultA = 3을 얻습니다
resultB = add(resultA, 3); // 즉시 resultB = 6을 얻습니다
resultC = add(resultB, 4); // 즉시 resultC = 10을 얻습니다

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

이렇게 콜백을 사용하면 다음과 같습니다:

// 두 숫자를 원격으로 더하기
// API를 호출하여 결과를 얻기

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // 유명한 jQuery getJSON 콜백 API 사용
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // 콜백 1
    resultA = success; // 여기서 result = 3을 얻습니다

    addAsync(resultA, 3, success => {
        // 콜백 2
        resultB = success; // 여기서 result = 6을 얻습니다

        addAsync(resultB, 4, success => {
            // 콜백 3
            resultC = success; // 여기서 result = 10을 얻습니다

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

데모: https://jsbin.com/barimo/edit?html,js,console

이 구문은 콜백이 깊게 중첩되어 있어 사용자 친화적이지 않습니다.

깊게 중첩된 콜백 피하기

프로미스는 깊게 중첩된 콜백을 피하는 데 도움이 될 수 있습니다. 같은 예제의 프로미스 버전을 살펴보겠습니다:

// 원격으로 두 숫자를 더하기 위해 옵저버블 사용

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // ES6 fetch API를 사용하여 프로미스를 반환합니다
	// .json()이 무엇인가요? https://developer.mozilla.org/ko/docs/Web/API/Body/json
    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)
    });

프로미스를 사용하면 .then으로 콜백을 평평하게 만들 수 있습니다. 어떤 면에서는 콜백이 중첩되지 않아 더 깔끔해 보입니다. ES7 async 구문을 사용하면 이 예제를 더욱 향상시킬 수 있습니다.

옵저버블

프로미스에 안착하기 전에, 옵저버블이라는 비동기 데이터를 다루는 데 도움이 되는 것이 등장했습니다.

옵저버블로 작성된 동일한 데모를 살펴보겠습니다. 이 예제에서는 옵저버블을 위해 RxJS를 사용하겠습니다.

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

function addAsync(num1, num2) {
    // ES6 fetch API를 사용하여 프로미스를 반환합니다
    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 => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

옵저버블은 더 흥미로운 일을 할 수 있습니다. 예를 들어, delay 함수를 3초 동안 지연시키거나 특정 횟수만큼 호출을 재시도할 수 있습니다.

...

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

RxJs에 대한 나의 게시물 중 하나를 여기에서 읽을 수 있습니다.

결론

콜백과 프로미스에 익숙해지는 것은 중요합니다. 이해하고 사용하세요. 아직 Observable에 대해서는 걱정하지 마세요. 상황에 따라 세 가지 모두 개발에 영향을 줄 수 있습니다.

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises