Javascript Promise

3 분 소요

Callback hell

async(1, function({
    async(2, function({
        async(3, function({
            async(4, funciton({
                // do something
            )}
        )}
    )}
})

javascript에서는 비동기 작업시에 콜백함수를 같이 넘겨주는 형태로 코드를 자주 작성하는데, 이것이 여러개가 중첩되면 가독성이 떨어지고 디버깅을 어렵게하는 콜백 지옥에 빠지게 된다.

try {
  setTimeout(() => { 
    throw new Error('Error!'); 
  }, 1000);
} catch (e) {
  console.log(e);
}

에러처리에서도 문제가 있다. setTimeout의 콜백 함수가 Web API, Task Queue, Event Loop를 거쳐 다시 Call Stack으로 돌아 왔을때는 try, catch 구문이 더이상 남아있지 않아 의도한대로 에러를 캐치할 수 없다.

이러한 문제들을 해결하기 위해서 ES6에서 Promise 패턴이 제안되었다.

Promise

Syntax

function doSomething() {
    return new Promise((resolve, reject) =>  {
        setTimeout(() => resolve('success1'), 1000);
    })
}

doSomething()
.then(val => {
    console.log(val);
    return 'success2';
}).then(val => {
    console.log(val);
    return 'success3';
}).then(val => {
    console.log(val);
    throw new Error('error1');
}).catch(e => {
    console.log(e);
})

> success1
> success2
> success3
> Error: error1

Promise 생성자 함수를 이용하여 새로운 Promise 객체를 만들 수 있으며, 비동기 작업을 수행할 콜백함수의 parameter로 resolve, reject 함수를 전달받는다. 성공시 resolve, 실패시 reject 를 통해 값을 전달 할 수 있다.

doSomething() 함수를 호출하면 Promise 객체가 생성되어 비동기 작업을 수행하고, 완료된 뒤 resolve()를 통해 넘겨준 결과값을 then()을 통해 접근할 수 있다.
then() 또한 새로운 Promise 객체를 반환하며, return 값을 다음 then()에서 파라미터로 받을 수 있다. 이를 Promise chaining이라고 한다.

reject를 통해 전달된 값이나, promise chaining 도중 에러가 발생했을 때는 catch()를 통해 처리할 수 있다. then()함수의 두번째 파라미터로 처리할 수도 있지만 catch()를 사용하는 것이 훨씬 직관적이다.

단순한 값을 반환하는 대신 Promise를 반환하는 함수들을 전달할 수 도 있다.

getUserInfo()
.then(parseValue)
.then(auth)
.then(getFriends)
.catch(e => console.log(e));

function parseValue() {
    return new Promise((param) => {
        // ...
    })
}

getUserInfo, parseValue, auth, display 함수 모두 프로미스 객체를 리턴하는 함수인 것이다.

Status

Promise 객체는 생성되고 종료될 떄까지의 라이프 사이클동안 3가지의 상태 값을 가진다.

const p = new Promise(resolve => {
        setTimeout(() => resolve(3), 1000)
        })

p (Promise)
├── __proto__: Promise
├── [[PromiseStatus]]: "pending" -> "fulfilled"
└── [[PromiseValue]]: undefined -> 3
  • pending: Promise 객체가 생성되면 최초 Pending(대기) 상태가 된다.
  • fulfilled: Promise 객체가 resolve 된 된 상태로, then()을 통해 결과값을 받아올 수 있다.
  • rejected: Promise 객체가 reject 되거나 에러가 발생한 상태로, catch()를 통해 결과값이나 에러값을 받아올 수 있다.

settled: Promise 객체가 resolve 또는 reject 된 상태를 뜻하는 용어.

finally

function doSomething() {
    return new Promise((resolve) => {
        setTimeout(() => resolve('then'), 1000);
    })
}

doSomething()
.then(val => {
    console.log(val);
    return 'finally';
})
.finally(() => {
    return 'then aggain';   
})
.then(val => {
    console.log(val);
})

> then 
> finally
Promise
.reject(3)
.finally(()=>{})
.catch(val => console.log(val)) // => 3

ES9(ES2018)에서 추가되었다.
try-catch 문의 finally 처럼 Promise 객체가 settled 되었을 때 사용할 수 있다.
하지만 콜백함수의 parameter로는 아무 값도 받아올 수 없으며 이전 Promise 객체의 리턴 값은 다음 .then() 또는 catch()에 전달된다.

Static Methods

Javascript에서는 생성자 함수도 객체이므로 메소드를 가지고 있다.

Promise.resolve(), Promise.reject()

const resolvedPromise = Promise.resove(3);
resolvedPromise.then(val => console.log(val) // => 3

const rejectedPromise = Promise.reject(4);
rejectedPromise.catch(val => console.log(val) // => 4

원하는 값을 Promise 객체를 통해 전달하고 싶을 때 사용할 수 있다.

Promise.all()

Promise.all([
    new Promise(resolve => setTimeout(() => resolve(3), 3000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
])
.then(val => {
    console.log(val) // => [ 3, 2, 1 ]
})

Promise 객체 여러개를 담은 배열을 전달받아 병렬로 실행시킨다.
모든 Promise가 성공된 후 처음 전달한 순서대로 결과 값을 받아올 수 있는 새 Promise 객체를 반환한다.
전달된 Promise 중 하나라도 실패하면 가장 먼저 실패한 Promise가 reject한 값을 즉시 반환한다.

Promise.allSettled()

Promise.allSettled([
    new Promise(resolve => setTimeout(() => resolve(3), 3000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise((resolve,reject) => setTimeout(() => reject(1), 1000)),
])
.then(results => {
    results.forEach(result => console.log(result))
})

> { status: 'fulfilled', value: 3 }
> { status: 'fulfilled', value: 2 }
> { status: 'rejected', value: 1 }

ES11(ES2020)에서 추가 되었다.
Promise.all()은 reject되는 Promise 객체가 하나라도 있을 경우 해당 결과값만 즉각 리턴했지만 이 함수는 모두에 대한 값을 배열로 반환 받을 수 있다.

Promise.race()

Promise.race([
    new Promise(resolve => setTimeout(() => resolve(3), 3000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise((resolve, reject) => setTimeout(() => reject(1), 1000)),
])
.then(val => {
    console.log(val) // not called
})
.then(val => {
    console.log(val) // => 1
})

가장 먼저 settled 된 Promise 객체가 처리한 결과를 반환(resolve or reject) 하는 새로운 Promise 객체를 리턴한다.

Promise.any()

Promise.any([
    new Promise(resolve => setTimeout(() => resolve(3), 3000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
])
.then(val => {
    console.log(val) // => 1
})

현재 stg-3 단계이지만 크롬, 파이어폭스 등에 구현되어 있다.
가장 먼저 resolve 된 Promise 객체가 처리한 결과를 resolve 하는 새로운 Promise 객체를 리턴한다.
Promise.all 의 반대이다.

Reference