ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ES6+ | Promise
    JavaScript/ES6+ 2020. 4. 7. 11:12

    Promise 사용 배경_비동기 처리 예

    setTimeout, addEventListener 메서드, XMLHttpRequest 객체의 작업은 비동기로 실행됨
    비동기 작업은 동기와 다르게 순차적으로 실행되지 않음
    자바스크립트에서 비동기 코드의 순서를 동기적으로 보장받으려면 콜백 함수를 사용해야 함


    비동기 코드 실행 순서

    console.log("A");
    setTimeout(() => console.log("B"), 0);
    console.log("C");
    
    // A → C → B
    • setTimeout 함수는 인수로 받은 콜백 함수를 n밀리초 뒤에 실행되도록 예약만 하고 바로 다음 코드가 실행됨
    • setTimeout 함수가 콜백 함수를 0초 후에 실행되도록 했으나
      실제로는 호출 스택에 실행 문맥이 남아 있을 때는 그 작업이 끝날 때까지 기다렸다가
      0초 후에 가능한 한 빨리 실행됨

    비동기 코드의 실행 순서를 동기적으로 보장 받기

    const sleep = callback => {
        setTimeout(() => callback(), 1000);
    }
    
    sleep(() => {
        console.log("A");
        sleep(() => {
            console.log("B");
            sleep(() => {
                console.log("C");
            });
        });
    });
    
    // A → B → C
    • sleep 함수는 callback 이라는 콜백 함수를 1초 후(1000밀리초 = 1초)에 실행하도록 구현된 비동기 처리 함수
    • 위와 같이 콜백 함수를 중첩하게 되면 간단한 코드임에도 이해하기가 어려워짐
    • 이런 상황을 콜백 지옥(Callback Hell)이라고 부르는데
      Promise를 사용하면 비동기 처리 코드를 보다 간결하게 작성할 수 있음

    Promise

    Promise는 비동기 처리를 실행하고 그 처리가 끝난 후에 다음 처리를 실행하기 위한 용도로 사용됨


    Promise 객체 생성

    Promise를 사용하기 위해서는 먼저 Promise 객체를 생성해야 함

    const promise = new Promise((resolve, reject) => {
        /* JavaScript Code... */
    });
    • Promise에는 실행하고자 하는 함수를 인수로 넘기는데 이 함수는 다음과 같은 인수를 받음
    • resolve: 함수 안의 처리가 끝났을 때 호출해야 하는 콜백 함수,
      resolve 함수에는 어떠한 값도 인수로 넘길 수 있으며, resolve의 인수는 다음 처리를 실행하는 함수에 전달
    • reject: 함수 안의 처리가 실패했을 때 호출해야 하는 콜백 함수,
      reject 함수에는 어떤 값도 인수로 넘길 수 있으며, 대부분의 경우 에러 메시지 문자열을 인수로 전달

    Promise 사용 예제

    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log("A");
            resolve();
        }, 1000);
    });
    
    promise.then(() => {
        console.log("B");
    });
    
    // A → B
    // 1초 뒤에 A와 B가 순서대로 콘솔에 찍힘

    Promise를 종료시키는 resolve 함수와 then 메서드

    resolve 함수는 Promise를 종료시키는 함수
    resolve 함수에 인수로 전달된 값은 then 메서드에 인수로 넘긴 함수에 전달되어 다음 처리를 위해 사용됨


    then 메서드

    promise.then(onFullfilled);
    • onFullfilled 함수는 성공 콜백 함수라고 하며 promise 안의 처리가 정상적으로 끝났을 때 호출
    • onFullfilled 함수는 response를 인자로 받는데, 이는 promise 안에서 resolve 함수에 전달한 인자임

    then 메서드 사용 예제

    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            const name = prompt(`이름을 입력하세요`);
            resolve(name);    // promise를 종료시키며 then 메소드의 인자로 주어진 함수에 인자로 전달
        }, 1000);
    });
    
    promise.then((response) => {
        console.log(`안녕하세요 ${response}님!`);
    });
    
    // 안녕하세요 하이님!

    Promise를 실패로 처리하는 reject 함수와 catch 메서드

    reject 함수 역시 Promise를 종료시킴
    reject 함수에도 값을 전달할 수 있으며 reject 함수가 실행되면
    then 메서드에 넘긴 함수는 실행되지 않으며 대신 catch 메서드에 넘긴 함수가 실행됨


    catch 메서드

    promise.catch(onRejected);
    • onRejected 함수는 실패 콜백 함수라고 하며 promise 안의 처리가 실패로 끝났을 때 호출
    • onRejected 함수는 error를 인자로 받으며 이는 promise 안에서 reject 함수를 실행했을 때 넘긴 인자임

    catch 메서드 사용 예제

    입력한 숫자가 10 미만이면 then에 넘긴 함수가 실행되고
    그렇지 않은 경우에는 catch에 넘긴 함수가 실행 되는 예제

    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            const n = parseInt(prompt(`10 미만의 수를 입력하세요`));
            if(n < 10){
                resolve(n);
            } else {
                reject(`에러! ${n}(은)는 10 이상입니다.`);
            }
        }, 1000);
    });
    
    promise
    .then(num => {
        console.log(`입력한 수는 ${num}(으)로 10 미만입니다.`);
    })
    .catch(error => {
        console.error(error);
    });
    
    // 입력한 수는 3(으)로 10 미만입니다.
    // 에러! 11은 10 이상입니다.

    then 메서드의 두 번째 매개 변수

    then 메서드에는 두 번째 매개 변수로 실패 콜백 함수 지정 가능
    그러면 then 메서드에서 catch 메서드에서 다룰 내용까지 처리 가능

    promise.then(onFullfilled, onRejected);
    // promise 성공 시 onFullfilled 함수가 실행되고
    // 실패 시 onRejected 함수가 실행됨

    사용 예제

    바로 위의 예제 코드를 then에서 모두 처리하는 형식으로 수정한 예제

    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            const n = parseInt(prompt(`10미만의 수를 입력하세요`));
            if(n <= 10){
                resolve(n);
            } else {
                reject(`에러! ${n}(은)는 10 이상입니다.`);
            }
        }, 1000);
    });
    
    promise
    .then(num => {
        console.log(`입력한 수는 ${num}(으)로 10 미만입니다.`);
    },
    error => {
        console.error(error);
    })
    
    // 입력한 수는 7(으)로 10 미만입니다.
    // 에러! 15은 10 이상입니다.

    ※ catch 메서드를 사용했을 때와 동일하게 동작하지만, catch 메서드를 사용하는 편이 가독성이 높아 보임


    Promise 상태

    Promise가 반환되는 코드가 있을 때 콘솔창을 확인해 보면
    Promise 객체 내부의 [[PromiseStatus]] 에
    "pending", "resolved", "rejected"와 같은 Promise의 상태를 확인할 수 있음

    1. Pending: 대기 상태이며 비동기 처리가 완료되기 전의 상태
    2. Fullfilled: 이행(완료) 상태이며 비동기 처리가 완료되고 Promise가 결과값을 반환한 상태
      직접 확인하면 "resolved"라고 되어있는 것을 확인할 수 있음
    3. Rejected: 비동기 처리가 실패하였거나 오류가 발생한 상태

    ※ Pending 상태가 아닌 Fullfilled 와 Rejected 상태를 합쳐서 Settled 상태라고 하며,
    'resolved'는 Promise가 다른 Promise의 상태에 맞추기 위해 처리되거나 상태가 '잠김'인 것을 의미함


    Promise 처리 과정

     

    [ 출처: MDN_Promise ]


    Promise가 실행하는 콜백 함수에 인수 넘기기

    • Promise를 생성할 때 인자로 넘기는 함수는 resolve, reject를 인자로 받음
    • 따라서 Promise 내부에 또 다른 인수를 넘기고 싶을 때에는 Promise를 반환하는 함수를 먼저 만들고
    • 그 함수에 Promise 내부 함수에서 사용하고자 하는 인수를 넘김

    예제

    Promise 내부 함수에 인수를 넘기는 방법을 다룬 예제

    const buyAsync = (myMoney) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                const payment = parseInt(prompt('지불 금액은?'));
                const balance = myMoney - payment;
                if(balance > 0){
                    console.log(`지불한 금액은 ${payment}원`);
                    resolve(balance);
                } else {
                    reject(`잔액은 ${myMoney}원이므로 지불 불가`);
                }
            }, 1000);
        });
    }
    
    buyAsync(1000)
    .then(balance => {
        console.log(`잔액은 ${balance}원 입니다.`)
    })
    .catch(error => {
        console.error(error);
    })
    
    /*
    999 입력 →
    지불한 금액은 999원
    잔액은 1원 입니다.
    
    1001 입력 →
    잔액은 1000원이므로 지불 불가
    */

    ※ buyAsync 함수에 넘긴 인자를 Promise 객체가 실행하는 익명 함수 안에서 사용


    Promise로 비동기 처리 연결하기

    • Promise로 비동기 처리를 여러 개 연결해서 순차적으로 실행하려면
      then 메서드 안에서 실행하는 성공 콜백 함수가 Promise 객체를 반환하도록 함
    • then 메서드 체인으로 Promise 작업 연결 가능

    예제

    앞에서 작성한 예제를 3번 순차적으로 반복 실행되도록 작성한 예제

    const buyAsync = (myMoney) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                const payment = parseInt(prompt('지불 금액은?'));
                const balance = myMoney - payment;
                if(balance > 0){
                    console.log(`지불한 금액은 ${payment}원`);
                    resolve(balance);
                } else {
                    reject(`잔액은 ${myMoney}원이므로 지불 불가`);
                }
            }, 1000);
        });
    }
    
    buyAsync(1000)
    .then(balance => {
        console.log(`잔액은 ${balance}원 입니다.`);
        return buyAsync(balance);
    })
    .then(balance => {
        console.log(`잔액은 ${balance}원 입니다.`);
        return buyAsync(balance);
    })
    .then(balance => {
        console.log(`잔액은 ${balance}원 입니다.`);
        return buyAsync(balance);
    })
    .catch(error => {
        console.error(error);
    })
    
    /*
    지불한 금액은 100원  → 최초 실행
    잔액은 900원 입니다. → 첫 번째 then
    지불한 금액은 200원
    잔액은 700원 입니다. → 두 번째 then
    지불한 금액은 300원
    잔액은 400원 입니다. → 세 번째 then
    지불한 금액은 200원
    
    최초 실행 이후에 then 메소드가 3차례 연결되어 있기 때문에
    buyAsync 함수는 총 4번 실행됨
    */

    ※ 위의 예제에서는 then 메서드가 같은 Promise 객체를 반환하지만
    then마다 다른 Promise 객체를 반환하게 하면 다른 비동기 처리를 연결해서
    순차적으로 실행하게 할 수 있음


    Promise.all() 비동기 처리 여러 개를 병렬로 실행

    지금까지는 비동기 처리 여러 개를 직렬로 연결해서 순서대로 처리했지만
    Promise 객체의 all 메서드를 사용하면 여러 개의 비동기 처리들을 병렬로 실행 가능

    Promise.all(iterable);
    • 모든 처리가 성공적으로 끝났을 때만 다음 작업을 실행하게 됨
    • all 메서드의 인수인 iterable은 Promise 객체가 요소로 들어있는 반복 가능(iterable)한 객체
      ex) [ { Promise }, { Promise } ,{ Promise } ]
    • 인수로 넘긴 모든 Promise 객체가 resolve 함수를 호출하면
      then 메서드에 지정한 함수 실행
    • then 메서드에서는 각 Promise 객체의 resolve 함수에 넘겨진 인자들을
      배열 형태의 response로 전달 받음
    • 실패로 끝난 Promise 객체가 하나라도 있다면 가장 먼저 실패로 끝난
      Promise 객체에서 실행한 reject 함수의 인자를 실패 콜백 함수로 전달

    Promise.all() 예제

    위에서 작성했던 buyAsync 함수에 name이라는 매개변수를 추가해서
    여러 명이 상품에 대한 값을 지불하는 코드를 작성

    const buyAsync = (name, myMoney) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                const payment = parseInt(prompt(
                    `${name}님, 지불하고자 하는 금액을 입력하세요.`
                ));
                const balance = myMoney - payment;
                if(balance > 0){
                    console.log(
                        `${name}: 지불한 금액은 ${payment}원`
                    );
                    resolve(balance);
                } else {
                    reject(
                    `${name}: 잔액은 ${myMoney}원이므로 지불 불가`
                    );
                }
            }, 1000);
        });
    }
    
    Promise.all([
        buyAsync("Pathas", 1000),
        buyAsync("MIN", 1500),
        buyAsync("HI", 1200)
    ])
    .then(balanceArray => console.log(`잔액: ${balanceArray}`))
    .catch(error => console.error(error));
    
    /*
    === 정상 실행 ===
    Pathas: 지불한 금액은 999원
    MIN: 지불한 금액은 1499원
    HI: 지불한 금액은 1199원
    잔액: 1,1,1
    
    === 에러 발생 ===
    ! Pathas: 잔액은 1000원이므로 지불 불가
    MIN: 지불한 금액은 1원
    HI: 지불한 금액은 1원
    */

    ※ 첫 번째 Promise 요소에서 에러가 발생한 경우,
    그 다음 Promise 객체들이 실행되기는 하지만 then 메서드는 작동하지 않고 catch 메서드만 최초 1회 실행됨

    (세 번의 실행 모두 에러가 발생해도 catch 메서드는 처음 한 번만 실행됨)


    Promise.race() 비동기 처리 여러 개를 경쟁적으로 병렬 연결

    Promise.race 메서드는 경주(race)에서 승리한 작업,
    즉 가장 먼저 종료한 Promise 객체의 결과만 다음 작업으로 전달

    Promise.race(iterable)
    • 먼저 종료한 작업이 성공했을 때는 성공 콜백 호출
      실패한 경우에는 실패 콜백 호출
    • 나머지 작업도 실행되기는 하지만 가장 먼저 종료한 작업의 결괏값만 반환
      다시 말해, 가장 먼저 실행된 resolve 함수의 인자가 then 메서드로 전달
    • iterable은 Promise.all 에서 설명한 것과 동일

    Promise.race() 예제

    Promise.all 에서 작성한 예제를 Promise.race로 바꿔서 작성한 예제

    const buyAsync = (name, myMoney) => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                const payment = parseInt(prompt(
                    `${name}님, 지불하고자 하는 금액을 입력하세요.`
                ));
                const balance = myMoney - payment;
                if(balance > 0){
                    console.log(
                        `${name}: 지불한 금액은 ${payment}원`
                    );
                    resolve(balance);
                } else {
                    reject(
                    `${name}: 잔액은 ${myMoney}원이므로 지불 불가`
                    );
                }
            }, 1000);
        });
    }
    
    Promise.race([
        buyAsync("Pathas", 1000),
        buyAsync("MIN", 1500),
        buyAsync("HI", 1200)
    ])
    .then(balanceFirst => console.log(`잔액: ${balanceFirst}`))
    .catch(error => console.error(error));
    
    /*
    === 정상 실행 ===
    Pathas: 지불한 금액은 111원
    잔액: 889 → 모든 Promise가 완료되기를 기다리지 않고 실행
    MIN: 지불한 금액은 444원
    HI: 지불한 금액은 222원
    
    === 에러가 가장 먼저 발생 ===
    Pathas: 잔액은 1000원이므로 지불 불가
    MIN: 지불한 금액은 2원
    HI: 지불한 금액은 2원
    
    === 에러가 중간에 발생 ===
    Pathas: 지불한 금액은 444원
    잔액: 556
    ___ MIN 에서 에러 발생 ___
    HI: 지불한 금액은 1원
    */

    ※ 에러가 중간에 발생한 경우
    MIN(두 번째 Promise)에서 에러를 발생시켜도 이미 성공 콜백 함수가 호출된 이후이기 때문에 에러 내용이 콘솔창에 찍히지 않으며, HI(마지막 Promise)도 실행되기는 하지만 then 메서드로 실행이 이어지지 않는 것을 볼 수 있음


    며칠 전에 fetch API를 사용해보다가 Promise를 제대로 알고 있지 못하다고 느껴서 전체적으로 정리해보았습니다.

     

    참고 도서: [모던 자바스크립트 입문 / 저자: 이소 히로시 / 출판사: 길벗]

    댓글

Designed by Tistory.