💌 이 글은 드림코딩의 비동기 3부작 | 콜백 - 프로미스 - async & await 에서 콜백과 프로미스에 대해 다룹니다
이 글은 콜백 지옥을 탈출하기 위해 프로미스를 쓰는 방법에 대해 알아보는 글이다. 당연히 콜백이 무엇인지부터 알아야 하는데, 그러기위해서는 비동기적 처리와 동기적 처리에 대해 알아야 한다.
자바스크립트는 동기적(synchronous)이다
동기적이라는 것은 hoisting 이후 코드가 나타나는 순으로 자동으로 동작한다는 것이고, 작동 순서를 예측할 수 있다는 것을 말한다.
비동기적이라는 것은 언제 코드가 실행될지 예측할 수 없음을 말한다.
예시 : setTimeout
은 지정된 시간이 지나면 콜백함수를 호출하는 브라우저 API로, 대표적인 비동기 방식이다. 자바스크립트는 동기적으로 코드를 처리하다 setTimeout
을 만나면 브라우저에 요청을 보내고, 그 응답을 기다리지 않고 다음 코드로 넘어간다.
시간이 오래 걸리는 요청이 발생하면 비동기적으로 처리하여 처리되는 동안 다른 일을 하고 있을 수 있게 한다. (CPU리소스 낭비 방지)
setTimeout
으로 브라우저에 요청을 보냈던 것이 처리가 되면 이를 실행할 수 있도록 나중에 다시 불러야 하는데, 이 불러달라는 것에 착안하여 Call Back 함수라고 이름이 붙었다.
함수 A 호출에서 함수 B가 인자로 전달될 때, 함수 B를 콜백 함수라 말한다. 즉, 함수 A의 매개변수로 전달되어 특정 이벤트 발생 후 다시 호출되는 함수 B를 말한다.
자바스크립트에서 비동기적 프로그래밍을 하기 위해 콜백 함수를 사용하기는 하지만 콜백을 항상 비동기일때만 쓰는 것은 아니다.
function init(print){
print();
}
init(()=>console.log('sync'));
function init(print, timeout){
setTimeout(print, timeout);
}
init(()=>console.log('async'),2000);
콜백 함수 안에서 콜백 함수를 부르고 또 부르고... 비동기적 처리를 위해 콜백 함수를 반복해서 사용하는 것을 콜백 지옥이라고 한다.
어디서 어떤식으로 연결되었는지 한눈에 파악하기 어렵다.
비즈니스 로직을 한눈에 이해하기 어렵다.
에러 해결, 디버깅, 문제분석, 유지보수가 어렵다.
class UserStorage {
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) {
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 password');
userStorage.loginUser( //로그인 진행
id,
password,
user => { //loginUser 성공시
userStorage.getRoles(
user,
userWithRole => { //getRoles성공
alert( //로그인이 잘 됐다는 메세지 출력
`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`
);
},
error => { //getRoles실패
console.log(error);
}
);
},
error => { //loginUser 실패시
console.log(error);
}
);
📝 다음 문단에서 다룰 Promise
를 통해 콜백 지옥을 해결할 수 있다.
Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다. 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있습니다. 다만 최종 결과를 반환하지는 않고, 대신 프로미스를 반환해서 미래의 어떤 시점에 결과를 제공합니다. MDN
자바스크립트 내장 객체. 콜백함수 대신 비동기를 간편하게 처리할 수 있도록 도와주는 객체
비동기적 작업이 완료되면 프로미스의 Consumer 메소드를 통해 프로미스를 불러온다.
성공 또는 실패만 한다 !
성공 : 정해진 장시간의 기능을 수행후, 정상적으로 수행되었다면 성공의 메세지와 함께 처리된 결과값을 전달함.
실패 : 기능 수행 중 예상치 못한 문제가 발생하면 에러를 전달함.
const promise = new Promise(function(resolve, reject) {
executor // Promise의 콜백함수
});
클래스이기 때문에 new
를 통해 새 객체를 생성함
executor의 인수 resolve
와 reject
는 자바스크립트 엔진이 미리 정의한 함수이므로 따로 만들 필요가 없다. 따라서 개발자는 executor 안 코드만 작성하면 됨
executor 안에는 파라미터로 넘겨받은 콜백 resolve
, reject
중 하나를 반드시 호출해야 함
executor는 promise
의 상태를 fulfilled
와 rejected
둘 중 하나로 변화시킨다
📢 ❗ 새로운 promise
가 만들어지는 순간 그 안의 executor가 자동적으로, 즉각적으로 실행되는 점에 유의해야한다. 사용자가 요구하지도 않았는데 불필요한 네트워크 통신이 일어날 수 있기 때문이다.
promise
는 다음 중 하나의 상태를 가진다.
대기(pending): 이행하거나 거부되지 않은 초기 상태
이행(fulfilled): 연산이 성공적으로 완료됨. resolve
거부(rejected): 연산이 실패함. reject
이행, 거부된 상태의 프로미스를 처리된(settled) 프로미스라고 부른다
new Promise((resolve, reject) => {
doing some heavy work (network, read files)
});
resolve
: 정상 수행 후 최종 결과 반환
reject
: 수행중 문제발생시 호출. Error object를 반환한다.
ex. reject(new Error('no network'));
heavy work : promise를 통해 비동기적으로 처리하는 것이 좋다. 시간이 걸리기 때문에 동기적으로 처리하게 되면 이 일을 하는 동안 다른 코드를 실행할 수 없기 때문임.
프로미스에서 소비함수 역할을 하는 메소드 then
, catch
, finally
then
: 첫 번째 인수는 프로미스 성공시의 결과를, 두 번째 인수는 실패시의 에러를 받는다. 인수를 하나만 전달하면 성공시의 결과만 다룬다.
catch
: 에러가 발생한 경우만 다룬다.
finally
: 성공,실패와 상관없이 무조건 마지막에 호출된다.
💬 예제코드
Promise
.then((value)=>{
console.log(value);
});
.catch(error=>{ // reject 콜백함수 값
console.log(error);
});
.finally(()=>{
console.log('finally');
});
then
메소드 안의 value는 프로미스가 성공하여 얻은 resolve
콜백함수 값을 받는다.
catch
메소드 안의 error는 프로미스가 실패하여 얻은 reject
콜백함수 값을 받는다.
Consumer 간결하게 쓰기
파라미터가 1개일 때 함수이름만 쓰면 암묵적으로 그 함수가 매개변수로 전달되어 더 간결하게 작성할 수 있다.
💬 예제코드
// 정직한 코드
getHen()
.then((hen) => {
return getEgg(hen);
})
.then((egg) => {
return cook(egg);
})
.then((meal) => {
console.log(meal);
})
// 간결한 코드
getHen()
.then(getEgg) // then이 받아오는 value를 바로 getEgg 함수에 전달
.then(cook)
.then(console.log);
async
·await
을 활용할 수 있다.then
메소드는 값을 바로 전달할 수도 있고, return
으로 비동기인 프로미스를 전달할 수도 있다.💬 예제코드
const fetchNumber = new Promise((resolve,reject)=>{
setTimeout(()=>resolve(1), 1000);
//1초 있다가 숫자 1을 전달
});
fetchNumber
.then(num => num*2) // 2 num에 숫자 1 전달됨 (성공), 이후 숫자를 2배
.then(num => num*3) // 6
.then(num => { // 5
return new Promise ((resolve, reject)=>{
setTimeout(()=>resolve(num-1), 1000);
});
})
.then(num => console.log(num)); // 5 총 2초 걸림
const getHen = () =>
new Promise((resolve, reject) => {
setTimeout(() => resolve('🐓'), 1000);
});
const getEgg = hen => // 위의 promise 정상 처리 완료시 닭을 전달받아 getEgg호출
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${hen} => 🥚`), 1000);
});
const cook = egg => // 위의 promise 정상 처리 완료시 계란을 전달받아 getcook호출
new Promise((resolve, reject) => {
setTimeout(() => resolve(`${egg} => 🍳`), 1000);
});
getHen()
.then(getEgg) // 🐓 전달받아 getEgg 호출
.then(cook) // 🥚 전달받아 cook 호출
.then(console.log); //cook 완료 후 🍳 출력
// 3초 후 🐓=>🥚=>🍳
네트워크에 문제가 생겨 프로미스 .then(getEgg)
가 실패한 상황
setTimeout(() => reject(new Error(`error! ${hen} => 🥚`)), 1000)
getHen()
.then(getEgg)
.then(cook)
.then(console.log)
.catch(console.log); 📌
달걀을 받아오는 부분에서 에러가 발생했어도 에러가 제일 밑으로 전달되며 console.log
가 제일 마지막에 찍힌다.
getHen()
.then(getEgg)
.catch(error => {
return '🍕';
})
.then(cook)
.then(console.log)
🐓 => 🥚 => 🍳
대신 🍕 => 🍳
가 출력됨..then(getEgg)
에서 발생한 에러를 바로 처리하고 싶다면 그 밑에 catch
를 작성한다.그럼 이제 위에서 보았던 콜백 지옥 코드를 promise
를 사용하여 해결해보자
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject)=>{
setTimeout(() => {
if (
(id === 'ellie' && password === 'dream') ||
(id === 'coder' && password === 'academy')
) {
resolve(id);
} else {
reject(new Error('not found'));
}
}, 2000);
});
}
getRoles(user) {
return new Promise ((resolve, reject) => {
setTimeout(() => {
if (user === 'ellie') {
resolve({ name: 'ellie', role: 'admin' });
} else {
reject(new Error('no access'));
}
}, 1000);
});
}
}
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');
userStorage
.loginUser(id, password) // (1)로그인 성공하면
.then(userStorage.getRoles) // (2)를 수행, 성공하면
.then(user => alert( // (3)을 수행 - 로그인이 잘 됐다는 메세지 출력
`Hello ${user.name}, you have a ${user.role} role`))
.catch(error => alert('error')); // error 대응