[JS] 비동기 처리, 콜백 지옥 해결 두 마리 토끼를 잡는 Promise

2021. 4. 14. 07:11Javascript/문법

 이번 포스팅은 Promise의 기본적인 사용법과 메서드에 관해 정리해보고자 한다. 나는 이 글을 읽는 여러분들이 Promise가 왜 필요하는지에 대한 이유를 알고있다고 생각하기 때문에 그것에 관하여 따로 설명하지 않을 것이다. 단 모르고 있다면, 지난번에 포스팅했던 [JS] 비동기 처리의 시작 콜백, 그리고 콜백지옥이라는 글을 읽어봤으면 한다.

 

1. Promise를 왜 써야하는가?

Promise를 써야하는 이유를 두 가지로 정리해보면 다음과 같다.

 

  • 비동기 함수를 동기화 시키기 위해서
  • 콜백 지옥을 해결하기 위해서

 

2. Promise는 어떻게 사용해야 하는가?

 - Promise의 기본적인 사용법은 다음과 같다.

/*
	두 번째 줄과 같이 상황에 따라 함수 이름을 내 의도에 맞게 커스텀할 수도 있다.
*/

const promise = new Promise((resolve, reject) => {});
const promise = new Promise((customResolve, customReject) => {});

 

 여기서 resolve, reject는 자바스크립트가 Promise에게 제공하는 콜백함수다. 두 콜백함수에 대한 설명은 다음과 같다.

  • Resolve(value): 일이 성공적으로 끝나는 경우에 호출하며, value 값과 함께 Promise 객체를 반환한다.
  • Reject(error): 에러가 있을 경우에 호출하며, error 값을 가진 Promise 객체를 반환한다.

 

아래 코드는 resolve, reject를 사용하지 않았을 때, resolve만 사용했을 때, reject만 사용했을 경우에 반환되는 promise 값들을 콘솔로 출력하기 위한 코드다. 

const penddingPromise = new Promise((resolve, reject) => {});
const fullfilledPromise = new Promise((resolve, reject) => {resolve("현재 state는 fullfilled 입니다.")});
const rejectedPromise = new Promise((resolve, reject) => {reject("현재 state는 rejected 입니다.")});

console.log(penddingPromise);
console.log(fullfilledPromise);
console.log(rejectedPromise);

 

콘솔에 나타난 출력결과는 다음과 같다. 

 

[그림 1] 출력 결과

 

[그림 1]에서 우리가 주목해야 할 것은 괄호 안에 들어있는 pending, fulfilled, rejected라는 상태 값이다. 저 값들은 Promise가 가지고 있는 세 개의 상태 값이다. 각 상태가 의미하는 바는 다음과 같다.

 

  • pending(대기): 위 코드에서는 resolve, reject 두 개의 콜백함수 모두 사용하지 않았을 때 나타나는 상태 값으로, 자바스크립트 MDN에서는 안에 있는 코드를 이행하거나 거부하지 않은 초기 상태라고 규정하고 있다.
  • fulfilled(이행): 위 코드에서는 resolve만을 사용했을 때 나타나는 상태 값으로, 자바스크립트 MDN에서는 연산이 성공적으로 완료되었을 경우에 나타나는 상태라고 규정하고 있다.
  • rejected(거부): 위 코드에서는 reject만을 사용했을 때 나타나는 상태 값으로, 자바스크립트 MDN에서는 연산이 실패했을 경우에 나타나는 상태라고 규정하고 있다.

 

 

 Promisestatevalue는 내부 속성이기 때문에 직접 접근할 수가 없다. 이 문제를 해결하기 위해서는 then, catch, finally 메서드를 사용해주면 된다. then 메서드는 아래와 같이 사용할 수 있다.

const promise = new Promise(() => {
	setTimeout(resolve, 1000, "작업이 1초후에 완료되었습니다");
});

promise.then((value) => {
	console.log(value); //then 메서드를 이용해서 promise 내부 속성인 value에 접근할 수 있다.
})

 

에러 처리를 하기 위해서는 catch를 사용하거나, then의 두 번째 파라미터에 Promise가 거부되었을 때의 콜백함수를 넣어주면 된다. 그런데 대부분의 사람은 catch를 이용해서 에러처리하는 것을 권장한다.

const promise = new Promise(() => {
	setTimeout(reject, 1000, "에러가 발생하였습니다.");
});

promise.then((value) => {
	console.log(value);
}).catch((error) => {
	console.log(error);
});
const promise = new Promise((resolve, reject) => {
    setTimeout(reject, 1000, "에러가 발생하였습니다.");
});

promise.then(
    (value) => console.log("작업 성공!"),
    (error) => console.log("에러 발생!")
)

 

 

finally 메서드는 Promise의 state가 fulfilled이든 rejected간에 항상 마지막에 실행되는 메서드이다.

const promise = new Promise(() => {
	setTimeout(reject, 1000, "에러가 발생하였습니다.");
});

promise.then((value) => {
	console.log(value);
}).catch((error) => {
	console.log(error);
}).finally(() => {
    console.log("난 항상 마지막!");
});

 

 

그렇다면, 다음 코드의 출력 결과는 어떻게 될까?

const promise = new Promise((resolve, reject) => {
	setTimeout(resolve, 2000, "작업이 정상적으로 완료되었습니다.");
    setTimeout(reject, 1000, "에러 발생");
});

promise.then(
	() => {console.log("작업 성공!")}
    ).catch(
    () => {console.log("에러 발생")}
    );

 

 

정답은 "에러 발생" 이다.  어느 자바스크립트 메소드나 똑같겠지만, then 다음에 catch가 사용되지 않는다. 정상적으로 실행이 완료되면 then 메서드만 실행되고, 에러가 발생했을 경우에만 catch만 실행된다는 것을 알아두도록 하자.

 

 

- Promise 체이닝

 Promise 체이닝then 메서드의 체인(사슬)을 통해 전달된다는 점에서 착안한 아이디어로 순차적으로 비동기 작업을 처리해야할 때 유용한 기법이다. 아래 코드를 보면 왜 체이닝이라고 부르는지 단번에 이해할 수 있을 것이다.

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

promise.then((number) => {
    console.log("첫 번째 then",number); // 2
    return number * 2;
}).then((number) => {
    console.log("두 번째 then", number); // 4
    return number * 2;
}).then((number) => {
    console.log("세 번째 then", number); // 8
}); 

 

 

작동 순서는 다음과 같다.

  1. 1 초 후에 숫자 2 라는 result 속성을 가지고 있는 Promise 객체가 반환된다.
  2. 따라서 첫 번째 then에서의 number 값은 2가 된다. 그리고 number에 2를 곱한 값을 다음 메서드에 전달한다.
  3. 두 번째 then에서의 number 값은 4가 된다. 그리고 number에 2를 곱한 값을 다음 메서드에 전달한다.
  4. 세 번째 then에서의 number 값은 8이 된다.

 

여기서 주의해야할 점은 then에 return을 안해주면 값이 전달되지 않아 undefined가 출력된다는 점이다.

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

promise.then((number) => {
    console.log("첫 번째 then",number);
}).then((number) => {
    console.log("두 번째 then", number);
});

 

 

 

그리고 다음과 같이 새로운 Promise를 then안에서 반환하면, 새로운 Promise의 작업이 끝나야만 두 번째 then 메소드가 실행되게 함으로써 비동기를 순차적으로 처리할 수 있다.

const promise = new Promise((resolve, reject) => {
    const number = 2;
	setTimeout(() => resolve("후에 실행"), 1000);
});

promise.then((value) => {
    console.log("1초", value);
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("후에 실행"), 1000);
    });
}).then((value) => {
    console.log("2초", value);
});

 

 

그런데, 도중에 에러를 발생시키면 Promise는 다음 then 메서드를 실행시키지 않고, 바로 catch 메서드를 실행시킨다.

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

promise.then((number) => {
    console.log(number);    // 2
    return number * 2;
}).then((number) => {
    console.log(number);    // 4
    throw new Error("에러 발생!") // 에러 발생
    return number * 2;
}).then((number) => {
    console.log(number);    // 8, 하지만 에러가 발생되었기 때문에 이 부분은 출력되지 않음.
}).catch((error) => {
    console.log(error);
});

 

3. Promise 메서드 all, allSettled 

이 메서드들은 다수의 Promise를 한 번에 처리하고 싶을 때 유용하다. 예를 들면, 우리가 음원을 디스플레이 해주는 사이트를 제작한다고 가정해보자. 나는 Home 화면에 신곡 리스트 20, 인기곡 TOP 100에 대한 API 데이터를 받아 디스플레이 하고 싶다. 그리고 두 데이터를 모두 불러와질 때까지 로딩 화면을 화면에 띄우고 싶다. 이런 상황에서 Promise를 사용한다면 then을 두 번 사용해야한다. 하지만 불러와야하는 데이터가 10개라면? then을 열 번 써야한다. 이 정도 횟수의 then 사용은 코드의 가독성을 크게 떨어트린다. 이런 상황에서는 all 메서드를 쓰는 것이 훨씬 낫다.  다음 코드를 입력하고 실행시켜보자.

const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 1000, "First"));
const promise2 = new Promise((resolve, reject) => setTimeout(resolve, 2000, "Second"));
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 3000, "Third"));

const promises = [promise1, promise2, promise3];
const parentPromise = Promise.all(promises).then((values) => {
    console.log(values);
});

 

 

위 코드는 세 개의 Promise를 배열에 담아 Promise.all의 아규먼트로 넣어줬다. 그리고 정확히 3초 후에 배열 데이터 ["First", "Second", "Third"]가 콘솔에 출력된 것을 볼 수 있을 것이다. Promise.allpromises 안에 있는 모든 Promise가 fulfilled 상태여야만한다. 그래야 그것에 맞는 프로미스를 반환하기 때문이다. 따라서 위 예제에서 3초 후에 values가 콘솔에 출력되는 이유도 3초가 된 시점에 모든 Promise들이 fulfilled 상태가 됐기 때문이다.

 

 

하지만, 만약 promises안에 있는 세 개의 Promise 중에 하나라도 rejected 상태가 된다면, Promise.all은 rejected 상태가 되어 에러 데이터를 가진 프로미스를 반환한다.

const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 1000, "First"));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 2000, "Second"));
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 3000, "Third"));

const promises = [promise1, promise2, promise3];
// 2초 후에 에러 콘솔 출력
const parentPromise = Promise.all(promises).then((values) => {
    console.log(values);
});

 

 

하나라도 데이터를 불러오는데 실패하면, rejected 상태를 가진 프로미스를 반환한다. 이것은 너무나도 큰 리스크다. 그러면, 에러난 Promise들을 빼고 나머지 fulfilled 상태인 Promise 들을 출력하는 방법이 없을까? Promise.allSettled 메서드를 이용하면 된다.

const promise1 = new Promise((resolve, reject) => setTimeout(resolve, 1000, "First"));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 2000, "Second"));
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 3000, "Third"));

const promises = [promise1, promise2, promise3];
const parentPromise = Promise.allSettled(promises).then((values) => {
    console.log(values);
}).catch((error) => console.log("에러 발생!"));


두 번째 프로미스 promise2가 rejected 상태일지라도, Promise.allSettled은 에러로 인식을 하지 않고, 각각의 상태를 가진 객체를 담은 배열 데이터를 result에 담아 반환한다.

[그림 2] Promise.allSettled 반환 데이터

 

4. 맺음말

정확하게 면접에서 질문을 받았던 내용은 비동기 처리를 어떻게하며, Promise.all을 사용했을 때 하나라도 rejected 상태가 되면 어떻게 되고, 이를 위한 해결방법이 무엇이냐였다. 여기서 제대로 답변을 못했지만, 친절하게 면접관님이 어떻게 되는지 잘 설명을 해주셨다. 개발 공부를 하시는 모든 분들이 공감하시겠지만, 직접 해보지 않으면 금방 까먹게되고, 설명만 듣게 되면 내 것이 되지 않는다. 비록 지금을 갈 길이 멀지만 조금씩 한 단계 밟아가면서 내 자신이 성장하는 것을 보고 있다면 그만큼 뿌듯한 일도 없을 것 같다. 이상으로 비동기 처리, 콜백 지옥을 모두 잡는 Promise에 대한 정리 글을 마칠까한다. 개인적으로 이 글을 읽는 모두가 도움이 되었으면 한다. 근거 있는 비판과 피드백은 언제든지 환영합니다. 감사합니다.

 

 

5. 참고 자료

'Javascript > 문법' 카테고리의 다른 글

[JS] 비동기 처리의 시작 콜백, 그리고 콜백 지옥  (0) 2021.04.11
[JS] 자료의 형 변환  (0) 2020.06.13
[JS] 자바스크립트의 자료형  (0) 2020.06.10
[JS] 변수와 상수  (0) 2020.06.08
[JS] Hello World 출력  (0) 2020.06.07