지난번 포스트에서 callback 함수에 대해 알아보고 callback을 많이 사용할 경우 callback 지옥을 경험하게 됨을 확인했다. 그래서 그런 콜백함수를 대체해서 Promise를 어떻게 사용할 수 있는지 적어보려 한다.
(왜 하필 Promise라고 이름을 지었는지...작명센스가 최악이다...왜 약속이지...)
일단 객체인데, 비동기를 간편하게 처리할 수 있도록 도와주는 자바스크립트안에 내장되어 있는 객체이다.
앞서 말한 것 처럼, Callback 지옥을 경험하고 나서 등장한 것도 있겠지만, Promise는 보통 네트워크 통신이나 파일에서 큰 데이터를 읽는 등의 무거운 작업을 수행할 때 많이 사용된다.
이유는 이러한 작업을 동기적으로 실행한다면 즉, 이 무거운 작업이 다 끝날 때 까지 그 어떤 코드도 실행되지 않고 멈춰서 모두가 이 코드가 끝나기만을 바라보고 있다면 어플리케이션 성능에 아주 치명적이기 때문에, 시간이 걸리는 무거운 작업들은 Promise에 넣어서 처리하여 비동기로 즉, 이거하는 동안 다른 것을 실행 할 수 있도록 하기 위해서 Promise를 사용한다.
Producer는 Promise를 생성하는 역할로 자바스크립트에서 제공하는 Promise클래스를 이용하여 만들고, 비동기로 작업할 코드들을 작성하여 그 값을 return해 주는 역할을 한다.
예를 들어서 이렇게 만들어 본다면,
const promise = new Promise((resolve, reejct) => {
// doing sth heavy work (network or read file)
// 성공했을 때는 resolve()
// 실패했을 때는 reject()
console.log("doing sth...");
});
console에는 아래내용이 바로 찍히게 된다.
이것이 의미하는 것은 우리가 프로미스를 만드는 순간, 전달한 excutor라는 콜백함수가 바로 실행되는 것을 확인할 수 있다.
그말인 즉슨, Promise안에 네트워크 통신을 하는 코드를 작성한다면, 프로미스가 만들어지는 그 순간 네트워크 통신을 수행하게 되는 것이다.
그래서 만약 사용자가 버튼을 눌렀을 때 네트워크 요청을 해야하는 경우라면, Promise를 생성하는 순간 코드가 바로 실행된다는 것을 유의하여 코드를 작성해야 할 것이다.
이 사실을 간과했다가 불필요한 네트워크 통신을 하는 경우가 왕왕 있다.
When new Promise is created, the excutor runs automatically.
Promise를 생성하는 더 자세한 모습은 아래와 같다.
const promise = new Promise((resolve, reejct) => {
// doing sth heavy work (network or read file)
console.log("doing sth...");
setTimeout(() => {
resolve("Noah"); // 성공했을 때 Noah라는 값을 return하게 된다.
reejct(new Error("no network")); // 실패했을 때는 "no network'라는 값을 return하게 된다.
//new Error는 자바스크립트에서 제공하는 클래스다.
}, 2000);
});
Consumer는 return된 Promise를 받은 후에 then, catch, finally로 값을 가공하는 역할을 한다.
위에서 작성한 Producer로 부터 return된 Promise값을 받는 Consumer를 아래와 같이 작성하게 되면,
promise //
.then((value) => {
console.log("success", value);
});
콘솔에는 2초후에 아래와 같이 찍히게 된다.
그런데 만약에 Promise를 return하는 Producer를 아래와 같이 네트워크 통신이 실패한 상황으로 제작하고
const promise = new Promise((resolve, reejct) => {
// doing sth heavy work (network or read file)
console.log("doing sth...");
setTimeout(() => {
//resolve("Noah"); // 성공했을 때 Noah라는 값을 return하게 된다.
reject(new Error("no network")); // 실패했을 때는 "no network'라는 값을 return하게 된다.
//new Error는 자바스크립트에서 제공하는 클래스다.
}, 2000);
});
Consumer를 아래와 같이 제작한다면
promise.then((value) => {
console.log(value);
});
콘솔에는 아래와 같은 Error Message가 찍히게 된다.
이는 우리가 Consumer에서 reject 되었을 때를 처리해 주지 않았기 때문에 발생하는 Error Message이다.
그래서 이러한 경우에는 성공했을 때와 에러가 발생했을 때 둘다를 고려해서 아래와 같이 작성하게 되면,
promise //
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
});
아래와 같이 콘솔에 찍히는 것을 볼 수 있다.
마지막으로 최근에 추가된 finally가 있는데, 이는 성공하든 실패하든 상관없이 실행하고 싶은 기능이 있다면 여기에 넣어주면 된다.
promise //
.then((value) => {
console.log(value);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log("anyway finally");
});
이렇게 작성해 주면 성공했을 때든,
실패했을 때든 둘다 출력되는 것을 볼 수 있다.
여기서 한가지 짚고 넘어갈 점은 Promise의 then을 호출하게 되면 똑같은 Promise를 return하기 때문에 catch는 그 return 된 Promise에 대해서 catch를 호출 하게 되는 것이다.
즉 chaining이라 함은 연쇄적으로 코드가 실행되는데, 바로 전 값을 받아오는 것을 말한다.
쉽게 말하자면 then, catch는 그위에서 return 된 값에 대한 then과 catch이다.
예를 들어, 아래와 같이 Promise를 return하는 함수가 있을 때
const getHen = () => // 닭을 받아오는 함수
new Promise((resolve, reject) => {
setTimeout(() => resolve("🐔"), 1000);
});
const getEgg = (hen) => // 달걀을 받아오는 함수
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error(`error ${hen} => 🥚`)), 1000);
});
const cook = (egg) => // 받은 달걀로 요리하는 함수
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${egg} => 🍳`), 1000);
});
getHen()
.then((hen) => getEgg(hen))
.catch((error) => { // 위의 return 값에 대한 Error 처리
return "🥖";
})
.then((egg) => cook(egg))
.then((meal) => console.log(meal))
.catch(console.log); // 위의 return 값에 대한 Error 처리
두번째 위치한 catch는 getEgg()에서 Promise return이 실패한 경우에 대한 catch이고 오류가 없다면 바로 다음 then으로 넘어가게 되는 것이다. 또한 마지막에 위치한 catch는 그 위 2개의 then에 대한 내용이다.
암튼, 자바스크립트에서 비동기를 콜백지옥이 아니고 더 간편하게 사용하기 위함을 기억하자.
마지막으로 Callback Hell에서 Promise로 바뀌는지 살펴보고자 한다.
// Callback Hell example
class UserStorage {
// 로그인 하는 API
loginUser(id, password, onSuccess, onError) {
setTimeout(() => {
if (
(id === 'ellie' && password === 'dream') ||
(id === 'coder' && password === 'academy')
) {
onSuccess(id);
} else {
onError(new Error('not found'));
}
}, 2000);
}
getRoles(user, onSuccess, onError) {
// 역할을 받아오는 API
setTimeout(() => {
if (user === 'ellie') {
onSuccess({ name: 'ellie', role: 'admin' });
} else {
onError(new Error('no access'));
}
}, 1000);
}
}
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your passrod');
userStorage.loginUser(
id,
password,
user => { // 로그인 성공했을 때
userStorage.getRoles( // 성공했을 때 역할을 받아온다.
user,
userWithRole => { // 역할가져오기에 성공 했을 때
alert(
`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`
);
},
error => { // 역할가져오기에 실패 했을 때
console.log(error);
}
);
},
error => { // 로그인 실패했을 때
console.log(error);
}
);
아이디와 패스워드를 받아오면 -> 거기서 ID를 받아오고 -> 그 아이디를 잘 받아오게 되면 -> 역할을 가져올 수 있도록 한다.
마지막 부분을 Promise로 바꾸게 되면,
const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter your passrod");
userStorage //
.loginUser(id, password)
.then((user) => userStorage.getRoles(user))
.then((user) => alert(`Hello ${user.name}, you have a ${user.role} role`))
.catch((error) => console.log(error));
[출처]
https://www.youtube.com/watch?v=JB_yU6Oe2eE (자바스크립트 12. 프로미스 개념부터 활용까지 JavaScript Promise | 프론트엔드 개발자 입문편 (JavaScript ES6))
https://www.youtube.com/watch?v=Sn0ublt7CWM (JavaScript - Promise (then, catch))
https://www.youtube.com/watch?v=PasFh_t1mhY (JavaScript Promise 2 - new Promise)