만자의 개발일지

[JavaScript] 프로미스(Promise)란 본문

JavaScript

[JavaScript] 프로미스(Promise)란

박만자 2022. 3. 18. 02:22

프로미스(Promise)

자바스크립트는 비동기 처리를 위해 콜백함수를 사용합니다. 하지만 콜백을 너무 남용하게 되면 우리가 흔히 부르는 콜백 지옥에 빠질 수가 있습니다.

콜백 지옥

또한 에러처리도 힘들 뿐더러 여러 개의 비동기 처리를 한번에 하는데 한계가 있습니다.

이런 콜백 함수의 단점을 보완하며 비동기 처리에 사용되는 객체를 프로미스(Promise)라 합니다.

 

프로미스를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 비동기 처리 시점을 명확하게 표현할 수 있다.
  • 연속된 비동기 처리 작업을 수정, 삭제, 추가하기 편하고 유연하다.
  • 비동기 작업 상태를 쉽게 확인할 수 있다.
  • 코드의 유지 보수성이 증가한다.

 

프로미스는 객체이기 때문에 생성자 함수를 호출하여 인스턴스화할 수 있습니다.

프로미스의 생성자 함수는 resovlereject함수를 인자로 전달받는 콜백함수를 인자로 전달받습니다.

프로미스는 인자로 전달받은 콜백 함수를 내부에서 비동기 처리 합니다.

 

Promise는 비동기 처리가 성공(fulfilled) 또는 실패(rejected) 등의 상태 정보를 갖게됩니다.

resolve 함수가 호출 된경우 성공된 상태이고, reject 함수가 호출 된 경우 실패 상태이게 됩니다.

 

아래 예제를 통해 살펴보도록 하겠습니다.

 

promise 라는 이름의 전역 변수에 프로미스를 할당 하였고 그 안에는 1 + 1 이 2라면 resolve 함수를 호출하고 아니면 reject 함수를 호출하도록 구현했습니다.

그 다음 프로미스의 후속 처리 메소드then(), catch()를 통해 비동기 처리 결과 메세지를 전달받아 처리하였습니다.

const promise = () => new Promise((resolve, reject) => {
    let a = 1 + 1

    if(a == 2) {
        resolve('success')
    } else {
        reject('failed')
    }
})

promise().then((message) => {
    console.log('This is in the then ' + message)
}).catch((message) => {
    console.log('This is in the catch' + message)
})

 

 

후속 처리 메소드

프로미스로 구현된 비동기 함수를 호출하는 측에서는 프로미스 객체의 후속 처리 메소드(then, catch)를 통해 비동기 처리 결과 또는 에러 메세지를 전달받아 처리합니다.

 

then

  • then 메소드는 두 개의 콜백 함수를 인자로 전달 받습니다.
  • 첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 경우)시에 실행됩니다.
  • 두 번째 콜백 함수는 실패(rejected, reject 함수가 호출된 경우)시에 실행됩니다.
  • then 메소드는 기본적으로 프로미스를 반환합니다.

catch

  • catch 메소드는 비동기 처리 혹은 then 메소드 실행 중 발생한 에러(예외)가 발생하면 호출됩니다.
  • catch 메소드 역시 프로미스를 반환합니다.

 

 

프로미스의 에러 처리 방법

프로미스를 이용한 에러 처리 방법은 두 가지가 있습니다.

 

첫 번째 방법은 then 메소드의 두 번째 인자로 예외를 처리하는 방법입니다.

const promise = () => new Promise((resolve, reject) => {
    let a = 1 + 1

    if(a == 3) {
        resolve('success')
    } else {
        reject('failed')
    }
})

promise().then((message) => {
    console.log('This is in the then ' +  message)
}, (error) => {
    console.log('This is in the then ' +  error)
})
This is in the then failed

 

두 번째 방법은 catch 메소드를 이용하여 예외를 처리하는 방법입니다.

const promise = () => new Promise((resolve, reject) => {
    let a = 1 + 1

    if(a == 3) {
        resolve('success')
    } else {
        reject('failed')
    }
})

promise().then((message) => {
    console.log('This is in the then ' +  message)
}).catch((error) => {
    console.log('This is in the catch ' + error)
})
This is in the then failed

 

프로미스에서 에러 처리는 가급적 catch 메소드를 이용하시는 것이 좋습니다.

이유는 다음과 같습니다.

아래 코드를 보시면 resolve 함수가 정상적으로 호출되고 프로미스가 성공 상태가 되며 then 메소드의 첫 번째 콜백함수가 실행됩니다.

첫 번째 콜백함수에서 에러가 발생하였고 두 번째 콜백함수가 에러를 잡아내지 못하고 있습니다.

const promise = () => new Promise((resolve, reject) => {
    let a = 1 + 1

    if(a == 2) {
        resolve('success')
    } else {
        reject('failed')
    }
})

promise().then((message) => {
    console.log('This is in the then ' +  message)
    throw new Error('failed')
}, (error) => {
    console.log('This is in the then ' + error)
})
This is in the then success
(node:52580) UnhandledPromiseRejectionWarning: Error: failed

 

catch 메소드를 사용하는 경우는 어떨까요?

catch 메소드를 사용하는 경우에는 then 메소드의 첫 번째 콜백함수에서 발생하는 에러를 잡아내는 것을 보실 수 있습니다.

const promise = () => new Promise((resolve, reject) => {
    let a = 1 + 1

    if(a == 2) {
        resolve('success')
    } else {
        reject('failed')
    }
})

promise().then((message) => {
    console.log('This is in the then ' +  message)
    throw new Error('failed')
}).catch((error) => {
    console.log('This is in the catch ' +  error)
})
This is in the then success
This is in the catch Error: failed

따라서 프로미스의 에러를 처리할 때에는 더 많은 상황의 예외를 처리할 수 있는 catch 메소드를 사용하시는 것이 좋습니다.

 

 

프로미스 체이닝

비동기 함수의 결과를 가지고 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩되어 콜백 지옥이 발생할 수 있습니다.

프로미스는 후속 처리 메소드를 체이닝하여 프로미스를 반환하는 여러개의 비동기 함수들을 연결하여 사용할 수 있습니다.

 

예제를 통해 살펴보도록 하겠습니다.

 

첫번째로 호출한 비동기 함수(promise)의 결과값을 then 후속 처리 메소드를통해 두번째 비동기 함수로 전달하였고 그 결과값을 메세지로 출력하게됩니다.

const promise = (result) => {
    return new Promise((resolve, reject) => {
        if(result == 'success')
            resolve('success')
        else
            reject('failed')
    })
}

promise('success')
    .then(promise) // .then(result => promise(result))
    .then(message => console.log('This is in the then ' + message))
    .catch(error => console.log('This is in the catch ' + error))
This is in the then success

 

정적 메소드

프로미스는 5가지 정적 메소드를 제공합니다.

정적메소드이기 때문에 객체의 생성없이 사용가능합니다.

Promise.resolve

Promise.resolve 메소드는 인자값을 래핑하여 프로미스를 반환합니다. (fulfilled)

const promise = Promise.resolve('success') // new Promise(resolve => resolve('success'))
promise.then(message => console.log('This is in the then ' + message))
This is in the then success

Promise.reject

Promise.reject 메소드역시 인자값을 래핑하여 프로미스를 반환합니다. (rejected)

const promise = Promise.reject('failed') // new Promise((resolve, reject) => reject('failed'))
promise.catch(error => console.log('This is in the catch ' + error))
This is in the catch failed

Promise.all

Promise.all 메소드는 프로미스가 담겨있는 배열과 같은 이터러블 객체를 인자로 받습니다.

인자로 전달받은 모든 프로미스를 병렬로 처리하고 그 결과값을 배열에 담아 resolve로 반환합니다.

 

만약 여러개의 프로미스를 순차적으로 처리한다면 어떻게 될까요?

 

첫번째 프로미스가 종료된후에 두번째 프로미스가 실행되고, 두번째 프로미스가 종료된후에 세번째 프로미스가 실행되게 됩니다.

물론 프로미스가 다른 프로미스를 의존하고 있는 경우에는 순차적으로 처리할 필요가 있지만 다음의 경우에는 서로 의존하고있지 않기 때문에 순차적으로 처리할 필요가 없습니다.

const promise1 = () => new Promise(resolve => setTimeout(() => resolve(1), 1000))
const promise2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000))
const promise3 = () => new Promise(resolve => setTimeout(() => resolve(3), 3000))

promise1().then(result => {
    console.log(result) // 프로그램을 실행하고 1초뒤에 수행됨
    return promise2()
}).then(result => {
    console.log(result) // 프로그램을 실행하고 3초뒤에 수행됨 (1 + 2)
    return promise3()
}).then(result => {
    console.log(result) // 프로그램을 실행하고 6초뒤에 수행됨 (1 + 2 + 3)
})
1
2
3

 

때문에 서로 의존관계이지 않은 여러 프로미스들을 이터러블 객체에 담아 Promise.all 메소드를 이용해 한번에 병렬처리 할 수 있습니다.

가장 마지막으로 끝나는 프로미스를 기준으로 수행되고, 모든 프로미스가 fullfilled 상태가 되면 결과값을 배열에 담아 새로운 프로미스를 반환합니다.

프로미스를 수행하던 도중 하나라도 에러(rejected)가 발생하면 rejected 상태가 되고 수행을 종료합니다.

Promise.all([
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(3), 3000))
]).then(console.log) // 프로그램을 실행하고 3초뒤에 실행됨
.catch(console.log)
[ 1, 2, 3 ]

Promise.race

Promise.race 메소드는 Promise.all 메소드와 동일하게 프로미스가 담겨있는 이터러블 객체를 인자로 받지만, Promise.all 과 달리 병렬로 처리하지 않고 가장 먼저 끝나는 프로미스의 결과값을 resolve로 반환합니다.

Promise.race([
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(3), 3000))
]).then(console.log) 
.catch(console.log)
1

Promise.allSettled

Promise.allSettled 메소드역시 Promise.all 메소드와 동일하게 프로미스가 담겨있는 이터러블 객체를 인자로 받고 병렬로 처리합니다.

다만 Promise.all 의 경우 프로미스를 수행하던 도중 하나라도 에러(rejected)가 발생하면 rejected 상태가 되고 수행을 종료하게되지만, Promise.allSettled 메소드의 경우 rejected 상태가 되어도 수행을 종료하지않고, 프로미스가 수행된 상태와 결과값을 배열에 담아 resolve로 반환합니다.

Promise.allSettled([
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
    new Promise((resolve, reject) => setTimeout(() => reject(2), 2000))
]).then(console.log)
[
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 2 }
]

Promise.all 메소드와 또다른 차이점은 각각의 프로미스 처리결과를 객체로 나타내고 status 프로퍼티를 가지게됩니다. fullfilled 상태인 경우 value 프로퍼티를 가지게되고, rejected 상태인 경우 reason 프로퍼티를 가지게됩니다.

 

참고

Comments