아카이브/JavaScript

[코어 자바스크립트] 04 콜백함수

kaaay 2021. 12. 5. 21:08
728x90

 

  1. 콜백함수란?
  2. 제어권
  3. 콜백 함수는 함수다
  4. 콜백 함수 내부의 this에 다른 값 바인딩하기
  5. 콜백 지옥과 비동기 제어

1. 콜백함수란?

콜백 함수(callback function)는 다른 코드의 인자로 넘겨주는 함수.

callback은 '되돌아 호출해달라'는 명령으로, 어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보내는 것이다.
이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출한다.

즉, 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.

콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행할 것이다.


2. 제어권

호출 시점

[콜백 함수 예제- setInterval]

var count = 0;
var timer = setInterval(function() {
	console.log(count);
    if (++count > 4) clearInterval(timer);
}, 300);

 

setInterval의 구조

var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);
  • scope에는 Window 객체 또는 Worker의 인스턴스가 들어올 수 있다.
  • 매개변수로는 func(함수), delay(밀리초 단위의 숫자) 값을 반드시 전달해야 하고, 세 번째 매개변수부터는 선택적이다.
  • func에 넘겨준 함수는 매 delay(ms) 마다 실행되며, 그 결과 어떠한 값도 리턴하지 않는다.
  • setInterval을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값이 반환된다.
  • 이를 변수에 담는 이유는 반복 실행되는 중간에 종료 (clearInterval)할 수 있게 하기 위함.

[콜백 함수 예제- setInterval 2]

var count = 0;
var cbFunc = function () {
	console.log(count);
  	if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);

 

코드를 실행하면 콘솔창에 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되며, 4가 출력된 후 종료된다.
setInterval이란 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자, 제어권을 넘겨받은 setInterval이 스스로의 판단에 따라 적절한 시점에 이 익명 함수를 실행한 것이다.

 

콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

 

인자

[콜백 함수 예제- Array.prototype.map]

var newArr = [10, 20, 30].map(function (currentValue, index) {
	console.log(currentValue, index);
    return currentValue +5;
});
console.log(newArr);  // [15, 25, 35];

 

map 메서드의 구조

Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)
  • 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다.
  • map 메서드는 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다.
  • 콜백 함수의 첫 번째 인자에는 배열요소의 현재값, 두 번째 인자에는 현재 인덱스, 세번째 인자에는 map메서드의 대상이 되는 배열 자체가 담긴다.
  • map 메서드 인자의 순서를 임의로 바꾸어 사용할 경우, 전혀 다른 결과값이 나온다.
콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.

 

this

콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

[콜백 함수 예제- Array.prototype.map 구현]

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;
};
  • 메서드 구현의 핵심은 call/ apply 메서드에 있다.
  • 제어권을 넘겨받을 코드에서 call/ apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩한다.

3. 콜백 함수는 함수다

콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출된다.

 

[메서드를 콜백 함수로 전달한 경우]

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

 


4. 콜백 함수 내부의 this에 다른 값 바인딩하기

객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없게 된다.
그럼에도 콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면 어떻게 해야할까?
별도의 인자로 this를 받는 함수의 경우는 여기에 원하는 값을 넘겨주면 되지만, 그렇지 않은 경우에는 this를 다른 변수에 담아 사용하게 하고, 이를 클로저로 만드는 방식이 많이 쓰였다.

 

[콜백 함수 내부의 this에 다른 값을 바인딩하는 방법- 전통적인 방식]

var obj1 = {
	name: 'obj1',
    func: function () {
    	var self = this;
        return function () {
        	console.log(self.name);
        };
    }
};
var callback = obj1.func();
setTimeout(callback, 1000);

 

[콜백 함수 내부에서 this를 사용하지 않은 경우]

var obj1 = {
	name: 'obj1'
    func: function() {
    	console.log(obj1.name);
    }
};
setTimeout(obj1.func, 1000);

 

[func 함수의 재활용]

...
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);

 

[콜백 함수 내부의 this에 다른 값을 바인딩하는 방법- bind 메서드 활용]

var obj1 = {
	name: 'obj1',
    func: function() {
    	console.log(this.name);
    }
};
setTimeout(obj1.func.bind(obj), 1000);

var obj2 = { name: 'obj2' };
setTimeout(obj.func.bind(obj2), 1500);

5. 콜백 지옥과 비동기 제어

콜백 지옥 (callback hell)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상.
주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하곤 하는데, 가독성이 떨어지며 코드를 수정하기도 어렵다.

 

콜백지옥은 들여쓰기 수준이 과도하게 깊어지고 값이 전달되는 순서가 아래서 위로 향하고 있어 어색하게 느껴진다.

가독성 문제와 어색함을 동시에 해결하는 가장 간단한 방법은 익명의 콜백 함수를 모두 기명함수로 전환하는 것이다.

하지만 일회성 함수를 전부 변수에 할당하는 것이 오히려 헷갈릴 소지가 있기도 하다.

 

그래서 ES6에서는 Promise, Generator 등이 도입됐고, ES2017에서는 async/ await가 도입되었다.

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행된다.

즉, 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) {
    	coffeList += (coffeeList ? ',' : '') + await addCoffe(name);
    };
    await _addCoffee('에스프레소');
    await _addCoffee('아메리카노');
    await _addCoffee('카페모카');
    await _addCoffee('카페라떼');
};
coffeMaker();