콜백 함수는 다른 코드(함수 또는 메서드)의 인자로 넘겨주는 함수이다. 이에 따라 제어권도 함께 넘겨준다.
콜백 함수는 위임받은 함수는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행한다.
아래 예시에서 cbFunc 가 callback 함수이다.
제어권을 넘겨 받은 setInterval 이 내부 로직에 따라 callback 함수를 실행한 것이다. (콜백 함수 호출 시점에 대한 제어권)
var count = 0;
var cbFunc = function() {
console.log(count);
if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
Array.prototype.map() 매서드는 배열 내의 모든 요소 각각에 대해 주어진 함수(콜백 함수)를 호출한 결과를 모아 새로운 배열을 반환한다.
arr.map(callback(currentValue[, index[, array]])[, thisArg])
map 메서드를 호출해서 원하는 배열을 얻기 위해서는 map 매서드에 정의된 규칙에 따라 함수를 작성해야 한다. (콜백 함수의 인자로 넘어올 값들 및 그 순서도 포함된다. )
따라서, 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것이지에 대한 제어권을 가진다.
기본적으로 콜백 함수는 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우 그 대상을 참조한다.
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;
};
위 예시 코드를 보면 call 메서드를 통해 this를 명시적으로 바인딩 해준다. (없으면 전역객체 바인딩)
addEventListener는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 의 this를 그대로 넘기도록 정의되어 있다.
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener(
'click',
function(e) {
console.log(this, e); // (3) <button id="a">클릭</button>
} // MouseEvent { isTrusted: true, ... }
);
콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출된다.
var obj = {
vals: [1, 2, 3],
logValues: function(v, i) {
console.log(this, v, i);
},
};
obj.logValues(1, 2); // { vals: [1, 2, 3], logValues: f } 1 2
[4, 5, 6].forEach(obj.logValues); // Window { ... } 4 0
// Window { ... } 5 1
// Window { ... } 6 2
obj메서드인 logValues가 forEach의 콜백 함수로 전달되어 this가 전역객체를 바라보고 있다.
함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 매서드가 아닌 함수인 것이다.
bind 메서드를 활용하면 콜백 함수 내부의 this에 다른 값을 바인딩할 수 있다.
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);
콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다.
비동기적인 작업을 처리하기 위해 콜백 함수를 사용하는데 가독성이 떨어지고 코드 수정도 어렵다.
자바스크립트 진영은 비동기적인 일련의 작업을 동기적으로, 혹인 동기적인 것처럼 보이게끔 처리해주는 장치를 마련하고자 노력해 왔다.
ES6에서는 Promise, Generator, ES2017에서는 async/await가 도입됐다.
Promise를 활용한 비동기 처리 예시를 보자
new Promise(function(resolve) {
setTimeout(function() {
var name = '에스프레소';
console.log(name);
resolve(name);
}, 500);
})
.then(function(prevName) {
return new Promise(function(resolve) {
setTimeout(function() {
var name = prevName + ', 아메리카노';
console.log(name);
resolve(name);
}, 500);
});
})
Promise의 인자로 넘겨주는 콜백 함수는 호출 시에 바로 실행되지만, 그 내부에 resolve 또는 reject 함수는 호출하는 구문이 있을 경우 resolve 또는 reject가 실행되기 전까지 then 또는 catch 구문으로 넘어가지 않는다.
따라서 비동기 작업의 완료 시에 resolve, reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.
다음으로는 Generator를 확인해보자.
var addCoffee = function(prevName, name) {
setTimeout(function() {
coffeeMaker.next(prevName ? prevName + ', ' + name : name);
}, 500);
};
var coffeeGenerator = function*() {
var espresso = yield addCoffee('', '에스프레소');
console.log(espresso);
var americano = yield addCoffee(espresso, '아메리카노');
console.log(americano);
var mocha = yield addCoffee(americano, '카페모카');
console.log(mocha);
var latte = yield addCoffee(mocha, '카페라떼');
console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();
Generator 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메서드를 가지고 있다.
next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다.
다시 next 메서드를 호출하면 다음 yield에서 함수의 실행을 멈춘다.
즉, 비동기 작업이 완료되는 시점마다 next 메서드를 호출한다면 Generator 함수 내부의 소스가 순차적으로 진행된다.
마지막으로는 Async/await를 활용한 예시를 보자
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('에스프레소');
console.log(coffeeList);
await _addCoffee('아메리카노');
console.log(coffeeList);
await _addCoffee('카페모카');
console.log(coffeeList);
await _addCoffee('카페라떼');
console.log(coffeeList);
};
coffeeMaker();
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실직적인 비동기 작업이 필요한 위치마다 await를 표기하면 뒤의 내용이 Promise로 자동 전환되고 resolve된 이후에야 다음으로 진행된다.