콜백 지옥 callback hell
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
setTimeout(() => {
console.log(4);
setTimeout(() => {
console.log(5);
}, 500);
}, 500);
}, 500);
}, 500);
}, 500);
💡 연속적으로 비동기 코드를 써야 하는 경우
- 위와 같이 콜백 함수 안에 또 다른 콜백 함수를 넣어야 하는 상황 발생 - 콜백 지옥
- 횟수가 많을수록 가독성도 낮아지고 직관성이 떨어짐
- 실전에서는 더더욱 복잡하고 난해해짐
🏃🏃🏃 릴레이 예제
- 철수, 영희, 돌준, 정아, 길돈이 치례로 이어달리기는 하는 코드
- ⭐ 각 골인시간 기록 - 이전 콜백함수의 결과가 다음 콜백함수로 넘겨져 축적됨
- 한 주자라도 데드라인(밀리초)을 넘기면 실패 - 주자마다 다른 실패 메시지 출력
- 완주 실패시 😢 완주 실패 - ${전체 소요시간} 출력
- 실패든 성공이든 마지막에 - - 경기 종료 - - 출력
const DEADLINE = 1400;
function relayRun (name, start, nextFunc, failMsg) {
console.log(`👟 ${name} 출발`);
const time = 1000 + Math.random() * 500;
setTimeout(() => {
if (time < DEADLINE) {
console.log(`🚩 ${name} 도착 - ${(start + time)/1000}초`);
nextFunc?.(start + time);
} else {
console.log(failMsg);
console.log(`😢 완주 실패 - ${(start + time)/1000}초`);
}
if (time >= DEADLINE || !nextFunc) {
console.log('- - 경기 종료 - -');
}
}, time);
}
relayRun('철수', 0, start1 => {
relayRun('영희', start1, start2 => {
relayRun('돌준', start2, start3 => {
relayRun('정아', start3, start4 => {
relayRun('길돈', start4, null, '아아, 아깝습니다...');
}, '정아에게 무리였나보네요.');
}, '돌준이 분발해라.');
}, '영희가 완주하지 못했네요.');
}, '철수부터 광탈입니다. ㅠㅠ');
프로미스 promise
- (보통 시간이 걸리는) 어떤 과정 이후 주어진 동작을 실행할 것이란 약속
- 중첩된 비동기 코드를 직관적이고 연속적인 코드로 작성할 수 있도록 함
- 👉 MDN 문서 보기
const borrow = 20;
// 빌린 돈의 10%를 더해 값겠다는 약속
// reject는 지금 사용하지 않음
const payWith10perc = new Promise((resolve, reject) => {
resolve(borrow * 1.1);
});
payWith10perc
.then(result => {
console.log(result + '만원');
});
- 새로운 약속을 하는 코드
- 인자로 받는 콜백함수의 첫 번째 인자 resolve ( 이름은 관례 ) - 약속 이행 성공시, 반환할 값 넣어 실행
프로미스 인스턴스 ( 만들어진 약속 ) 의 then 메서드
- resolve를 통해 ( 약속대로 ) 반환된 결과를 인자로 하는 콜백 함수를 넣음
- ⭐ 또 다른 프로미스를 반환 - 체이닝 가능
- 👉 MDN 문서 보기 - ( 추가 인자 )
const borrow = 20;
const payWith10perc = new Promise((resolve, reject) => {
// 💡 내부에서 비동기 코드 사용
setTimeout(() => {
resolve(borrow * 1.1);
}, 1000); // 1초 후 갚겠음
});
// ⚠️ 콘솔에서 분리해서 실행하면 안 됨!
// 프로미스가 생성되는 순간부터 시간 경과
payWith10perc
.then(result => {
console.log(result + '만원');
});
- 일반적으로 내부에 비동기 코드를 사용
- 시간이 소모되는 비동기 과정 후 ~를 반환하겠다는 약속
const borrow = 20;
const payWith10perc = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) {
// 💡 돈을 값을 수 없게 되었을 때
reject('사업 망함'); // 보통 실패사유나 관련 설명을 넣음
}
resolve(borrow * 1.1);
}, 1000); // 1초 후 갚겠음
});
payWith10perc
.then(result => {
console.log(result + '만원');
}
// 💡 두 번째 인자로 reject를 받는 콜백을 넣을 수 있지만
// 아래처럼 catch로 진행하는 것이 더 깔끔함
)
.catch(msg => {
console.error(msg);
})
.finally(() => {
console.log('기한 종료');
});
생성자 Promise
- 인자로 받는 콜백함수의 두 번째 인자 reject ( 이름은 관례 ) - 약속 이행 실패시, 반환할 값 넣어 실행
- reject가 실행되면 resolve는 무시됨
프로미스 인스턴스의
- catch 메서드 : reject를 통해 ( 실패로 인해 ) 반환된 결과를 인자로 하는 콜백 함수를 넣음
- finally 메서드 : 성공하든 실패하든 실행할 콜백 함수 - 필요할 때만 사용
- then과 더불어 메서드 체이닝으로 사용
// ⭐ then은 연속적으로 메서드 체이닝 가능
new Promise((resolve) => {
resolve(2);
})
.then(i => i * 4)
.then(i => i - 3)
.then(i => i ** 2)
.then((i) => {
console.log(i);
});
💰 10% 이자, 채무자 파산가능성 10%, 5번 빌려주기
// 빌린 금액으로 약속을 하는 함수
function moneyLend (borrow) {
return new Promise((resolve, reject) => {
console.log(`채무 ${borrow}만원`);
setTimeout(() => {
if (Math.random() < 0.1) {
reject('채무자 파산');
}
resolve(borrow * 1.1);
}, 1000);
});
}
moneyLend(20)
.then(result => moneyLend(result))
.then(moneyLend) // 인자를 하나 받아서 그대로 쓰므로
.then(moneyLend) // 이렇게 줄여버릴 수 있음
.then(moneyLend)
.then(result => {
console.log(`💰 반납 ${result}만원`);
})
.catch(msg => {
console.error(msg);
})
.finally(() => {
console.log('- - 대금업 종료 - -');
});
🏃🏃🏃 릴레이 예제 프로미스로 구현
const DEADLINE = 1400;
function getRelayPromise (name, start, failMsg) {
console.log(`👟 ${name} 출발`);
// 💡 랜덤 시간만큼 달리고 결과를 반환하겠다는 약속을 만들어 반환
return new Promise((resolve, reject) => {
const time = 1000 + Math.random() * 500;
setTimeout(() => {
if (time < DEADLINE) {
console.log(`🚩 ${name} 도착 - ${(start + time)/1000}초`);
resolve(start + time);
} else {
console.log(failMsg);
reject((start + time) / 1000);
}
}, time);
})
}
getRelayPromise('철수', 0, '철수부터 광탈입니다. ㅠㅠ')
.then(result => {
return getRelayPromise('영희', result, '영희가 완주하지 못했네요.');
})
.then(result => {
return getRelayPromise('돌준', result, '돌준이 분발해라.');
})
.then(result => {
return getRelayPromise('정아', result, '정아에게 무리였나보네요.');
})
.then(result => {
return getRelayPromise('길돈', result, '아아, 아깝습니다...');
})
.catch(msg => {
console.log(`😢 완주 실패 - ${msg}초`);
})
.finally(() => {
console.log('- - 경기 종료 - -');
});
여러 프로미스를 병렬 처리하기 위한 Promise의 정적 메서드들
// 다섯 주자들이 동시에 질주
// 데드라인(밀리초) 안에 들어오지 못하면 탈락
let DEADLINE = 1450;
function getRunPromise (name) {
return new Promise((resolve, reject) => {
const time = 1000 + Math.random() * 500;
setTimeout(() => {
if (time < DEADLINE) {
console.log(`🚩 ${name} 도착 - ${(time)/1000}초`);
resolve({name, time});
} else {
reject((`${name} 시간초과`));
}
}, time);
});
}
console.log(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getRunPromise)
);
- 프로미스의 배열을 받아 동시에 진행
- 모두 성공하면 resolve된 값들을 배열로 반환 - then으로 받음
- 하나라도 실패하면 catch 실행
// 한 명이라도 탈락하면 전체 탈락
Promise.all(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getRunPromise)
)
.then(console.log)
.catch(console.error)
.finally(() => {
console.log('- - 경기 종료 - -');
});
// 성공시 탑3 표시
Promise.all(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getRunPromise)
)
.then(arr => {
return arr.sort((a, b) => {
return a.time - b.time
})
.map(({name}) => name)
.splice(0, 3)
.join(', ');
})
.then(top3 => {
console.log(`탑3: ${top3}`);
})
.catch(console.error)
.finally(() => {
console.log('- - 경기 종료 - -');
});
- 주어진 프로미스들의 결과를 배열로 출력
- 실패 유무 관계없이 then으로 배열 반환
Promise.allSettled(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getRunPromise)
)
.then(console.log)
// ⚠️ catch는 동작하지 않음
.finally(() => {
console.log('- - 경기 종료 - -');
});
Promise.allSettled(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getRunPromise)
)
.then(arr => {
return {
succ: arr.filter(result => {
return result.status === 'fulfilled'
}),
fail: arr.filter(result => {
return result.status === 'rejected'
})
}
})
.then(res => {
res.succ.sort((a, b) => {
return a.value.time - b.value.time;
});
console.log(
`완주: ${res.succ.length}명 (1등: ${res.succ[0].value.name})`
);
console.log(
`탈락: ${res.fail.length}명`
);
})
.finally(() => {
console.log('- - 경기 종료 - -');
});
- 가장 먼저 성공한 프로미스의 결과를 then으로 반환
- 모두 실패시 오류 발생
Promise.any(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getRunPromise)
)
.then(console.log)
// ⚠️ 모두 실패해도 catch는 동작하지 않음
.finally(() => {
console.log('- - 경기 종료 - -');
});
- ⭐ 성공이든 실패든 첫 결과물 then 또는 catch로 반환
// 다섯 주자들이 선택한 도착지로 질주
// 도착지에 '꽝'이 있으면(50% 확률) 실패
function getBombRunPromise (name) {
return new Promise((resolve, reject) => {
const time = 1000 + Math.random() * 500;
setTimeout(() => {
console.log(`🚩 ${name} 도착 - ${(time)/1000}초`);
if (Math.random() < 0.5) {
resolve((`🙌 ${name} 승리!`));
} else {
reject((`💣 ${name} 꽝!`));
}
}, time);
});
}
Promise.race(
'철수,영희,돌준,정아,길돈'
.split(',')
.map(getBombRunPromise)
)
.then(console.log)
.catch(console.error)
.finally(() => {
console.log('- - 경기 종료 - -');
});