✔️ 콜백 함수 : 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수로 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행한다.
var newArr = [10, 20, 30].map(function (currentValue, index) {
console.log(currentValue, index);
return currentValue + 5;
});
console.log(newArr);
map 메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과들을 만든다.
✔️ 콜백 함수의 this
// Array.prototype.map 직접 구현
Array.prototype.map = function (callback, thisArg) {
var mappedArr = [];
for (var i = 0; i < this.length; i++) {
var mappedValue = callback.call(thisArg || window, this[i], i, this);
mappedArr[i] = mappedValue;
}
return mappedArr;
};
this에는 thisArg 값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정한다. 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩 하기 때문에 this에 다른 값이 담기게 된다.
var obj = {
vals: [1, 2, 3],
logValues: function (v, i) {
console.log(this, v, i);
},
};
obj.logValues(1, 2); // 1
[(4, 5, 6)].forEach(obj.logValues); // 2
1 : obj 객체의 logValues는 메서드로 정의됐다. obj.logValues로ㅓ 메서드의 이름 앞에 점이 있으니 메서드로서 호출한 것이다. 따라서 this는 obj를 가리킨다.
2 : forEach 함수의 콜백 함수로서 메서드를 전달했다. obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj.logValues가 가리키는 함수만 전달한 것이다. 따라서 함수 내부에서의 this는 전역객체를 바라보게 된다.
❓ 콜백 함수 내부에서 this가 객체를 바라보게 하고 싶으면?
👉 ES5 이전 : this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하고, 이를 클로저로 만든다.
var obj1 = {
name: "obj1",
func: function () {
var self = this;
return function () {
console.log(self.name);
};
},
};
var callback = obj1.func();
setTimeout(callback, 1000);
👉 ES5 이후 : bind 메서드 이용
var obj1 = {
name: "obj1",
func: function () {
console.log(this.name);
},
};
setTimeout(obj1.func.bind(obj1), 1000);
var obj2 = { name: "obj2" };
setTimeout(obj1.func.bind(obj2), 1500);
✔️ 동기 vs 비동기
사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나(setTimeout), 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나(addEventListener), 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등(XMLHttpRequest), 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드다.
✔️ 콜백지옥
콜백지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상으로 가독성이 떨어질뿐더러 코드를 수정하기도 어렵다.
개선방법
Promise
new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류구문(catch)로 넘어가지 않는다. 따라서 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.
var addCoffee = function (name) {
return function (prevName) {
return new Promise(function (resolve) {
setTimeout(function () {
var newName = prevName ? prevName + ", " + name : name;
console.log(newName);
resolve(newName);
}, 500);
});
};
};
addCoffee("에스프레소")()
.then(addCoffee("아메리카노"))
.then(addCoffee("카페모카"));
Generator
Generator 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메서드를 갖고 있다. 이 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다. 이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 함수의 실행을 멈춘다. 따라서 비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소스의 동기적 표현이 가능하다.
var addCoffee = function (prevName, name) {
setTimeout(function () {
coffeeMaker.next(prevName ? prevName + ", " + name : name);
}, 500);
};
var coffeeGenerator = function* () {
var espresso = yield addCoffee("", "에스프레소");
var americano = yield addCoffee(espresso, "아메리카노");
var mocha = yield addCoffee(americano, "카페모카");
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();
Promise + async / await
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에야 다음으로 진행한다. 즉 Promise의 then과 흡사한 효과를 얻을 수 있다.
var addCoffee = function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(name);
}, 500);
});
};
var coffeeMaker = async function () {
var coffeeList = "";
var _addCoffee = async function (name) {
coffeeList += (coffeeList ? "," : "") + (await addCoffee(name));
};
await _addCoffee("에스프레소");
await _addCoffee("아메리카노");
await _addCoffee("카페모카");
};
coffeeMaker();