호출 시점
콜백함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권
을 가진다.
아래 예시에서는 0.3초라는 시점에 콜백함수를 호출하게 된다.
var count = 0;
var cbFunc = function () {
console.log(count);
if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
// 실행 결과
// 0 (0.3sec)
// 1 (0.6sec)
// 2 (0.9sec)
// 3 (1.2sec)
// 4 (1.5sec)
→ 원래 cbFunc()를 수행한다면 그 호출주체와 제어권은 모두 사용자가 된다.
→ setInterval로 넘겨주게 되면 그 호출주체와 제어권은 모두 setInterval이 된다.
code | 호출 주체 | 제어권 |
---|---|---|
cbFunc(); | 사용자 | 사용자 |
setInterval(cbFunc, 300); | setInterval | setInterval |
인자
인자의 순서까지도 제어권이 위임받은 코드에게 있다.
예를들어 map 함수의 경우
// map 함수에 의해 새로운 배열을 생성해서 newArr에 담고 있네요!
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 ]
자, 그렇다면 콜백함수에서 여러분이 넣으신 currentValue, index
이 변수의 순서를 바꾸면 어떻게 될지 확인해보자.
// map 함수에 의해 새로운 배열을 생성해서 newArr에 담고 있네요!
var 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 ]
this
콜백 함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조한다.
하지만, 예외사항으로 제어권을 넘겨받을 코드에서 콜백 함수에 this가 될 대상을 지정한 경우에는 그 대상을 참조한다.
아래 예시는 map함수를 직접 구현하는 코드이다.
핵심은
call
,apply
에 있다.
// Array.prototype.map을 직접 구현해봤어요!
Array.prototype.mapaaa = function (callback, thisArg) {
var mappedArr = [];
for (var i = 0; i < this.length; i++) {
// call의 첫 번째 인자는 thisArg가 존재하는 경우는 그 객체, 없으면 전역객체
// call의 두 번째 인자는 this가 배열일 것(호출의 주체가 배열)이므로,
// i번째 요소를 넣어서 인자로 전달
var mappedValue = callback.call(thisArg || global, this[i]);
mappedArr[i] = mappedValue;
}
return mappedArr;
};
const a = [1, 2, 3].mapaaa((item) => {
return item * 2;
});
console.log(a);
→ 그러니까 제어권을 넘겨받을 코드에서 call
apply
메서드의 첫 번째 인자에서 콜백 함수에서 사용될 this를 명시적으로 binding하기 때문에 this에 다른 값이 담길 수 있는 것이다.
콜백 함수로 어떤 객체의 메서드를 전달하더라도, 그 메서드는 메서드가 아닌 함수로 호출한다.
아래 예시에서 전달하는 obj.logValues
는 obj
와 아무 상관없이 그저 함수만 전달하는 것이다.
var obj = {
vals: [1, 2, 3],
logValues: function(v, i) {
console.log(this, v, i);
}
};
//method로써 호출
obj.logValues(1, 2);
//callback => obj를 this로 하는 메서드를 그대로 전달한게 아니에요
//단지, obj.logValues가 가리키는 함수만 전달한거에요(obj 객체와는 연관이 없습니다)
[4, 5, 6].forEach(obj.logValues);
call 메서드 사용
var obj1 = {
name: 'obj1',
func: function() {
var self = this; //이 부분!
return function () {
console.log(self.name);
};
}
};
// ---------------------------------
// obj1의 func를 직접 아래에 대입해보면 조금 더 보기 쉽습니다!
var obj2 = {
name: 'obj2',
func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);
// 역시, obj1의 func를 직접 아래에 대입해보면 조금 더 보기 쉽습니다!
var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
bind 메서드 사용 가장 좋은 방법
var obj1 = {
name: 'obj1',
func: function () {
console.log(this.name);
}
};
//함수 자체를 obj1에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj1로 고정해줘!
setTimeout(obj1.func.bind(obj1), 1000);
var obj2 = { name: 'obj2' };
//함수 자체를 obj2에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj2로 고정해줘!
setTimeout(obj1.func.bind(obj2), 1500);
콜백 지옥
주로 이벤트 처리 및 서버 통신과 같은 비동기 작업을 수행할 때, 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 헬 수준
인 경우를 말한다.
동기 vs 비동기
동기 : synchronous
비동기 : a + synchronous ⇒ async라고들 흔히 부르죠
실행 중인 코드의 완료 여부와 무관하게
즉시 다음 코드로 넘어가는 방식
setTimeout, addEventListner 등
별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드
(출처 : https://velog.io/@mrbartrns/til-16-asynchronous-of-js)
콜백 지옥의 예시와 해결방안
콜백 지옥의 예시
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,
"에스프레소"
);
(1) 첫번째 해결 방법 - 기명 함수로 변환
가독성은 좋아졌으나, 이런식으로 이름을 다 붙히는 건 근본적인 해결책은 아닌 것 같다.
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, '에스프레소');
(2) 비동기 작업의 동기적 표현하기.
Promise
, Generator
, async/await
같은 것들을 사용할 수 있다.Promise (1)
Promise 간단 설명
new
연산자로 호출한Promise
의 인자로 넘어가는 콜백은 바로 실행된다.- 그 내부의
resolve
(또는reject
) 함수를 호출하는 구문이 있을 경우,reslove
(또는 reject) 둘 중 하나가 실행되기 전까지는 다음(then
), 오류(catch
)로 넘어가지 않는다.- 비동기 작업이 완료될 때 비로소
resolve
,reject
를 호출한다.
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);
});
});
Promise (2) - Closer
반복 부분을 재귀적으로 표현한 코드. trigger를 걸어주기 위해 클로저 개념이 나온다.
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('카페라떼'));
Promise (3) - Generator
<aside> 💡 이터러블 객체 (Iterable)
생소한 제너레이터 문법이다.
*가 붙은 함수가 제너레이터 함수로 실행하면 Iterator
객체가 반환된다. (next()
를 가지고 있음.)
iterator
는 객체가 next
메서드로 순환할 수 있는 개체로, next
메서드 호출 시, Generator
함수 내부에서 가장 먼저 등장하는 yield
에서 stop
이후 다시 메서드를 호출하면 멈췄던 부분 → 그 다음의 yield
까지 실행 후 stop
한다.
즉, 비동기 작업이 완료되는 시점마다 next
메서드를 호출해주면 Generator
함수 내부소스가 위에서 아래로 순차적으로 진행된다.
</aside>