콜백 함수가 뭐에요?
콜백(callback) 함수는 다른 코드의 인자로 넘겨주는 함수입니다. 콜백 함수를 넘겨 받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행합니다. 예를 들면, 어떤 함수 X를 실행합니다. 함수 X는 다음과 "특정 조건이 되면 함수 Y를 호출"과 같은 명령을 실행합니다. 함수 X는 특정 조건이 됐는지 스스로 판단하고 Y를 호출합니다. 다시 말해, Y 함수를 제어할 권리는 X 함수에게 있습니다. 이때 적절한 시점에 호출되는 Y 함수가 콜백 함수입니다.
'제어할 권리'을 구체적으로 살펴보기
var count = 0;
var countFunction = function () {
console.log(count);
if (++count > 4) { // count에 먼저 1이 더해지고 비교 연산을 수행한다.
clearInterval(timer);
}
};
var timer = setInterval(countFunction, 300);
// setInterval()의 반환 값인 ID 값을 timer에 담아
// setInterval() 함수를 종료(clearInterval()) 할 수 있도록 함.
/* 출력 결과
0
1
2
3
4
*/
호출 시점
콜백(callback) 함수는 다른 코드의 인자로 넘겨주는 함수입니다. 콜백 함수(이 함수의 제어권도 포함하여)를 넘겨 받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행합니다.
위 예제에 대하여 위 인용문은 아래와 같습니다.
countFunction
는setInterval
의 인자로 넘겨주는 함수입니다.countFunction
(이 함수의 제어권도 포함하여)를 넘겨 받은setInterval
이 를countFunction
을 필요에 따라0.3초
마다 실행합니다.
인자
var newArray = [10, 20, 30].map(function (currentValue, index) {
console.log(currentValue, index);
return currentValue + 5;
});
console.log(newArray);
/* 실행 결과
10 0
20 1
30 2
[ 15, 25, 35 ]
*/
map
메서드는 첫 번째 인자로 callback
함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있습니다. 두 번째 인자가 생략되면 this
는 전역 객체를 가리킵니다. map
메서드의 첫 번째 인자인 callback
함수의 첫번째 인자로 배열의 요소 중 현재 값이, 두번째 인자로 현재 값의 (배열에서의) 인덱스가, 세 번째 인자에는 map
메서드의 대상인 배열 그 자체가 담깁니다.
당연하겠지만, 인자의 순서를 바꾸어 사용할 수 없다는 것을 명심해야 합니다. 순서는 중요합니다. 위 예제에서 명명된 인자의 이름, currentValue
와 index
는 사람이 작성한 것이고 그것이 각각 carrot
, melon
이 되어도 함수는 그저 정해진 규칙대로 작동합니다. 컴퓨터는 자바스크립트 언어의 규칙을 그대로 실행할 뿐입니다. map
메서드는 (어딘가에 작성된) 자신의 코드에 따라 인자의 순서를 구별해 결과를 반환합니다.
이처럼 콜백 함수의 제어권을 넘겨 받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지를 제어하는 권리를 가집니다.
this
콜백 함수도 함수이기 때문에 기본적으로
this
가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로this
가 될 대상을 지정한 경우, 그 대상을 참조하게 된다.
별도의 this
를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 책에서는 map 메서드를 (핵심적인 내용만) 직접 구현했습니다.
Array.prototype.map = function (callback, thisArg) {
var mappedArray = [];
for (var i = 0; i < this.length; i++) {
var mappedValue = callback.call(thisArg || window, this[i], i, this);
mappedArray[i] = mappedValue;
}
return mappedArray;
}
메서드 구현의 핵심은 call
, apply
메서드에 있습니다. call
메서드의 첫번째 인자인 this
가 가리키는 값은 thisArg
인자가 있을 경우 thisArg
, 없으면 window
를 가리키도록 합니다. 두 번째 인자로 this
객체의 i
번째 값, 세 번째 인자로 callback
함수를 호출하는 this
객체 자체입니다. 이 때 this
객체는 map
함수 내부의 this
객체이고, map
함수는 어떤 배열의 메서드로서 호출되는 것이 때문에 this
가 가리키는 것은 배열 그 자체가 됩니다. 즉, 두 번째 인자는 배열의 i
번째 값, 세 번째 인자는 배열 자체입니다.
너만 함수냐? 콜백 함수도 함수다!
콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출됩니다. 즉, 그 함수의 this는 전역 객체를 가리키게 됩니다.
var obj = {
values: [1, 2, 3],
logValues: function (value, index) {
console.log(this, value, index);
},
};
obj.logValues(1, 2); // (1)
[4, 5, 6].forEach(obj.logValues); // (2)
/* 실행 결과
{ values: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2
Window {...} 4 0
Window {...} 5 1
Window {...} 6 2
*/
(1) 명령은 logValues
함수를 obj
의 메서드로서 호출한 것입니다. 따라서 logValues
내부의 this
는 obj를 가리킵니다.
(2) 명령에서 [forEach](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
의 콜백 함수로 전달된 obj.logValues
는 obj의 메서드로서 '호출'된 것이 아니라, obj
의 프로퍼티인 logValues
라는 함수를 그대로 전달한 것입니다. 따라서 '특정한 시점과 조건'에 콜백 함수가 호출될 때 가리키는 logValues
의 this
는 (forEach
의 특별한 규칙이 없었으므로) 전역 객체를 가리키게 됩니다.
콜백 함수 내부의 this에 다른 값 바인딩하기
그럼에도 불구하고 콜백 함수 내부에서 this
가 특정 객체를 가리키게 하고 싶다면 어떻게 해야 할까요? 별도의 인자로 this를 받는 함수의 경우, 여기에 원하는 값을 넘겨주면 됩니다. 하지만 그렇지 않은 경우, this
의 제어권도 넘겨주므로, 사용자가 임의로 값을 바꿀 수 없습니다. 그래서 전통적으로, this
를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this
대신 그 변수를 사용하게 합니다. 그리고 이를 클로저로 만드는 방식이 많이 쓰였습니다.
-
콜백 함수 내부
this
에 다른 값을 바인딩하는 전통적인 방법var obj1 = { name: "myNameObj1", func: function () { var self = this; return function () { console.log(self.name); }; }, }; var callback = obj1.func(); setTimeout(callback, 1000); /* 실행 결과 myNameObj1 */
callback
변수에 메서드로서 호출된func
함수의 반환 값인 익명 함수가 담깁니다. 이 익명함수는 obj1의 프로퍼티인 함수func
안에서 호출된self
객체의name
프로퍼티를 출력합니다. self는 메서드로서 호출됐기 때문에this
가 가리키는obj1
을 가리킵니다. 따라서self.name
은obj1.name
이 되고 최종적으로myNameObj1
이 호출됩니다.실제 this를 사용하지도 않고 번거로운 방법입니다.
-
콜백 함수 내부에서 this를 사용하지 않는 방법
var obj1 = { name: "myNameObj1", func: function () { console.log(obj1.name); }, }; setTimeout(obj1.func, 1000); /* 실행 결과 myNameObj1 */
훨씬 간결하고 직관적인 방법이지만, this를 활용할 수 없게 됐습니다. 어떤 방법으로도 다른 객체를 바라보게 할 수 없습니다.
-
첫 번째 방법에서 func 함수의 재활용
var obj1 = { name: "obj1", func: function () { var self = this; return function () { console.log(self.name); }; }, }; var callback = obj1.func(); setTimeout(callback, 1000); 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); /* 실행 결과 obj1 obj2 obj3 */
callback2
에는obj2
의func
를 메소드로서 호출한 결과를 담았습니다.obj2.func
는obj1
의func
함수입니다. 따라서callback2
가 전달되어 호출되면,obj2
의 메소드로서func
가 호출되고,obj1
의 func가 메서드가 아닌 함수로서 호출됩니다.obj1
의func
함수에서this
는obj2
를 가리킵니다. 따라서self.name
은obj2
가 됩니다.callback3
에는obj1
의func
함수에call
메서드를 호출했고,this
로 사용할 인자로obj3
을 전달한 함수를 담았습니다. 즉,callback3
에는this
로obj3
를 사용한obj1
의func
의 반환 값이 담깁니다. 따라서callback3
가 콜백 함수로 전달되어 호출되면obj3
의name
이 출력됩니다. -
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), 1000); /* 실행 결과 obj1 obj2 */
bind 메서드는 call 비슷하지만 즉시 함수를 호출하는 것이 아니라 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 합니다.
func
콜백 지옥과 비동기 제어
콜백 함수를 호출하는 콜백 함수를 호출하는 콜백 함수를 ...
용어 정의
- 콜백 지옥(callback hell)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상입니다. 가독성이 떨어지고 코드를 수정하기 어렵습니다. 콜백 지옥은 피해야 합니다.
- 비동기는 동기의 반댓말로, 동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식입니다. 비동기적 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어갑니다.
대부분의 코드는 동기적 코드입니다. 반면 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나, 사용자의 직접적 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기를 한다거나, 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이왔을 때 비로소 어떤 함수로 실행하도록 대기하는 등, 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적 코드입니다.
이런 비동기 코드에서 사용되는 콜백 함수를 익명 함수로 작성하고, 그 안에서 비동기 코드를 작성하고 또 콜백 함수를 익명 함수로 작성한다면 콜백 지옥에 쉽게 빠지게 됩니다.
콜백 지옥의 가독성 문제와 수정 어려움을 해결하는 가장 간단한 방법은 익명의 콜백 함수를 모두 기명 함수로 전환하는 것입니다.
이후 자바스크립트에서 비동기적인 일련의 작업을 동기적으로, 혹은 동기적인 것처럼 보이게끔 처리해주는 장치를 마련하고자 노력했습니다. (이 내용은 이후 포스팅에 담겠습니다!)
- ES6에서는
Promise
,Generator
등이 도입됐습니다. - ES2017에서는
async
/await
가 도입됐습니다.
정리
- 콜백 함수는 다른 코드에 인자를 넘겨줌으로써 그 제어권도 함께 위임한 함수입니다.
- 제어권을 넘겨 받은 코드는 다음과 같은 제어권을 가집니다.
- 콜백 함수를 호출하는 시점을 스스로 판단해 실행합니다.
- 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있습니다. 이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 됩니다.
- 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있습니다. 정하지 않은 경우에는 전역 객체를 바라봅니다. 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 됩니다.
- 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행됩니다.
- 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉬우니 대처 방법을 알고 있어야 합니다. 그 방법에는 Promise, Generator, async/await 가 있습니다.
참고 및 출처
- setInterval() | https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval
- map() | https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map
- forEach() | https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
'프로그래밍-학습기록 > Javascript' 카테고리의 다른 글
[Javascript] 프로토타입 찬찬히 이해해보기 (0) | 2021.02.16 |
---|---|
클로저는 글로 적기 글렀어. (0) | 2021.02.15 |
[Javascript] 다양한 상황에서 this는 무엇을 가리킬까 (0) | 2021.02.12 |
자바스크립트에서 실행 컨텍스트(execution context)가 도대체 뭐하는 애에요 (0) | 2021.02.10 |
자바스크립트 데이터 타입(기본형, 참조형) 이해하기 (0) | 2021.02.09 |