다른 코드의 인자로 넘겨주는 함수. 콜백함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행할 것이다.
콜백함수는 제어권
과 관련이 깊다.
var count=0;
var timer=setInterval(function(){
console.log(count);
if(++count>4)clearInterval(timer);
},300);
위 코드를 보면 2번째 줄에서 timer변수를 선언하고 여기에 setInterval을 실행한 결과를 할당했다.
setInterval을 호출할 때 두 개의 매개변수를 전달했는데, 바로 익명함수와 300 이라는 숫자이다.
위 코드를 확인하기 쉽게 아래와 같이 수정해보자.
var count=0;
var cbFunc=function(){
console.log(count);
if(++count>4)clearInterval(timer);
};
var timer=setInterval(cbFunc,300);
=>setInterval에 전달한 첫 번째 인자인 cbFunc함수는 0.3초마다 자동으로 실행될 것이다. 콜백함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 가진다.
var newArr=[10,20,30].map(function(currentValue,index){
console.log(currentValue,index);
return currentValue + 5;
});
console.log(newArr);
코드를 해석해보면 배열[10,20,30]에 map 메서드를 호출하고 있고 이때 첫번째 매개변수로 익명함수를 전달한다.
우선 Array의 prototype에 담긴 map 메서드
는 아래와 같은 구조를 가진다.
Array.prototype.map(callback[,thigArg]) callback:function(currentValue,index,array)
map 메서드는 첫 번째 인자로 callback함수를 받고, 생략 가능한 두 번째 인자로 콜백함수 내부에서 this로 인식할 대상을 특정할 수 있다.thisArg를 생략할 경우에는 일반적인 함수와 마찬가지로 전역객체가 바인딩된다.
map메서드는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백함수를 반복 호출하고 함수 실행결과들을 모아 새로운 배열을 만든다.
위 예제 코드를 다시보면
1)인덱스 0에 대한 콜백함수는, currentValue:10,index:0
각 값을 출력한 다음15(10+5)를 반환
2)인덱스1에 대한 콜백함수, currentValue:20,index:1
25(20+5)를 반환
3)인덱스 2에 대한 콜백함수, currentValue:30,index:2
35(30+5)를 반환
👉 결과적으로 [15,25,35]라는 새로운 배열이 만들어져서 변수 newArr에 담기고 console.log(newArr)에 [15,25,35]가 출력될 것이다.
🧐 인자의 순서를 임의로 바꾸어 사용하는 경우
var newArr=[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]
컴퓨터는 인자의 이름에 상관없이 그냥 순회중인 배열 중 현재 요소의 값을 배정하는 것이다. index를 currentvalue라고 생각해서 값을 더하니 [5,6,7]의 결과가 나온다.
💜 map메서드를 호출해서 원하는 배열을 얻으려면 map메서드에 정의된 규칙에 따라 함수를 작성해야 한다. map 메서드에 정의된 규칙에는 콜백 함수의 인자로 넘어올 값들 및 그 순서도 포함된다.
콜백 함수도 함수이기에 기본적으로는 this가 전역객체를 참조하지만 제어권을 넘겨받을 코드에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다. 콜백함수 내부에서의 this를 살펴보자.
setTimeout(function(){console.log(this);},300);//Window{...}
[1,2,3,4,5].forEach(function(x){
console.log(this);//Window{...}
});
document.body.innerHTML +='<button id="a">클릭</button>';
document.body.querySelector('#a')
.addEventListener('click',function(e){
console.log(this,e);
}
);
위 코드를 보면 setTimeout은 내부에서 콜백 함수를 호출할 때,call 메서드의 첫 번째 인자에 전역객체를 넘기기에 콜백 함수 내부에서의 this가 전역객체를 가리킨다.
foreach는 별도의 인자로 this를 넘겨주지 않았기에 전역객체를 가리키게 된다.
콜백함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출한다.
var obj={
vals:[1,2,3],
logValues:function(v,i){
console.log(this,v,i);
}
};
obj.logValues(1,2);
//logValues는 메서드로서 호출이 되었다.따라서 this는 obj를 가리키고, 인자로 넘어온 1,2가 호출된다.
[4,5,6].forEach(obj.logValues);//Window{...}
//이 메서드를 forEach함수의 콜백함수로서 전달했다.
//forEach에 의해 콜백이 함수로서 호출되고,
//별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역객체를 바라보게 된다.
콜백함수 내부에서 this가 객체를 바라보게 하고 싶다면 어떻게 해야할까?
(1)전통적인 방식:
var obj1={
name:'obj1',
func:function(){
var self=this;
return function(){
console.log(self.name);
};
}
};
var callback=obj1.func();
//obj1.func를 호출하면 앞서 선언한 내부 함수가 반환되어 callback변수에 담긴다
setTimeout(callback,1000);
//위 callback을 setTimeout함수에 인자로 전달하면 1초 뒤 callback이 실행되면서 'obj1'을 출력할 것이다
obj1.func메서드 내부에서 self 변수에 this를 담고, 익명함수를 선언과 동시에 반환했다.
이 방식은 실제로 this를 사용하지도 않고 번거롭다..
(2)func함수 재활용:
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);
callback2에는 obj2의 func를 실행한 결과를 담아 이를 콜백으로 사용했다. callback3의 경우 obj1의 func를 실행하면서 this를 obj3가 되도록 지정해 이를 콜백으로 사용했다.
전통적인 방식은 불편하고 메모리를 낭비하는 측면이 있다. 이러한 전통적인 방식의 아쉬움을 보완하는 방법으로는 bind메서드
를 활용하는 것이다.
(3) bind 메서드 활용
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);
콜백지옥: 콜백함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당할 수 없을 정도로 깊어지는 현상
현대의 자바스크립트는 웹의 복잡도가 높아진만큼 비동기적인 코드의 비중이 예전보다 훨씬 높아졌다. 그와 동시에 콜백지옥에 빠지기도 쉬워졌다.
자바스크립트는 비동기적인 작업을 동기적으로 보이게끔 처리해주는 장치를 마련하고자 끊임없이 노력했다.
비동기작업의 동기적 표현 1)Promise
new Promise(function(resolve){
setTimeout(function(){
var name="에스프레소";
console.log(name);
resolve(name);
},500);
}).then(function (prevName){
return new Promise(function (resolve)
.....
new연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve,reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then(다음),catch(오류)구문으로 넘어가지 않는다.
2)Async&await
ES2017에 등장한 새로운 기능으로 비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것이다.
⭐️ 총정리
콜백함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수
제어권을 넘겨받은 코드는 다음과 같은 제어권을 가짐
1) 콜백함수를 호출하는 시점을 스스로 판단해서 실행
2)콜백함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있다. 이순서를 따르지 않으면 엉뚱한 결과가 나옴
3)콜백함수의 this가 무엇을 바라보도록 할지 정해져 있는 경우도 있다. 정하지 않은 경우 전역객체를 바라본다. 사용자 임의로 this를 바꾸고 싶으면 bind메서드를 활용하면 된다.
4)비동기 제어를 위해 콜백 함수를 사용하다보면 콜백 지옥에 빠지기 쉽다. promise,generator,async&await등 콜백지옥에서 벗어날 수 있는 방법들이 등장하고 있음