본문 바로가기
프로그래밍 언어/JavaScript

[JavaScript] 프로미스( Promise )

by 민졈 2022. 3. 8.

 

 

이번 시간에는 프로미스에 대해 배워볼 것이다.

 

 

어떤 유명한 상점에 갔을 때 그 상점 만의 시그니처 상품을 사려고 했을 때 한번에 사면 정말 좋지만 부득이하게

재고가 없거나 이미 품절인 상태이거나 입고지연이 된다거나 하는 여러가지 이유로 상품을 사게되지 못할 때도 있다.

 

그렇다면 이럴 때 계속해서 매일 상점을 방문해서 재고 확인을 해야할까??

이렇게 된다면 왔다갔다 하는 시간도 낭비하고 체력적으로도 힘들 것이다.

매우 좋지 않은 방법임을 알 수 있다.

 

 

이럴 때 상점에 전화번호를 적어두고 상품이 들어오면 연락을 달라고 하는것이 가장 합리적인 방법이 될 것이다.

상품이 들어오는 시간까지 다른 작업을 할 수 있기 때문에 시간 절약을 할 수 있다.

 

 

 

이러한 상황이 있을 때 사용하는 것이 프로미스이다.

 

 

 

const promise = new Promise(( resolve, reject ) => {  //code..  });

 

사용 방법은 위와 같다.

 

 

인수는 resolve와 reject를 받는데 resolve를 사용하면 성공한 경우이고,

reject를 사용하면 reject의 뜻처럼 실패한 경우에 사용한다.

 

이렇게 어떤 일이 완료된 이후에 실행되는 함수를 콜백함수(Callback Function)라고 한다.

 

 

new Promise 객체가 반환하는 promise 객체는 state와 result(undefined)를 프로퍼티로 받는다.

 

 

처음 state는 pending(대기) 상태였다가 성공해서 resolve(value) 가 호출되면 fulfilled(이행됨) 가 된다.

이때 result의 값은 resolve에서 전달한 value 값을 가진다.

 

 

반면 실패하면, 즉 reject(error)가 호출된다면 state는 rejected(거부됨)으로 바뀌고 이때 result는 error가 된다.

 

 

 

const promise = new Promise((resolve, reject) => {
    setTimeout( () => {
        resolve('OK')
    }, 3000);
});

이 코드는 3초 뒤에 state가 fulfilled로 바뀔 것이고 result에는 'OK'가 들어가게 될 것이다.

 

 

반대로 실패를 가정한 코드로 바꿔 본다면

const promise = new Promise((resolve, reject) => {
    setTimeout( () => {
        reject(new Error('error...'))
    }, 3000)
});

3초 뒤에 state는 rejected가 되고 result에 error가 되는 것이다.

 

 

위의 코드들은 모두 상점의 주인 즉, 판매자의 입장에서 본 코드이다.

 

그렇다면 구매자의 입장에서 보는 코드는 어떻게 될까?

 

.then을 사용하고 첫번째 함수는 이행 되었을 때 실행하고 두번째 함수는 거부 되었을 때 실행한다.

위의 코드와 연결해서 본다면 result에는 'OK'가 들어가고 err에는 error 값이 들어가는 것이다.

promise.then(
    function(result){},
    function(err){}
);

 

함수를 작성한다면 아래처럼 작성할 수 있다.

promise.then(
    function(result){
        console.log(result + '가지러 가자.');
    },
    function(err){
        console.log('다시 주문해주세요...');
    }
);

 

 

 

then을 사용해도 되지만 이외에 사용할 수 있는 것이 catch와 finally이다.

 

 

 

catch는 에러가 발생할 때 사용할 수 있다.

위의 코드를 catch를 사용해서 바꾸면 아래와 같다.

promise.then(
    function(result){}
).catch(
    function(err) {}
)

catch를 사용하게 된다면 then만 사용했을 때와 같이 동작하지만 catch를 사용해준다면 가독성이 더 좋고 then에서 발생하는 오류를 더 잘 잡아줄 수 있다는 장점이 있다.

 

 

 

 

반면 finally는 함수가 이행이 되었던 이행되지 않았던 간에 결과에 상관없이 무조건 실행된다.

promise.then(
    function(result){}
).catch(
    function(err) {}
).finally(
    function(){
        console.log('--주문 끝--')
    }
)

 

 

 

const promise = new Promise((resolve, reject) => {
    setTimeout( () => {
        resolve('OK')
    }, 1000);
});

console.log("시작");

promise.then((result) => {
    console.log(result);
})
    .catch((err) => {
        console.log(err);
    }
).finally(() => {
        console.log("끝");
    }
);

이런 함수가 있다면 결과는 시작 => 1초 뒤 OK => 끝 의 과정을 거치게 될 것이다.

 

 

resolve를 reject로 바꾼다면

const promise = new Promise((resolve, reject) => {
    setTimeout( () => {
        // resolve('OK');
        reject(new Error("err..."));
    }, 1000)
});

console.log("시작");

promise.then((result) => {
    console.log(result);
})
    .catch((err) => {
        console.log(err);
    }
).finally(() => {
        console.log("끝");
    }
);

시작 => Error: err.... => 끝이 된다.

 

이행되었던 되지 않았던 끝이라는 문구는 계속 나오게 된다.

 

 

 

callback 함수를 호출하는 세개의 함수를 작성해준다.

const fn1 = (callback) => {
    setTimeout(function () {
        console.log("1번 주문 완료");
        callback();
    }, 1000);
};

const fn2 = (callback) => {
    setTimeout(function () {
        console.log("2번 주문 완료");
        callback();
    }, 3000);
};

const fn3 = (callback) => {
    setTimeout(function () {
        console.log("3번 주문 완료");
        callback();
    }, 2000);
};

 

프로미스를 사용하지 않으면 아래처럼 작성하게 된다.

각각의 함수는 모두 callback 함수를 호출하기 때문에 fn1 함수 호출 후 callback으로 fn2를 호출

fn2는 fn3을 호출하고 더이상 호출할 함수가 없기 때문에 콘솔로 마무리 해준다.

console.log("시작");

fn1(function(){
    fn2(function(){
        fn3(function() {
            console.log("주문 끝났습니다");
        })
    })
});

 

콘솔을 확인하면

시작

1번 주문 완료 (1초 뒤) 

2번 주문 완료 (3초 뒤) 

3번 주문 완료 (2초 뒤)

주문 끝났습니다

 

이렇게 나타나게 된다.

아무 오류도 뜨지않고 정상적으로 작동하지만 이렇게 코드의 깊이가 깊어지면서 callback 함수를 계속해서 호출하는 것을 Callback Hell, 콜백 지옥이라고 부른다.

이 콜백 지옥을 프로미스를 사용해서 개선해보도록 하자.

 

const f1 = () => {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("1번 주문 완료");
        }, 1000);
    });
};

const f2 = (message) => {
    console.log(message);
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("2번 주문 완료");
        }, 3000);
    });
};

const f3 = (message) => {
    console.log(message);
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("3번 주문 완료");
        }, 2000);
    });
};

console.log("시작");

f1( )
.then((res) => f2(res))
.then((res) => f3(res))
.then((res) => console.log(res))
.catch(console.log)
.finally(() => {
    console.log("끝");
});

위의 코드와 똑같이 동작하지만 프로미스를 사용한다면 콜백 지옥에 빠지지 않게 작성할 수 있다.

이렇게 프로미스가 연결되고 연결되고 연결되는 것을

 

프로미스 체이닝(Promises chaining) 이라고 한다.

 

 

만약 두번째 함수가 res가 아닌 rej가 된다면 어떻게 작동할까?

const f1 = () => {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("1번 주문 완료");
        }, 1000);
    });
};

const f2 = (message) => {
    console.log(message);
    return new Promise((res, rej) => {
        setTimeout(() => {
            // res("2번 주문 완료");
            rej("xxxx");
        }, 3000);
    });
};

const f3 = (message) => {
    console.log(message);
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("3번 주문 완료");
        }, 2000);
    });
};

 

f2가 실행되면 실패한 것이기 때문에 넘겨주는 result 값이 error가 되고,

f3의 res로 넘어가지 않고 바로 finally 로 가는 것을 알 수 있다.

시작 => 1번 주문 완료 => xxx => 끝   이렇게 동작한다.

 

 

 

 

위의 코드들은 각각 정해놓은 시간 1초, 3초, 2초에 해당하는 시간을 거치며 작동한다.

그렇지만 프로미스에서는 이를 동시에 작동시킬 수 있다.

 

 

이때 사용하는것이 Promise.all이다.

 

Promise.all

Promise.all은 배열을 받는다.

이후 then을 사용할 수 있다. 코드를 작성하면 아래와 같다.

Promise.all( [ f1( ), f2( ), f3( ) ] )
.then(res => {
    console.log(res);
});

then은 배열 안의 작업이 모두 완성되어야 실행이 된다. 즉 모든 작업이 완료되어야 값을 사용할 수 있는 것이다.

Promise.all을 사용하면 [" 1번 주문 완료" , "2번 주문 완료", "3번 주문 완료" ] 이렇게 진행이 된다. 

각 프로미스로 전달한 값이 배열로 전달된다.

 

Promise.all을 사용한다면 시간을 절약할 수 있다.

 

 

그렇다면 만약 세개 중 한개의 함수에 res가 아닌 rej 를 넣어주고 Promise.all을 실행 한다면 결과는 어떻게 될까?

 

이렇게 오류가 나게된다. 이유는 한번에 실행하기 때문..!!

 

그 전의 코드에서는 순서대로 작동하기 때문에 오류가 나면 거기서 끝났는데 오류를 가진 코드를 포함해서 한번에 작동시키니 오류가 나게 되는 것이다.

 

Promise.all 는 하나라도 누락되면 페이지를 보여주지 않으려고 할 때 주로 사용한다.

 

 

 

Promise.race

Promise.all과 사용방법은 같다.

 

Promise.race([f1(), f2(), f3()])
.then(res => {
    console.log(res);
});

 

모든 동작이 끝나야 완료되는 Promise.all과는 달리 Promise.race는 하나의 동작이라도 끝나면 마무리가 된다.

 

용량이 큰 이미지들을 로딩할 때 하나의 이미지라도 다운을 완료한다면 다운된 이미지를 보여줄 때 사용한다.

 

 

 

 

 

 

 

 

 

 

 

* 본 포스팅은 코딩앙마님의 자바스크립트 중급 강좌를 기반으로 작성된 글입니다.

 

 

 

 

댓글