정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.
콜백함수는 다른 코드의 인자로 넘겨주는 함수이다. 콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행 할 것이다.
Call+Back
어떤 함수 X를 호출하면서 동시에 '특정 조건일 때 함수 Y를 실행해서 나에게 알려줘!'라는 요청을 함께 보내는 것이다. 이 요청을 받은 함수X는 해당 조건이 갖춰졌는지 여부를 스스로 판단해서 Y를 직접 호출한다.
콜백함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다. 콜백함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행 할 것이다.
아래 예제1-1를 살펴보면,
<예제1>
let count = 0;
let countTime = function () {
console.log(count);
if(++count > 4) clearInterval(timer);
};
let timer = setInterval(countTime, 300);
// --실행 결과--
//0 (0.3초)
//1 (0.6초)
//2 (0.9초)
//3 (1.2초)
//4 (1.5초)
1) setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 contTime 함수를 넘겨주자,
2) 제어권을 넘겨 받은 setInterval이 스스로 판단에 따라 적절한 시점(0.3초마다)이 익명 함수를 실행했다.
📍 콜백 함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 갖는다.
<예제2>
let 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]
🧐우선, map()메서드는 어떻게 동작할까?
Array.map(callback[,thisArg]) callback: function(currentValue, index, array)
첫 번째 인자로 callback함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다. thisArg를 생략하면 일반적인 함수처럼 전역객체에 바인딩된다. map메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내서 콜백함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다.
다시 예제2를 보면
1) 배열[10,20,30]의 각 요소를 처음부터 하나씩 꺼내서 콜백 함수를 실행
2) 첫 번째(index 0)에 대한 콜백함수는 currentValue에 10이, index에는 인덱스 0이 담겨 실행
3) 각 값을 출력하고, 15(10+5)를 반환
...두 번째(index 1), 세 번째(index 2) 각 각 동일하게 콜백함수를 실행하고 나면
4)[15, 25, 35]라는 새로운 배열을 만들어 newArr에 담긴다.
만약 map메서들를 제이쿼리의 방식처럼 인자의 순서를 바꾸어 사용하면?
우리는 순서바뀌더라도 index
, currentValue
단어로 접근하니까 순서상관없이 각 단어의 의미가 바뀌는 것이 아니니까 문제 없을거라 생각할 수 있다. 하지만, 저 단어들은 그저 이름일뿐이다.
컴퓨터는 그저 첫 번째, 두 번째의 순서에 의해서만 각각을 구분하고 인식한다!
<예제2.1>
let newArr2 = [10,20,30].map(function(index, currentValue){
console.log(index, currentValue);
return currentValue+5;
});
console.log(newArr2);
// --실행 결과--
//10 0
//20 1
//30 2
//[5, 6, 7]
따라서 예제2.1를 실행하면 [5, 6, 7]이라는 결과가 나온다. currentValue
라는 인자의 위치가 두 번째라서 컴퓨터는 여기에 인덱스 값을 부여했기 때문이다.
😎콜백 함수의 제어권을 가진 코드(함수 또는 메서드) 규칙을 따라야 한다!
콜백함수를 받아서 처리할 코드, 제어권을 가지고 있는 코드(함수 또는 메서드)에 정의된 규칙에 따라 함수를 작성해야 한다.
즉,map()
메서드를 호출해서 원하는 배열을 얻으려면 map메서드에 정의된 규칙에 따라 함수를 작성해야한다. 콜백함수를 호출하는 주체가 사용자가 아닌 map메서드이므로 map메서드가 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지가 전적으로 map메서드에 달린 것이다.
콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.
Array.prototype.map = function (callback, thisArg) {
let mappedArr = [];
for (let i = 0; i < this.length; i++) {
const mappedValue = callback.call(thisArg || window, this[i], i, this);
mappedArr[i] = mappedValue;
}
return mappedArr;
}
const result = [10, 20, 30].map(function (value, index) {
console.log(this, value, index);
return value + 100;
});
console.log(result);
// 실행 결과
// Window 10 0
// Window 20 1
// Window 30 2
// [110, 120, 130]
const anotherResult = [10, 20, 30].map(function (value, index) {
console.log(this, value, index);
return value + 100;
}, [1, 2, 3]);
console.log(anotherResult);
// 실행 결과
// [1, 2, 3] 10 0
// [1, 2, 3] 20 1
// [1, 2, 3] 30 2
// [110, 120, 130]
제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩한다.
- thisArg가 있는 경우에는 해당 값을 콜백 함수의 this로 지정
- thisArg가 없는 경우에는 전역 객체(window)를 this로 지정
📍 그리고 thisArg로 넘기는 것은 콜백 함수의 this를 지정하는 것이지, map메서드의 this를 지정하는 게 아니라서 결과 값에는 영향이 없다.(둘 다 [110, 120, 130]
)
const cbFunc = function () {
console.log(this);
};
setTimeout(cbFunc, 300);
// 실행 결과
// Window
const arr = [1, 2, 3, 4, 5];
arr.forEach(cbFunc);
// 실행 결과
// Window
// Window
// Window
// Window
// Window
arr.forEach(cbFunc, arr);
// 실행 결과
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
.addEventListener('click', cbFunc);
// 실행 결과
// <button id="a">클릭</button>
setTimeout
: 내부에서 콜백 함수를 호출할 때 call 메서드의 첫번째 인자에 전연 객체를 넘기기 때문에 콜백 함수 내부에서의 this는 전역 객체를 가리킴
forEach
: 별도의 인자로 this를 받는 경우. 콜백 함수 다음 인자로 this로 지정할 객체를 넘기지 않은 경우에는 this가 전역 객체를 가리키고, this로 지정할 객체를 넘긴 경우에는 this가 해당 객체를 가리킴
addEventListener
: 내부에서 콜백 함수를 호출 할 때 call 메서드의 첫번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서 this가 addEventListener를 호출 한 주체 HTML 엘리먼트를 가리킴
콜백 함수는 함수다. 콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출된다.
const obj = {
vals: [1, 2, 3],
logValues: function (v, i) {
console.log(this, v, i);
}
};
obj.logValues(1, 2); // 🔹여기서 logValues는 obj객체의 메서드임!
// 따라서 this는 obj를 가리킴
// obj, 1, 2
[4, 5, 6].forEach(obj.logValues); //🔹여기서는 obj.logValues가 가리키는 함수만 전달! forEach에 의해 콜백이 함수로서 호출되고, 별도의 this지정인자 없으니까 함수내부에서 this는 전역객체를 바라봄
// 실행 결과
// Window, 4, 0
// Window, 5, 1
// Window, 6, 2
어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐이다.
객체의 메서드를 콜백 함수로 전달하면 객체의 메서드가 아니라 함수로 전달되기 때문에 해당 객체를 this로 바라볼 수 없다. 그럼에도 콜백함수 내부에서 this가 객체를 바라보게 하고 싶다면??
1) 전통적인 방식
const obj1 = {
name: 'obj1',
func: function () {
const self = this;
return function () {
console.log(self.name);
}
}
};
const callback = obj1.func();
setTimeout(callback, 1000);
// 실행 결과
// obj1
2) this를 사용하지 않은 경우
const obj1 = {
name: 'obj1',
func: function () {
console.log(obj1.name);
}
};
setTimeout(obj1.func, 1000);
// 실행 결과
// obj1
3) func함수 재활용
const obj1 = {
name: 'obj1',
func: function () {
const self = this;
return function () {
console.log(self.name);
}
}
};
const callback = obj1.func();
setTimeout(callback, 1000);
// 실행 결과
// obj1
const obj2 = {
name: 'obj2',
func: obj1.func
};
const callback2 = obj2.func();
setTimeout(callback2, 1500);
// 실행 결과
// obj2
const obj3 = { name: 'obj3' };
const callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
// 실행 결과
// obj3
4) ✨bind메서드 활용
const obj1 = {
name: 'obj1',
func: function () {
console.log(this.name);
}
};
setTimeout(obj1.func.bind(obj1), 1000);
// 실행 결과
// obj1
const obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);
// 실행 결과
// obj2
🤓이어서 콜백지옥과 비동기 제어에 대해 알아보자
[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
https://velog.io/@modolee/core-javascript-04-part1
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map
퍼가요~♡