[JS] 콜백함수
콜백 함수
다른 코드의 인자로 넘기는 함수로 제어권도 해당코드에게 부여
1. 제어권
1.1 호출 시점
콜백을 인자로 받는 코드에 콜백함수를 넘겨서 호출시점을 제어하게 할 수 있다. 예를 들어 setTimeout, setInterval 함수의 경우 첫 번째 인자로 콜백함수, 두 번째 인자로 시간을 받는데, 해당 시간이 흐른 뒤에야 콜백함수를 호출한다.
var count = 0;
const intervalId = setInterval(() => {
console.log("안녕");
count += 1;
if (count > 6) {
clearInterval(intervalId);
}
}, 1000); // 1초 마다 안녕이 출력 -> 7번째에 interval종료
setInterval 함수의 경우 콜백함수 호출을 종료하기 위해선 clearInterval 함수를 사용한다. 즉, 콜백함수의 호출 시작, 종료에 제어권을 다른 함수가 쥐고 있다.
1.2 인자
콜백을 인자로 받는 코드는 콜백함수의 인자에도 제어권이 있다. 예를 들어 map 함수의 경우 다음과 같은 인자를 갖는다.
Array.prototype.map(callback[,thisArgs])
callback: function(currentValue, index, array)
여기서 callback 함수의 인자를 순서대로 (현재 인덱스 배열값, 인덱스, 배열)로 지정하고 있다. 당연히 아래 코드를 출력하면 아래처럼 출력이 된다.
[1, 2, 3].map((currentValue, index, array) => {
console.log(currentValue, index, array);
}); // 1 0 [1,2,3], 2 1 [1,2,3], 3 2 [1,2,3]
그렇다면 콜백함수 인자의 식별자를 바꾸면 어떻게 될까?
[1, 2, 3].map((index, array, currentValue) => {
console.log(currentValue, index, array);
});
식별자의 의미대로 인덱스 , 배열, 현재값이 담겼을까?
답은 아니다. 여전히 1 0 [1,2,3], 2 1 [1,2,3], 3 2 [1,2,3]
출력된다. index라고 식별자명을 지었어도 map의 콜백함수의 인자값은 무조건 순서대로 (현재 인덱스 배열값, 인덱스, 배열)이기 때문이다.
1.3 this
콜백을 인자로 받는 코드는 콜백함수의 인자로 this에 어떤 값을 바인딩할지도 제어한다. 예로 addEventListener 함수의 경우 콜백함수 호출시 call 메서드 첫 번째 인자에 addEventListener의 this를 바인딩한다.
document.body.querySelector("#a").addEventListener("click", function (e) {
console.log(this, e); // #a쿼리문을 가진 html 요소 클릭이벤트 객체
});
2. 콜백함수는 함수다
호출주체가 존재하더라도 this에 전역객체 바인딩된
당연한 말 같지만 메서드와 함수의 차이를 생각하면 그렇지도 않다. 원래 메서드는 this에 호출주체가 바인딩된다. 그런데 콜백함수가 되는 순간 호출주체가 없는 함수처럼 this에 전역객체가 바인딩된다.
var name = "sad";
const obj = {
name: "happy",
method: function () {
console.log(this.name);
},
};
obj.method(); // 'happy'
setTimeout(obj.method, 1000); //sad
이런 결과가 나타나는 이유는 obj를 this로 하는 메서드를 그대로 전달한 게 아니라 obj.method가 가리키는 함수만 전달했기 때문이다.
2.1 해결 방법
2.1.1 변수에 this 담아서 우회
일단 콜백함수로 사용되기 전 해당 메서드 내부의 this는 obj이기 때문에 그 값을 변수 self에 바인딩해준다. 그리고 self의 name을 출력하는 코드를 짜고, 콜백함수로 직접 obj.method를 넣는 대신 obj.method의 반환값을 넣는다.
var name = "sad";
const obj = {
name: "happy",
method: function () {
var self = this;
console.log(self.name);
},
};
var callback = obj.method();
setTimeout(callback, 1000); //'happy'
하지만 this를 사용하지 않기에 재사용성이 좋지 않고 변수를 여러개 만들어야 된다는 점에서 불편하다.
2.1.2 변수에 메서드의 반환값을 담아 사용
사실 위 방법에서 변수 self에 this를 바인딩해서 우회하지 않아도 callback에 반환값을 첫번째인자로 넘기면 당연히 this는 obj다.
var name = "sad";
const obj = {
name: "happy",
method: function () {
console.log(this.name);
},
};
var callback = obj.method();
setTimeout(callback, 1000); //'happy'
이 방법은 this를 사용하긴하지만 변수를 만들어 반환값을 할당한다는 점이 여전히 불편하다.
2.1.3 this 대신 객체 직접 사용
this 대신 객체에 직접 접근해 값을 가져오는 방법도 있다. 위 방법들 보다 간결하지만 this를 재활용하지 않고 객체를 지정해 메모리 낭비를 초래한다.
var name = "sad";
const obj = {
name: "happy",
method: function () {
console.log(obj.name);
},
};
setTimeout(obj.method, 1000); //'happy'
2.1.4 bind(thisArgs)
그래서 등장한 메서드가 bind다. bind는 첫번째 인자로 this로 바인딩할 값을 넣어 새로운 함수로 반환하기 때문에 아래 코드 실행시 this는 obj를 가리킨다.
var name = "sad";
const obj = {
name: "happy",
method: function () {
console.log(this.name);
},
};
setTimeout(obj.method.bind(obj), 1000); //'happy'
3. 콜백지옥
자바스크립트는 비동기 방식의 코드로 이뤄져 있다. 비동기식은 순서를 보장하지 않기 때문에 순서를 제어해야 하는 상황에서 콜백함수를 끝없이 사용하는 문제가 발생할 수 있다. 이를 콜백지옥이라고 부른다.
예) coffeeList 만들기
만약 에스프레소, 아메리카노, 카페모카, 카페라떼가 0.5초 마다 순서대로 console에 찍히는 비동기식을 구현한다면 다음과 같다.
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,
"에스프레소"
);
가독성도 떨어지고 값이 전달되는 순서가 아래에서 위로 향하기 때문에 어색하다. setTimeout을 쓰되 좀 더 간단히 나타내려면 다음과 같이 익명의 콜백함수를 기명함수로 전환해주면 된다.
(function () {
var coffeeList = "";
var addEspresso = (name) => {
coffeeList = name;
console.log(coffeeList);
setTimeout(addAmericano, 500, "아메리카노");
};
var addAmericano = (name) => {
coffeeList = name;
console.log(coffeeList);
setTimeout(addMocha, 500, "카페모카");
};
var addMocha = (name) => {
coffeeList = name;
console.log(coffeeList);
setTimeout(addLatte, 500, "카페라떼");
};
var addLatte = (name) => {
coffeeList = name;
console.log(coffeeList);
};
setTimeout(addEspresso, 500, "에스프레소");
})();
이러면 실행 순서대로 함수를 볼 수 있어서 가독성도 좋고 즉시함수로 만든다면 변수도 은닉이 가능하기에 위 방법 보다 낫다. 하지만 재사용성이 떨어지고, 해당 함수명을 일일이 살펴봐야 하기에 여전히 불편하다.
4. 비동기 제어
이런 불편함을 해결하기 위해 비동기 제어를 사용한다.
4.1 Promise
ES6부터 사용, new 연산자로 호출한 Promise의 인자 콜백 함수는 내부에 resolve, reject를 호출하는 경우 둘 중 하나가 실행된 이후에 then, 또는 catch로 넘어갈 수 있다. 따라서 아래 함수의 경우 resolve(name) 이후에 then으로 넘어갈 수 있고, resolve로 name을 받아 이전 이름과 현재 커피명을 합쳐 최종적으로 coffeeList를 출력할 수 있다.
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);
});
});
이를 함수화한다면 더욱 짧게 나타낼 수 있다.
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("카페라떼"));
4.2 Generator
function 뒤에 *를 붙이면 Generator 함수를 만들 수 있다. Generator 함수 실행 시 Iterator가 반환되고, Iterator의 next 메서드를 호출하면 가장 먼저 나온 yield에서 멈추고 해당 부분 완료 후 그다음으로 넘어갈 수 있다. 따라서 아래 Generator 함수 coffeeGenerator의 경우, 각각의 커피를 addCoffee로 출력할 때 앞에 yield를 붙여서 커피가 나오는 순서를 제어할 수 있다.
var addCoffee = function (prevName, name) {
setTimeout(function () {
coffeeMaker.next(prevName ? prevName + "," + name : prevName);
}, 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();
4.3 Promise & async/ await
ES2017부턴 더욱 편리한 async/await 기능이 등장했다. 비동기 함수에 async를 붙이고 순서를 기다려야 하는 코드 앞에 await를 붙이면 Promise then과 비슷한 결과를 얻을 수 있다.
var addCoffee = function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(name);
}, 500);
});
};
var coffeeMaker = async () => {
var coffeeList = "";
var _addCoffee = async (name) => {
coffeeList = coffeeList
? coffeeList + "," + (await addCoffee(name))
: await addCoffee(name);
};
await _addCoffee("에스프레소");
console.log(coffeeList);
await _addCoffee("아메리카노");
console.log(coffeeList);
await _addCoffee("카페모카");
console.log(coffeeList);
await _addCoffee("카페라떼");
console.log(coffeeList);
};
coffeeMaker();
퀴즈
-
new 연산자와 호출한 Promise의 콜백함수는 reject함수가 호출됐다면, 호출되더라도 실행되지 않는다 (o/x)
-
newCoffee 호출 후 1초 뒤 콘솔창 출력결과는?
let newCoffee = new Promise(function (resolve) {
resolve("아메리카노");
setTimeout(() => resolve("에스프레소"), 200);
});
newCoffee.then(console.log);
댓글남기기