콜백함수란?
콜백 함수는 다른 코드의 인자로 넘겨주는 함수이다. 콜백 함수는 제어권과 관련이 깊다.
즉, 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
제어권
var count = 0;
var cbFunc = function () {
console.log(count);
if(++count > 4) clearInterval(timer);
}
var timer = setInterval(cbFunc, 300);
위 코드를 실행하면 콘솔창에는 0.3초에 한 번씩 count가 0부터 1씩 증가하며 출력되다가 4가 출력된 이후 종료된다.
마지막 줄 setInterval 은 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로의 판단에 따라 적절한 시점에(0.3초마다) 이 익명함수를 실행했다.
이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.
인자
var newArr = [10, 20, 30].map(function (currentValue, index) {
console.log(currentValue, index);
return currentValue + 5;
});
console.log(newArr)
map 메서드는 첫 번째 인자로 콜백 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다.
만약 이를(thisArg) 생략할 경우, 일반적인 함수와 마찬가지로 전역객체가 바인딩된다.
또한, 콜백 함수 내에 사용된 인자는 이름이 아닌 순서에 따라 각각 구분하고 인식한다.
this
콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.
Array.prototype.map = function(callback, thisArg) {
var mappedArr = [];
for (var i = 0; i < this.length; i++) {
var mappedValue = callback.call(thisArg || window, this[i], i, this);
mappedArr[i] = mappedValue;
};
return mappedArr;
}
간단하게 map 메서드 동작 원리를 구현한 코드다.
핵심은 call/apply 메서드에 있다. this에는 thisArg 값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정하고, 첫 번째 인자에는 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소 값을, 두 번째 인자에는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다.
그 결과가 변수 mappedValue에 담겨 mappedArr의 i번째 인자에 할당된다.
즉, this에 다른 값이 담길 수 있는 이유는 바로 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기 때문이다.
document.body.innerHTML += `<button id='a'>클릭</button>`
document.body.querySelector('#a').addEventListener('click', function(e){
console.log(this) // <button id='a'>클릭</button>
})
특히 addEventListener 같은 이벤트의 경우 콜백 함수 내부에서의 this는 호출한 주체인 HTML 요소를 갖게된다. 이는 매개변수로 받게 되는 이벤트 객체의 currentTarget과 동일하다.
콜백 함수는 함수다
콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출된다.
var obj = {
vals = [1,2,3],
logValues: function(v, i) {
console.log(this, v, i)
}
};
obj.logValues(1,2); // {vals: [1,2,3], logValues: f} 1 2
[4,5,6].forEach(obj.logValues); // Window {...} 4 0
// Window {...} 5 1
// Window {...} 6 2
obj객체의 logValues는 메서드 이름 앞에 점이 있으니 메서드로 호출한 것이다. 따라서 this는 obj를 가리키고, 인자로 넘어온 1,2가 출력된다.
forEach함수의 콜백 함수로서 전달한 코드는 obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj.logValues가 가리키는 함수만 전달한 것이다. 이 함수는 메서드로서 호출할 때가 아니므로 obj와의 직접적인 연관이 없어진다. 따라서 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역객체를 바라본다.
즉, 어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아니라 함수일 뿐이다.
콜백 함수 내부의 this에 다른 값 바인딩하기
그렇다면 콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면 어떻게 해야 할까?
전통적으로는 this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식이 많이 쓰였다.
func 함수 재활용
var obj1 = {
name: 'obj1',
func: function() {
console.log(obj1.name);
}
}
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가 되도록 지정해 이를 콜백으로 사용했다. 위 코드를 실행해보면 실행 시점으로부터 1.5초 후에는 'obj2'가, 실행 시점으로부터 2초 후에는 'obj3'이 출력된다.
ES5의 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), 2000);
bind 메서드를 사용해 간편하게 this를 지정할 수 있다.
콜백 지옥과 비동기 제어
콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다.
주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하곤 하는데, 가독성이 떨어질뿐더러 코드를 수정하기도 어렵다.
비동기는 동기의 반대말이다. 동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식이다. 반대로 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어간다. CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적인 코드이다.
// 콜백 지옥 예시
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,"아메리카노"
);
// 결과:
// 아메리카노
// 아메리카노, 카페모카
// 아메리카노, 카페모카, 카페라떼
// 아메리카노, 카페모카, 카페라떼, 에스프레소
목적 달성에는 지장이 없지만 들여쓰기 수준이 과도하게 깊어졌을뿐더러 값이 전달되는 순서가 '아래에서 위로' 향하고 있어 어색하게 느껴진다.
// 콜백 지옥 해결 - 기명함수로 변환
var coffeeList = "";
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, "에스프레소");
};
var addEspresso= function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
};
setTimeout(addAmericano, 500, '아메리카노');
콜백 함수들을 모두 기명함수로 변환해 코드의 가독성을 높이고 함수 선언화 호출을 구분한다면 위에서부터 아래로 순서대로 읽을 수 있다.
또한 변수를 최상단으로 끌어올림으로써 외부에 노출되게 됐지만 전체를 즉시 실행 함수 등으로 감싸면 간단히 해결된다.
// 콜백 지옥 해결 - 비동기 작업의 동기적 표현(Promise)
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("에스프레소"));
ES6의 Promise를 이용해 비동기 작업의 동기적 표현이 가능하다.
new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부의 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(catch)으로 넘어가지 않는다. 따라서 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출한다.
// 콜백 지옥 해결 - 비동기 작업의 동기적 표현(Generator)
var addCoffee = function (prevName, name) {
setTimeout(function () {
coffeeMaker.next(prevName ? prevName + ", " + name : name);
}, 500);
};
var coffeeGenerator = function* () {
var americano = yield addCoffee("", "아메리카노");
console.log(americano);
var mocha = yield addCoffee("", "카페모카");
console.log(mocha);
var latte = yield addCoffee("", "카페라떼");
console.log(latte);
var espresso = yield addCoffee("", "에스프레소");
console.log(espresso);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();
'*'이 붙은 함수가 바로 ES6의 Generator 함수다.
Generator 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메서드를 가지고 있다. 이 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다. 이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 함수의 실행을 멈춘다.
즉, 비동기 작업이 완료되는 시점마다 next 메서드를 호출해 내부 소스가 위에서부터 아래로 순차적으로 진행된다.
// 콜백 지옥 해결 - 비동기 작업의 동기적 표현(Promise + Async/await)
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();
ES2017에서는 가독성이 뛰어나면서도 작성법이 간단한 async/await이 추가되었다.
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행한다.
즉, Promise의 then과 흡사한 효과를 얻을 수 있다.
Reference
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week6 - 프로토타입 (0) | 2024.11.26 |
---|---|
[JavaScript] 코어 자바스크립트 week5 - 클로저 (1) | 2024.11.18 |
[JavaScript] 코어 자바스크립트 week3 -this (0) | 2024.11.05 |
[JavaScript] 코어 자바스크립트 week2 -실행 컨텍스트 (0) | 2024.10.28 |
[JavaScript] 코어 자바스크립트 week1 - 데이터 타입 (3) | 2024.10.22 |