이 글은 📕코어 자바스크립트 책을 바탕으로 정리한 글입니다.
콜백 함수(callback function)는 다른 코드의 인자로 넘겨주는 함수이다.
콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행할 것이다.
콜백 함수를 이해하기 위한 간단한 예시를 보자.
A와 B는 다음 날 아침 8시에 약속이 있다.
A는 수시로 잠에서 깨어 시계를 확인하고, 반면 B는 알람시계를 세팅한다.
A는 수시로 시간을 구하는 함수를 직접 호출했다.
반면 B는 시계의 알람을 설정하는 함수를 호출했고, 해당 함수는 호출 당시에는 아무것도 하지 않다가 B가 정해준 시각이 됐을 때 비로소 '알람을 울리는' 결과를 반환한다.
시간 정보를 제공하는 시계 입장에서 생각해보면 A의 경우 요청할 때마다 수동적으로 시간 정보를 제공하기만 한 반면,
B의 경우에는 요청을 받은 뒤 자체적으로 무언가를 수행하다가 적절한 시점에 적극적으로 통보했다.
A의 경우 시계 함수의 제어권은 A에게 있고, B는 시계 함수에게 요청을 하면서 알람을 울리는 명령에 대한 제어권을 시계에게 넘겨준 것이다.
🤔 callback은 '부르다', '호출(실행)하다'의 의미인 call과, '뒤돌아오다', '되돌다'는 의미인 back의 합성어로, '되돌아 호출해달라'는 명령이다.
이처럼 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수라고 할 수 있다.
var count = 0;
var timer = setInterval(function () {
console.log(count);
if (++count > 4) clearInterval(timer);
}, 300);
// 0
// 1
// 2
// 3
// 4
우선 setInterval
의 구조를 살펴보자
var intervalID = scope.setInterval(func, delay[, param1, param2, ...])
우선 scope
에는 Window
객체 또는 Worker
의 인스턴스가 들어올 수 있다.
두 객체 모두 setInterval
메서드를 제공하기 때문인데, 일반적인 브라우저 환경에서는 window
를 생략해서 함수처럼 사용 가능하다.
매개변수로는 func
, delay
값을 반드시 전달해야하고, 세 번째 매개 변수부터는 선택적이다.
func
에 넘겨준 함수는 매 delay(ms)
마다 실행되며, 그 결과 어떠한 값도 리턴하지 않는다.
setInterval
를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값이 반환된다.
이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval)할 수 있게 하기 위함이다.
위 코드를 좀 더 확인하기 쉽게 수정해보자.
var count = 0;
var cbFunc = function () {
console.log(count);
if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
// 0 (0.3s)
// 1 (0.6s)
// 2 (0.9s)
// 3 (1.2s)
// 4 (1.5s)
code | 호출 주체 | 제어권 |
---|---|---|
cbFunc() | 사용자 | 사용자 |
setInterval(cbFunc, 300) | setInterval | setInterval |
setInterval
이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval
이 스스로의 판단에 따라 적절한 시점에(0.3초마다) 이 익명함수를 실행했다.
이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.
var newArr = [10, 20, 30].map(function (currentValue, index) {
console.log(currentValue, index);
return currentValue + 5;
});
console.log(newArr);
Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)
map
메서드는 첫 번째 인자로 callback
함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this
로 인식할 대상을 특정할 수 있다.
thisArg
를 생략할 경우에는 일반적인 함수와 마찬가지로 전역객체가 바인딩된다.
map
메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내 콜백 함수를 반복 호출하고,
콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다.
map
메서드에 정의된 규칙에는 콜백 함수의 인자로 넘어올 값들 및 그 순서도 포함이 돼있다.
콜백 함수를 호출하는 주체가 사용자가 아닌 map
메서드이므로,
map
메서드가 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지가 전적으로 map
메서드에 달린 것이다.
이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.
this
별도의 this
를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 map
메서드를 직접 구현해보자
Array.prototype.map = function (callback, thisArg) {
var mappedArr = [];
for (let 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
값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정하고, 첫 번째 인자에는 메서드의 this
가 배열을 가리킬 것이므로 배열의 i번째 요소 값을, 두 번째 인자에는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다.
그 결과가 변수 mappedValue
에 담겨 mappedArr
의 i번째 인자에 할당된다.
이제 this
에 다른 값이 담기는 이유를 알 수 있다.
바로 제어권을 넘겨받을 코드에서 call
/apply
메서드의 첫 번째 인자에 콜백 함수 내부에서의 this
가 될 대상을 명시적으로 바인딩하기 때문이다.
setTimeout(function () {
console.log(this); // (1) window {...}
}, 300);
[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>
// PointerEvent {isTrusted: true, ...}
});
(1)의 setTimeout
은 내부에서 콜백 함수를 호출할 때 call
메서드의 첫 번째 인자에 전역객체를 넘기기 때문에 콜백 함수 내부에서의 this
가 전역객체를 가리킨다.
(2)의 forEach
는 별도의 인자로 this
를 받는 경우에 해당되지만 별도의 인자로 this
를 넘겨주지 않았기 때문에 전역객체를 가리킨다.
(3)의 addEventListener
는 내부에서 콜백 함수를 호출할 때 call
메서드의 첫 번째 인자에 addEventListener
메서드의 this
를 그대로 넘기도록 정의돼있기 때문에 콜백 함수 내부에서의 this
가 addEventListener
를 호출한 주체인 HTML 엘리먼트를 가리키게 된다.