[JavaScript] 프로미스(Promise) + 체이닝, 에러 핸들링

프로미스(Promise) 내용이 실행은 되었지만 결과를 아직 반환하지 않은 객체이다.

프로미스(Promise)는 자바스크립트 비동기 처리에 사용되는 객체이다.

비동기 처리란 순서대로 동작하는것이 아닌 오래 걸리는 작업은 뒤에서 따로 수행하면서 그동안 뒤에있는 작업들을 순서대로 수행하는 자바스크립트의 특성을 말한다.

프로미스는 보통 API를 통해 서버에서 데이터를 받아오거나 파일을 읽어오는 등의 역할을 수행하기 위해서 사용된다.


프로미스는 기본적으로 아래와 같이 작성한다.

let promise = new Promise((resolve, reject) => {
// executor
});

new Promise에 전달되는 콜백 함수는 executor(실행자, 실행 함수)함수라고 부른다.

new Promise를 만들면 콜백 함수인 executor 함수는 자동적으로 즉각 실행되는데, executor 함수 내부에서 원하는 일(비동기 작업)을 처리할 수 있다.

이러한 콜백함수 executor는 항상 resolve 콜백 함수와 reject 콜백 함수를 받는다. 이 함수들은 프로미스 객체를 선언하면 자동으로 생성되므로 따로 선언하지 않아도 된다.



executor를 실행하고 결과를 즉시 얻든, 늦게 얻든 상관없이 인자로 넘겨진 resolve, reject 콜백 함수 중 하나를 반드시 호출해야 한다.


두 콜백 함수는 사용방법을 보면

resolve(value) 콜백 함수는 일이 성공적으로 처리된 경우 그 결과인 프로미스를 value로 전달하고

reject(error) 콜백 함수는 에러가 발생하면 에러 객체를 error로 전달한다.

즉 비동기적으로 작업을 수행하고 처리 결과에 따라 성공시 resolve를 실패시 reject를 호출한다.




프로미스를 사용하려면 state 상태에 대해서도 알아야한다.

promise 상태 변화

promise 객체는 내부 프로퍼티를 갖고있는데 state 와 result 를 프러퍼티로 갖는다.

state는 처음에 비동기적인 작업을 수행하는 동안에는 "pending(보류)"이며 

비동기 작업이 성공적으로 수행되면 resolve 콜백 함수를 호출하고 state는 "fulfiiled(이행된)"으로 변하며 result의 값으로 promise 객체를 전달한다.

비동기 작업이 실패하면 reject 콜백 함수를 호출하고 state는 "rejected(실패)"로 변경되며 result로 에러 객체( new Error( ) )를 전달한다.

resolve 또는 reject 콜백 함수가 호출된 뒤에는 state는 "settled(처리된)"으로 변경된다.


state에 대해 요약하면

pending(보류) : 비동기 처리가 아직 수행되지 않은 상태 / resolve 또는 reject 콜백 함수가 아직 호출되지 않은 상태이다.

fulfilled(이행된) : 비동기 처리가 수행된 상태(성공) / resolve 콜백 함수가 호출된 상태이다.

rejected(거부된) : 비동기 처리가 정상적으로 수행 못한 상태(실패) / reject 함수가 호출된 상태이다.

settled(처리된) : 비동기 처리가 수행된 상태 (성공 또는 실패) / resolve 또는 reject 함수가 호출된 상태이다.




이렇게 프로미스의 성공 실패 여부에 따라 전달된 값을 .then / .catch / .finally 메서드를 사용하여 값을 핸들링 할 수 있다.

이렇게 값을 핸들링하는 메서드들을 소비자(consumer)라고 부른다.



.then()

let promise = new Promise(function (resolve, reject) {
setTimeout(() => resolve("working success!!!"), 2000);
});

promise.then(
(result) => console.log(result) // 2초 뒤에 working success!!! 출력
);


.then() 은 프로미스가 정상적으로 수행되어서 resolve(value) 콜백 함수를 통해 전달된 value값인 프로미스를 then의 인자로 받아 값을 제어할 수 있다. 

.then()으로 값을 제어한 후 반환된 값 또한 프로미스 이다.

.then()으로 받아오는 값을 바로 다른 함수의 인자로 전달하여 값을 리턴하고 그 리턴된 값을 가지고 또 .then을 이용하여 코드를 작성할 수도 있다.



.catch()

let promise = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error("working 실패!!!")), 2000);
});

promise //
.then((result) => console.log(result)) // reject 콜백함수를 사용했음로 동작 X
.catch(console.log) // 2초 뒤에 Error: working 실패!!! 출력
.finally(console.log("무조건 실행!!!")); // 무조건 실행


.catch() 는 프로미스에서 에러가 발생하여 reject(new Error('error')) 콜백 함수가 실행되었을때 전달된 에러 값을 제어할 수 있다.

then에서 처럼 화살표 함수를 사용해도 되지만 

catch에서처럼 사용해도 자동으로 console.log의 값으로 에러 객체가 전달되어 출력된다.

비동기적으로 코드가 수행되므로 아래와 같이 finally가 먼저 실행되어 출력되고 2초뒤에 에러가 출력된다.





.finally()

try ~ catch 절에 finally 절이 있듯이 프로미스에도 finally가 있다.

finally는 프로미스가 정상적으로 수행되어도, 에러가 발생하여도 무조건 실행 된다.

즉, finally()를 작성하면 성공하여도 .then()과 finally() 가 수행되고 실패하여도 .then()과 finally()가 실행된다.

그리고 finally 핸들러엔 인수가 없고 프로미스가 이행되었는지 거부되었는지 알 수 없다.

finally에서는 성공, 실패 여부와 상관없이 동작을 수행한다.

추가적으로 finally는 then이나 catch 이전에 사용되어도 상관없다.



요약해보면

resolve(성공 리턴 값) => then()으로 연결

reject(실패 리턴 값) => catch()로 연결

finally => 무조건 실행




마지막으로 몇가지 알면 좋은 사항

1. promise 의 콜백 함수 executor 내부에서 resolve나 reject중 하나가 호출되면 그 아래에 다른 코드들은 모두 무시된다.

let promise = new Promise((resolve, reject) => {
resolve("done");

reject(new Error("error")); // 무시 된다.
setTimeout(() => resolve("value")); // 무시 된다.
});

2. promise 내부에는 resolve 나 reject 콜백함수가 없으면 state가 계속 pending 상태로 남게되므로 꼭 resolve 나 reject 콜백함수를 작성해주어야 한다.


3. new Promise 만들면 executor 함수는 자동적으로 즉각 실행된다. 그러므로 사용자가 필요로하지않는 데이터를 불러오는 불필요한 사항이 발생할 있다. 이점을 유의해야한다.!!!!




프로미스 체이닝

순차적으로 처리해야하는 비동기 작업이 여러개 있을 때도 프로미스를 사용해야 해결할 수 있다.

예시1
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
});

promise
.then((result) => {
console.log(result); // 1
return result * 2;
})
.then((result) => {
console.log(result); // 2
return result * 2;
})
.then((result) => {
console.log(result); // 4
return result * 2;
});


예시1과 같이 프로미스 체이닝이 가능한 이유는 위에서 말했듯이 promise.then()을 호출하면 프로미스가 값으로 반환되기 때문에 반환된 프로미스를 가지고 then을 연속해서 호출할 수 있다.

연속해서 사용되는 then은 이전의 then에서 반환되는 프로미스 값을 핸들링하는 것이다.

즉, 최초에 반환된 프로미스에 then을 여러개 추가한것이 아니라 then으로 반환된 프로미스에 then을 호출한 것이다.

new Promise => .then => .then => .then

예시2
const process1 = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve("고기"), 1000);
});

const process2 = (meat) =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`잘 구워진 ${meat}`), 1000);
});

const process3 = (grilledMeat) =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${grilledMeat}를 손님한테 서빙한다.`), 1000);
});

process1()
.then((meat) => process2(meat))
.then((grilledMeat) => process3(grilledMeat))
.then((result) => console.log(result)) // 잘 구워진 고기를 손님한테 서빙한다.
.catch((error) => console.log(error));

예시2와 같이 프로미스 체이닝을 사용할 수도 있다.

process1에서 반환된 프로미스가 process2로 전달되고  process에서 반환된 프로미스가 process3으로 전달


프로미스 에러 핸들링

에러 핸들링 예시1
const process1 = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve("고기"), 1000);
});

const process2 = (meat) =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error(`${meat}가 모두 타버렷다.`)), 1000);
});

const process3 = (grilledMeat) =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${grilledMeat}를 손님한테 서빙한다.`), 1000);
});

process1()
.then((meat) => process2(meat))
.then((grilledMeat) => process3(grilledMeat))
.then((result) => console.log(result))
.catch((error) => console.log(error)); // Error: 고기가 모두 타버렷다.

프로미스 체인에서 에러를 처리할때 에러가 발생하는 시점에 catch를 작성하지않아도 프로미스가 거부되면 흐름중에 제일 가까운 reject 콜백 함수로 핸들러가 이동한다.

그러므로 catch는 then을 모두 작성한 뒤 마지막에 적는 방식으로 에러를 핸들링 할 수 있다.


에러 핸들링 예시2
const process1 = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve("고기"), 1000);
});

const process2 = (meat) =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error(`${meat}가 모두 타버렷다.`)), 1000);
});

const process3 = (grilledMeat) =>
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${grilledMeat}를 손님한테 서빙한다.`), 1000);
});

process1()
.then((meat) => process2(meat))
.catch((error) => {
return "새로운 고기";
})
.then((grilledMeat) => process3(grilledMeat))
.then((result) => console.log(result)); // 새로운 고기를 손님한테 서빙한다.

에러 핸들링 예시1 코드 처럼 에러가 발생했을때 에러 반환 뿐만아니라 

바로위 에러 핸들링 예시2 처럼 중간에 에러를 처리해서 원하는 방식으로 해결 할 수 있도록 에러 핸들링도 가능하다.


댓글