이전에 학습한 내용을 복기하고자 이 글을 작성합니다. 개선해야 할 점들을 댓글로 남겨주시면 감사드리겠습니다.
✔️ 콜백 함수는 다른 코드(함수 또는 메소드)의 인자로 넘겨주는 함수를 말한다
✔️ 매개변수를 넘겨 받은 함수는 callback 함수를 필요에 따라 즉시 실행(synchronously)할 수도 있고, 아니면 나중에 (asynchronously)실행할 수도 있다.
// 동기적으로 3초 마다 배열안에 있는 수가 출력된다. (블로킹)
[1, 2, 3].forEach((num) => {
// 콘솔 한 번에 매 3초가 걸린다.
// 이 함수의 콜백 함수는 동기적으로 호출된다.
callbackFn(() => console.log(num), 3000);
});
function callbackFn(callback) {
const start = Date.now();
let let = start;
while ( now - start < 3000 ) {
now = Date.now();
}
callback();
}
cosnole.log("ended");
// 콘솔 출력까지 총 수행 시간 3 * 3 = 9초
// 의도적으로 타이머의 기능이 필요하다면 동기적 콜백 호출이 필요할 수도 있다.
// 비동기적으로 3초 이후 배열의 수가 출력된다. (논 블로킹)
[1, 2, 3].forEach((num) => {
// 자바스크립트 런타임에 존재하는 Web API에 setTiemout 함수를 사용함으로써 3초 이후 콘솔이 모두 출력된다.
// 이 함수의 콜백 함수는 비동기적으로 호출이 된다.
setTimeout(() => console.log(num), 3000);
});
console.log("ended");
// 콘솔 출력까지 총 수행 시간은 3초
// 블로킹을 해결하고, 속도 개선을 위해선 비동기적 콜백 호출을 이용한다.
✔️ 콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다.
setTimeout(function (name) {
let coffeeList = name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
}, 500, "아메리카노");
}, 500, "카페라떼");
}, 500, "카페모카");
let coffeeList = "";
const addMocha = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addLatte, 500, "카페라떼");
}
const addLatte = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addAmericano, 500, "아메리카노");
}
const addAmericano = function (name) {
coffeeList = name;
console.log(coffeeList);
}
setTimeout(addMocha, 500, "카페모카");
위와 같은 방식은 가독성을 높일뿐 아니라 함수 선언과 함수 호출만 구분할 수 있다면 위에서 아래로 순서대로 읽어내려가는 데 어려움이 없다.
변수가 외부에 노출되기는 했지만 즉시 실행 함수로 감싸면 간단히 해결이 가능하다.
자바스크립트는 비동기적인 방법은 동기적으로 보이게끔 처리해주는 노력을 꾸준히 해왔다. 위 방법은 하나의 역사로 참고해주면 좋다.
최근에는, ES6에서 도입된 Promise, Generator와 ES2017에서 도입된 async/await이 가장 많이 사용된다.
new Promise((resolve) => {
setTiemout(() => {
const name = "카페모카";
console.log(name);
resolve(name);
}, 500);
}).then((result) => {
return new Promise((resolve) => {
const name = result + ", 카페라떼";
console.log(name);
resolve(name);
});
}).then((result) => {
return new Promise((resolve) => {
const name = result + ", 아메리카노";
console.log(name);
resolve(name);
});
});
new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만, 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중하나가 실행되기 전까지는 then(resolve 되었을 때) 또는 catch(reject 되었을 때) 구문으로 넘어가지 않는다.
따라서, 비동기 작업이 완료될 때 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.
const addCoffee = function (name) {
return function (prevName) {
return new Promise((resolve) => {
setTimeout(() => {
const newName = prevName ? (prevName + ", " + name) : name;
console.log(newName);
resolve(newName);
}, 500)
}) ;
}
}
addCoffee("카페모카")()
.then(addCoffee("카페라떼"))
.then(addCoffee("아메리카노"))
위 방법은 반복적인 내용을 함수화 해서 더욱 짧게 표현한 것이다.
2번째, 3번째줄은 클로저가 형성 되어있다.
const addCoffee = function (prevName, name) {
setTimeout(() => {
coffeeMaker.next(prevName ? `${prevName}, ${name}` : name);
}, 500);
};
const coffeeGenerator = function* () {
const mocha = yield addCoffee("", "카페모카");
console.log(mocha);
const latte = yield addCoffee(mocha, "카페라떼")
console.log(latte);
const americano = yield addCoffee(latte, "아메리카노");
console.log(americano);
}
const coffeeMaker = coffeeGenerator();
coffeeMaker.next();
함수 뒤에 "*"이 붙은 함수가 바로 Generator함수이다.
Generator 함수를 실행하면 Iterator가 반환되는데, next라는 메소드를 가지고 있다.
next 메소드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다.
이후 다시 next 메소드를 호출하면 앞서 멈췄던 부분 부터 시작해서 그 다음에 등장하는 yield에서 함수 실행을 멈춘다.
비동기 작업이 완료되는 시점마다 next 메소드를 호출하면 Gnerator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행된다.
const addCoffee = function (name) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(name);
}, 500);
});
}
const coffeeMaker = async function () {
let coffeeList = "";
const _addCoffee = async function (name) {
coffeeList += (coffeeList ? ", ": "") + await addCoffee(name);
}
await _addCoffee("카페모카");
console.log(coffeeList);
await _addCoffee("카페라떼");
console.log(coffeeList);
await _addCoffee("아메리카노");
console.log(coffeeList);
}
coffeeMaker();
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음 다음으로 진행된다.
즉 Promise의 then과 흡사한 효과를 얻을 수 있다.
콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.
1) 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.
2) 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있다.
3) 콜백 함수의 this가 무엇을 바라보도록 할지 정해야 하는 경우도 있다. 정해지지 않은 경우엔 전역 객체르 바라보고, 임의로 변경하고 싶다면 bind 메서드를 사용하면 된다.
어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행된다.
Promise, Generator, Async/Await 등을 이용하여 콜백 지옥을 어느정도 해결할 수 있다.