콜백 지옥?
// 코드 예시 1)
getCurrentPosition(() => {
setTimeout(() => {
doMoreAsyncStuff(() => {
...
})
}, 1000)
}, ...);
// 코드 예시 2)
function trackUserHandler() {
navigator.geolocation.getCurrentPosition(
posData => {
setTimeout(() => {
console.log(posData);
}, 2000)
},
error => {
console.log(error);
}
);
}
이렇게 콜백 함수 안에 콜백 함수가 있는 모습을 콜백 지옥이라고 한다.
특히 코드 예시 2)의 경우, getCurrentPosition으로 posData를 받아오는 경우가 첫번째 콜백함수, 그 콜백함수 안에서 setTimeout으로 2초 뒤에 콘솔 로그를 찍는게 두번째 콜백함수다.
이러한 콜백 함수의 중첩된 사용은 지양하는 것이 좋다.
하지만 이런 로직을 짜야만 하는 경우에는 코드를 어떻게 써야 될까?
Promise
ES6에서 도입된 프로미스가 이 문제를 해결해줄 수 있다.
프로미스는 비동기 작업을 보다 쉽게 하기 위해 비동기 코드를 감싸는 "객체"로, 3가지 상태를 가진다.
- Pending: 비동기 작업이 끝나기를 기다릴 때
- Fulfilled: 비동기 작업이 성공적으로 끝났을 때. 비동기 작업의 성공 결과를 결괏값으로 갖게 됨.
- Rejected: 비동기 작업이 실패했을 때. 비동기 작업에서 발생한 오류를 결괏값으로 갖게 됨.
그럼 코드 예시 2)를 프로미스 코드로 바꿔보자.
// setTimeout, getCurrentPosition은 promise를 지원하지 않기 때문에 별도로 함수를 만들어 사용. ('내장 API의 프로미스화')
const setTimer = (duration) => {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Done'); // setTimeout이 완료되면 resolve 함수 호출
}, duration);
}); // 이 생성자에 전달하는 함수는 '바로 실행'됨
return promise; // setTimer를 호출할 때마다 promise 실행
}
const getPosition((opts) => {
const promise = new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
success => {
resolve(success);
},
error => {},
opts
);
})
return promise;
});
function trackUserHandler() {
let positionData;
// promise function 'getPosition' with PROMISE CHAINING(★★★)
getPosition()
.then(posData => {
positionData = posData;
return setTimer(2000); // getPosition이 프로미스 작업을 완료했더라도 함수가 return 되면 보류중으로 바뀜 (리턴된 함수가 해결될 때까지)
}, err => {
console.log(err); // promise가 실패한 경우, reject로 리턴된 error가 then의 두번째 인자에서 처리된다. (첫번째 인자: promise가 성공한 경우)
}
})
// .catch(err => {
// console.log(err);
// return "keep going..."
// }) // 이렇게 catch 블록에서 오류를 다뤄도 되지만 위치 선정이 중요하고, 은근 까다로워서 일단 then의 두번째 인자로 처리.
.then((data) => {
console.log(data, positionData); // 여기서 data는 setTimer 프로미스가 리턴한 것
})
setTimer(0).then(() => {
console.log("Timer done!"); // setTimer가 promise를 리턴하기 때문에 then 사용 가능
})
console.log("Getting Position...")
}
button.addEventListener("click", trackUserHandler);
이렇게 하면 중첩이 사라지므로 콜백 지옥에서 벗어날 수도 있고, 콜백 중첩으로 인해 데이터 처리가 꼬이는 현상도 방지할 수 있다.
HTTP 요청할 때 거의 무조건 쓰인다고 보면 되니까 잘 익혀두자.
+) 프로미스에는 finally()라는 블록이 있는데
catch, then 블록 다음에 또다른 then이 있으면 프로미스가 다시 완료가 아닌 PENDING, 보류 상태가 된다.
이후 더이상 then이 남아있지 않으면 SETTLED 상태가 되고, 이때 말 그대로 마지막에 finally()를 사용하는 것이다.
finally는 이전에 해결됐든 거부됐든 상관없이 무조건 도달하게 된다.
// Promise finally() 사용 예시
somePromiseCode()
.then(firstResult => {
return 'done with first promise';
})
.catch(err => {
console.log(err);
// 암묵적으로 새로운 프로미스를 리턴함 (then처럼)
})
.finally(() => {
// promise가 SETTLED 상태. ==> 새로운 프로미스를 리턴하지 않음
// do final cleanup work here
})
Async & Await
ES6에서 도입된 프로미스의 대체 코드이자 비동기 코드의 꽃(?)이라 불리는 Async & Await 방식이 있다.
Promise와 연관지어 학습하면 효과가 더 좋을 것 같아 한 곳에 몰아넣었다.
이 방식은 then으로 체이닝된 Promise 코드를 동기 코드의 형태로 사용할 수 있다.
유의할 점은 함수에만 적용 가능하다는 것이다.
그래서 trackUserHandler에 적용된 Promise 코드를 Async 코드로 변환해보면
async function trackUserHandler(){
const posData = await getPosition();
const timerData = await setTimer(2000);
console.log(timerData, posData);
}
너무나 간결해진다.
이 원리에 대해 설명하자면,
async는 자스 코드를 내부적으로 변환해준다.
겉으로는 await으로 인해 함수 실행이 블로킹 되는 것 같지만,
실제로는 알아서 then 블록들로 변환되어 프로미스로서 작동하고 있는 것이다. (async&await도 내장 'Promise' API.)
그래서 훨씬 간결한 프로미스를 작성할 수 있게 된다.
다만 위 코드는 에러 처리 구문이 없다.
async await 방식에서는 try catch로 에러를 처리한다.
async function trackUserHandler(){
let posData, timerData;
try {
posData = await getPosition();
timerData = await setTimer(2000);
} catch (err) {
console.log(err);
}
console.log(timerData, posData);
}
이 방식에도 단점이 있는데,
동일한 함수 내에서 await이 걸린 작업들과 다른 작업들이 동시에 실행될 수 없다.
바꿔 말하면 try catch 구문 이후의 작업들은 모두 catch까지 끝나고 나서야 실행된다는 것이다.
async function trackUserHandler(){
let posData, timerData;
try {
posData = await getPosition();
timerData = await setTimer(2000);
} catch (err) {
console.log(err);
}
console.log(timerData, posData);
/*** 상단의 await 작업들로 인해 아래 작업들이 동시에 실행되지 못함 ***/
setTimer(0).then(() => {
console.log("Timer done!");
})
console.log("Getting Position...")
}


따라서
1. 동시에 여러 작업을 실행해야 하는 경우
2. 어떤 이유에서든 먼저 시작되어야 하는 함수가 있는 경우
3. 작업들을 차례로 실행하고 싶지 않은 경우
에는 async await말고 promise 문법을 사용해야 한다.
+) await만 따로 쓰기: IIFE(함수 즉시 실행) 방식 사용 (자주 사용되진 않음)
(async function () {
await setTimer(1000);
})();
이외에도 프로미스가 제공하는 메서드들이 있는데 부차적인 내용이라 게시글을 따로 작성했다.
* 프로미스 번외:
https://didi1129.tistory.com/7
'JS' 카테고리의 다른 글
| 번들링을 하는 이유? (0) | 2024.09.06 |
|---|---|
| 모듈화 정리 (0) | 2024.09.06 |
| HTTP 요청하기 & 에러 처리 (+ Axios) (0) | 2024.09.05 |
| HTTP 통신을 해보자 (0) | 2024.08.30 |
| 프로미스 번외 (0) | 2024.08.29 |