
👉 ‘코어 자바스크립트’의 ‘04 콜백 함수’를 읽으며 기억할 내용들과 새로 알게 된 내용을 정리하였습니다.
콜백 함수: 다른 코드의 인자로 넘겨주는 함수. 필요에 따라 적절한 시점에 실행. → 제어권과 관련이 깊다.A와 B는 08시에 약속. 적어도 6시에 기상해야 함.
A는 불안한 마음에 수시로 깨어 시계 확인. 잠을 설치다가 5시에 기상.
B는 6시에 알람을 세팅하고, 꿀잠을 잠.
알람을 울리는 결과를 반환.⇒ 시계는 A에게 요청마다 수동적으로 시간을 제공. B에게 요청을 받은 후 자체적으로 수행하다가 적절한 시점에 적극적으로 통보.
⇒ A의 경우 시계 함수 제어권이 A에게, B의 경우 알람을 울리는 명령에 대한 제어권을 시계에게.
var count = 0;
var timer = setInterval(function () {
console.log(count);
if (++count > 4) clearInterval(timer);
}, 300);var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);setInterval()
- scope에는 Window 객체 or Worker의 인스턴스. 두 객체 모두 setInterval 메서드를 제공.
- 일반적인 브라우저 환경에서는 window를 생략해서 함수처럼 사용 가능.
- 매개변수로 func, delay 값은 required이고, 3번째부터 optional.
- func에 넘겨준 함수는 매 delay(ms)마다 실행. return은 없음.
- setInterval()을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값 반환.
- 이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval())하기 위함.
var count = 0;
var cbFunc = function () {
console.log(count);
if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
// -- 실행 결과 --
// 0 (0.3초)
// 1 (0.6초)
// 2 (0.9초)
// 3 (1.2초)
// 0 (1.5초)
timer 변수: setInveral의 ID 값.
| code | 호출 주체 | 제어권 |
|---|---|---|
| cbFunc(); | 사용자 | 사용자 |
| setInterval(); | setInterval | setInterval |
콜백함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 가진다.
var newArr = [10, 20, 30].map(function (currentValue, index) {
console.log(currentValue, index);
return currentValue + 5;
});
console.log(newArr);
// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [15, 25, 35]Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)map()
jQuery의 메서드들은 기본적으로 첫 번째 인자에 index, 두 번째 인자에 currentValue.
var newArr = [10, 20, 30].map(function (index, currentValue) {
console.log(index, currentValue);
return currentValue + 5;
});
console.log(newArr);
// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [5, 6, 7]
콜백 함수를 호출하는 주체가 map 메서드이므로 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 / apply 메서드.
this에는 thisArg 값이 있을 경우 thisArg, 없을 경우 전역객체 지정.
첫 번째 인자에 this 배열의 i 번째 요소 값, 두 번째 인자에 i 값, 세 번째 인자에 배열 자체를 지정 호출.
그 결과를 mappedValue에 담아 mappedArr의 i 번째 요소에 할당.
this에 다른 값이 담기는 이유: 제어권을 넘겨받을 코드에서 call / apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩.
setTimeout(function () { console.log(this); }, 300); // (1) Window { ... }
[1, 2, 3, 4, 5].forEach(function (x) {
console.log(this); // (2) Window { ... }
};
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, ... }
(1): setTimeout은 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째 인자에 전역객체를 넘기므로 콜백 함수 내부에서의 this가 전역객체를 참조.
(2): forEach는 별로의 인자로 this를 받지만, 별도의 인자를 지정하지 않아 전역객체를 참조.
(3): addEventListener는 내부에서 콜백 함수를 호출할 때, call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기므로 콜백 함수 내부에서의 this가 addEventListener를 호출한 주체인 HTML element를 참조.
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
var obj1 = {
name: 'obj1',
func: function () {
var self = this;
return function () {
console.log(self.name);
};
}
};
var callback = obj1.func();
setTimeout(callback, 1000);
var obj1 = {
name: 'obj1',
func: function () {
console.log(obj1.name);
}
};
settimeout(obj1.func, 1000);
var obj1 = {
name: 'obj1',
func: function () {
var self = this;
return function () {
console.log(self.name);
};
}
};
var callback = obj1.func();
setTimeout(callback, 1000);
var obj2 = {
name: 'obj2',
func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);
var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
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);
콜백 지옥: 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상.비동기(Asynchronous): 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드 실행.별도의 요청, 실행 대기, 보류 등.동기(Synchronous): 현재 실행 중인 코드가 완료된 후 다음 코드 실행.즉시 처리가 가능한 대부분의 코드는 동기적 코드.setTimeout(function (name) {
var coffeeList = name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
}, 500, '카페라떼');
}, 500, '카페모카');
}, 500, '아메리카노');
}, 500, '에스프레소');
아래에서 위로 향하고 있어 어색.var coffeeList = '';
var addEspresso = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addAmericano, 500, '아메리카노');
};
var addAmericano = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addMocha, 500, '카페모카');
};
var addMocha = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addLatte, 500, '카페라떼');
};
var addLatte = function (name) {
coffeeList = name;
console.log(coffeeList);
};
setTimeout(addEspresso, 500, '에스프레소');
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);
}).then(function (prevName) {
return new Promise(function (resolve) {
setTimeout(function () {
var name = prevName + ', 카페모카';
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);
});
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('카페모카'))
.then(addCoffee('카페라떼'));
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();
function* 함수가 Generator 함수.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();
this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있다. 정하지 않은 경우에는 전역객체를 바라본다. 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 된다.ECMAScript에는 Promise, Generator, async / await 등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법들이 속속 등장하고 있다.