프로미스를 이용한 비동기 처리

박민우·2023년 10월 1일
2

JavaScript

목록 보기
9/14
post-thumbnail

콜백 함수를 이용한 비동기 처리의 단점에서 살펴본 단점들을 해결하기 위해 프로미스를 이용해 비동기 처리를 할 수 있다.


📌 프로미스란?

Promise란, 자바스크립트 비동기 처리에 사용되는 객체로, 비동기 함수 호출 또는 비동기 연산이 완료되었을 때, 이후에 처리할 함수나 에러를 처리하기 위한 함수를 설정할 수 있다.


📌 프로미스 기본 사용법

const promise = new Promise((resolve, reject) => {
    if(/* 비동기 처리 성공 */){
        resolve('result');
    } else { /* 비동기 처리 실패 */
        reject('failure reason');
    }
});
promise().then((value)=> {
    
}).catch((value) => {
    
});
  • 위와 같은 방식으로 Promise 객체를 만들 수 있고, Promise 객체를 반환하는 Promise() 함수의 인자로 콜백 함수를 선언할 수 있고, 이 콜백 함수의 인자로는 resolvereject가 올 수 있다.

    resolvereject 함수 모두 특정 값을 다음 실행으로 전달해 비동기적으로 작업할 수 있도록 해주는 함수로, 우리가 이 함수를 새로 정의해주는 것이 아니라 이미 정의되어있는 이 함수들을 특정 인자와 함께 호출만 하는 것이다.

  • 보통 Promise() 내 인수로 전달된 콜백 함수 내에서 비동기 처리를 수행하고, 이 비동기 처리의 성공과 실패에 따라 resolve 또는 reject를 실행한다.

  • resolve

    promise 내부에서 비동기 상황이 정상적으로 종료될 때 실행시키는 함수로, resolve(value) 이렇게 resolve 함수의 인자로 value 값을 전달하면서 호출하면, 이 값이 then 구문의 콜백함수의 인자로 전달되어 then 구문으로 작업의 흐름을 이어갈 수 있다. 즉, resolve 함수는 비동기 상황이 종료될 때 특정 값을 다음 실행으로 전달하기 위해 사용하는 함수로, 프로미스의 상태를 fulfilled로 변경한다.

  • reject

    promise 내부에서 비동기 상황이 비정상적으로 종료되거나 오류 상황일 때, 호출하는 함수로 reject(value)를 통해 value 값을 catch 구문에서 사용하며 실행을 이어갈 수 있다. reject는 프로미스의 상태를 rejected로 변경한다.


📌 프로미스의 상태와 처리 결과

프로미스의 상태

프로미스는 현재 비동기 처리가 어떻게 진행되고 있는지에 따라 상태 정보를 갖는다.

프로미스의 상태 정보의미상태 변경 조건
pending비동기 처리가 아직 수행되지 않음프로미스가 생성된 직후 기본 상태
fulfilled비동기 처리가 성공함resolve 함수 호출
rejected비동기 처리가 실패함reject 함수 호출

fulfilled 또는 rejected 상태를 settled 상태라고 한다. 비동기 처리가 수행된 상태를 말한다.

프로미스는 pending => settled 상태로 변화할 수 있지만, 일단 settled 상태가 되면 더는 다른 상태로 변화할 수 없다.


지난 글이었던 콜백 함수를 이용한 비동기 처리의 단점에서 보았던 GET 요청 코드를 프로미스를 이용해 작성해보고, 프로미스의 상태가 어떻게 변화하는지 살펴보자.

const promiseGet = url => {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET, url');
        xhr.send();
        
        xhr.onload = () => {
            if(xhr.status === 200){
                resolve(JSON.parse(xhr.response))
            } else {
                reject(new Error(xhr.status));
            }
        };
    });
};

promiseGet('baseURL/posts/1');
  1. promiseGet('baseURL/posts/1');를 통해 생성된 직후의 프로미스는 기본적으로 pending 상태이다.
  2. 이후 서버로부터 응답이 오면, 결과에 따라 프로미스의 상태가 변경된다.
    • 비동기 처리가 성공하면 그 결과를 resolve 함수에 인수로 전달하면서 호출하고, 프로미스는 fulfilled 상태로 변경된다.
    • 비동기 처리가 실패하면 에러를 reject 함수에 인수로 전달하면서 호출하고, 프로미스는 rejected 상태로 변경된다.

이처럼 프로미스의 상태는 resolve 또는 reject 함수를 호출하는 것으로 결정된다.

프로미스의 처리 결과

위의 그림처럼, 프로미스는 비동기 처리 상태와 더불어 비동기 처리 결과도 가지고 있다.

const fulfilled = new Promise(resolve => resolve(7));
console.log(fulfilled);

위처럼 fulfilled된 프로미스의 코드를 개발자 도구에서 실행시켜보면

PromiseState와 더불어 PromiseResult라는 정보도 가지고 있음을 알 수 있다.

즉, 프로미스는 비동기 처리 상태처리 결과를 관리하는 객체이다.


📌 프로미스 후속 처리 메서드

위에서 resolve와 reject를 통해 비동기 처리의 상태를 변화할 수 있음을 배웠고, 이렇게 상태가 변화하면 이에 따른 후속 처리를 해야한다. 이를 위해 프로미스 후속 메서드인 then, catch, finally를 사용할 수 있다.

then

new Promise(resolve => resolve('fulfilled'))
	.then(v => console.log(v), e => console.error(e));

then 메서드는 두 개의 콜백함수를 인수로 받을 수 있다.

  • 첫번째 콜백함수

    프로미스가 fulfilled 상태가 되면 호출된다. 이 때 콜백함수는 프로미스의 비동기 처리 결과를 인자로 전달받는다.

    위의 개발자도구에서 살펴봤듯이 promise가 가지고 있는 PromiseResult를 인자로 전달해주는 것이다. 위 코드에서는 'fulfilled' 값이 콜백함수의 인자로 전달되는 것이다.

  • 두번째 콜백함수

    프로미스가 rejected 상태가 되면 호출된다. 이 때에도 콜백함수는 프로미스의 비동기 처리 결과를 인자로 전달받는다.

then 메서드는 언제나 프로미스를 반환한다.

  • 만약 then 메서드의 콜백 함수가 프로미스를 반환하면 그 프로미스를 그대로 반환하고
  • 콜백 함수가 프로미스가 아닌 값을 반환하면 그 값을 암묵적으로 resolve 또는 reject 하여 프로미스를 생성해 반환한다.

여기서도 우리는 이미 정의된 then, catch, finally 메서드에 인자만 넣어서 호출할 뿐이다. 따라서 then을 호출할 때 콜백함수만 인자로 넣는 것이지, then 함수를 새로 정의하는 것이 아니다.

catch

catch 메서드는 한 개의 콜백함수를 인자로 전달받는다. catch 메서드의 콜백함수는 프로미스가 rejected 상태인 경우만 호출된다.

new Promise((_,reject) => reject(new Error('rejected')))
	.catch(e => console.log(e));

사실 catch 메서드는 then(undefined, onRejected)와 동일하게 작동한다. 따라서 then 메서드와 마찬가지로 언제나 프로미스를 반환한다.

따라서 사실 위의 코드는 아래 코드와 동일하게 작동한다.

new Promise((_,reject) => reject(new Error('rejected')))
	.then(undefined, e => console.log(e));

catch를 통한 에러처리

비동기 처리에서 발생한 에러는 then의 두번째 인자 콜백함수를 통해 처리할 수도 있지만, 보통 catch를 통해 하는 것이 권장된다.

promiseGet()
	.then(res => {  ~~ })
	.catch(err => console.error(err));

catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면

  • 비동기 처리에서 발생한 에러(rejected 상태)뿐만 아니라
  • then 메서드 내부에서 발생한 에러

까지 모두 캐치할 수 있고, then 메서드를 통한 에러처리보다 가독성이 좋고 명확하다.

그렇다면, then 메서드의 두번째 인자로 rejected 되었을 때 호출될 콜백함수를 지정하지 않고, 첫번째 인자만 지정해준다면, rejected 되었을 때는 then 메서드는 실행되지 않는 것?


finally

new Promise(() => reject(new Error('rejected')))
	.catch(e => console.log(e));

finally 메서드의 콜백 함수는 프로미스의 성공 또는 실패와 상관없이 무조건 한번 호출된다. 또한, 언제나 프로미스를 반환한다.

=> 그래서 finally 메서드는 프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하다.


다음과 같은 후속 처리 메서드를 사용해 GET 요청 코드에 대한 후속 처리를 다음과 같이 할 수 있다.

promiseGet('baseURL/posts/1')
	.then(res => console.log(res))
	.catch(err => console.log(err))
	.finally(() => console.log('Bye!'));

📌 콜백 함수를 이용한 비동기 처리의 단점 해결

콜백함수를 이용한 비동기 처리의 단점이었던

  • 콜백 지옥으로 인한 나쁜 가독성
  • 에러 처리의 곤란

을 프로미스를 통해 해결할 수 있다.

1. 콜백 헬을 해결

위에서 배운 프로미스의 후속 처리 메서드 then, catch, finally를 사용하여 콜백 헬을 해결할 수 있다.

비동기 작업을 두 번 해야하는 상황이 있다고 생각해보자.

  1. baseUrl/posts/1 경로를 통해 서버에서 특정 포스트에 대한 userId를 비동기적으로 받아오고, 이후 이 작업이 완료되면
  2. userId를 가지고 baseUrl/users/userId 경로를 통해 서버에서 유저에 대한 정보를 받아와야 한다.

이 작업을 콜백함수와 프로미스를 사용해 각각 구현해보자.

콜백함수 버전

const url = "baseURL";

get(`${url}/posts/1`, ({ userId }) => { // 1번째 작업 
    console.log(userId); 
   
    get(`${url}/users/${userId}`, userInfo => { // 2번째 작업
        console.log(userInfo)
    })
})

콜백함수를 사용한다면 콜백 함수를 중첩해서 비동기 요청을 구현해야 한다. 가독성이 별로 좋지 않다.

프로미스 버전

const url = "baseURL";

promiseGet(`${url}/posts/1`) 
	.then(({userId}) => promiseGet(`${url}/users/${userId}`)) // 1번째 작업
	.then(userInfo => console.log(userInfo)) // 2번째 작업 
	.catch(err => console.error(err));

프로미스를 사용한 방식은 콜백함수 방식보다 훨신 더 직관적이다.

연속적으로 처리하고 싶은 일들을 then 메서드로 연결하고 비동기 처리 중 발생한 오류를 catch문을 통해 잡아낼 수 있다. 이렇게 후속 처리 메서드들을 연결하는 것을 프로미스 체이닝이라고 하고, 이는 각 메서드가 언제나 프로미스를 반환하기 때문에 가능하다.
여기서 각 프로미스는 상태처리 결과를 가지고 이 상태에 따라서 then 혹은 catch가 실행되고, 각 메서드 안에서 처리 결과을 인자로 받아 연속적으로 일을 처리할 수 있는 것이다.

다만, 프로미스도 콜백 패턴을 사용하므로 콜백 함수를 사용하지 않는 것은 아니다.

2. 에러 처리의 해결

비동기 처리에 대한 후속 처리는 후속 메서드인 then, catch, finally를 사용하여 수행할 수 있고, 비동기 처리에서 발생한 에러는 then 메서드의 두 번째 콜백 함수catch 메서드로 처리할 수 있다.


then 메서드 사용

const wrongUrl = "아무거나";

promiseGet(wrongUrl).then(
res => console.log(res),
err => console.error(err)
);

catch 메서드 사용

const wrongUrl = "아무거나";

promiseGet(wrongUrl)
    .then(res => console.log(res))
	.catch(err => console.error(err));

🙇🏻‍♂️ 참고

모던 자바스크립트 Deep Dive

profile
꾸준히, 깊게

0개의 댓글