JavaScript

(CallBack)콜백 함수

추운날_너를_기다리며 2024. 8. 14. 16:47

1. CallBack 함수란 다른 코드의 인자로 넘겨주는 함수입니다.

다른 코드의 인자로 넘겨준다는 뜻은 콜백함수를 넘겨받는 코드가 있다는 얘기로 대표적으로 forEach, setTimeout 등이 있습니다.

예시로 밑을 보시면 되겠습니다.

setTimeout(function() {
    console.log("hello");
}, 1000);

 

const numbers = [1, 2, 3, 4, 5];

numbers.forEach(function(number) {
    console.log(number);
});

 

2. CallBack 함수는 action에 대한 제어권을 넘겨줄테니 너가 알고 있는 그 Logic으로 처리를 해달라는 뜻입니다.

그럼 여기서 제어권에 대한 이야기를 해야겠죠?

CallBack 함수는 어떤 제어권을 넘길까요? 호출 시점, 인자, this

 

[1] 호출 시점에 관한 것은 예로 들겠습니다.

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

var timer = setInterval(cbFunc, 300);

위의 코드를 보면 cbFunc함수를 인자로 넘겨 받은 SetInterval 코드에서 콜백함수를 언제 호출할지에 대한 제어권을 가지게 됩니다.

따라서 cbFunc는 호출주체와 제어권을 모두 setInterval에게 있는 것이 됩니다.

 

[2] 인자에 대한 제어권을 넘긴다는 뜻은 인자(의 순서)까지도 제어권이 그에게 있다.

이 뜻을 설명하기 위해서는 map 함수로 예를 드는 것이 가장 쉽습니다.

map 함수는 각 배열의 요소를 변환하여 새로운 배열을 반환시킵니다.

즉, 기존 배열을 조합해서 새로운 배열을 생성하는 역할입니다.

따라서 map 함수는 배열에 대한 method입니다.

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

// 결과는 뭐가 될까?
console.log(newArr);

위의 그림을 보면 [10, 20, 30]배열에 map 메서드를 통해서 각 자리에 + 5를 더하고 새로운 배열을 만들어 냅니다.

결과 값이 [15, 25, 35]가 되겠죠.

map 메서드 안에 인자 currentValue, index 인자의 순서를 바꿔서 작성하면 어떻게 될까요?

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

결과값은 [5, 6, 7]이 나오게 됩니다. 

currentValue가 우리가 원래 알던 index의 값으로 변했다는 것을 알 수 있습니다.

즉, 우리의 배열은 사용자가 정한 규칙이 아닌 map 메서드에 정의된 규칙대로 작성해야 합니다.

제어권이 넘어갈 map 함수의 규칙에 맞게 '넘긴 인자들은' 호출됩니다.

 

[3] this의 제어권이 넘겨진다는 뜻은 원래의 callback 함수는 함수이기 때문에 this가 전역객체를 참조하지만 그것 조차 제어권이 넘겨져 this를 가르키는 것에 대한 제어권을 넘겨받은 주체에게 있습니다.

앞전 시간에서 우리는 콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조한다라는 것을 알고 있지만

예외사항이 있었죠?

제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조한다.

여기서 핵심은 call, apply 메서드에 있습니다.

// 명시적으로 넣어준다.
// prototype으로 만든 함수는 두번째 인자로 항상 this를 받는다 보기 편하게 thisArg라한다.
Array.prototype.map123 = function(callback, thisArg) {
    // map 함수에서 return할 결과 배열
    var mappedArr = [];

    for (var i = 0; i < this.length; i++) {
        // callback함수를 thisbinding 해주기 위해 즉시 실행 함수 call() 사용
        var mappedValue = callback.call(thisArg || global, this[i]);
        mappedArr[i] = mappedValue;
    }
   
    return mappedArr;
};

var newArr = [1, 2, 3].map123(function(number) {
    return number * 2;
});

console.log(newArr);

결과 값이 [2, 4, 6] 이나옵니다.

그 이유는 callback함수를 인자로 받아서 call로 thisbinding을 해줘서 가르키는 this를 [1,2,3]으로 해줬기 때문입니다.

 

또다른 예를 들겠습니다.

[1, 2, 3, 4, 5].forEach(function (x) {
    console.log(this); 
}, {x : 1});

여기서 {x : 1}이 없다면 this는 global을 가르킵니다.

명시적으로 나타내서 this를 x는 1인것으로 두번째 인자로 넣어준거죠.

왜냐하면 prototpye으로 만든 함수 두번째 인자도 this를 받아주고 forEach에 받는 두번째 인자도 this를 가르키기 때문입니다.

 

다시한번더 call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.

call()은 인수 목록을, 반면에 apply()는 인수 배열 하나를 받는다는 점이 중요한 차이점입니다.

apply : func.apply(thisArg, [argsArray]);

call : func.call(thisArg[, arg1[, arg2[, ...]]])

 

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

예시로 알아보겠습니다.

var obj = {
    vals: [1, 2, 3],
    logValues: function(v, i) {
        console.log('>>> test start');
        if( this === global) {
            console.log('this가 global입니다. 원하지 않은 결과!');
        } else {
            console.log(this, v, i);
        }

        console.log('>>> test end');
    }
};

여기서 호출하는 것을 

obj.logValues(1, 2);

위에처럼 호출하게 되면 { vals: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2 로 아주 이쁘게 됩니다.

왜냐하면 method로서 호출을 했기 때문에 this가 가르키는 것이 obj라는 것으로 v는 vals의 1 i는 vals의 2를 가르키게 되겠죠.

또다른 예시로

[4, 5, 6].forEach(obj.logValues(1, 4));

위에처럼 호출 하면 결과 값은 에러가 나옵니다.

왜냐하면 forEach함수에 callback함수를 넣어줘야하는데 함수 자체를 넣은 것이 아닌 요소를 같이 넣어줬기 때문입니다.

[4, 5, 6].forEach(obj.logValues);

그렇다면 위에처럼 함수 원형을 넣으면 v에는 4 i에는 5가 들어갈까요? 아닙니다.

왜냐하면 obj.logValues는 obj를 this로 가리키는 메서드를 그대로 전달한 것이 아닌 obj.logValues가 가리키는 함수만 전달한 것이기 때문에 obj 객체와는 관련이 없습니다.

 

따라서 콜백 함수는 함수다라는 뜻은 콜백함수에 우리가 보기엔 정확한 this를 가르키는 메서드로 호출을 함수로써 하더라도 말그대로 메서드가 아닌 함수로만 호출이 된 것이기 때문에 this는 global을 가리키게 됩니다.

 

4. 콜백 함수 내부의 this에 다른 값 바인딩하기 (bind메서드를 활용)

위의 것을 이해하기 위해서는 의문점이 있어야합니다.

1) 콜백 함수 내부에서 this가 문맥에 맞는 객체를 바라보게 할 수는 없는지?

2) 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법이 있는지?

3) 즉, 3.에서의 문제점을 해결할 방법이 있는지

 

가장 간단한 방식으로는 this를 함수에 선언해 주면 됩니다.

var obj1 = {
    name: 'obj1',
    func: function() {
        var self = this; //이 부분!
        return function () {
            console.log(self.name);
        };
    }
};

// 단순히 함수만 전달한 것이기 때문에, obj1 객체와는 상관이 없어요.
// 메서드가 아닌 함수로서 호출한 것과 동일하죠.
var callback = obj1.func();
setTimeout(callback, 1000);

위의 예시처럼 var self = this; 이부분 입니다.

 

또다른 방식으로는 this를 사용하지는 않지만 장점을 놓치는 방식입니다.

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

말그대로 obj1 객체의 name을 가리키면 되는 일이죠.

 

하지만 위의 방식은 100점자리가 아니기 때문에 100점짜리의 방식인 bind메서드를 이용하겠습니다.

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

//함수 자체를 obj1에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj1로 고정해줘!
setTimeout(obj1.func.bind(obj1), 1000);

var obj2 = { name: 'obj2' };
//함수 자체를 obj2에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj2로 고정해줘!
setTimeout(obj1.func.bind(obj2), 1500);

.bind(this로 가리키고 싶은 객체) 를 콜백함수를 넣어줄 때 붙여주기만 하면 됩니다.

프로그래머들은 역시나 간결하고 깔끔하게 나타내는 방법을 결국 찾아내고 갈구해낸답니다.