개발공부/자바스크립트

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

jennayeo 2021. 6. 27. 13:54

 

콜백 함수란?

어떤 함수를 호출하며 특정 조건일때 이 함수를 실행하달라고 요청하는 콜백함수는 제어권과 관련이 깊다.

 

제어권

호출시점

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

// 결과
>> 0 (0.3초)
>> 1 (0.6초)
>> 2 (0.9초)
>> 3 (1.2초)
>> 4 (1.5초)
  • 이코드는 count가 4를 초과 하기전까지 콜백함수 cbFunc를 0.3초마다 자동으로 실행한다.
  • setInterval이라고 하는 '다른 코드'에 cbFunc 함수를 넘겨주자 제어권을 넘격받은 setInterval이 스스로의 판단에 따라 적절한 시점에(0.3초마다) 이 함수를 실행한다.
  • 이처럼 콜백함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

 

인자

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

// 결과
>> 10 0
>> 20 1
>> 30 2
>> [15, 25, 35]

1. 배열 [10, 20, 30]의 각요소를 하나씩 꺼내어 콜백함수 실행 -> 처음엔 currentValue = 10, index = 0이 담긴다.

2. 각 값을 출력한 다음 currentValue + 5가 반환된다.

3. 배열의 끝까지 반복 실행한다.

✋🏻map메서드

"메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다. 콜백 함수의 첫번째 인자에는 배열의 요소 중 현재값이, 두 번째 인자에는 현재값의 인덱스가, 세번째 인자에는 map메서드의 대상이 되는 배열 자체가 담긴다."

 

만약 map 메서드를 제이쿼리의 방식처럼 순서를 바꾸어 사용해본다면?

(제이쿼리의 메서드들은 기본적으로 첫 번째 인자에 index가, 두 번째 인자에 currentValue가 온다.)

var newArr2 = [10, 20, 30].map(function (index, currentValue) {
	console.log(index, currentValue);
    return currentValue + 5;
});
console.log(newArr2);

// 결과
>> 10 0
>> 20 1
>> 30 2
>> [5, 6, 7]

사용자가 index, currentValue 순서로 출력해달라고 명령했지만, map메서드는 currentValue, index순서로 원래부터 정의된 규칙이기 때문에, 사용자가 첫번째 인자의 이름을 index로하건, nothing으로하건 컴퓨터는 currentValue로 알아들을 것이다.

=> map 메서드를 호출해서 원하는 배열을 얻으려면 map메서드에 정의된 규칙에 따라 함수를 작성해야한다.

🍒 콜백 함수를 호출하는 주체가 사용자가 아닌 map메서드이므로 map메서드가 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지가 전적으로 map 메서드에게 달린것이다.
🍒이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.

 

this

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

Array.prototype.map = function (callback, thisArg) {
	var mappedArr = [];
    forr (var i = 0; i < this.length; i++) {
    	var mappedValue = callback.call(thisArg || window, this[i], i, this);
        mappedArr[i] = mappedValue;
    }
    return mappedArr;
};

this에는 thisArg값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정하고, 첫 번째 인자에는 메서드의 this가 배열을 가리킨다.

콜백 함수는 함수다

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

// obj 객체의 logValues는 메서드로 정의되었다.

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
  • 첫번째 호출은 메서드로서 호출하였다. 결과는 this는 obj를 가르키고, 인자로 넘어온 1, 2가 출력된다.
  • 두번째 호출은 forEach 함수의 콜백 함수로 메서드를 전달하고있다. forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하지않았으니 함수 내부에서의 this는 전역객체를 바라본다.
  • obj를 this로 하는 메서드를 그대로 전달한 것이아니라, obj.logValues가 가리키는 함수만 전달한 것이다.

 

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

객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라 볼 수 없지만.. 그럼에도 콜백 함수 내부에서 this가 객체를 바라보게 하고싶다면??

전통적인 방식으로는 this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 아주 번거러운 방식을 많이 사용했다고 한다.

this를 변수에 담아서 익명함수를 선언과 동시에 반환했다. 이 콜백이 setTimeout함수에 인자로 전달되면 1초뒤 실행되면서 obj1이 출력되었다. (신기하다👀 this를 찍으면 윈도우 객체가 나오는데 이 this를 담고있는 hello를 찍으면 이 객체의 this가 잘 찍혀나온다.)

하지만 이 방식은 실제로 this를 사용하지도 않을뿐더러 번거로운 방법이다. 오히려 this를 사용할 필요가 없을 것같은데?

이처럼 this를 사용하지않고 훨씬 간결하게 원하는 결과를 출력할 수 있지만 더이상 작성한 함수를 this를 이용해 다양한 상황에 재활용할 수 없게 되었다.

 

이제는 ES5에서 등장한 bind메서드를 이용하여 this에 다른 값을 바인딩 할 수있다.

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

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

 

콜백 지옥과 비동기 제어

콜백지옥이란?

콜백 함수를 익명함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상

 

비동기란?

동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식

반면, 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식을 말한다.

ex) setTimeout, addEventListener, XMLHttpRequest

 

요즘엔 웹의 복잡도가 높아진 만큼 비동기적인 코드의 비중도 높아지면서 콜백 지옥에 빠지기가 쉬워졌다.

// 콜백지옥의 예시
// 0.5초 주기마다 커피 목록 수집&출력

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);
                
                setTimeout(function (name) {
                	coffeeList += '.' + name;
                    console.log(coffeeList);
                }, 500, '카페라떼');
          }, 500, '카페모카');
     }, 500, '아메리카노');
}, 500, '에스프레소');

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

하지만 이 역시 일회성 함수를 전부 변수에 할당해야하는 번거러운 작업일 수 있기때문에 ES6에서 도입된 Prormise, Generator, ES2017에서 도입된 async/await를 공부해보자.

 

>> Promise, Generator & async/await 공부하러가기!

 

🌻 정리

  • 콜백함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.
    1. 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.
    2. 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져있다. 이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻는다.
    3. 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있다. 정하지않은 경우에는 전역객체를 바라본다. 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 된다.
  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행된다.
  • 비동기 제어를 위해 콜백함수를 사용하다 보면 콜백 지옥에 빠지기 쉽다. 최근의 ECMAScript에는 Promise, Generator, async/await등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법들이 속속 등장하고 있다.

 

 

출처: 정재남, 코어 자바스크립트, 위키북스(2019), 4장 콜백 함수