콜백 함수(callback function)은 다른 코드의 인자로 넘기는 함수를 의미한다. 넘겨받은 코드는 콜백 함수를 적절한 시기에 호출하여 실행시킨다. 콜백함수의 예로 자주 쓰이는 것이 있다.
6시에 일어나기 위해 알람을 맞춰놓고 푸욱 자는 A
6시에 일어나기 위해 매번 일어나 6시인지를 확인하는 B
A는 시계에게 알림에 대한 권한을 위임하고 제어권을 넘긴 것이고, B는 자신이 제어권을 가지고 계속 함수를 호출하는 것이다. 이렇듯 callback은 제어권과 관련이 깊다.
let count = 0;
const cbFunc = () => {
console.log(count);
if (++count > 4) clearInterval(timer)
}
const timer = setInterval(cbFunc, 300);
위 코드는 0.3초마다 count
변수의 값을 1씩 늘리고 count
값이 4를 초과하면 중단하라는 cbFunc
함수의 실행을 setInterval
에게 넘겼다. 이로써 호출 주체는 setInterval
이고 제어권도 setInterval
에게 있게 된다.
const newArr = [10, 20, 30].map((value, index) => {
console.log(value, index);
return value + 5;
})
console.log(newArr) // [15, 25, 35]
위 코드에서 map
메소드의 콜백함수는 첫번째 인자로 현재 값, 두번째 인자로 인덱스를 받기로 정의해두었다. map
메소드의 인자를 (value,index)
에서 (index,value)
로 변경한다고 해도 실제로 첫번째 인자로 들어오는 값은 [10, 20, 30]
의 값(10, 20, 30)이 된다. 즉, 정의된 규칙에 따라 콜백함수를 작성해야한다.
기존에 정리한 내용(this)를 보면 콜백 함수의 this는 전역객체를 가리킨다. 콜백함수도 "함수"이기 때문이다. 하지만 제어권을 넘겨받을 코드에서 콜백에 별도로 this가 될 대상을 지정한 경우 this는 해당 객체를 가리키게 된다. 이외에도 this가 가르킬 객체를 직접 명시할 수도 있다.
Array.prototype.customFilter = function(callback, thisArgs) {
const filteredArr = [];
for(let i = 0; i < this.length; i++) {
if(callback.call(thisArgs || window, this[i],i)) {
filteredArr.push(this[i])
}
}
return filteredArr;
}
const arr = [10, 20, 30]
console.log(arr.customFilter((value,i) => value > 10)); // [20, 30]
console.log(arr.customFilter((_,i) => i> 0)); // [30]
콜백함수의 this를 명시적으로 만들기 위해 customFilter를 만들어 보았다. call
메소드를 통해 thisArgs
가 없을 때는 window객체를 가르키도록 명시하였다.
const consoleObj = {
state: "loading",
getState: function() {
console.log("the state of console is", this.state);
}
}
consoleObj.getState() // the state of console is loading
setTimeout(consoleObj.getState, 0); // the state of console is undefined
consoleObj
의 getState
메소드는 this.state를 출력한다. consoleObj.getState()
는 호출주체인 consoleObj
의 state
값을 출력하는 반면, setTimeout
의 콜백함수로 호출을 하게 되면 정상적으로 출력되지 않게 된다. setTimeout의 콜백함수는 "함수"로서 호출되었다는 얘기다. this를 직접 바인딩 해주어야 올바르게 작동할 수 있다.
콜백함수가 "함수"와 "메소드" 두 곳에 모두 사용하기 위해, bind
메소드를 이용하면 좋다.
const consoleObj = {
state: "loading",
getState: function() {
console.log("the state is", this.state);
}
}
setTimeout(consoleObj.getState.bind(consoleObj), 0) // the state is loading
const drawObj = {
state: "drawing",
}
setTimeout(consoleObj.getState.bind(drawObj), 0) // the state is drawing
콜백함수는 비동기 처리에서 많이 사용된다. 비동기 처리를 동기적으로 실행하기 위해, 콜백함수를 여러번 중첩해서 사용하다보면 들여쓰기가 많아져 가독성이 떨어지게 되고, 코드 수정도 어렵게 된다.
setTimeout((name) => {
let coffeeList = name;
console.log(coffeeList);
setTimeout((name) => {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout((name) => {
coffeeList += ', ' + name;
console.log(coffeeList)
setTimeout((name) => {
coffeeList += ', ' + name;
console.log(coffeeList)
}, 500, "카페모카");
}, 500, "카페라떼");
}, 500, '아메리카노');
}, 500, '에스프레소');
코드를 아래에서부터 읽어 위를 해석해야 하는 난해한 상황이 된다.
비동기 표현을 보다 동기적으로 표현하기 위해, ES6+에서는 Promise
, async/await
, Generator
와 같은 방식을 이용하여 비동기를 처리하고 있다.
new Promise((resolve, reject) => {
setTimeout(() => {
var name = "에스프레소";
console.log(name);
resolve(name);
}, 500);
}).then(prevName => {
return new Promise(resolve => {
setTimeout(() => {
var name = prevName + ", 아메리카노";
console.log(name)
resolve(name);
}, 500);
})
}).then(prevName => {
return new Promise(resolve => {
setTimeout(() => {
var name = prevName + ", 카페모카";
console.log(name)
resolve(name);
}, 500);
})
}).then(prevName => {
return new Promise(resolve => {
setTimeout(() => {
var name = prevName + ", 카페라떼";
console.log(name)
resolve(name);
}, 500);
})
})
코드는 들여쓰기가 되지 않고, 차례대로 위부터 순서대로 실행되고, 읽는 방향도 위에서 아래로 일반적이기 때문에 중첩콜백보다 가독성이 좋아졌다. ES2017에서는 이보다 더 가독성을 올려주는 async/await
가 있다.
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);
await _addCoffee("카페라떼");
console.log(coffeeList);
}
coffeeMaker();
then
메소드의 체이닝을 통해 동기적으로 사용하는 방식보다 조금 더 동기적인 표현으로 느껴진다. 비동기적으로 실행되지만 코드는 위에서 아래로 그대로 읽으면 되기 때문에 가독성이 좋다.
const addCoffee = function(prevName, name) {
setTimeout(() => {
coffeeMaker.next(prevName ? prevName + ", " + name : name);
}, 500);
}
const coffeeGenerator = function* () {
const espresso = yield addCoffee("","에스프레소")
console.log(espresso);
const americano = yield addCoffee(espresso, "아메리카노");
console.log(americano)
const mocha = yield addCoffee(americano, "카페모카");
console.log(mocha)
const latte = yield addCoffee(mocha, "카페라떼");
console.log(latte)
}
const coffeeMaker = coffeeGenerator();
coffeeMaker.next();
Generator에 대한 설명은 여기서 확인해보면 된다. Generator
는 yield
키워드를 통해 값을 리턴하면서 동시에 suspend를 하며, 이를 통해 순차적으로 실행할 수 있도록 도와주는 키워드이다. 흐름 제어도 같이 할 수 있다는 장점이 있다.
참고
코어 자바스크립트 - 정재남