24.01.11 TIL - 콜백함수(1)
1.콜백함수
1) 정의: 다른 코드의 인자로 넘겨주는 함수. 인자로 넘겨준다는 말은 넘겨받는 코드가 있다는 것!!
forEach, setTimeout이 그 예시가 되겠다.
2) 콜백함수를 넘겨받은 코드는 이 콜백함수를 필요에 따라 적절한 시점에 실행하게 된다.(제어권이 그들에게 있다.)
// setTimeout
setTimeout(function() {
console.log("Hello, world!");
}, 1000);
// forEach
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number) {
console.log(number);
});
3) callback = 제어권을 넘겨줄테니 너가 알고있는 그 로직으로 처리해줘!
4) 즉 콜백함수는 다른 코드(함수 or 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위힘한 함수. 콜백함수를 위임받은 코드는 자체적으로 내부 로직에 의해 이 콜백함수를 적절한 시점에서 실행.(이 시점도 위임받은 코드가 정한다.)
2. 제어권
1) 호출시점
콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가짐
아래의 제어권을 넘겨 받은 코드인 setInterval가 언제 콜백함수를 호출할지에 대한 제어권을 가지게 됨.
var count = 0;
// timer : 콜백 내부에서 사용할 수 있는 '어떤 게 돌고있는지'
// 알려주는 id값
var timer = setInterval(function() {
console.log(count);
if(++count > 4) clearInterval(timer);
}, 300);
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로 넘겨주면 모두 setInerval이 가짐.
인자의 순서도 제어권이 그 코드에 있기 때문에 제어권이 넘어갈 함수의 규칙에 맞게 호출해야 한다.
2) this
콜백함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조한다. 하지만 제어권을 넘겨받을 코드에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조한다.
// 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를 명시적 바인딩 하기 때문에 this에 다른 값이 담길 수 있다.
3. 콜백 함수도 함수
콜백함수로 어떤 객체의 메서드를 전달하더라도, 그 메서드는 메서드가 아닌 함수로 호출
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);
4. 콜백 함수 내부의 this에 다른 값 바인딩하기
콜백 함수 내부의 this에 다른 값을 바인딩하는 방법 -> binding 메서드
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);
5. 콜백 지옥과 비동기 제어
1) 콜백 지옥(Callback Hell)
a. 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 헬 수준인 경우를 말해요.
b. 주로 이벤트 저리 및 서버 통신과 같은 비동기적 작업을 수행할 때 발생.
c. 가독성이 hell이고 수정도 어렵다.
2) 동기 vs 비동기
a. 동기(synchronous)
i) 현재 실행중인 코드가 끝나야 다음 코드를 실행
ii) CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드
iii) 계산이 복잡해서 CPU가 계산하는 데에 오래 걸리는 코드 역시 동기적 코드
b. 비동기(a + synchronous) => async
i) 실행중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
ii) setTimeout, addEventListner 등
iii) 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드.
c. 웹의 복잡도가 올라갈수록 비동기적 코드의 비중이 늘어납니다.
3)콜백지옥의 해결방안
a. 기명함수로 변환 : 가독성이 좋지만 한번만 쓰고 말텐데 이름을 다 붙이는 것은 비효율적.
b. 비동기적 작업의 동기적 표현
i) Promise : 비동기 처리에 대해, 처리가 끝나면 알려달라는 약속
- new 연산자로 호출한 Promise의 인자로 넘어가는 콜백은 바로 실행
- 그 내부의 resolve/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);
});
});
위의 반복부분을 함수화 한 코드이다. 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('카페라떼'));
ii) Generator
-이터러블 객체(iterable)
*가 붙은 함수가 제너레이터 함수이다. 실행하면 Iterator 객체가 반환(next()를 가지고 있음)된다.
비동기 작업이 완료되는 시점마다 next 메서드를 호출해주면 Generator함수 내부소스가 위->아래 순으로 진행됨.
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();
iii) Promise + Async/await (ES6 함수)
비동기 작업을 수행코자 하는 함수 앞에 async 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 붙여주면 된다. Promise ~ then과 동일한 효과를 얻을 수 있다.
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();