콜백 함수(callback function)란 다른 코드의 인자로 자신이 넘어가 적절한 시점에 실행이 되어지는 함수입니다.
우선 setInterval
의 구조를 살펴보면
var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);
func
으로 넘겨준 함수는 매 delay(ms)
마다 실행되며, 그 결과 어떠한 값도 반환하지 않습니다. 다만 setInterval
을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID값이 반환됩니다. 이를 이용하여 특정 시점에 반복 실행을 종료(clearInterval
)할 수도 있습니다.
콜백 함수를 인자로 넘겨받은 setInterval
의 예시를 보면
var count = 0;
var cbFunc = function(){
console.log(count);
if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
콜백 함수가 된 cbFunc
은 setInterval
의 판단에 따라 적절한 시점(0.3초 마다)에 실행되었습니다. 콜백 함수의 제어권이 인자로 넘겨준 setInterval
에게로 넘어가게 된 것입니다.
이와 같이 콜백 함수를 인자로 받는 함수들에 대해 더 알아보겠습니다.
Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)
map 메서드는 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있습니다. 이를 생략할 경우에는 전역객체가 바인딩됩니다. 또한 콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재값이, 두 번째 인자에는 현재값의 인덱스가, 세 번째 인자에는 map 메서드의 대상이 되는 배열 자체가 담깁니다.
map 메서드의 동작 방식은 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내서 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만듭니다.
var newArr = [10, 20, 30].map(function(currentValue, index){
return currentValue + 5;
})
console.log(newArr); // [15, 25, 35]
별도의 this
를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 간단하게 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;
}
call / apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this
가 될 대상을 명시적으로 바인딩하기 때문에 this
에 다른 값이 담길 수 있었던 것입니다.
var obj = {
vals: [1, 2]
logValues: function(v, i){
console.log(this, v, i);
}
};
obj.logValues(1, 2); // { vals: [1, 2], logValues: f } 1 2
[3, 4].forEach(obj.logValues); // Window { ... } 3 4
logValues
는 obj
객체의 메서드로 정의되었습니다. 그래서 obj.logValues(1, 2)
을 실행했을 때 this
는 obj
를 가리키고 인자로 넘어온 1 2
가 출력됩니다.
반면 forEach
함수의 콜백 함수로 전달된 obj.logValues
의 경우 함수만 전달한 것이기 때문에 obj
와의 직접적인 연관이 없어지며 forEach
에 의해 함수로서 호출되어지게 됩니다. 즉, 별도로 this
를 지정하지 않았기에 함수 내부에서의 this
는 전역객체를 바라보게 됩니다.
그럼 콜백 함수 내부에서의 this
에 다른 값을 바인딩 할 수는 없을까요?
전통적으로는 this
를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식이 많이 쓰였습니다.
var obj1 = {
name: 'obj1',
func: function(){
var self = this;
return function(){
console.log(self.name);
};
}
};
var callback = obj1.func();
setTimeout(callback, 1000); // obj1 { ... }
var obj2 = {
name: 'obj2',
func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1500); // obj2 { ... }
var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000); // obj1 { ... }
Q. 여기서
callback
식별자에 함수를 할당한 이유는 무엇일까요?
하지만 ES5에 bind
라는 메서드가 추가되면서 훨씬 간단하게 구현이 가능해졌습니다.
var obj1 = {
name: 'obj1',
func: function(){
console.log(this.name);
}
};
setTimeout(obj1.func.bind(obj1), 1000); // obj1 { ... }
var obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500); // obj2 { ... }
콜백 지옥(callback hell)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상입니다.
setTimeout
, addEventListener
, XMLHttpRequest
와 같이 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드입니다.그렇지만 웹의 복잡도가 높아지면서 비동기적인 코드의 비중이 훨씬 높아졌고 Javascript 진영은 비동기적인 일련의 작업을 동기적으로, 혹은 그렇게 보이게끔 처리해주는 장치를 마련하고자 끊임없이 노력해 왔습니다. ES6에서는 Promise
, Generator
등이 도입됐고, ES2017에서는 async/await
가 도입되었습니다.
setTimeout(function(name){
var coffeeList = name;
setTimeout(function(name){
coffeeList += ', ' + name;
setTimeout (function(name){
...
}, 500, '카페라떼');
}, 500, '아메리카노');
}, 500, '에스프레소');
이를 기명함수로 변환하면 아래와 같이 변환할 수 있습니다.
var coffeeList = '';
var addEspresso = function(name){
coffeeList = name;
setTimeout(addAmericano, 500, '아메리카노');
}
var addAmericano = function(name){
coffeeList += ', ' + name;
setTimeout(addLatte, 500, '카페라떼');
}
...
가독성 문제가 해결되긴 했지만 변수가 너무 많이 할당되어 헷갈릴 소지가 있습니다.
var addCoffee = function(name){
return function(prevName){
return new Promise(function(resolve){
setTimeout(function(){
var newName = prevName ? (prevName + ', ' + name) : name;
resolve(newName);
}, 500);
});
};
};
addCoffee('에스프레소')()
.then(addCoffee('아메리카노'))
.then(addCoffee('카페라떼'));
...
Promise
를 이용한 방식입니다. new
연산자와 함께 호출한 Promise
의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve
또는 reject
함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then
또는 catch
구문으로 넘어가지 않습니다.
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 coffeeMaker = coffeeGenerator();
coffeeMaker.next();
Generator
를 이용한 방식입니다. *
이 붙은 함수가 Generator
함수인데 이는 next
라는 메서드를 가지고 있는 Iterator(반복자)
가 반환됩니다. 이 next
메서드를 호출하면 Generator
함수 내부의 yield
에서 실행을 멈추며 다시 next
메서드를 호출하면 멈췄던 부분부터 다시 코드를 실행합니다.
var addCoffee = function(name){
return new Promise(function(resolve){
setTimeout(function(){
resolve(name);
}, 500);
});
};
var coffeeMaker = async function(){
var coffeeList = '';
var _addCoffee = async function(){
coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
}
await _addCoffee('에스프레소');
await _addCoffee('아메리카노');
...
}
ES2017에서 도입된 async/await
를 이용한 방식입니다. 비동기 작업을 수행하고자 하는 함수 앞에 async
를 표기하고, 함수 내부에서 비동기 작업이 필요한 위치마다 await
을 표기합니다. 이것만으로도 뒤의 내용을 Promise
로 자동 전환하고, 해당 내용이 resolve
된 이후에야 다음으로 진행합니다.